重构 getDefaultPool 函数,将重复逻辑统一处理并支持基于解锁等级的动态权重计算。新实现: 1. 统一处理所有卡牌类型的配置映射 2. 自动累加所有小于等于当前等级的配置项 3. 根据解锁等级动态计算权重,高等级卡牌出现概率更高 4. 保留原有兜底逻辑作为最后防线
389 lines
15 KiB
TypeScript
389 lines
15 KiB
TypeScript
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]];
|
||
}
|
||
}
|