feat(英雄系统): 实现英雄升级和经验系统

- 在 HeroViewComp 中扩展怪物死亡事件数据,包含等级和类型信息
- 在 SingletonModuleComp 中实现完整的经验计算和升级逻辑
- 在 MissionComp 中添加经验获取和升级事件处理
- 在 RogueConfig 中添加经验计算公式和怪物经验配置
- 添加等级同步机制防止ECS数据覆盖
This commit is contained in:
walkpan
2026-01-03 10:09:35 +08:00
parent 7583ca7a37
commit 56452795bb
4 changed files with 317 additions and 185 deletions

View File

@@ -9,6 +9,7 @@ import * as exp from "constants";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { Attrs } from "./config/HeroAttrs";
import { time } from "console";
import { getLevelExp } from "../map/RogueConfig";
/**
* 用远程数据覆盖本地数据(统一方法)
* @param remoteData 远程数据(云端或本地调试)
@@ -89,6 +90,40 @@ export class SingletonModuleComp extends ecs.Comp {
},
gold: 200, // 金币数据MVVM绑定字段
};
/**
* 更新英雄经验
* @param exp 获得的经验值
*/
updateHeroExp(exp: number) {
if (!this.vmdata.hero) return;
this.vmdata.hero.exp += exp;
// console.log('[smc] 英雄升级 经验:' + this.vmdata.hero.exp + ' 等级:' + this.vmdata.hero.lv + ' 上限:' + this.vmdata.hero.exp_max);
// 确保 exp_max 初始化
if (this.vmdata.hero.exp_max <= 0) {
this.vmdata.hero.exp_max = getLevelExp(this.vmdata.hero.lv || 1);
}
while (this.vmdata.hero.exp >= this.vmdata.hero.exp_max) {
this.vmdata.hero.exp -= this.vmdata.hero.exp_max;
this.vmdata.hero.lv++;
// 更新下一级所需经验
this.vmdata.hero.exp_max = getLevelExp(this.vmdata.hero.lv);
// 触发升级事件
oops.message.dispatchEvent(GameEvent.CanUpdateLv, { lv: this.vmdata.hero.lv });
}
// 更新进度条显示 (0-1)
if (this.vmdata.hero.exp_max > 0) {
this.vmdata.hero.exp_pre = Math.floor(this.vmdata.hero.exp / this.vmdata.hero.exp_max * 100);
} else {
this.vmdata.hero.exp_pre = 0;
}
}
vmAdd() {
VM.add(this.vmdata, "data");
}
@@ -225,7 +260,16 @@ export class SingletonModuleComp extends ecs.Comp {
// 基础信息
h.name = heroAttrs.hero_name;
h.type = heroAttrs.type;
h.lv = heroAttrs.lv;
// 防止 ECS 旧数据覆盖 VM 新数据 (如果 VM 里的等级更高,说明刚升级还没同步到 ECS)
if (heroAttrs.lv > h.lv) {
h.lv = heroAttrs.lv;
} else if (h.lv > heroAttrs.lv) {
// 此时应该反向同步?或者等待 CanUpdateLv 事件处理
// 这里暂时保持 VM 的高等级,不被 ECS 覆盖
} else {
h.lv = heroAttrs.lv;
}
// 动态属性
h.hp = Math.floor(heroAttrs.hp);
@@ -241,10 +285,6 @@ export class SingletonModuleComp extends ecs.Comp {
h.crt = Math.floor(heroAttrs.Attrs[Attrs.CRITICAL] || 0);
h.as = Math.floor(heroAttrs.Attrs[Attrs.AS] || 0);
}
updateHeroExp(exp:number){
this.vmdata.hero.exp += exp;
this.vmdata.hero.exp_pre =Math.floor(this.vmdata.hero.exp/this.vmdata.hero.exp_max)
}
error(){
oops.gui.toast("数据处理异常,请重试或重新登录")
}

View File

@@ -404,7 +404,10 @@ export class HeroViewComp extends CCComp {
// 根据阵营触发不同事件
if(this.model.fac === FacSet.MON){
oops.message.dispatchEvent(GameEvent.MonDead, {
hero_uuid: this.model.hero_uuid,
uuid: this.model.hero_uuid,
lv: this.model.lv,
is_boss: this.model.is_boss,
is_elite: this.model.is_big_boss, // 暂时映射 is_big_boss 为 elite或者由 MissionComp 二次判断
position: this.node.position
});
}

View File

@@ -3,11 +3,13 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { FightSet} from "../common/config/GameSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { getLevelExp, getMonsterExp, MonsterCost, MonType } from "./RogueConfig";
import { GameEvent } from "../common/config/GameEvent";
import { HeroViewComp } from "../hero/HeroViewComp";
import { UIID } from "../common/config/GameUIConfig";
import { SkillView } from "../skill/SkillView";
import { FightSet } from "../common/config/GameSet";
const { ccclass, property } = _decorator;
@@ -33,7 +35,7 @@ export class MissionComp extends CCComp {
this.on(GameEvent.FightEnd,this.fight_end,this)
this.on(GameEvent.MissionEnd,this.mission_end,this)
this.on(GameEvent.DO_AD_BACK,this.do_ad,this)
// this.on(GameEvent.CanUpdateLv,this.show_uplv_button,this)
this.on(GameEvent.CanUpdateLv,this.onLevelUp,this)
}
protected update(dt: number): void {
if(!smc.mission.play||smc.mission.pause){
@@ -45,6 +47,28 @@ export class MissionComp extends CCComp {
}
}
// 升级奖励触发
onLevelUp(event: string, args: any) {
console.log(`[MissionComp] 英雄升级到 ${args.lv} 级!`);
// 同步等级到 ECS 组件,防止被 updateHeroInfo 覆盖回旧值
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
const attrs = e.get(HeroAttrsComp);
if (attrs && attrs.is_master) {
attrs.lv = args.lv;
// 这里可以扩展:更新英雄属性,如 HP 上限等
}
});
// 触发奖励选择界面 (暂时留空)
this.showLevelUpReward();
}
showLevelUpReward() {
// TODO: 显示三选一技能/属性奖励界面
console.log("[MissionComp] 显示升级奖励界面 (TODO)");
}
//奖励发放
do_reward(){
// 奖励发放
@@ -56,7 +80,31 @@ export class MissionComp extends CCComp {
do_mon_dead(event:any,data:any){
// console.log("[MissionComp] do_mon_dead",event,data)
smc.vmdata.mission_data.mon_num--
// 计算并增加经验
// data 应该是怪物组件或包含怪物信息的对象
if (data && data.uuid) {
// 默认值处理
const level = data.lv || 1;
// 类型推断
let type = MonType.NORMAL;
if (data.is_boss) {
type = MonType.BOSS;
} else if (data.is_elite) {
type = MonType.ELITE;
} else {
// 兜底策略根据Cost判断是否为精英怪
const cost = MonsterCost[data.uuid] || 1;
if (cost >= 10) {
type = MonType.ELITE;
}
}
// 获取怪物经验
const exp = getMonsterExp(data.uuid, level, type);
smc.updateHeroExp(exp);
}
}
do_hero_dead(event:any,data:any){

View File

@@ -65,182 +65,6 @@ enum GrowthType {
LOGARITHMIC = 0.5 // 对数级 - Speed
}
/**
* 计算波次因子
* @param stage 当前波次
* @param timeInSeconds 游戏进行时间(秒)
* @returns 波次因子 (0-1之间15分钟时达到最大)
*/
function calculateWaveFactor(stage: number, timeInSeconds: number = 0): number {
const MAX_GAME_TIME = 15 * 60; // 15分钟 = 900秒
const effectiveTime = timeInSeconds || (stage * 30); // 如果没有时间数据用波次估算每波30秒
const factor = Math.min(effectiveTime / MAX_GAME_TIME, 1.0);
return factor;
}
/**
* 应用成长公式到基础属性
* @param baseStat 基础属性值
* @param waveFactor 波次因子 (0-1)
* @param growthType 成长类型
* @returns 成长后的属性值
*/
function applyGrowthFormula(baseStat: number, waveFactor: number, growthType: GrowthType): number {
// 公式: Current_Stat = Base_Stat * (1 + waveFactor * 0.15) ^ Growth_Type
const growthMultiplier = Math.pow(1 + waveFactor * 0.15, growthType);
return Math.floor(baseStat * growthMultiplier);
}
/**
* 获取怪物动态成长属性
* @param stage 当前波次
* @param uuid 怪物ID
* @param monType 怪物类型
* @param timeInSeconds 游戏进行时间(秒)
* @returns 怪物属性
*/
export function getMonAttr(stage: number, uuid: number, monType: MonType = MonType.NORMAL, timeInSeconds: number = 0): MonAttrs {
const baseMonster = HeroInfo[uuid];
if (!baseMonster) {
console.warn(`[RogueConfig] 未找到怪物ID: ${uuid}`);
return { hp: 100, mp: 100, ap: 10, def: 0, speed: 100 };
}
// 计算波次因子
const waveFactor = calculateWaveFactor(stage, timeInSeconds);
// 根据怪物类型应用额外的倍率
let typeMultiplier = 1.0;
if (monType === MonType.ELITE) {
typeMultiplier = 2.0; // 精英怪2倍属性
} else if (monType === MonType.BOSS) {
typeMultiplier = 5.0; // Boss 5倍属性
}
// 应用不同的成长类型
const hp = applyGrowthFormula(baseMonster.hp, waveFactor, GrowthType.EXPONENTIAL) * typeMultiplier;
const ap = applyGrowthFormula(baseMonster.ap, waveFactor, GrowthType.LINEAR) * typeMultiplier;
const speed = applyGrowthFormula(baseMonster.speed, waveFactor, GrowthType.LOGARITHMIC);
// MP和DEF使用线性成长
const mp = applyGrowthFormula(baseMonster.mp, waveFactor, GrowthType.LINEAR);
const def = applyGrowthFormula(baseMonster.def, waveFactor, GrowthType.LINEAR) * typeMultiplier;
return {
hp: Math.floor(hp),
mp: Math.floor(mp),
ap: Math.floor(ap),
def: Math.floor(def),
speed: Math.floor(speed)
};
}
/**
* 根据波次生成怪物配置
* @param stage 当前波次
* @returns IMonsConfig数组
*/
export function getStageMonConfigs(stage: number): IMonsConfig[] {
const monsterConfigs: IMonsConfig[] = [];
// 基础怪物列表从heroset.ts中获取
const normalMons = [5201, 5301, 5401, 5501, 5601, 5602, 5603, 5604];
const eliteMons = [5701];
// 根据波次生成怪物配置
// 波次越高,怪物数量越多,精英怪物出现概率越高
const baseCount = 5 + Math.floor(stage / 2); // 基础数量每2波增加1
const eliteChance = Math.min(stage * 0.05, 0.3); // 精英怪概率最高30%
const bossWave = stage % 10 === 0; // 每10波出Boss
if (bossWave && stage > 0) {
// Boss波
monsterConfigs.push({
uuid: 5701,
type: MonType.BOSS,
level: stage
});
} else {
// 普通波
for (let i = 0; i < baseCount; i++) {
// 随机决定是否生成精英怪
const isElite = Math.random() < eliteChance;
const monList = isElite ? eliteMons : normalMons;
const randomUuid = monList[Math.floor(Math.random() * monList.length)];
monsterConfigs.push({
uuid: randomUuid,
type: isElite ? MonType.ELITE : MonType.NORMAL,
level: stage,
position: i % 5
});
}
}
return monsterConfigs;
}
// ==========================================
// 新增基于威胁点数Threat Budget的刷怪系统
// ==========================================
// 怪物消耗点数配置
export const MonsterCost: Record<number, number> = {
5201: 1, // 兽人战士 (Warrior)
5301: 3, // 兽人斥候 (Assassin)
5401: 5, // 兽人卫士 (Tank)
5501: 4, // 兽人射手 (Remote)
5601: 10, // 兽人自爆兵 (Mechanic)
5602: 8, // 兽人召唤师
5603: 6, // 兽人祭司 (Healer)
5604: 6, // 兽人图腾师
5701: 50, // 兽人首领 (Elite/Boss)
};
// 刷怪权重接口
interface SpawnWeight {
uuid: number;
weight: number;
}
/**
* 根据游戏时间获取刷怪权重
* @param timeInSeconds 游戏时间(秒)
*/
function getSpawnWeights(timeInSeconds: number): SpawnWeight[] {
const minutes = timeInSeconds / 60;
if (minutes < 2) {
// 0-2min: 匀速群落 - 100% 战士
return [{ uuid: 5201, weight: 100 }];
} else if (minutes < 5) {
// 2-5min: 快速干扰 - 70% 战士, 30% 刺客
return [
{ uuid: 5201, weight: 70 },
{ uuid: 5301, weight: 30 }
];
} else if (minutes < 10) {
// 5-10min: 阵地博弈 - 50% 战士, 40% 刺客, 10% 攻城/治疗
return [
{ uuid: 5201, weight: 50 },
{ uuid: 5301, weight: 40 },
{ uuid: 5401, weight: 5 }, // 攻城
{ uuid: 5603, weight: 5 } // 治疗
];
} else if (minutes < 14) {
// 10-14min: 极限生存 - 30% 战士, 50% 刺客, 20% 机制/精英
return [
{ uuid: 5201, weight: 30 },
{ uuid: 5301, weight: 50 },
{ uuid: 5601, weight: 10 }, // 机制怪
{ uuid: 5701, weight: 10 } // 精英
];
} else {
// 15min: 剧情杀/决战 - 100% Boss
return [{ uuid: 5701, weight: 100 }];
}
}
/**
* 全局刷怪配置接口
@@ -384,3 +208,220 @@ export function generateMonstersFromBudget(timeInSeconds: number, heroHpRatio: n
return monsters;
}
/**
* 计算波次因子
* @param stage 当前波次
* @param timeInSeconds 游戏进行时间(秒)
* @returns 波次因子 (0-1之间15分钟时达到最大)
*/
function calculateWaveFactor(stage: number, timeInSeconds: number = 0): number {
const MAX_GAME_TIME = 15 * 60; // 15分钟 = 900秒
const effectiveTime = timeInSeconds || (stage * 30); // 如果没有时间数据用波次估算每波30秒
const factor = Math.min(effectiveTime / MAX_GAME_TIME, 1.0);
return factor;
}
/**
* 应用成长公式到基础属性
* @param baseStat 基础属性值
* @param waveFactor 波次因子 (0-1)
* @param growthType 成长类型
* @returns 成长后的属性值
*/
function applyGrowthFormula(baseStat: number, waveFactor: number, growthType: GrowthType): number {
// 公式: Current_Stat = Base_Stat * (1 + waveFactor * 0.15) ^ Growth_Type
const growthMultiplier = Math.pow(1 + waveFactor * 0.15, growthType);
return Math.floor(baseStat * growthMultiplier);
}
/**
* 获取怪物动态成长属性
* @param stage 当前波次
* @param uuid 怪物ID
* @param monType 怪物类型
* @param timeInSeconds 游戏进行时间(秒)
* @returns 怪物属性
*/
export function getMonAttr(stage: number, uuid: number, monType: MonType = MonType.NORMAL, timeInSeconds: number = 0): MonAttrs {
const baseMonster = HeroInfo[uuid];
if (!baseMonster) {
console.warn(`[RogueConfig] 未找到怪物ID: ${uuid}`);
return { hp: 100, mp: 100, ap: 10, def: 0, speed: 100 };
}
// 计算波次因子
const waveFactor = calculateWaveFactor(stage, timeInSeconds);
// 根据怪物类型应用额外的倍率
let typeMultiplier = 1.0;
if (monType === MonType.ELITE) {
typeMultiplier = 2.0; // 精英怪2倍属性
} else if (monType === MonType.BOSS) {
typeMultiplier = 5.0; // Boss 5倍属性
}
// 应用不同的成长类型
const hp = applyGrowthFormula(baseMonster.hp, waveFactor, GrowthType.EXPONENTIAL) * typeMultiplier;
const ap = applyGrowthFormula(baseMonster.ap, waveFactor, GrowthType.LINEAR) * typeMultiplier;
const speed = applyGrowthFormula(baseMonster.speed, waveFactor, GrowthType.LOGARITHMIC);
// MP和DEF使用线性成长
const mp = applyGrowthFormula(baseMonster.mp, waveFactor, GrowthType.LINEAR);
const def = applyGrowthFormula(baseMonster.def, waveFactor, GrowthType.LINEAR) * typeMultiplier;
return {
hp: Math.floor(hp),
mp: Math.floor(mp),
ap: Math.floor(ap),
def: Math.floor(def),
speed: Math.floor(speed)
};
}
/**
* 根据波次生成怪物配置
* @param stage 当前波次
* @returns IMonsConfig数组
*/
export function getStageMonConfigs(stage: number): IMonsConfig[] {
const monsterConfigs: IMonsConfig[] = [];
// 基础怪物列表从heroset.ts中获取
const normalMons = [5201, 5301, 5401, 5501, 5601, 5602, 5603, 5604];
const eliteMons = [5701];
// 根据波次生成怪物配置
// 波次越高,怪物数量越多,精英怪物出现概率越高
const baseCount = 5 + Math.floor(stage / 2); // 基础数量每2波增加1
const eliteChance = Math.min(stage * 0.05, 0.3); // 精英怪概率最高30%
const bossWave = stage % 10 === 0; // 每10波出Boss
if (bossWave && stage > 0) {
// Boss波
monsterConfigs.push({
uuid: 5701,
type: MonType.BOSS,
level: stage
});
} else {
// 普通波
for (let i = 0; i < baseCount; i++) {
// 随机决定是否生成精英怪
const isElite = Math.random() < eliteChance;
const monList = isElite ? eliteMons : normalMons;
const randomUuid = monList[Math.floor(Math.random() * monList.length)];
monsterConfigs.push({
uuid: randomUuid,
type: isElite ? MonType.ELITE : MonType.NORMAL,
level: stage,
position: i % 5
});
}
}
return monsterConfigs;
}
/**
* 无限等级经验配置
* @param level 当前等级
* @returns 升级所需经验值
*/
export function getLevelExp(level: number): number {
// 基础经验
const baseExp = 100;
// 增长因子 (每级增加20%)
const growthFactor = 1.2;
// 公式: Exp = Base * (Factor ^ (Level - 1))
// 1级: 100
// 2级: 120
// 3级: 144
// 10级: ~515
// 20级: ~3194
return Math.floor(baseExp * Math.pow(growthFactor, level - 1));
}
/**
* 获取怪物掉落经验
* @param monsterUuid 怪物ID
* @param monsterLevel 怪物等级
* @param monsterType 怪物类型
*/
export function getMonsterExp(monsterUuid: number, monsterLevel: number, monsterType: MonType): number {
// 基础掉落经验 (可以从 MonsterCost 推导,或者单独配置)
const baseDrop = MonsterCost[monsterUuid] ? MonsterCost[monsterUuid] * 5 : 5;
// 类型加成
let typeMultiplier = 1.0;
if (monsterType === MonType.ELITE) typeMultiplier = 3.0;
if (monsterType === MonType.BOSS) typeMultiplier = 10.0;
// 等级加成 (每级增加10%)
const levelMultiplier = 1 + (monsterLevel - 1) * 0.1;
return Math.floor(baseDrop * typeMultiplier * levelMultiplier);
}
// 怪物消耗点数配置
export const MonsterCost: Record<number, number> = {
5201: 1, // 兽人战士 (Warrior)
5301: 3, // 兽人斥候 (Assassin)
5401: 5, // 兽人卫士 (Tank)
5501: 4, // 兽人射手 (Remote)
5601: 10, // 兽人自爆兵 (Mechanic)
5602: 8, // 兽人召唤师
5603: 6, // 兽人祭司 (Healer)
5604: 6, // 兽人图腾师
5701: 50, // 兽人首领 (Elite/Boss)
};
// 刷怪权重接口
interface SpawnWeight {
uuid: number;
weight: number;
}
/**
* 根据游戏时间获取刷怪权重
* @param timeInSeconds 游戏时间(秒)
*/
function getSpawnWeights(timeInSeconds: number): SpawnWeight[] {
const minutes = timeInSeconds / 60;
if (minutes < 2) {
// 0-2min: 匀速群落 - 100% 战士
return [{ uuid: 5201, weight: 100 }];
} else if (minutes < 5) {
// 2-5min: 快速干扰 - 70% 战士, 30% 刺客
return [
{ uuid: 5201, weight: 70 },
{ uuid: 5301, weight: 30 }
];
} else if (minutes < 10) {
// 5-10min: 阵地博弈 - 50% 战士, 40% 刺客, 10% 攻城/治疗
return [
{ uuid: 5201, weight: 50 },
{ uuid: 5301, weight: 40 },
{ uuid: 5401, weight: 5 }, // 攻城
{ uuid: 5603, weight: 5 } // 治疗
];
} else if (minutes < 14) {
// 10-14min: 极限生存 - 30% 战士, 50% 刺客, 20% 机制/精英
return [
{ uuid: 5201, weight: 30 },
{ uuid: 5301, weight: 50 },
{ uuid: 5601, weight: 10 }, // 机制怪
{ uuid: 5701, weight: 10 } // 精英
];
} else {
// 15min: 剧情杀/决战 - 100% Boss
return [{ uuid: 5701, weight: 100 }];
}
}