Files
pixelheros/assets/script/game/common/config/CardSet.ts
panw 5c5954b7d5 feat(卡牌系统): 重构卡牌选择逻辑并添加属性卡类型支持
- 在GameSet枚举中添加Attr卡牌类型
- 在GameEvent中添加UseAttrCard事件
- 重构CardSet模块,统一使用GameSet中的CardType枚举
- 重构MissionCardComp模块,支持混合模式卡牌选择和强制类型获取
- 添加等级升级事件处理,优化卡牌获取逻辑
2026-01-14 20:37:40 +08:00

329 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";
import { CardType } from "./GameSet";
/**
* 统一卡牌信息接口 (用于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.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 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.Partner:
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.Partner:
// 优先使用 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列表 (用于去重或排除已拥有)
* @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]];
}
}