Files
pixelheros/assets/script/game/hero/Hero.ts
pan d9bccd7c93 refactor: 将代码中的card_lv统一替换为rarity
本次提交完成了全项目范围内术语的统一替换,将原本指代卡牌品质/等级的card_lv字段全部重命名为rarity,对齐了设计文档中的命名规范,避免了术语混淆:
1.  更新了所有配置文件、组件类、数据结构中的字段定义
2.  同步修改了注释、文档说明中的相关表述
3.  修正了英雄、技能、装备卡牌的品质显示逻辑
4.  调整了背景颜色切换、属性计算等相关业务逻辑
2026-09-02 16:42:54 +08:00

344 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, resolveReviveByLv, 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.revive = hero.revive;
model.ap_bonus = hero.ap_bonus;
model.hp_bonus = hero.hp_bonus;
model.def_bonus = hero.def_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;
// 构建技能表并注入运行时冷却字段 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);
model.runtime_revive = resolveReviveByLv(model.revive, hero_lv);
// 应用额外属性加成(成长类型每级递增 + hp/ap/def_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.hp_bonus) {
let hpExtra = 0;
for (const e of model.hp_bonus) { if (e.lv <= hero_lv) hpExtra += e.value; }
model.hp += hpExtra; model.hp_max += hpExtra;
}
if (model.ap_bonus) {
let apExtra = 0;
for (const e of model.ap_bonus) { if (e.lv <= hero_lv) apExtra += e.value; }
model.ap += apExtra;
}
if (model.def_bonus) {
let defExtra = 0;
for (const e of model.def_bonus) { if (e.lv <= hero_lv) defExtra += e.value; }
model.defense += defExtra;
}
// 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}`);
}
}
}