Files
pixelheros/assets/script/game/common/config/CardSet.ts
walkpan b577350003 refactor(CardSet): 重构默认卡池获取逻辑,支持动态权重计算
重构 getDefaultPool 函数,将重复逻辑统一处理并支持基于解锁等级的动态权重计算。新实现:
1. 统一处理所有卡牌类型的配置映射
2. 自动累加所有小于等于当前等级的配置项
3. 根据解锁等级动态计算权重,高等级卡牌出现概率更高
4. 保留原有兜底逻辑作为最后防线
2026-01-16 20:11:02 +08:00

389 lines
15 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, 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<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.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<number, number[]> | 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<number>();
// 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]];
}
}