将间隔效果的处理逻辑从 HeroAttrsComp 中分离,改为由 HeroBuffSystem 统一收集并应用效果,同时触发 HeroViewComp 中的视觉反馈。这提高了关注点分离,使属性计算与视图更新解耦,便于维护和扩展新的间隔效果类型。
594 lines
20 KiB
TypeScript
594 lines
20 KiB
TypeScript
import { Vec3, _decorator , v3,Collider2D,Contact2DType,Label ,Node,Prefab,instantiate,ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, 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 { mLogger } from "../common/Logger";
|
||
import { HeroSpine } from "./HeroSpine";
|
||
import { BoxSet, FacSet, FightSet } 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 { 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";
|
||
import { Attrs } from "../common/config/HeroAttrs";
|
||
|
||
|
||
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 {
|
||
@property({ tooltip: "是否启用调试日志" })
|
||
private debugMode: boolean = false; // 是否启用调试模式
|
||
|
||
// ==================== View 层属性(表现相关)====================
|
||
as: HeroSpine = null!
|
||
status:String = "idle"
|
||
scale: number = 1; // 显示方向
|
||
box_group:number = BoxSet.HERO; // 碰撞组
|
||
realDeadTime:number=2
|
||
deadCD:number=0
|
||
monDeadTime:number=0.5
|
||
// 血条显示相关
|
||
lastBarUpdateTime:number = 0; // 最后一次血条/蓝条/护盾更新时间
|
||
// ==================== UI 节点引用 ====================
|
||
private top_node: Node = null!;
|
||
|
||
// ==================== 直接访问 HeroAttrsComp ====================
|
||
get model() {
|
||
// 🔥 修复:添加安全检查,防止ent为null时的访问异常
|
||
if (!this.ent) {
|
||
mLogger.warn(this.debugMode, 'HeroViewComp', "[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);
|
||
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.init();
|
||
}
|
||
|
||
/** 初始化/重置视图状态 */
|
||
init() {
|
||
this.status = "idle";
|
||
this.deadCD = 0;
|
||
this.lastBarUpdateTime = 0;
|
||
this.as.idle()
|
||
|
||
// 初始化 UI 节点
|
||
this.initUINodes();
|
||
|
||
/** 方向 */
|
||
this.node.setScale(this.scale*Math.abs(this.node.scale.x), 1*this.node.scale.y); // 确保 scale.x 为正后再乘方向
|
||
this.top_node.setScale(this.scale*this.top_node.scale.x,1*this.top_node.scale.y);
|
||
|
||
/* 显示角色血*/
|
||
this.top_node.getChildByName("hp").active = true;
|
||
|
||
this.top_node.getChildByName("mp").active = true
|
||
|
||
this.top_node.getChildByName("shield").active = false;
|
||
// 初始隐藏血条(有更新时才显示)
|
||
this.top_node.active = false;
|
||
|
||
// 🔥 重置血条 UI 显示状态
|
||
if (this.model) {
|
||
this.hp_show();
|
||
}
|
||
}
|
||
|
||
/** 初始化 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 ) return;
|
||
if(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
|
||
} ;
|
||
|
||
// 处理血条显示计时(2秒无更新则隐藏)
|
||
if (this.lastBarUpdateTime > 0) {
|
||
const timeSinceLastUpdate = Date.now() / 1000 - this.lastBarUpdateTime;
|
||
if (timeSinceLastUpdate >= 2) {
|
||
this.top_node.active = false;
|
||
this.lastBarUpdateTime = 0;
|
||
}
|
||
}
|
||
|
||
// ✅ View 层职责:处理表现相关的逻辑
|
||
this.processDamageQueue(); // 伤害数字显示队列
|
||
|
||
// ✅ 按需更新 UI(脏标签模式)- 只在属性变化时更新
|
||
if (this.model.dirty_hp) {
|
||
this.hp_show();
|
||
this.model.dirty_hp = false;
|
||
}
|
||
|
||
|
||
if (this.model.dirty_shield) {
|
||
this.show_shield(this.model.shield, this.model.shield_max);
|
||
this.model.dirty_shield = false;
|
||
}
|
||
}
|
||
|
||
|
||
/** 显示护盾 */
|
||
private show_shield(shield: number = 0, shield_max: number = 0) {
|
||
this.lastBarUpdateTime = Date.now() / 1000;
|
||
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() {
|
||
this.lastBarUpdateTime = Date.now() / 1000;
|
||
// 不再基于血量是否满来决定显示状态,只更新进度条
|
||
let hp=this.model.hp;
|
||
let hp_max=this.model.hp_max;
|
||
// mLogger.log(this.debugMode, 'HeroViewComp', "hp_show",hp,hp_max)
|
||
|
||
let targetProgress = hp / hp_max;
|
||
let hpNode = this.top_node.getChildByName("hp");
|
||
let hpProgressBar = hpNode.getComponent(ProgressBar);
|
||
let hpbProgressBar = hpNode.getChildByName("hpb").getComponent(ProgressBar);
|
||
|
||
if (targetProgress < hpProgressBar.progress) {
|
||
// 扣血:先扣血(hp),再跟(hpb)
|
||
hpProgressBar.progress = targetProgress;
|
||
this.scheduleOnce(() => {
|
||
if(hpbProgressBar && hpbProgressBar.isValid) hpbProgressBar.progress = targetProgress;
|
||
}, 0.15);
|
||
} else {
|
||
// 加血:先加底(hpb),再加血(hp)
|
||
hpbProgressBar.progress = targetProgress;
|
||
this.scheduleOnce(() => {
|
||
if(hpProgressBar && hpProgressBar.isValid) hpProgressBar.progress = targetProgress;
|
||
}, 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) {
|
||
this.as.do_atked()
|
||
// 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+50, 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 = 50) {
|
||
let pos = v3(0, 60);
|
||
pos.y = pos.y + y;
|
||
Tooltip.load(pos, type, value, s_uuid, this.node);
|
||
}
|
||
|
||
/** 血量提示(伤害数字) */
|
||
private hp_tip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 0) {
|
||
let x = this.node.position.x;
|
||
// 获取怪物高度的一半,定位到中心点
|
||
let halfHeight = 0;
|
||
const transform = this.node.getComponent(UITransform);
|
||
if (transform) {
|
||
halfHeight = transform.height / 2;
|
||
}
|
||
|
||
// 起点设为怪物中心位置 + 20偏移
|
||
let ny = this.node.position.y + halfHeight + 20;
|
||
let pos = v3(x, ny, 0);
|
||
Tooltip.load(pos, type, value, s_uuid, this.node.parent);
|
||
}
|
||
|
||
/** 护盾吸收提示 */
|
||
shield_tip(absorbed: number) {
|
||
this.hp_tip(TooltipTypes.life, absorbed.toFixed(0));
|
||
}
|
||
|
||
/** 治疗特效 */
|
||
private heathed() {
|
||
var path = "game/skill/buff/heathed";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.parent = this.node;
|
||
}
|
||
private deaded(){
|
||
var path = "game/skill/end/atked";
|
||
var prefab: Prefab = oops.res.get(path, Prefab)!;
|
||
var node = instantiate(prefab);
|
||
node.parent = this.node;
|
||
}
|
||
// 注意:BaseUp 逻辑已移到 HeroAttrSystem.update()
|
||
// 注意:updateTemporaryBuffsDebuffs 逻辑已移到 HeroAttrSystem.update()
|
||
|
||
get isActive() {
|
||
return this.ent.has(HeroViewComp) && this.node?.isValid;
|
||
}
|
||
/** 状态切换(动画) */
|
||
status_change(type:string){
|
||
if(this.status === type) return;
|
||
this.status = type;
|
||
if(this.model.is_dead || this.model.is_reviving) 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.shield_max);
|
||
}
|
||
|
||
health(hp: number = 0) {
|
||
// ✅ 仅显示特效和提示,不调用 hp_show()
|
||
if(hp<=99) return;
|
||
this.heathed();
|
||
this.hp_tip(TooltipTypes.health, hp.toFixed(0));
|
||
this.top_node.active = true;
|
||
this.lastBarUpdateTime = Date.now() / 1000;
|
||
}
|
||
|
||
mp_add(mp: number = 0) {
|
||
// ✅ 仅显示提示,不调用 mp_show()
|
||
this.hp_tip(TooltipTypes.addmp, mp.toFixed(0));
|
||
this.top_node.active = true;
|
||
this.lastBarUpdateTime = Date.now() / 1000;
|
||
}
|
||
|
||
playIntervalEffect(attr: Attrs, value: number, s_uuid: number) {
|
||
if (!this.node || !this.node.isValid) return;
|
||
this.top_node.active = true;
|
||
this.lastBarUpdateTime = Date.now() / 1000;
|
||
if (attr === Attrs.hp) {
|
||
if (value > 0) {
|
||
this.heathed();
|
||
this.hp_tip(TooltipTypes.health, value.toFixed(0), s_uuid);
|
||
} else if (value < 0) {
|
||
this.in_atked("atked", this.model?.fac == FacSet.HERO ? 1 : -1);
|
||
this.hp_tip(TooltipTypes.life, Math.abs(value).toFixed(0), s_uuid);
|
||
}
|
||
return;
|
||
}
|
||
if (attr === Attrs.shield) {
|
||
if (this.model && this.model.shield > 0) {
|
||
this.show_shield(this.model.shield, this.model.shield_max);
|
||
}
|
||
this.hp_tip(TooltipTypes.health, Math.abs(value).toFixed(0), s_uuid);
|
||
return;
|
||
}
|
||
if (attr === Attrs.IN_FROST && value > 0) {
|
||
this.in_iced(0.3);
|
||
return;
|
||
}
|
||
if (attr === Attrs.IN_STUN && value > 0) {
|
||
this.in_yun(0.3);
|
||
}
|
||
}
|
||
|
||
alive(){
|
||
// 重置复活标记 - 必须最先重置,否则status_change会被拦截
|
||
this.model.is_reviving = false;
|
||
|
||
this.model.is_dead=false
|
||
this.model.is_count_dead=false
|
||
this.as.do_buff();
|
||
this.status_change("idle");
|
||
this.model.hp =this.model.hp_max*50/100;
|
||
this.top_node.active=false
|
||
this.lastBarUpdateTime=0
|
||
|
||
// 恢复怪物行动
|
||
if (this.model.is_master) {
|
||
smc.mission.stop_mon_action = false;
|
||
mLogger.log(this.debugMode, 'HeroViewComp', "[HeroViewComp] Hero revived, resuming monster action");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调度复活逻辑
|
||
* @param delay 延迟时间(秒)
|
||
*/
|
||
scheduleRevive(delay: number) {
|
||
this.scheduleOnce(() => {
|
||
this.alive();
|
||
}, delay);
|
||
}
|
||
|
||
/**
|
||
* 死亡视图表现
|
||
* 由 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
|
||
|
||
// 怪物使用0.5秒死亡时间,英雄使用realDeadTime
|
||
if(this.model.fac === FacSet.MON){
|
||
this.realDeadTime = this.monDeadTime;
|
||
}
|
||
|
||
// 播放死亡特效
|
||
this.deaded();
|
||
this.as.dead();
|
||
|
||
|
||
}
|
||
realDead(){
|
||
// 🔥 修复:添加model安全检查,防止实体销毁过程中的空指针异常
|
||
if (!this.model) {
|
||
mLogger.warn(this.debugMode, 'HeroViewComp', "[HeroViewComp] realDead called but model is null, skipping");
|
||
return;
|
||
}
|
||
if(this.model.fac === FacSet.HERO){
|
||
// 英雄死亡:延迟触发死亡事件
|
||
// 🔥 只有主角死亡才触发游戏结束判定
|
||
if (this.model.is_master) return
|
||
}
|
||
// 🔥 方案B:治理性措施 - 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞
|
||
const collider = this.getComponent(Collider2D);
|
||
if (collider) {
|
||
collider.enabled = false;
|
||
}
|
||
|
||
|
||
// 根据阵营触发不同事件
|
||
if(this.model.fac === FacSet.MON){
|
||
oops.message.dispatchEvent(GameEvent.MonDead, {
|
||
uuid: this.model.hero_uuid,
|
||
lv: this.model.lv,
|
||
is_boss: this.model.is_boss,
|
||
is_elite: this.model.is_big_boss, // 暂时映射 is_big_boss 为 elite,或者由 MissionComp 二次判断
|
||
position: this.node.position
|
||
});
|
||
}
|
||
this.ent.destroy();
|
||
|
||
}
|
||
do_atked(damage:number,isCrit:boolean,s_uuid:number,isBack:boolean=false){
|
||
// 受到攻击时显示血条,并更新最后更新时间
|
||
this.top_node.active = true;
|
||
this.lastBarUpdateTime = Date.now() / 1000;
|
||
|
||
if (damage <= 0) return;
|
||
|
||
// 视图层表现
|
||
let SConf=SkillSet[s_uuid]
|
||
if (isBack) this.back()
|
||
this.showDamage(damage, isCrit, SConf.DAnm);
|
||
}
|
||
|
||
private isBackingUp: boolean = false; // 🔥 添加后退状态标记
|
||
|
||
//后退
|
||
back(){
|
||
// 🔥 防止重复调用后退动画
|
||
if (this.isBackingUp) return;
|
||
this.isBackingUp = true; // 🔥 设置后退状态
|
||
|
||
if(this.model.fac==FacSet.MON) {
|
||
let tx=this.node.position.x+FightSet.BACK_RANG
|
||
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-5
|
||
if(tx < -320) tx=-320
|
||
tween(this.node)
|
||
.to(0.1, { position:v3(tx,this.node.position.y,0)})
|
||
.call(() => {
|
||
this.isBackingUp = false; // 🔥 动画完成后重置状态
|
||
})
|
||
.start()
|
||
}
|
||
}
|
||
// 伤害计算和战斗逻辑已迁移到 HeroBattleSystem
|
||
|
||
|
||
playSkillEffect(skill_id:number) {
|
||
let skill = SkillSet[skill_id]
|
||
mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] skill_id'+skill_id,skill)
|
||
if (!skill) return;
|
||
switch(skill.act){
|
||
case "max":
|
||
this.as.max()
|
||
break
|
||
case "atk":
|
||
this.as.atk()
|
||
break
|
||
case "buff":
|
||
this.as.buff()
|
||
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.in_atked(anm, this.model.fac==FacSet.HERO?1:-1);
|
||
if (isCrit) {
|
||
this.hp_tip(TooltipTypes.crit, damage.toFixed(0));
|
||
} else {
|
||
this.hp_tip(TooltipTypes.life, damage.toFixed(0));
|
||
}
|
||
}
|
||
reset() {
|
||
// 清理残留的定时器和缓动
|
||
this.unscheduleAllCallbacks();
|
||
Tween.stopAllByTarget(this.node);
|
||
|
||
// 清理碰撞器事件监听
|
||
const collider = this.getComponent(Collider2D);
|
||
if (collider) {
|
||
collider.off(Contact2DType.BEGIN_CONTACT);
|
||
}
|
||
this.deadCD=0
|
||
this.lastBarUpdateTime=0
|
||
|
||
// 清理伤害队列
|
||
this.damageQueue.length = 0;
|
||
this.isProcessingDamage = false;
|
||
|
||
// 节点生命周期由 Monster 对象池管理,此处不再销毁
|
||
// if (this.node && this.node.isValid) {
|
||
// this.node.destroy();
|
||
// }
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|