Files
pixelheros/assets/script/game/hero/HeroViewComp.ts
panw a2e3dd4924 fix: 修复BOSS技能配置错误并优化血条震动逻辑
- 将BOSS(兽人首领)的技能从[6001,6003]更正为[6002,6004],以匹配设计意图
- 重构血条震动逻辑,将震动目标从hp子节点改为顶层top节点,提升稳定性
- 在组件销毁时增加对top节点缓动的清理,避免残留动画
2026-03-19 15:22:59 +08:00

631 lines
21 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 { Vec3, _decorator , v3,Collider2D,Contact2DType,Label ,Node,Prefab,instantiate,ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Tween, Color, BoxCollider2D, UITransform, UIOpacity} 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 { TooltipTypes } from "../common/config/GameSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { Tooltip } from "../skill/Tooltip";
import { timedCom } from "../skill/timedCom";
import { Attrs } from "../common/config/HeroAttrs";
import { oneCom } from "../skill/oncend";
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!;
private topOpacity: UIOpacity = null!;
private topBasePos: Vec3 = v3();
private readonly barIdleOpacity: number = 153;
private readonly barActiveOpacity: number = 255;
private readonly idleOpacityDelay: number = 0.25;
private readonly restoreBarIdleOpacity = () => {
this.setTopBarOpacity(false);
};
// ==================== 直接访问 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,
}> = [];
private isProcessingDamage: boolean = false;
private damageInterval: number = 0.01; // 伤害数字显示间隔
private effectLifeTime: number = 0.8;
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("cd").active = true
this.top_node.getChildByName("shield").active = false;
this.top_node.active = true;
this.setTopBarOpacity(false);
// 🔥 重置血条 UI 显示状态
if (this.model) {
this.hp_show();
}
}
/** 初始化 UI 节点引用 */
private initUINodes() {
this.top_node = this.node.getChildByName("top");
this.topOpacity = this.top_node.getComponent(UIOpacity) || this.top_node.addComponent(UIOpacity);
this.topBasePos = this.top_node.position.clone();
const hpNode = this.top_node.getChildByName("hp");
if(this.model.fac==FacSet.HERO){
hpNode.getChildByName("Bar").getComponent(Sprite).color=new Color("#2ECC71")
}
// 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
} ;
// ✅ 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;
}
}
public cd_show(){
this.top_node.getChildByName("cd").getComponent(ProgressBar).progress = this.model.s_cd/this.model.s_cd_max;
}
/** 显示护盾 */
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;
}
/** 显示血量 */
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_max > 0 ? hp / hp_max : 0;
targetProgress = clamp(targetProgress, 0, 1);
let hpNode = this.top_node.getChildByName("hp");
let hpProgressBar = hpNode.getComponent(ProgressBar);
const prevProgress = hpProgressBar.progress;
if (targetProgress < hpProgressBar.progress) {
this.activateTopBar();
this.playHpBarShake();
}
hpProgressBar.progress = targetProgress;
if (targetProgress > prevProgress) {
this.setTopBarOpacity(false);
}
}
private isFullHp(): boolean {
if (!this.model) return false;
if (this.model.hp_max <= 0) return false;
return this.model.hp >= this.model.hp_max;
}
private setTopBarOpacity(isActive: boolean) {
if (!this.topOpacity || !this.topOpacity.isValid) return;
if (isActive) {
this.topOpacity.opacity = this.barActiveOpacity;
return;
}
this.topOpacity.opacity = this.isFullHp() ? this.barIdleOpacity : this.barActiveOpacity;
}
private activateTopBar() {
this.setTopBarOpacity(true);
this.unschedule(this.restoreBarIdleOpacity);
this.scheduleOnce(this.restoreBarIdleOpacity, this.idleOpacityDelay);
}
private playHpBarShake() {
if (!this.top_node || !this.top_node.isValid) return;
Tween.stopAllByTarget(this.top_node);
this.top_node.setPosition(this.topBasePos);
tween(this.top_node)
.by(0.04, { position: v3(-3, 0, 0) })
.by(0.04, { position: v3(6, 0, 0) })
.by(0.04, { position: v3(-5, 0, 0) })
.by(0.04, { position: v3(2, 0, 0) })
.call(() => {
this.top_node.setPosition(this.topBasePos);
})
.start();
}
/** 升级特效 */
private lv_up() {
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
}
/** 攻击力提升特效 */
private ap_up() {
this.spawnTimedFx("game/skill/buff/buff_apup", this.node, 1.0);
}
/** 受击特效 */
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) {
const node = this.spawnTimedFx("game/skill/buff/iced", this.node, t);
}
/** 眩晕特效 */
private in_yun(t: number = 1, ap: number = 0) {
const node = this.spawnTimedFx("game/skill/buff/buff_yun", this.node, t);
if (!node) return;
let height = this.node.getComponent(UITransform).height;
node.setPosition(v3(0, height));
}
/** 技能提示 */
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));
}
public palayBuff(anm: string = ""){
if(anm==="") return;
var path = "game/skill/buff/" + anm;
this.spawnTimedFx(path, this.node, this.effectLifeTime);
}
public playReady(anm: string = ""){
if(anm==="") return;
var path = "game/skill/ready/" + anm;
this.spawnAnimEndFx(path, this.node, undefined);
}
public playEnd(anm: string = ""){
if(anm==="") return;
var path = "game/skill/end/" + anm;
this.spawnAnimEndFx(path, this.node, undefined);
}
/** 治疗特效 */
private heathed() {
this.spawnAnimEndFx("game/skill/buff/heathed", this.node, undefined);
}
private deaded(){
this.spawnAnimEndFx("game/skill/end/atked", this.node, undefined);
}
private createFxNode(path: string, parent: Node | null, worldPos?: Vec3): Node | null {
if (!parent || !parent.isValid) return null;
const prefab: Prefab = oops.res.get(path, Prefab)!;
if (!prefab) return null;
const node = instantiate(prefab);
if (!node || !node.isValid) return null;
node.parent = parent;
if (worldPos) {
node.setWorldPosition(worldPos);
}
return node;
}
private spawnTimedFx(path: string, parent: Node | null, life: number = 0.8, worldPos?: Vec3): Node | null {
const node = this.createFxNode(path, parent, worldPos);
if (!node) return null;
const ender = node.getComponent(oneCom);
if (ender) ender.destroy();
const timer = node.getComponent(timedCom) || node.addComponent(timedCom);
timer.time = Math.max(0.2, life);
return node;
}
private spawnAnimEndFx(path: string, parent: Node | null, worldPos?: Vec3): Node | null {
const node = this.createFxNode(path, parent, worldPos);
if (!node) return null;
const timer = node.getComponent(timedCom);
if (timer) timer.destroy();
node.getComponent(oneCom) || node.addComponent(oneCom);
return 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.lastBarUpdateTime = Date.now() / 1000;
}
mp_add(mp: number = 0) {
// ✅ 仅显示提示,不调用 mp_show()
this.hp_tip(TooltipTypes.addmp, mp.toFixed(0));
this.lastBarUpdateTime = Date.now() / 1000;
}
playIntervalEffect(attr: Attrs, value: number, s_uuid: number) {
if (!this.node || !this.node.isValid) return;
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.activateTopBar();
this.playHpBarShake();
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=true
this.setTopBarOpacity(false);
this.lastBarUpdateTime=0
}
/**
* 调度复活逻辑
* @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; // 防止重复触发,必须存在防止重复调用
// 怪物使用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.MON){
}
if(this.model.fac === FacSet.HERO){
}
// 🔥 方案B治理性措施 - 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞
const collider = this.getComponent(Collider2D);
if (collider) {
collider.enabled = false;
}
this.ent.destroy();
}
do_atked(damage:number,isCrit:boolean,s_uuid:number,isBack:boolean=false){
// 受到攻击时更新最后更新时间
this.activateTopBar();
this.lastBarUpdateTime = Date.now() / 1000;
if (damage <= 0) return;
// 视图层表现
let SConf=SkillSet[s_uuid]
const hitAnm = EAnmConf[SConf?.DAnm]?.path || "atked";
if (isBack) this.back()
this.in_atked(hitAnm, this.model.fac==FacSet.HERO?1:-1);
this.showDamage(damage, isCrit);
}
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
playSkillAnm(act:string="") {
mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] act'+act,)
if (act==="") return;
switch(act){
case "max":
this.as.max()
break
case "atk":
this.as.atk()
break
case "buff":
this.as.buff()
break
}
}
/** 显示伤害数字 */
showDamage(damage: number, isCrit: boolean) {
this.damageQueue.push({
damage,
isCrit,
});
}
/** 处理伤害队列 */
private processDamageQueue() {
if (this.isProcessingDamage || this.damageQueue.length === 0) return;
this.isProcessingDamage = true;
const damageInfo = this.damageQueue.shift()!;
this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit);
// 设置延时处理下一个伤
this.scheduleOnce(() => {
this.isProcessingDamage = false;
}, this.damageInterval);
}
/** 立即显示伤害效果 */
private showDamageImmediate(damage: number, isCrit: boolean) {
if (!this.model) return;
this.hp_show();
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);
if (this.top_node && this.top_node.isValid) {
Tween.stopAllByTarget(this.top_node);
this.top_node.setPosition(this.topBasePos);
}
// 清理碰撞器事件监听
const collider = this.getComponent(Collider2D);
if (collider) {
collider.off(Contact2DType.BEGIN_CONTACT);
}
this.deadCD=0
this.lastBarUpdateTime=0
this.unschedule(this.restoreBarIdleOpacity);
// 清理伤害队列
this.damageQueue.length = 0;
this.isProcessingDamage = false;
// 节点生命周期由 Monster 对象池管理,此处不再销毁
// if (this.node && this.node.isValid) {
// this.node.destroy();
// }
}
}