- 移除通过 setSiblingIndex 手动设置层级的方式 - 新增 HERO、LINE1、LINE2、SKILL 等容器节点自动管理层级 - 调整英雄、怪物、技能等实体的父节点到对应容器 - 优化提示信息的位置偏移量
484 lines
17 KiB
TypeScript
484 lines
17 KiB
TypeScript
import { Vec3, _decorator , v3,Collider2D,Contact2DType,Label ,Node,Prefab,instantiate,ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Color, BoxCollider2D, UITransform} from "cc";
|
||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||
import { HeroSpine } from "./HeroSpine";
|
||
import { BoxSet, FacSet } from "../common/config/GameSet";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
import { EAnmConf, SkillSet,} from "../common/config/SkillSet";
|
||
import { oops } from "db://oops-framework/core/Oops";
|
||
import { GameEvent } from "../common/config/GameEvent";
|
||
import { TooltipTypes } from "../common/config/GameSet";
|
||
import { Attrs, } from "../common/config/HeroAttrs";
|
||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||
import { Tooltip } from "../skill/Tooltip";
|
||
import { timedCom } from "../skill/timedCom";
|
||
import { HeroInfo, HType } from "../common/config/heroSet";
|
||
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
/** 角色显示组件 */
|
||
export interface BuffInfo {
|
||
value: number;
|
||
remainTime?: number;
|
||
}
|
||
@ccclass('HeroViewComp') // 定义Cocos Creator 组件
|
||
@ecs.register('HeroView', false) // 定义ECS 组件
|
||
export class HeroViewComp extends CCComp {
|
||
// ==================== View 层属性(表现相关)====================
|
||
as: HeroSpine = null!
|
||
status:String = "idle"
|
||
scale: number = 1; // 显示方向
|
||
box_group:number = BoxSet.HERO; // 碰撞组
|
||
usePower:boolean = false;
|
||
useMp:boolean = false;
|
||
realDeadTime:number=10
|
||
deadCD:number=0
|
||
// 血条显示相关
|
||
hpBarShowTime:number = 5; // 血条显示持续时间(秒)
|
||
hpBarShowCD:number = 0; // 血条显示计时器
|
||
// ==================== UI 节点引用 ====================
|
||
private top_node: Node = null!;
|
||
|
||
// ==================== 直接访问 HeroAttrsComp ====================
|
||
get model() {
|
||
// 🔥 修复:添加安全检查,防止ent为null时的访问异常
|
||
if (!this.ent) {
|
||
console.warn("[HeroViewComp] ent is null, returning null for model");
|
||
return null;
|
||
}
|
||
return this.ent.get(HeroAttrsComp);
|
||
}
|
||
|
||
private damageQueue: Array<{
|
||
damage: number,
|
||
isCrit: boolean,
|
||
delay: number,
|
||
anm:string,
|
||
}> = [];
|
||
private isProcessingDamage: boolean = false;
|
||
private damageInterval: number = 0.01; // 伤害数字显示间隔
|
||
onLoad() {
|
||
this.as = this.getComponent(HeroSpine);
|
||
//console.log("[HeroViewComp]:hero view comp ",this.FIGHTCON)
|
||
this.on(GameEvent.FightEnd,this.do_fight_end,this)
|
||
const collider = this.node.getComponent(BoxCollider2D);
|
||
this.scheduleOnce(()=>{
|
||
if (collider) {
|
||
collider.enabled = true; // 先禁
|
||
collider.group = this.box_group; // 设置为英雄组
|
||
}
|
||
|
||
},0.1)
|
||
// let anm = this.node.getChildByName("anm")
|
||
// anm.setScale(anm.scale.x*0.8,anm.scale.y*0.8);
|
||
}
|
||
/** 视图层逻辑代码分离演示 */
|
||
start () {
|
||
this.as.idle()
|
||
|
||
// 初始化 UI 节点
|
||
this.initUINodes();
|
||
|
||
/** 方向 */
|
||
this.node.setScale(this.scale*this.node.scale.x,1*this.node.scale.y);
|
||
this.top_node.setScale(this.scale*this.top_node.scale.x,1*this.top_node.scale.y);
|
||
// if(this.model && this.model.is_boss){
|
||
// this.top_node.position=v3(this.node.position.x,this.node.position.y+70,0)
|
||
// }
|
||
/* 显示角色血*/
|
||
this.top_node.getChildByName("hp").active = true;
|
||
this.usePower=HeroInfo[this.model.hero_uuid].type==HType.warrior||HeroInfo[this.model.hero_uuid].type==HType.assassin;
|
||
this.useMp=HeroInfo[this.model.hero_uuid].type==HType.mage||HeroInfo[this.model.hero_uuid].type==HType.remote||HeroInfo[this.model.hero_uuid].type==HType.support;
|
||
this.top_node.getChildByName("pow").active = this.usePower;
|
||
this.top_node.getChildByName("mp").active = this.useMp;
|
||
// 初始隐藏血条(被攻击后才显示)
|
||
this.top_node.active = false;
|
||
}
|
||
|
||
/** 初始化 UI 节点引用 */
|
||
private initUINodes() {
|
||
this.top_node = this.node.getChildByName("top");
|
||
let hp_y = this.node.getComponent(UITransform).height+10;
|
||
this.top_node.setPosition(0, hp_y, 0);
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* View 层每帧更新
|
||
* 注意:数据更新逻辑已移到 HeroAttrSystem,这里只负责显示
|
||
*/
|
||
update(dt: number){
|
||
if(!smc.mission.play || smc.mission.pause) return;
|
||
|
||
// 🔥 修复:添加安全检查,防止在实体销毁过程中访问null的model
|
||
if(!this.ent) return;
|
||
if (!this.model) return;
|
||
if(this.model.is_dead){
|
||
this.deadCD+=dt
|
||
if(this.deadCD>=this.realDeadTime){
|
||
this.deadCD=0
|
||
this.realDead()
|
||
}
|
||
return
|
||
} ;
|
||
|
||
// 处理血条显示计时
|
||
if (this.hpBarShowCD > 0) {
|
||
this.hpBarShowCD -= dt;
|
||
if (this.hpBarShowCD <= 0) {
|
||
// 时间到,隐藏血条
|
||
this.top_node.active = false;
|
||
this.hpBarShowCD = 0;
|
||
}
|
||
}
|
||
|
||
// ✅ View 层职责:处理表现相关的逻辑
|
||
this.processDamageQueue(); // 伤害数字显示队列
|
||
|
||
// ✅ 更新 UI 显示(数据由 HeroAttrSystem 更新)
|
||
this.hp_show(this.model.hp, this.model.Attrs[Attrs.HP_MAX]);
|
||
if(this.useMp) this.mp_show(this.model.mp, this.model.Attrs[Attrs.MP_MAX]);
|
||
if(this.usePower) this.pow_show(this.model.pow, this.model.Attrs[Attrs.POW_MAX]);
|
||
this.show_shield(this.model.shield, this.model.Attrs[Attrs.SHIELD_MAX]);
|
||
}
|
||
|
||
|
||
/** 显示护盾 */
|
||
private show_shield(shield: number = 0, shield_max: number = 0) {
|
||
if(!this.top_node.active) return
|
||
let shield_progress = shield / shield_max;
|
||
this.node.getChildByName("shielded").active = shield > 0;
|
||
this.top_node.getChildByName("shield").active = shield > 0;
|
||
this.top_node.getChildByName("shield").getComponent(ProgressBar).progress = shield_progress;
|
||
this.scheduleOnce(() => {
|
||
this.top_node.getChildByName("shield").getChildByName("pb").getComponent(ProgressBar).progress = shield_progress;
|
||
}, 0.15);
|
||
}
|
||
|
||
/** 显示血量 */
|
||
private hp_show(hp: number, hp_max: number) {
|
||
// 不再基于血量是否满来决定显示状态,只更新进度条
|
||
if(!this.top_node.active) return;
|
||
|
||
let hp_progress = hp / hp_max;
|
||
this.top_node.getChildByName("hp").getComponent(ProgressBar).progress = hp_progress;
|
||
this.scheduleOnce(() => {
|
||
this.top_node.getChildByName("hp").getChildByName("hpb").getComponent(ProgressBar).progress = hp_progress;
|
||
}, 0.15);
|
||
}
|
||
|
||
/** 显示魔法值 */
|
||
private mp_show(mp: number, mp_max: number) {
|
||
if(!this.top_node.active) return
|
||
this.top_node.getChildByName("mp").getComponent(ProgressBar).progress = mp / mp_max;
|
||
this.scheduleOnce(() => {
|
||
this.top_node.getChildByName("mp").getChildByName("mpb").getComponent(ProgressBar).progress = mp / mp_max;
|
||
}, 0.15);
|
||
}
|
||
private pow_show(pow: number, pow_max: number) {
|
||
if(!this.top_node.active) return
|
||
this.top_node.getChildByName("pow").getComponent(ProgressBar).progress = pow / pow_max;
|
||
this.scheduleOnce(() => {
|
||
this.top_node.getChildByName("pow").getChildByName("mpb").getComponent(ProgressBar).progress = pow / pow_max;
|
||
}, 0.15);
|
||
}
|
||
/** 升级特效 */
|
||
private lv_up() {
|
||
var path = "game/skill/buff/buff_lvup";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.parent = this.node;
|
||
}
|
||
|
||
/** 攻击力提升特效 */
|
||
private ap_up() {
|
||
var path = "game/skill/buff/buff_apup";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.parent = this.node;
|
||
}
|
||
|
||
/** 显示 Buff 特效 */
|
||
private show_do_buff(name: string) {
|
||
var path = "game/skill/buff/" + name;
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
let pos = v3(this.node.position.x, this.node.position.y + 20, this.node.position.z);
|
||
node.parent = this.node.parent;
|
||
node.setPosition(pos);
|
||
}
|
||
|
||
|
||
/** 受击特效 */
|
||
private in_atked(anm: string = "atked", scale: number = 1) {
|
||
var path = "game/skill/end/" + anm;
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.setScale(node.scale.x * scale, node.scale.y);
|
||
node.setPosition(this.node.position.x, this.node.position.y+8, this.node.position.z);
|
||
node.parent = this.node.parent;
|
||
}
|
||
|
||
/** 冰冻特效 */
|
||
private in_iced(t: number = 1, ap: number = 0) {
|
||
var path = "game/skill/buff/buff_iced";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.getComponent(timedCom).time = t;
|
||
node.getComponent(timedCom).ap = ap;
|
||
node.parent = this.node;
|
||
}
|
||
|
||
/** 眩晕特效 */
|
||
private in_yun(t: number = 1, ap: number = 0) {
|
||
var path = "game/skill/buff/buff_yun";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
let height = this.node.getComponent(UITransform).height;
|
||
node.setPosition(v3(0, height));
|
||
node.getComponent(timedCom).time = t;
|
||
node.getComponent(timedCom).ap = ap;
|
||
node.parent = this.node;
|
||
}
|
||
|
||
/** 技能提示 */
|
||
private tooltip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 120) {
|
||
let tip = ecs.getEntity<Tooltip>(Tooltip);
|
||
let pos = v3(0, 60);
|
||
pos.y = pos.y + y;
|
||
tip.load(pos, type, value, s_uuid, this.node);
|
||
}
|
||
|
||
/** 血量提示(伤害数字) */
|
||
private hp_tip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 120) {
|
||
let tip = ecs.getEntity<Tooltip>(Tooltip);
|
||
let x = this.node.position.x;
|
||
let ny = this.node.getComponent(UITransform).height + y;
|
||
let pos = v3(x, ny, 0);
|
||
tip.load(pos, type, value, s_uuid, this.node.parent);
|
||
}
|
||
|
||
/** 治疗特效 */
|
||
private heathed() {
|
||
var path = "game/skill/buff/heathed";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.parent = this.node;
|
||
}
|
||
|
||
// 注意:BaseUp 逻辑已移到 HeroAttrSystem.update()
|
||
// 注意:updateTemporaryBuffsDebuffs 逻辑已移到 HeroAttrSystem.update()
|
||
do_fight_end(){
|
||
this.as.do_buff()
|
||
}
|
||
get isActive() {
|
||
return this.ent.has(HeroViewComp) && this.node?.isValid;
|
||
}
|
||
/** 状态切换(动画) */
|
||
status_change(type:string){
|
||
this.status = type;
|
||
if(this.model.is_dead) return
|
||
if(type === "idle"){
|
||
this.as.idle();
|
||
} else if(type === "move"){
|
||
this.as.move();
|
||
}
|
||
}
|
||
add_shield(shield:number){
|
||
// 护盾数据更新由 Model 层处理,这里只负责视图表现
|
||
if(this.model && this.model.shield>0) this.show_shield(this.model.shield, this.model.Attrs[Attrs.SHIELD_MAX]);
|
||
}
|
||
|
||
health(hp: number = 0, is_num:boolean=true) {
|
||
// 生命值更新由 Model 层处理,这里只负责视图表现
|
||
this.heathed();
|
||
if(this.model) {
|
||
this.hp_show(hp, this.model.Attrs[Attrs.HP_MAX]);
|
||
}
|
||
}
|
||
|
||
alive(){
|
||
this.model.is_dead=false
|
||
this.as.do_buff();
|
||
this.status_change("idle");
|
||
this.model.hp =this.model.Attrs[Attrs.HP_MAX]*50/100;
|
||
this.top_node.active=false
|
||
this.hpBarShowCD=0
|
||
|
||
}
|
||
/**
|
||
* 死亡视图表现
|
||
* 由 HeroAtkSystem 调用,只负责视觉效果和事件通知
|
||
*/
|
||
do_dead(){
|
||
// 添加安全检查
|
||
if (!this.model) return;
|
||
|
||
// 防止重复触发
|
||
if(this.model.is_count_dead) return;
|
||
this.model.is_count_dead = true; // 防止重复触发,必须存在防止重复调用
|
||
this.top_node.active=false
|
||
// 播放死亡特效
|
||
this.as.dead();
|
||
|
||
|
||
}
|
||
realDead(){
|
||
// 🔥 修复:添加model安全检查,防止实体销毁过程中的空指针异常
|
||
if (!this.model) {
|
||
console.warn("[HeroViewComp] realDead called but model is null, skipping");
|
||
return;
|
||
}
|
||
if(this.model.fac === FacSet.HERO){
|
||
// 英雄死亡:延迟触发死亡事件
|
||
oops.message.dispatchEvent(GameEvent.HeroDead, {
|
||
hero_uuid: this.model.hero_uuid
|
||
});
|
||
}
|
||
// 根据阵营触发不同事件
|
||
if(this.model.fac === FacSet.MON){
|
||
oops.message.dispatchEvent(GameEvent.MonDead, {
|
||
hero_uuid: this.model.hero_uuid,
|
||
position: this.node.position
|
||
});
|
||
}
|
||
this.ent.destroy();
|
||
|
||
}
|
||
do_atked(damage:number,isCrit:boolean,s_uuid:number){
|
||
// 受到攻击时显示血条,并设置显示时间(即使伤害为0也显示)
|
||
this.top_node.active = true;
|
||
this.hpBarShowCD = this.hpBarShowTime;
|
||
|
||
if (damage <= 0) return;
|
||
|
||
// 视图层表现
|
||
let SConf=SkillSet[s_uuid]
|
||
this.back()
|
||
this.showDamage(damage, isCrit, SConf.DAnm); // 暴击状态由战斗系统内部处理, DAnm和EAnm共用设定数组
|
||
}
|
||
|
||
private isBackingUp: boolean = false; // 🔥 添加后退状态标记
|
||
|
||
//后退
|
||
back(){
|
||
// 🔥 防止重复调用后退动画
|
||
if (this.isBackingUp) return;
|
||
|
||
if(this.model.fac==FacSet.MON) {
|
||
this.isBackingUp = true; // 🔥 设置后退状态
|
||
let tx=this.node.position.x+30
|
||
if(tx > 320) tx=320
|
||
tween(this.node)
|
||
.to(0.1, { position:v3(tx,this.node.position.y,0)})
|
||
.call(() => {
|
||
this.isBackingUp = false; // 🔥 动画完成后重置状态
|
||
})
|
||
.start()
|
||
}
|
||
//英雄不再后退
|
||
// if(this.model.fac==FacSet.HERO) {
|
||
// let tx=this.node.position.x-30
|
||
// if(tx < -320) tx=-320
|
||
// tween(this.node).to(0.1, { position:v3(tx,this.node.position.y,0)}).start()
|
||
// }
|
||
}
|
||
// 伤害计算和战斗逻辑已迁移到 HeroBattleSystem
|
||
|
||
/** 死亡触发器(预留,用于天赋/特殊效果) */
|
||
do_dead_trigger(){
|
||
if(!this.model || this.model.is_dead || this.model.fac === FacSet.MON) return;
|
||
// 预留:天赋触发、复活检查等
|
||
}
|
||
|
||
/** 受击触发器(预留,用于天赋/特殊效果) */
|
||
do_atked_trigger(){
|
||
if(!this.model || this.model.is_dead || this.model.fac === FacSet.MON) return;
|
||
// 预留:反击、护盾触发等
|
||
}
|
||
|
||
|
||
|
||
/** 调试日志(已禁用) */
|
||
to_console(value:any, value2:any=null, value3:any=null){
|
||
// 调试用,生产环境已禁用
|
||
}
|
||
|
||
|
||
playSkillEffect(skill_id:number) {
|
||
let skill = SkillSet[skill_id]
|
||
switch(skill.act){
|
||
case "max":
|
||
this.as.max()
|
||
this.tooltip(TooltipTypes.skill, skill.name, skill_id)
|
||
break
|
||
case "atk":
|
||
this.as.atk()
|
||
break
|
||
}
|
||
}
|
||
|
||
|
||
/** 显示伤害数字 */
|
||
|
||
showDamage(damage: number, isCrit: boolean,DAnm:number) {
|
||
let anm=EAnmConf[DAnm].path // DAnm和EAnm共用设定数组
|
||
this.damageQueue.push({
|
||
damage,
|
||
isCrit,
|
||
delay: this.damageInterval,
|
||
anm
|
||
});
|
||
}
|
||
|
||
/** 处理伤害队列 */
|
||
private processDamageQueue() {
|
||
if (this.isProcessingDamage || this.damageQueue.length === 0) return;
|
||
|
||
this.isProcessingDamage = true;
|
||
const damageInfo = this.damageQueue.shift()!;
|
||
|
||
this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit,damageInfo.anm);
|
||
|
||
// 设置延时处理下一个伤
|
||
this.scheduleOnce(() => {
|
||
this.isProcessingDamage = false;
|
||
}, this.damageInterval);
|
||
}
|
||
|
||
/** 立即显示伤害效果 */
|
||
private showDamageImmediate(damage: number, isCrit: boolean, anm:string="atked") {
|
||
if (!this.model) return;
|
||
|
||
this.hp_show(this.model.hp, this.model.Attrs[Attrs.HP_MAX]);
|
||
this.in_atked(anm, this.model.fac==FacSet.HERO?1:-1);
|
||
if (isCrit) {
|
||
this.hp_tip(TooltipTypes.crit, damage.toFixed(0), damage);
|
||
} else {
|
||
this.hp_tip(TooltipTypes.life, damage.toFixed(0), damage);
|
||
}
|
||
}
|
||
reset() {
|
||
// 清理碰撞器事件监听
|
||
const collider = this.getComponent(Collider2D);
|
||
if (collider) {
|
||
collider.off(Contact2DType.BEGIN_CONTACT);
|
||
}
|
||
this.deadCD=0
|
||
this.hpBarShowCD=0
|
||
|
||
// 清理伤害队列
|
||
this.damageQueue.length = 0;
|
||
this.isProcessingDamage = false;
|
||
|
||
// 延迟销毁节点
|
||
this.scheduleOnce(() => {
|
||
this.node.destroy();
|
||
}, 0.1);
|
||
}
|
||
|
||
}
|