Files
pixelheros/assets/script/game/hero/Hero.ts
panFD ed738d2e30 refactor(hero): 重构额外属性加成系统,统一为bonus字段
1.  将原ap_bonus/hp_bonus/def_bonus三个字段合并为通用的bonus数组结构
2.  新增多属性同时配置能力,支持hp/ap/def百分比加成与crt/frz/stun/para固定百分点加成
3.  更新所有前端编辑器、服务端渲染与游戏运行时逻辑适配新配置格式
4.  添加数据兼容迁移逻辑,自动处理旧版英雄配置数据
2026-09-07 23:05:30 +08:00

348 lines
15 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 { instantiate, Node, Prefab, Vec3, v3, resources, SpriteFrame, Sprite, SpriteAtlas, BoxCollider2D, RigidBody2D, tween, Tween } from "cc";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { smc } from "../common/SingletonModuleComp";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
import { HeroInfo, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveSkillOverrides, calcGrowBonus, HeroDisVal, getHeroQualityCoef, getEffectiveStats, HERO_LV12_SPECIAL_LV, HERO_LV12_BASE_LV, HERO_LV12_SPECIAL_ATTR } from "../common/config/heroSet";
import { GameEvent } from "../common/config/GameEvent";
import { SkillTriggerType, HType } from "../common/config/heroSet";
import { SkillTriggerHelper } from "./SkillTriggerHelper";
import { Attrs } from "../common/config/HeroAttrs";
import { MoveComp } from "./MoveComp";
import { mLogger } from "../common/Logger";
import { FieldSkillType } from "../common/config/SkillSet";
import { applyTalentBonuses } from "../common/config/TalentSet";
/** 英雄实体:负责英雄节点创建、属性初始化、入场动画与销毁流程 */
@ecs.register(`Hero`)
export class Hero extends ecs.Entity {
/** 英雄数据组件引用 */
HeroModel!: HeroAttrsComp;
/** 英雄表现组件引用 */
View!: HeroViewComp;
/** 英雄移动组件引用 */
HeroMove!: MoveComp;
/** 调试开关,开启后输出实体层级等调试信息 */
debugMode: boolean = false;
/** 注册实体必需组件:移动 + 属性 */
protected init() {
this.addComponents<ecs.Comp>(
MoveComp,
HeroAttrsComp,
);
}
/** 销毁实体并释放视图节点,防止残留碰撞体与显示对象 */
destroy(): void {
// 优先销毁节点,避免实体销毁后场景仍残留可见对象
const view = this.get(HeroViewComp);
if (view && view.node && view.node.isValid) {
view.node.destroy();
}
const model = this.get(HeroAttrsComp);
if (model && model.posIndex >= 0) {
if (smc.mission.heroGrid[model.posIndex] === this.eid) {
smc.mission.heroGrid[model.posIndex] = -1;
}
}
// 手动移除组件,确保 ecs 侧引用及时释放
this.remove(HeroViewComp);
this.remove(HeroAttrsComp);
super.destroy();
}
static removeByEid(eid: number): boolean {
const targetEid = Math.floor(eid);
if (!targetEid) return false;
const entity = ecs.getEntityByEid(targetEid);
if (!entity) return false;
const model = entity.get(HeroAttrsComp);
if (!model || model.fac !== FacSet.HERO) return false;
entity.destroy();
return true;
}
/**
* 加载并初始化英雄
* 1) 创建节点并挂到 HERO 层
* 2) 初始化表现与属性数据
* 3) 播放下落入场并在落地后启用碰撞与移动
*/
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, dropToY: number = pos.y, hero_lv: number = 1, rarity: number = 1, posIndex: number = -1) {
// 英雄始终朝右,表现缩放固定为正向
scale = 1
// 英雄等级上限以配置为准,合成可升到 HERO_MAX_LV
if (hero_lv > FightSet.HERO_MAX_LV) hero_lv = FightSet.HERO_MAX_LV
// 英雄尺寸随等级做轻量放大,强化成长反馈
let size = 1
// 根据配置路径加载英雄预制体
var path = "game/heros/" + HeroInfo[uuid].path;
var prefab: Prefab = oops.res.get(path, Prefab)!;
var node = instantiate(prefab);
var scene = smc.map.MapView.scene;
// 统一挂到实体显示层 HERO 节点下
node.parent = scene.entityLayer!.node!.getChildByName("HERO")!;
const collider = node.getComponent(BoxCollider2D);
// 入场过程暂不参与碰撞,防止半空触发战斗逻辑
if (collider) collider.enabled = false;
node.setScale(size * node.scale.x, size * node.scale.y);
node.setPosition(pos)
// 输出节点层级信息,便于排查遮挡与渲染顺序问题
mLogger.log(this.debugMode, "hero", node.getSiblingIndex());
var hv = node.getComponent(HeroViewComp)!;
if (hero_lv == 3) { hv.playAllTime("plus") }
if (hero_lv == 4) { hv.playAllTime("plus") }
if (hero_lv == 5) { hv.playAllTime("plus") }
if (hero_lv == 6) { hv.playAllTime("plus2") }
// if(hero_lv == 9){hv.playAllTime("plus3")}
// if(hero_lv == 9){hv.playAllTime("plus4")}
// if(hero_lv == 9){hv.playAllTime("plus5")}
const model = this.get(HeroAttrsComp);
// 从配置中读取英雄静态数据
let hero = HeroInfo[uuid];
// 视图层参数:朝向与碰撞阵营
hv.scale = 1;
hv.box_group = BoxSet.HERO;
// 模型层参数:身份、阵营、等级、职业
model.hero_uuid = uuid;
model.hero_name = hero.name;
model.lv = hero_lv;
model.rarity = rarity;
model.base_rarity = hero.rarity ?? 1;
model.type = hero.type;
model.grow_type = hero.grow_type;
model.role = hero.role;
model.fac = FacSet.HERO;
// 攻击距离:配置优先;未配置按攻击定位取默认值(近战100 / 中程300 / 远程450)
model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long];
model.posIndex = posIndex;
if (posIndex >= 0) {
smc.mission.heroGrid[posIndex] = this.eid;
}
// 复制触发技能配置(新结构:按 s_uuid 分组 + lv 数组)
model.call = hero.call;
model.dead = hero.dead;
model.fstart = hero.fstart;
model.atking = hero.atking;
model.atked = hero.atked;
model.field = hero.field;
model.bonus = hero.bonus;
// 基础属性:固定基础值 × 品质系数(相邻品质差 5%),成长由 grow_type 线性递增提供
// TODO: 局外升级机制落地后,此处基础值将与存档强化累加重算
const qualityCoef = getHeroQualityCoef(hero.rarity ?? 1);
let base_ap = Math.round(hero.ap * qualityCoef);
let base_hp = Math.round(hero.hp * qualityCoef);
let base_def = Math.round(hero.def * qualityCoef);
model.base_ap = base_ap;
model.base_hp = base_hp;
model.defense = base_def;
// 英雄享受驻场百分比加成,怪物保持基础值
if (model.fac === FacSet.HERO) {
model.ap = model.getRuntimeAp(base_ap);
model.hp = model.hp_max = model.getRuntimeHp(base_hp);
} else {
model.ap = base_ap;
model.hp = model.hp_max = base_hp;
}
model.speed = hero.speed ?? 200;
// 闪避/命中:配置优先,缺省取全局默认值(百分点)
model.dodge = hero.dodge ?? FightSet.DEFAULT_DODGE;
model.accuracy = hero.accuracy ?? FightSet.DEFAULT_ACCURACY;
// 构建技能表并注入运行时冷却字段 ccd;overrides 按英雄等级 resolve 分档(下游无感知)
model.skills = {};
for (const key in hero.skills) {
const skill = hero.skills[key];
if (!skill) continue;
// 最终技能等级 = 初始技能等级 + 英雄等级增量,下限 0、上限封顶(无限升级下技能不涨过上限)
model.skills[skill.uuid] = { ...skill, lv: Math.min(FightSet.HERO_SKILL_MAX_LV, Math.max(0, skill.lv + hero_lv - 2)), ccd: 0, overrides: resolveSkillOverrides(skill, hero_lv) };
}
// 按当前英雄等级 resolve 触发技能/光环到运行时缓存,供 SkillTriggerHelper 消费
model.runtime_call = resolveTriggerByLv(model.call, hero_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, hero_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, hero_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, hero_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, hero_lv);
model.runtime_field = resolveFieldByLv(model.field, hero_lv);
// 复活已纳入 dead 触发组,无需单独解析 revive
// 应用额外属性加成(成长类型每级递增 + bonus 档位累加)
// 成长增量:仅英雄生效(model.fac 已为 HERO),按 grow_type 递增曲线累计
const grow = calcGrowBonus(hero, hero_lv);
model.hp += grow.hp; model.hp_max += grow.hp;
model.ap += grow.ap;
model.defense += grow.def;
if (model.bonus) {
// 百分比基数:基础×品质 + 成长快照(不含 bonus 自身,多档加法叠加不滚雪球)
const snapHp = model.hp_max, snapAp = model.ap, snapDef = model.defense;
let hpP = 0, apP = 0, defP = 0;
for (const e of model.bonus) {
if (e.lv > hero_lv) continue;
const a = e.attrs;
hpP += a.hp ?? 0; apP += a.ap ?? 0; defP += a.def ?? 0;
// 概率类:固定百分点直接累加
model.critical += a.crt ?? 0;
model.freeze_chance += a.frz ?? 0;
model.stun_chance += a.stun ?? 0;
model.paralyze_chance += a.para ?? 0;
}
const hpAdd = Math.round(snapHp * hpP / 100);
model.hp += hpAdd; model.hp_max += hpAdd;
model.ap += Math.round(snapAp * apP / 100);
model.defense += Math.round(snapDef * defP / 100);
}
// lv12 职业属性专项:达到 12 级时按职业对单项基础属性 +50%
// 基数为 lv11 当前等级有效值(基础 + 成长 + lv9 bonus,不含 lv12 自身),仅英雄生效
if (model.fac === FacSet.HERO && hero_lv >= HERO_LV12_SPECIAL_LV && model.role !== undefined) {
const lv11 = getEffectiveStats(hero, HERO_LV12_BASE_LV);
const attr = HERO_LV12_SPECIAL_ATTR[model.role];
if (attr === "def") model.defense += Math.round(lv11.def * 0.5);
else if (attr === "hp") { const add = Math.round(lv11.hp * 0.5); model.hp += add; model.hp_max += add; }
else if (attr === "ap") model.ap += Math.round(lv11.ap * 0.5);
}
// 天赋加成层:全局天赋(一阶基础/三阶超强全局)全局生效,职业天赋(二阶专精/三阶超强职业)按 role 过滤(仅英雄生效,怪物跳过)
// Why: 放在成长/bonus 之后、光环乘区之后,以基础快照为基数叠加,避免与乘区叠加成乘法膨胀
if (model.fac === FacSet.HERO) {
applyTalentBonuses(model, smc.getTalentLevels(), model.role);
}
// 缓存技能射程等派生数据,减少战斗帧内重复计算
model.updateSkillDistanceCache();
// 初始化属性系统(buff/debuff 等动态属性容器)
model.initAttrs();
// 将视图组件注册到实体,打通逻辑与表现
this.add(hv);
hv.init();
// 广播主角召唤事件,触发外部系统监听逻辑
oops.message.dispatchEvent(GameEvent.MasterCalled, {
eid: this.eid,
uuid,
hero_lv,
model
})
// 初始化移动组件:方向、目标 X、站位基准 Y
const move = this.get(MoveComp);
move.direction = 1;
move.targetX = resolveFormationTargetX(model.fac, model.type);
move.baseY = dropToY;
move.moving = false;
hv.as.idle();
this.playDropAnim(pos, dropToY);
}
/**
* 播放英雄入场(下落)动画
* @param pos 初始位置(通常在空中)
* @param dropToY 最终落地的 Y 坐标
*/
playDropAnim(pos: Vec3, dropToY: number) {
const view = this.get(HeroViewComp);
const node = view?.node;
if (!node || !node.isValid) return;
const model = this.get(HeroAttrsComp);
const move = this.get(MoveComp);
const collider = node.getComponent(BoxCollider2D);
// 入场过程暂不参与碰撞,防止半空触发战斗逻辑
if (collider) collider.enabled = false;
move.moving = false;
view.as.idle();
// 依据下落距离自适应入场时长,保证手感稳定
const dropDistance = Math.abs(pos.y - dropToY);
const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200));
// 停止旧动画后执行下落 tween,避免复用节点时动画叠加
Tween.stopAllByTarget(node);
tween(node)
.to(dropDuration, { position: v3(pos.x, dropToY, 0) })
.call(() => {
if (!node || !node.isValid) return;
// 落地后锁定最终位置,切换到落地完成状态
node.setPosition(pos.x, dropToY, 0);
view.playEnd("down");
move.moving = true;
// 落地后再启用碰撞,避免空中阶段触发伤害结算
if (collider) {
collider.enabled = true;
collider.group = BoxSet.HERO;
collider.apply();
}
// 落地后触发 call 技能
SkillTriggerHelper.trigger(SkillTriggerType.Call, model, view);
})
.start();
}
/** 重置入口:复用 destroy 的释放流程 */
reset() {
super.destroy();
}
}
/** 统一生命周期系统:按 fac 区分英雄与怪物并输出日志 */
@ecs.register('BattleEntityLifecycleSystem')
export class BattleEntityLifecycleSystem extends ecs.ComblockSystem
implements ecs.IEntityEnterSystem, ecs.IEntityRemoveSystem {
/** 仅处理拥有 MoveComp 的实体 */
filter() {
return ecs.allOf(MoveComp);
}
/** 基于阵营生成日志名称 */
private resolveLabel(heroAttrs: HeroAttrsComp | null) {
if (!heroAttrs) return "未知";
if (heroAttrs.fac === FacSet.HERO) return "英雄";
if (heroAttrs.fac === FacSet.MON) return "怪物";
return "未知";
}
/** 实体进入世界时记录日志 */
entityEnter(e: ecs.Entity): void {
const heroAttrs = e.get(HeroAttrsComp);
const label = this.resolveLabel(heroAttrs);
if (heroAttrs) {
mLogger.log(heroAttrs.debugMode, 'BattleEntityLifecycle', `${label}进入世界: ${heroAttrs.hero_name}`);
} else {
mLogger.log(true, 'BattleEntityLifecycle', `${label}进入世界: 实体ID ${e.eid}`);
}
}
/** 实体离开世界时记录日志 */
entityRemove(e: ecs.Entity): void {
const heroAttrs = e.get(HeroAttrsComp);
const label = this.resolveLabel(heroAttrs);
if (heroAttrs) {
mLogger.log(heroAttrs.debugMode, 'BattleEntityLifecycle', `${label}离开世界: ${heroAttrs.hero_name}`);
} else {
mLogger.log(true, 'BattleEntityLifecycle', `${label}离开世界: 实体ID ${e.eid}`);
}
}
}