Files
pixelheros/assets/script/game/hero/Hero.ts
walkpan a638f473a0 feat: 新增英雄出售功能并优化UI交互
- 在 Hero 类中添加 removeByEid 静态方法,用于安全移除英雄实体
- 在 HInfoComp 中集成出售按钮逻辑,点击可移除对应英雄并关闭信息面板
- 为 card.prefab 和 hnode.prefab 添加召唤/出售按钮及相关UI组件
- 调整 role_controller.prefab 面板高度并禁用部分组件
- 移除未使用的 hit-flash-white 场景资源文件
- 暂时注释 CardComp 中的触摸事件绑定以进行调试
2026-03-28 12:04:41 +08:00

249 lines
9.4 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, HeroPos, resolveFormationTargetX } from "../common/config/heroSet";
import { GameEvent } from "../common/config/GameEvent";
import { Attrs} from "../common/config/HeroAttrs";
import { MoveComp } from "./MoveComp";
import { mLogger } from "../common/Logger";
/** 英雄实体:负责英雄节点创建、属性初始化、入场动画与销毁流程 */
@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();
}
// 手动移除组件,确保 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) {
// 英雄始终朝右,表现缩放固定为正向
scale = 1
// 英雄等级在当前规则下上限为 3避免超配表范围
if(hero_lv>3) hero_lv=3
// 英雄尺寸随等级做轻量放大,强化成长反馈
let size=1+0.1*hero_lv
// 根据配置路径加载英雄预制体
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)!;
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.type = hero.type;
model.fac = FacSet.HERO;
// 基础属性按等级倍率初始化
model.ap = hero.ap*model.lv;
model.hp= model.hp_max = hero.hp*model.lv;
model.speed = hero.speed;
// 构建技能表并注入运行时冷却字段 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.max(0,skill.lv + hero_lv - 2), ccd: 0 };
}
// 缓存技能射程等派生数据,减少战斗帧内重复计算
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();
// 依据下落距离自适应入场时长,保证手感稳定
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);
hv.playEnd("down");
move.moving = true;
// 落地后再启用碰撞,避免空中阶段触发伤害结算
if (collider) {
collider.enabled = true;
collider.group = BoxSet.HERO;
collider.apply();
}
})
.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}`);
if (heroAttrs.fac === FacSet.HERO) {
oops.message.dispatchEvent(GameEvent.HeroDead, {
eid: e.eid,
model: heroAttrs
});
}
} else {
mLogger.log(true, 'BattleEntityLifecycle', `${label}离开世界: 实体ID ${e.eid}`);
}
}
}