Files
pixelheros/assets/script/game/hero/Hero.ts
pan a3375ec3b6 refactor(hero): 统一英雄攻击距离默认值并优化队友间距逻辑
1. 将英雄/怪物默认攻击距离调整为近战150、中程300、远程450,通过HeroDisVal统一管理
2. 新增同阵营单位移动和待机时的间距避让逻辑,避免单位重叠穿模,添加自然蠕动效果
2026-08-13 10:23:14 +08:00

348 lines
14 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, calcGrowBonus, HeroDisVal } 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";
/** 英雄实体:负责英雄节点创建、属性初始化、入场动画与销毁流程 */
@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, card_lv: 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.15
// 根据配置路径加载英雄预制体
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.card_lv = card_lv;
model.base_card_lv = hero.card_lv ?? 1;
model.type = hero.type;
model.grow_type = hero.grow_type;
model.role = hero.role;
model.fac = FacSet.HERO;
// 攻击距离配置优先未配置按攻击定位取默认值近战150 / 中程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.fend = hero.fend;
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;
// 基础属性:固定基础值,成长完全由 grow_type 指数递增提供(替代旧 ×2^(lv-1) 翻倍)
let base_ap = hero.ap;
let base_hp = hero.hp;
model.base_ap = base_ap;
model.base_hp = base_hp;
// 英雄享受驻场百分比加成,怪物保持基础值
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
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 };
}
// 按当前英雄等级 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_fend = resolveTriggerByLv(model.fend, 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_bonus/ap_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;
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;
}
// 缓存技能射程等派生数据,减少战斗帧内重复计算
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();
}
mergeToBirthAndDestroy(birthPos: Vec3, onDone?: () => void) {
const view = this.get(HeroViewComp);
const node = view?.node;
if (!node || !node.isValid) {
this.destroy();
if (onDone) onDone();
return;
}
const collider = node.getComponent(BoxCollider2D);
if (collider) {
collider.enabled = false;
}
const body = node.getComponent(RigidBody2D);
if (body) {
body.enabled = false;
}
const currentPos = node.getPosition();
const targetPos = v3(birthPos.x, birthPos.y, 0);
const moveDistance = Vec3.distance(currentPos, targetPos);
const moveDuration = Math.max(0.12, Math.min(0.3, moveDistance / 1200));
Tween.stopAllByTarget(node);
tween(node)
.to(moveDuration, { position: targetPos })
.call(() => {
this.destroy();
if (onDone) onDone();
})
.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}`);
}
}
}