import { AttrCards, AttrInfo, CanSelectAttrs, PotionCards, CanSelectPotions } from "./AttrSet"; import { talConf, ItalConf, CanSelectTalents } from "./TalSet"; import { SkillSet, SkillConfig, CanSelectSkills } from "./SkillSet"; import { HeroInfo, heroInfo, CanSelectHeros } from "./heroSet"; import { CardType, CardKind } from "./GameSet"; /** * 统一卡牌信息接口 (用于UI显示和逻辑处理) */ export interface ICardInfo { uuid: number; type: CardType; kind: CardKind; 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 = { 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.Partner, 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 kind = CardKind.Attr; 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; kind = CardKind.Attr; 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; kind = baseInfo.kind; break; case CardType.Skill: baseInfo = SkillSet[uuid]; if (!baseInfo) return null; name = baseInfo.name; desc = baseInfo.info; icon = baseInfo.icon; kind = CardKind.Skill; break; case CardType.Partner: baseInfo = HeroInfo[uuid]; if (!baseInfo) return null; name = baseInfo.name; desc = baseInfo.info; icon = baseInfo.icon; kind = CardKind.Partner; break; case CardType.Potion: baseInfo = PotionCards[uuid]; if (!baseInfo) return null; name = baseInfo.note || "药水"; desc = baseInfo.desc; icon = baseInfo.icon; kind = CardKind.Buff; // 药水归类为 Buff 类型的卡片显示 break; } return { uuid, type, kind, name, desc, icon, weight: 0, // 基础信息不包含权重,权重由配置决定 tag, payload: baseInfo }; } /** * 获取指定类型的全量池 * 自动累加所有 <= level 的配置项 * @param type 卡牌类型 * @param level 当前等级 */ function getDefaultPool(type: CardType, level: number = 1): IPoolItem[] { const items: IPoolItem[] = []; let configMap: Record | null = null; let defaultWeight = 100; switch (type) { case CardType.Attr: configMap = CanSelectAttrs; defaultWeight = 100; break; case CardType.Talent: configMap = CanSelectTalents; defaultWeight = 50; break; case CardType.Skill: configMap = CanSelectSkills; defaultWeight = 80; break; case CardType.Partner: configMap = CanSelectHeros; defaultWeight = 100; break; case CardType.Potion: configMap = CanSelectPotions; defaultWeight = 100; break; } if (configMap) { // 收集所有已解锁的ID (去重) const unlockedIds = new Set(); // 1. 遍历所有等级配置,收集 <= level 的项 Object.keys(configMap).forEach(lvlStr => { const lv = parseInt(lvlStr); // 忽略 99 (默认全开) 这种特殊标记,只处理正常等级逻辑 if (lv <= level && lv !== 99) { const ids = configMap![lv]; if (ids) { // 计算权重:等级越高,权重越高 // 基础权重 defaultWeight // 额外权重:(解锁等级 / 当前等级) * 基础权重 * 2 // 例如:当前10级 // 1级卡权重: 100 + (1/10)*200 = 120 // 9级卡权重: 100 + (9/10)*200 = 280 // 这样新解锁的卡牌出现概率显著高于旧卡牌 const extraWeight = Math.floor((lv / Math.max(1, level)) * defaultWeight * 2); const finalWeight = defaultWeight + extraWeight; ids.forEach(id => { // 如果已经存在(可能在低等级也配置了),取最大权重 const existing = items.find(i => i.id === id); if (existing) { existing.weight = Math.max(existing.weight, finalWeight); } else { items.push({ id, weight: finalWeight }); } unlockedIds.add(id); }); } } }); // 2. 如果当前等级没有任何解锁项,且存在 99 号默认配置,则回退使用 99 号配置 // 这种行为保持了原有逻辑的"兜底"特性,但更智能 if (unlockedIds.size === 0 && configMap[99]) { configMap[99].forEach(id => { items.push({ id, weight: defaultWeight }); unlockedIds.add(id); }); } // 3. 构建结果 (items 已经在循环中构建好了,这里不需要再从 unlockedIds 重新构建) // unlockedIds.forEach(id => items.push({ id, weight: defaultWeight })); } else { // 兜底逻辑:如果该类型没有配置表 (理论上不应发生,除非新增类型未配置) // 这里保留原有的全量兜底逻辑作为最后的防线 switch (type) { case CardType.Attr: Object.keys(AttrCards).forEach(key => items.push({ id: Number(key), weight: 100 })); break; case CardType.Talent: Object.keys(talConf).forEach(key => items.push({ id: Number(key), weight: 50 })); break; case CardType.Skill: Object.keys(SkillSet).forEach(key => items.push({ id: Number(key), weight: 80 })); break; case CardType.Partner: Object.keys(HeroInfo).forEach(key => { const id = Number(key); if (id < 5200) items.push({ id, weight: 100 }); }); break; case CardType.Potion: Object.keys(PotionCards).forEach(key => items.push({ id: Number(key), weight: 100 })); break; } } return items; } /** * 根据等级获取随机卡牌选项 * @param level 当前等级 * @param count 选项数量 (默认3个) * @param excludeUuids 排除的卡牌UUID列表 (用于去重或排除已拥有) * @param forcedType 强制指定卡牌类型 (用于特殊获取,如商店、技能书等) */ export function getCardOptions(level: number, count: number = 3, excludeUuids: number[] = [], forcedType?: CardType): ICardInfo[] { // 1. 获取该等级的池配置 // 如果强制指定类型,则构造一个只包含该类型的配置 const initialPoolConfigs = forcedType ? [{ type: forcedType, poolWeight: 100 }] : (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]]; } }