From 6ddfe7e2c45ac057f47c3279a3b9b83bb0d586b1 Mon Sep 17 00:00:00 2001 From: panw Date: Wed, 14 Jan 2026 20:22:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8D=A1=E7=89=8C=E7=B3=BB=E7=BB=9F):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=8D=A1=E7=89=8C=E9=80=89=E6=8B=A9=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E5=A2=9E=E5=8A=A0=E7=AD=89=E7=BA=A7=E5=88=86?= =?UTF-8?q?=E6=AE=B5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构卡牌选择系统,将原有的简单数组配置改为按等级分段的字典结构 - 为技能、英雄、天赋和属性分别添加 CanSelectXXX 配置 - 优化卡牌池构建逻辑,支持按等级筛选可用卡牌 - 改进权重随机算法,增加兜底机制 - 分离卡牌基础信息和权重配置,提高可维护性 --- assets/script/game/common/config/AttrSet.ts | 13 +- assets/script/game/common/config/CardSet.ts | 359 ++++++++++++------- assets/script/game/common/config/SkillSet.ts | 9 +- assets/script/game/common/config/TalSet.ts | 13 + assets/script/game/common/config/heroSet.ts | 11 +- 5 files changed, 264 insertions(+), 141 deletions(-) diff --git a/assets/script/game/common/config/AttrSet.ts b/assets/script/game/common/config/AttrSet.ts index 7e8f7724..5c345aac 100644 --- a/assets/script/game/common/config/AttrSet.ts +++ b/assets/script/game/common/config/AttrSet.ts @@ -25,4 +25,15 @@ import { Attrs } from "./HeroAttrs"; 2012:{uuid:2012, icon:"2001", attr: Attrs.SLOW_CHANCE, value: 10, showValue: 10, desc: "减速概率 +10%", isSpecial: true, note: "上限50%" }, 2013:{uuid:2013, icon:"2001", attr: Attrs.LIFESTEAL, value: 10, showValue: 10, desc: "吸血比例 +10%", isSpecial: true, note: "上限50%" }, 2014:{uuid:2014, icon:"2001", attr: Attrs.MANASTEAL, value: 10, showValue: 10, desc: "吸蓝比例 +10%", isSpecial: true, note: "上限50%" }, - } \ No newline at end of file + } + + export const CanSelectAttrs: Record = { + // 基础属性 + 2: [2001, 2002, 2003, 2004], + // 混合 + 4: [2001, 2002, 2003, 2004], + // 进阶属性 + 7: [2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2013], + // 默认全开 + 99: [2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014] +}; \ No newline at end of file diff --git a/assets/script/game/common/config/CardSet.ts b/assets/script/game/common/config/CardSet.ts index 1f464762..dddbc65f 100644 --- a/assets/script/game/common/config/CardSet.ts +++ b/assets/script/game/common/config/CardSet.ts @@ -1,7 +1,7 @@ -import { AttrCards, AttrInfo } from "./AttrSet"; -import { talConf, ItalConf } from "./TalSet"; -import { SkillSet, CanSelectSkills, SkillConfig } from "./SkillSet"; -import { HeroInfo, CanSelectHeros, heroInfo } from "./heroSet"; +import { AttrCards, AttrInfo, CanSelectAttrs } from "./AttrSet"; +import { talConf, ItalConf, CanSelectTalents } from "./TalSet"; +import { SkillSet, SkillConfig, CanSelectSkills } from "./SkillSet"; +import { HeroInfo, heroInfo, CanSelectHeros } from "./heroSet"; /** * 卡牌类型枚举 @@ -28,125 +28,170 @@ export interface ICardInfo { } /** - * 等级池配置项 + * 具体卡牌配置项 (内部使用) */ -export interface IPoolWeight { - type: CardType; - weight: number; - tag?: string; // 如果指定,只从该类型中包含此tag的卡牌中抽取 +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, weight: 100 }], - 2: [{ type: CardType.Attr, weight: 100 }], // 常规属性 - 3: [{ type: CardType.Talent, weight: 50 }, { type: CardType.Attr, weight: 50, tag: "special" }], // 天赋或特殊属性 - 4: [{ type: CardType.Attr, weight: 100 }], - 5: [{ type: CardType.Talent, weight: 100 }], - 6: [{ type: CardType.Hero, weight: 100 }], // 伙伴节点 - 7: [{ type: CardType.Attr, weight: 80 }, { type: CardType.Skill, weight: 20 }], - 8: [{ type: CardType.Attr, weight: 80 }, { type: CardType.Skill, weight: 20 }], - 9: [{ type: CardType.Attr, weight: 50, tag: "special" }, { type: CardType.Talent, weight: 50 }], - 10: [{ type: CardType.Talent, weight: 100 }], - 11: [{ type: CardType.Attr, weight: 70 }, { type: CardType.Skill, weight: 30 }], - 12: [{ type: CardType.Attr, weight: 70 }, { type: CardType.Skill, weight: 30 }], - 13: [{ type: CardType.Attr, weight: 100 }], - 14: [{ type: CardType.Attr, weight: 50, tag: "special" }, { type: CardType.Talent, weight: 50 }], - 15: [{ type: CardType.Talent, weight: 100 }], - 16: [{ type: CardType.Attr, weight: 60 }, { type: CardType.Skill, weight: 40 }], - 17: [{ type: CardType.Attr, weight: 60 }, { type: CardType.Skill, weight: 40 }], - 18: [{ type: CardType.Attr, weight: 50, tag: "special" }, { type: CardType.Talent, weight: 50 }], - 19: [{ type: CardType.Attr, weight: 100 }], - 20: [{ type: CardType.Talent, weight: 100 }], +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.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 }], }; -// ========== 卡牌池缓存 ========== -let _cachedPools: Map = new Map(); +// ========== 卡牌池构建逻辑 ========== /** - * 初始化并获取指定类型的完整卡牌池 + * 获取指定类型的卡牌信息(不含权重,仅基础信息) */ -function getFullPool(type: CardType): ICardInfo[] { - if (_cachedPools.has(type)) { - return _cachedPools.get(type)!; - } - - const pool: ICardInfo[] = []; +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: - // 转换属性配置 - Object.values(AttrCards).forEach(cfg => { - pool.push({ - uuid: cfg.uuid, - type: CardType.Attr, - name: cfg.desc.split(" ")[0] || "属性强化", // 简单处理名称 - desc: cfg.desc, - icon: cfg.icon, - weight: 100, // 属性默认权重 100 - tag: cfg.isSpecial ? "special" : undefined, - payload: cfg - }); - }); + 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: - // 转换天赋配置 - Object.values(talConf).forEach(cfg => { - pool.push({ - uuid: cfg.uuid, - type: CardType.Talent, - name: cfg.name, - desc: cfg.desc, - icon: cfg.icon, - weight: 50, // 天赋默认权重 50 - payload: cfg - }); - }); + baseInfo = talConf[uuid]; + if (!baseInfo) return null; + name = baseInfo.name; + desc = baseInfo.desc; + icon = baseInfo.icon; break; - case CardType.Skill: - // 转换技能配置 (仅包含 CanSelectSkills) - CanSelectSkills.forEach(uuid => { - const cfg = SkillSet[uuid]; - if (cfg) { - pool.push({ - uuid: cfg.uuid, - type: CardType.Skill, - name: cfg.name, - desc: cfg.info, - icon: cfg.icon, - weight: 80, // 技能默认权重 80 - payload: cfg - }); - } - }); + baseInfo = SkillSet[uuid]; + if (!baseInfo) return null; + name = baseInfo.name; + desc = baseInfo.info; + icon = baseInfo.icon; break; - case CardType.Hero: - // 转换英雄配置 (仅包含 CanSelectHeros) - CanSelectHeros.forEach(uuid => { - const cfg = HeroInfo[uuid]; - if (cfg) { - pool.push({ - uuid: cfg.uuid, - type: CardType.Hero, - name: cfg.name, - desc: cfg.info, - icon: cfg.path, // 使用 path 作为图标引用 - weight: 1000, // 英雄权重极高(如果池子里有的话) - payload: cfg - }); - } - }); + baseInfo = HeroInfo[uuid]; + if (!baseInfo) return null; + name = baseInfo.name; + desc = baseInfo.info; + icon = baseInfo.path; break; } - _cachedPools.set(type, pool); - return pool; + 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; } /** @@ -156,47 +201,94 @@ function getFullPool(type: CardType): ICardInfo[] { * @param excludeUuids 排除的卡牌UUID列表 (用于去重或排除已拥有) */ export function getCardOptions(level: number, count: number = 3, excludeUuids: number[] = []): ICardInfo[] { - // 1. 获取该等级的池配置,如果没有配置,默认给属性 - const poolConfigs = LevelPoolConfigs[level] || [{ type: CardType.Attr, weight: 100 }]; + // 1. 获取该等级的池配置 + // 必须复制一份,因为我们可能需要修改它(比如移除空的池子) + const initialPoolConfigs = LevelPoolConfigs[level] || [{ type: CardType.Attr, poolWeight: 100 }]; const result: ICardInfo[] = []; const excludeSet = new Set(excludeUuids); - // 循环抽取 count 次 + // 循环获取 count 张卡牌 for (let i = 0; i < count; i++) { - // 2.1 随机决定本次抽取的类型池 - const selectedPoolConfig = weightedRandomPool(poolConfigs); - if (!selectedPoolConfig) continue; + // 在每一轮获取中,我们使用一个临时的配置列表 + // 这样如果某个类型池空了,我们可以从临时列表中移除它,避免重复选中 + let currentConfigs = [...initialPoolConfigs]; + let cardFound = false; - // 2.2 获取该类型的所有卡牌 - let candidates = getFullPool(selectedPoolConfig.type); + while (currentConfigs.length > 0) { + // 2.1 随机决定本次抽取的类型池 + const selectedConfig = weightedRandomPool(currentConfigs); + if (!selectedConfig) break; // 理论上不会发生 - // 2.3 过滤 (Tag过滤 + 排除列表 + 已选中过滤) - candidates = candidates.filter(card => { - // Tag 匹配 - if (selectedPoolConfig.tag && card.tag !== selectedPoolConfig.tag) return false; - // 排除列表 - if (excludeSet.has(card.uuid)) return false; - // 当前轮次已选中去重 - if (result.find(r => r.uuid === card.uuid)) return false; - return true; - }); + // 2.2 获取该类型的所有候选卡牌 + // 直接使用默认全池 (传入level以获取该等级特定的默认配置) + const rawCandidates = getDefaultPool(selectedConfig.type, level); - // 2.4 如果该池子空了 (比如技能都学完了),尝试从属性池兜底 - if (candidates.length === 0) { - candidates = getFullPool(CardType.Attr).filter(c => !result.find(r => r.uuid === c.uuid)); + // 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 循环,重新随机类型 + } } - if (candidates.length > 0) { - // 2.5 按卡牌权重随机抽取一张 - const card = weightedRandomCard(candidates); - if (card) { - result.push(card); + // 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. 最终结果洗牌 (避免顺序固定) + // 3. 最终结果洗牌 (虽然逻辑上已经是随机的,但洗牌可以打乱类型顺序) shuffleArray(result); return result; @@ -204,17 +296,14 @@ export function getCardOptions(level: number, count: number = 3, excludeUuids: n // ========== 工具函数 ========== -/** - * 权重随机选择池配置 - */ -function weightedRandomPool(configs: IPoolWeight[]): IPoolWeight | null { +function weightedRandomPool(configs: IPoolConfig[]): IPoolConfig | null { if (!configs || configs.length === 0) return null; - const totalWeight = configs.reduce((sum, item) => sum + item.weight, 0); + const totalWeight = configs.reduce((sum, item) => sum + item.poolWeight, 0); let randomVal = Math.random() * totalWeight; for (const config of configs) { - randomVal -= config.weight; + randomVal -= config.poolWeight; if (randomVal <= 0) { return config; } @@ -222,9 +311,6 @@ function weightedRandomPool(configs: IPoolWeight[]): IPoolWeight | null { return configs[configs.length - 1]; } -/** - * 权重随机选择卡牌 - */ function weightedRandomCard(cards: ICardInfo[]): ICardInfo | null { if (!cards || cards.length === 0) return null; @@ -240,9 +326,6 @@ function weightedRandomCard(cards: ICardInfo[]): ICardInfo | null { return cards[cards.length - 1]; } -/** - * 数组洗牌 (Fisher-Yates) - */ function shuffleArray(array: any[]) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); diff --git a/assets/script/game/common/config/SkillSet.ts b/assets/script/game/common/config/SkillSet.ts index a6fd4141..72916a0c 100644 --- a/assets/script/game/common/config/SkillSet.ts +++ b/assets/script/game/common/config/SkillSet.ts @@ -292,4 +292,11 @@ export const EAnmConf: Record = { 9001:{uuid:9001,path:"atked",loop:false,time:0}, }; -export const CanSelectSkills = [6002, 6004, 6003, 6100]; +export const CanSelectSkills: Record = { + 1: [6002], + 2: [6004], + 3: [6003], + 4: [6100], + // 默认 + 99: [6002, 6004, 6003, 6100] +}; diff --git a/assets/script/game/common/config/TalSet.ts b/assets/script/game/common/config/TalSet.ts index 2671940c..91a45987 100644 --- a/assets/script/game/common/config/TalSet.ts +++ b/assets/script/game/common/config/TalSet.ts @@ -135,4 +135,17 @@ export const talConf: Record = { desc:"每升1级,永久增加2%的风怒概率"}, }; +export const CanSelectTalents: Record = { + // 3级开放攻击类天赋 + 3: [7001, 7003, 7005, 7008], + // 5级必出防御类 + 5: [7101, 7102, 7103], + // 9级混合 + 9: [7001, 7003, 7005, 7008, 7101, 7102, 7103], + // 20级终极天赋 + 20: [7301, 7302], + // 默认全开 + 99: [7001, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7101, 7102, 7103, 7104, 7201, 7301, 7302] +}; + // ========== 工具函数 ========== diff --git a/assets/script/game/common/config/heroSet.ts b/assets/script/game/common/config/heroSet.ts index 57107b92..2d6d3ac6 100644 --- a/assets/script/game/common/config/heroSet.ts +++ b/assets/script/game/common/config/heroSet.ts @@ -122,7 +122,16 @@ export interface heroInfo { info: string; // 描述文案 } -export const CanSelectHeros = [5001, 5002, 5003, 5004, 5005, 5006, 5007]; +export const CanSelectHeros: Record = { + 1: [5001, 5002], + 2: [5003], + 3: [5004], + 4: [5005], + 5: [5006], + 6: [5007], + // 默认全开(或根据需要留空) + 99: [5001, 5002, 5003, 5004, 5005, 5006, 5007] +}; export const HeroInfo: Record = { // ========== 英雄角色 ==========