Files
pixelheros/assets/script/game/common/config/CardSet.ts
panw 6ddfe7e2c4 feat(卡牌系统): 重构卡牌选择逻辑并增加等级分段配置
重构卡牌选择系统,将原有的简单数组配置改为按等级分段的字典结构
- 为技能、英雄、天赋和属性分别添加 CanSelectXXX 配置
- 优化卡牌池构建逻辑,支持按等级筛选可用卡牌
- 改进权重随机算法,增加兜底机制
- 分离卡牌基础信息和权重配置,提高可维护性
2026-01-14 20:22:18 +08:00

335 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AttrCards, AttrInfo, CanSelectAttrs } from "./AttrSet";
import { talConf, ItalConf, CanSelectTalents } from "./TalSet";
import { SkillSet, SkillConfig, CanSelectSkills } from "./SkillSet";
import { HeroInfo, heroInfo, CanSelectHeros } from "./heroSet";
/**
* 卡牌类型枚举
*/
export enum CardType {
Skill = 1, // 技能
Talent = 2, // 天赋
Attr = 3, // 属性
Hero = 4 // 英雄(伙伴)
}
/**
* 统一卡牌信息接口 (用于UI显示和逻辑处理)
*/
export interface ICardInfo {
uuid: number;
type: CardType;
name: string;
desc: string;
icon: string;
weight: number; // 抽取权重
tag?: string; // 标签 (如 "special" 表示特殊属性)
payload: any; // 原始配置数据
}
/**
* 具体卡牌配置项 (内部使用)
*/
export interface IPoolItem {
id: number; // 卡牌UUID
weight: number; // 该卡牌在池中的权重
}
/**
* 等级池配置项
* 仅定义类型和权重,具体卡牌内容由各模块的 CanSelectXXX 配置决定
*/
export interface IPoolConfig {
type: CardType; // 卡牌类型
poolWeight: number; // 该类型池被选中的概率权重
tag?: string; // 辅助筛选从全池中筛选带tag的如 "special"
}
// 默认单卡权重
const DEFAULT_CARD_WEIGHT = 100;
/**
* 1-20 级卡牌池配置表
* 定义每个等级可能出现的卡牌类型及其权重
*/
export const LevelPoolConfigs: Record<number, IPoolConfig[]> = {
1: [{ type: CardType.Skill, poolWeight: 100 }],
2: [{ type: CardType.Attr, poolWeight: 100 }], // 常规属性
3: [{ type: CardType.Talent, poolWeight: 50 }, { type: CardType.Attr, poolWeight: 50, tag: "special" }], // 天赋或特殊属性
4: [{ type: CardType.Attr, poolWeight: 100 }],
5: [{ type: CardType.Talent, poolWeight: 100 }],
6: [{ type: CardType.Hero, poolWeight: 100 }], // 伙伴节点
7: [{ type: CardType.Attr, poolWeight: 80 }, { type: CardType.Skill, poolWeight: 20 }],
8: [{ type: CardType.Attr, poolWeight: 80 }, { type: CardType.Skill, poolWeight: 20 }],
9: [{ type: CardType.Attr, poolWeight: 50, tag: "special" }, { type: CardType.Talent, poolWeight: 50 }],
10: [{ type: CardType.Talent, poolWeight: 100 }],
11: [{ type: CardType.Attr, poolWeight: 70 }, { type: CardType.Skill, poolWeight: 30 }],
12: [{ type: CardType.Attr, poolWeight: 70 }, { type: CardType.Skill, poolWeight: 30 }],
13: [{ type: CardType.Attr, poolWeight: 100 }],
14: [{ type: CardType.Attr, poolWeight: 50, tag: "special" }, { type: CardType.Talent, poolWeight: 50 }],
15: [{ type: CardType.Talent, poolWeight: 100 }],
16: [{ type: CardType.Attr, poolWeight: 60 }, { type: CardType.Skill, poolWeight: 40 }],
17: [{ type: CardType.Attr, poolWeight: 60 }, { type: CardType.Skill, poolWeight: 40 }],
18: [{ type: CardType.Attr, poolWeight: 50, tag: "special" }, { type: CardType.Talent, poolWeight: 50 }],
19: [{ type: CardType.Attr, poolWeight: 100 }],
20: [{ type: CardType.Talent, poolWeight: 100 }],
};
// ========== 卡牌池构建逻辑 ==========
/**
* 获取指定类型的卡牌信息(不含权重,仅基础信息)
*/
function getCardBaseInfo(type: CardType, uuid: number): ICardInfo | null {
let baseInfo: any = null;
let name = "";
let desc = "";
let icon = "";
let tag = undefined;
switch (type) {
case CardType.Attr:
baseInfo = AttrCards[uuid];
if (!baseInfo) return null;
name = baseInfo.desc.split(" ")[0] || "属性";
desc = baseInfo.desc;
icon = baseInfo.icon;
tag = baseInfo.isSpecial ? "special" : undefined;
break;
case CardType.Talent:
baseInfo = talConf[uuid];
if (!baseInfo) return null;
name = baseInfo.name;
desc = baseInfo.desc;
icon = baseInfo.icon;
break;
case CardType.Skill:
baseInfo = SkillSet[uuid];
if (!baseInfo) return null;
name = baseInfo.name;
desc = baseInfo.info;
icon = baseInfo.icon;
break;
case CardType.Hero:
baseInfo = HeroInfo[uuid];
if (!baseInfo) return null;
name = baseInfo.name;
desc = baseInfo.info;
icon = baseInfo.path;
break;
}
return {
uuid,
type,
name,
desc,
icon,
weight: 0, // 基础信息不包含权重,权重由配置决定
tag,
payload: baseInfo
};
}
/**
* 获取默认的全量池 (当配置未指定items时使用)
* @param type 卡牌类型
* @param level 当前等级可选用于从各模块的CanSelect配置中获取
*/
function getDefaultPool(type: CardType, level: number = 1): IPoolItem[] {
const items: IPoolItem[] = [];
switch (type) {
case CardType.Attr:
// 优先使用 CanSelectAttrs 中的配置
if (CanSelectAttrs[level]) {
CanSelectAttrs[level].forEach(id => items.push({ id, weight: 100 }));
} else if (CanSelectAttrs[99]) {
// 默认池
CanSelectAttrs[99].forEach(id => items.push({ id, weight: 100 }));
} else {
// 全量兜底
Object.keys(AttrCards).forEach(key => items.push({ id: Number(key), weight: 100 }));
}
break;
case CardType.Talent:
// 优先使用 CanSelectTalents 中的配置
if (CanSelectTalents[level]) {
CanSelectTalents[level].forEach(id => items.push({ id, weight: 50 }));
} else if (CanSelectTalents[99]) {
// 默认池
CanSelectTalents[99].forEach(id => items.push({ id, weight: 50 }));
} else {
// 全量兜底
Object.keys(talConf).forEach(key => items.push({ id: Number(key), weight: 50 }));
}
break;
case CardType.Skill:
// 优先使用 CanSelectSkills 中的配置
if (CanSelectSkills[level]) {
CanSelectSkills[level].forEach(id => items.push({ id, weight: 80 }));
} else if (CanSelectSkills[99]) {
// 默认池
CanSelectSkills[99].forEach(id => items.push({ id, weight: 80 }));
} else {
// 全量兜底
Object.keys(SkillSet).forEach(key => items.push({ id: Number(key), weight: 80 }));
}
break;
case CardType.Hero:
// 优先使用 CanSelectHeros 中的配置
if (CanSelectHeros[level]) {
CanSelectHeros[level].forEach(id => items.push({ id, weight: 100 }));
} else if (CanSelectHeros[99]) {
// 默认池
CanSelectHeros[99].forEach(id => items.push({ id, weight: 100 }));
} else {
// 全量兜底 (排除怪物)
Object.keys(HeroInfo).forEach(key => {
const id = Number(key);
if (id < 5200) items.push({ id, weight: 100 });
});
}
break;
}
return items;
}
/**
* 根据等级获取随机卡牌选项
* @param level 当前等级
* @param count 选项数量 (默认3个)
* @param excludeUuids 排除的卡牌UUID列表 (用于去重或排除已拥有)
*/
export function getCardOptions(level: number, count: number = 3, excludeUuids: number[] = []): ICardInfo[] {
// 1. 获取该等级的池配置
// 必须复制一份,因为我们可能需要修改它(比如移除空的池子)
const initialPoolConfigs = LevelPoolConfigs[level] || [{ type: CardType.Attr, poolWeight: 100 }];
const result: ICardInfo[] = [];
const excludeSet = new Set(excludeUuids);
// 循环获取 count 张卡牌
for (let i = 0; i < count; i++) {
// 在每一轮获取中,我们使用一个临时的配置列表
// 这样如果某个类型池空了,我们可以从临时列表中移除它,避免重复选中
let currentConfigs = [...initialPoolConfigs];
let cardFound = false;
while (currentConfigs.length > 0) {
// 2.1 随机决定本次抽取的类型池
const selectedConfig = weightedRandomPool(currentConfigs);
if (!selectedConfig) break; // 理论上不会发生
// 2.2 获取该类型的所有候选卡牌
// 直接使用默认全池 (传入level以获取该等级特定的默认配置)
const rawCandidates = getDefaultPool(selectedConfig.type, level);
// 2.3 过滤与构建完整信息
const validCandidates: ICardInfo[] = [];
for (const item of rawCandidates) {
// 排除全局排除项
if (excludeSet.has(item.id)) continue;
// 排除本轮已选项
if (result.find(r => r.uuid === item.id)) continue;
// 获取详情
const info = getCardBaseInfo(selectedConfig.type, item.id);
if (!info) continue;
// Tag 过滤
if (selectedConfig.tag && info.tag !== selectedConfig.tag) {
continue;
}
// 赋予配置的权重
info.weight = item.weight;
validCandidates.push(info);
}
// 2.4 检查该类型是否有可用卡牌
if (validCandidates.length > 0) {
// 有卡!随机抽取一张
const card = weightedRandomCard(validCandidates);
if (card) {
result.push(card);
cardFound = true;
break; // 成功获取一张,跳出内层循环,进行下一张的获取
}
} else {
// 没卡!从当前配置中移除这个类型,重试
const index = currentConfigs.indexOf(selectedConfig);
if (index > -1) {
currentConfigs.splice(index, 1);
}
// 继续 while 循环,重新随机类型
}
}
// 2.5 如果尝试了所有类型都没找到卡 (极少见兜底)
if (!cardFound) {
// 尝试从属性池硬拿一个不重复的
const attrItems = getDefaultPool(CardType.Attr);
for (const item of attrItems) {
if (excludeSet.has(item.id) || result.find(r => r.uuid === item.id)) continue;
const info = getCardBaseInfo(CardType.Attr, item.id);
if (info) {
info.weight = 100;
result.push(info);
cardFound = true;
break;
}
}
}
// 如果连兜底都找不到(比如所有属性都拿完了),那也没办法了,可能返回少于 count 张
if (!cardFound) {
console.warn(`[CardSet] 无法为等级 ${level} 找到足够的卡牌选项,当前已选: ${result.length}/${count}`);
break;
}
}
// 3. 最终结果洗牌 (虽然逻辑上已经是随机的,但洗牌可以打乱类型顺序)
shuffleArray(result);
return result;
}
// ========== 工具函数 ==========
function weightedRandomPool(configs: IPoolConfig[]): IPoolConfig | null {
if (!configs || configs.length === 0) return null;
const totalWeight = configs.reduce((sum, item) => sum + item.poolWeight, 0);
let randomVal = Math.random() * totalWeight;
for (const config of configs) {
randomVal -= config.poolWeight;
if (randomVal <= 0) {
return config;
}
}
return configs[configs.length - 1];
}
function weightedRandomCard(cards: ICardInfo[]): ICardInfo | null {
if (!cards || cards.length === 0) return null;
const totalWeight = cards.reduce((sum, item) => sum + item.weight, 0);
let randomVal = Math.random() * totalWeight;
for (const card of cards) {
randomVal -= card.weight;
if (randomVal <= 0) {
return card;
}
}
return cards[cards.length - 1];
}
function shuffleArray(array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}