feat(卡牌系统): 实现卡牌池配置及随机选择功能
添加卡牌类型枚举和统一卡牌信息接口 实现等级池配置表及卡牌池缓存机制 提供根据等级获取随机卡牌选项的功能 包含权重随机选择算法和数组洗牌工具函数
This commit is contained in:
@@ -0,0 +1,251 @@
|
|||||||
|
import { AttrCards, AttrInfo } from "./AttrSet";
|
||||||
|
import { talConf, ItalConf } from "./TalSet";
|
||||||
|
import { SkillSet, CanSelectSkills, SkillConfig } from "./SkillSet";
|
||||||
|
import { HeroInfo, CanSelectHeros, heroInfo } from "./heroSet";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卡牌类型枚举
|
||||||
|
*/
|
||||||
|
export enum CardType {
|
||||||
|
Skill = 1, // 技能
|
||||||
|
Talent = 2, // 天赋
|
||||||
|
Attr = 3, // 属性
|
||||||
|
Hero = 4 // 英雄(伙伴)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一卡牌信息接口 (用于UI显示和逻辑处理)
|
||||||
|
*/
|
||||||
|
export interface ICardInfo {
|
||||||
|
uuid: number;
|
||||||
|
type: CardType;
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
icon: string;
|
||||||
|
weight: number; // 抽取权重
|
||||||
|
tag?: string; // 标签 (如 "special" 表示特殊属性)
|
||||||
|
payload: any; // 原始配置数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等级池配置项
|
||||||
|
*/
|
||||||
|
export interface IPoolWeight {
|
||||||
|
type: CardType;
|
||||||
|
weight: number;
|
||||||
|
tag?: string; // 如果指定,只从该类型中包含此tag的卡牌中抽取
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1-20 级卡牌池配置表
|
||||||
|
* 定义每个等级可能出现的卡牌类型及其权重
|
||||||
|
*/
|
||||||
|
export const LevelPoolConfigs: Record<number, IPoolWeight[]> = {
|
||||||
|
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 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== 卡牌池缓存 ==========
|
||||||
|
let _cachedPools: Map<CardType, ICardInfo[]> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化并获取指定类型的完整卡牌池
|
||||||
|
*/
|
||||||
|
function getFullPool(type: CardType): ICardInfo[] {
|
||||||
|
if (_cachedPools.has(type)) {
|
||||||
|
return _cachedPools.get(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool: ICardInfo[] = [];
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedPools.set(type, pool);
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据等级获取随机卡牌选项
|
||||||
|
* @param level 当前等级
|
||||||
|
* @param count 选项数量 (默认3个)
|
||||||
|
* @param excludeUuids 排除的卡牌UUID列表 (用于去重或排除已拥有)
|
||||||
|
*/
|
||||||
|
export function getCardOptions(level: number, count: number = 3, excludeUuids: number[] = []): ICardInfo[] {
|
||||||
|
// 1. 获取该等级的池配置,如果没有配置,默认给属性
|
||||||
|
const poolConfigs = LevelPoolConfigs[level] || [{ type: CardType.Attr, weight: 100 }];
|
||||||
|
|
||||||
|
const result: ICardInfo[] = [];
|
||||||
|
const excludeSet = new Set(excludeUuids);
|
||||||
|
|
||||||
|
// 循环抽取 count 次
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
// 2.1 随机决定本次抽取的类型池
|
||||||
|
const selectedPoolConfig = weightedRandomPool(poolConfigs);
|
||||||
|
if (!selectedPoolConfig) continue;
|
||||||
|
|
||||||
|
// 2.2 获取该类型的所有卡牌
|
||||||
|
let candidates = getFullPool(selectedPoolConfig.type);
|
||||||
|
|
||||||
|
// 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.4 如果该池子空了 (比如技能都学完了),尝试从属性池兜底
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
candidates = getFullPool(CardType.Attr).filter(c => !result.find(r => r.uuid === c.uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
// 2.5 按卡牌权重随机抽取一张
|
||||||
|
const card = weightedRandomCard(candidates);
|
||||||
|
if (card) {
|
||||||
|
result.push(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 最终结果洗牌 (避免顺序固定)
|
||||||
|
shuffleArray(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 工具函数 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重随机选择池配置
|
||||||
|
*/
|
||||||
|
function weightedRandomPool(configs: IPoolWeight[]): IPoolWeight | null {
|
||||||
|
if (!configs || configs.length === 0) return null;
|
||||||
|
|
||||||
|
const totalWeight = configs.reduce((sum, item) => sum + item.weight, 0);
|
||||||
|
let randomVal = Math.random() * totalWeight;
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
randomVal -= config.weight;
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组洗牌 (Fisher-Yates)
|
||||||
|
*/
|
||||||
|
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]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user