Files
pixelheros/assets/script/game/common/config/CardSet.ts
panw c3fa6b6210 refactor(CardSet): 简化卡牌选项获取逻辑,移除冗余循环
- 将多类型权重随机选择改为直接使用指定类型
- 简化候选列表构建和抽取流程,避免嵌套循环
- 保留属性卡兜底逻辑,但仅在非属性类型且数量不足时触发
2026-01-30 15:50:57 +08:00

349 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, 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 type = forcedType !== undefined ? forcedType : CardType.Attr;
const result: ICardInfo[] = [];
const excludeSet = new Set(excludeUuids);
// 2. 获取该类型的候选池
const rawItems = getDefaultPool(type, level);
// 3. 构建候选列表(包含完整信息)
const candidates: ICardInfo[] = [];
for (const item of rawItems) {
if (excludeSet.has(item.id)) continue;
const info = getCardBaseInfo(type, item.id);
if (info) {
info.weight = item.weight;
candidates.push(info);
}
}
// 4. 抽取
for (let i = 0; i < count; i++) {
if (candidates.length === 0) break;
const card = weightedRandomCard(candidates);
if (card) {
result.push(card);
// 移除已选,防止重复
const idx = candidates.indexOf(card);
if (idx > -1) candidates.splice(idx, 1);
}
}
// 5. 兜底逻辑:如果数量不足,且当前类型不是属性,则用属性卡补齐
if (result.length < count && type !== CardType.Attr) {
const attrItems = getDefaultPool(CardType.Attr); // 兜底可以用全量属性池
for (const item of attrItems) {
if (result.length >= count) break;
// 检查是否已存在ID去重
if (excludeSet.has(item.id) || result.some(r => r.uuid === item.id)) continue;
const info = getCardBaseInfo(CardType.Attr, item.id);
if (info) {
info.weight = 100;
result.push(info);
}
}
}
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]];
}
}