Files
pixelheros/assets/script/game/hero/HeroViewComp.ts
pan 7e94560e95 fix(hero&shield): 调整护盾UI位置与护盾特效逻辑
调整了护盾预制体的y轴偏移位置,修复护盾状态更新时的特效显示逻辑:护盾耗尽时隐藏特效,护盾存在时显示常驻特效
2026-09-04 09:10:04 +08:00

554 lines
19 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, Node, Prefab, instantiate, tween, Tween, 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 { HeroTopBarComp } from "./HeroTopBarComp";
import { BuffComp } from "./BuffComp";
import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet";
import { smc } from "../common/SingletonModuleComp";
import { SkillSet, } from "../common/config/SkillSet";
import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { Tooltip } from "../skill/Tooltip";
import { timedCom } from "../skill/timedCom";
import { oneCom } from "../skill/oncend";
import { FlashSprite } from "./hit-flash-white/scripts/FlashSprite";
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 = ""
scale: number = 1; // 显示方向
box_group: number = BoxSet.HERO; // 碰撞组
realDeadTime: number = 0.1
deadCD: number = 0
monDeadTime: number = 0.1
// 血条显示相关
lastBarUpdateTime: number = 0; // 最后一次血条/蓝条/护盾更新时间
// ==================== UI 节点引用 ====================
private topBar: HeroTopBarComp = null!;
/** 是否正在播放大招动画(用于渲染层级置顶,由 HeroAnmComp 播放结束时清除) */
public isCastingMax: boolean = 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,
debuffIcon: string,
}> = [];
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 = "";
this.deadCD = 0;
this.lastBarUpdateTime = 0;
this.status_change("idle");
// 初始化头顶状态栏
const topNode = this.node.getChildByName("top");
this.topBar = topNode.getComponent(HeroTopBarComp) || topNode.addComponent(HeroTopBarComp);
this.topBar.init(this.model, this.scale);
/** 方向 */
this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向
// 🔥 重置描边
if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) {
flashSprite = this.node.getComponentInChildren(FlashSprite);
}
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
}
}
/**
* 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;
this.processDamageQueue();
if (this.model.is_dead) {
this.deadCD += dt
if (this.deadCD >= this.realDeadTime) {
this.deadCD = 0
this.realDead()
}
return
};
// ✅ 按需更新 UI(脏标签模式)- 只在属性变化时更新
if (this.model.dirty_hp) {
this.topBar.hpShow();
this.model.dirty_hp = false;
}
if (this.model.dirty_shield) {
this.topBar.shieldShow(this.model.shield);
if (this.model.shield <= 0) {
// 护盾耗尽时隐藏护盾特效
this.hideShieldFx();
} else {
// 护盾存在时显示常驻特效(内部有 isValid 防重复,不会重复创建)
this.showShieldFx();
}
this.model.dirty_shield = false;
}
// ✅ buff 变更脏标记:刷新 debuff 图标显隐(灼烧等)
const buffComp = this.ent.has(BuffComp) ? this.ent.get(BuffComp) : null;
if (buffComp && buffComp.dirty_buffs) {
this.topBar.debuffShow(buffComp);
buffComp.dirty_buffs = false;
}
}
/** 技能 CD 显示:由 SCastSystem 每帧 updateCD 后调用,刷新能量条进度 */
public cd_show() {
this.topBar.cdShow();
}
/** 升级特效 */
public lv_up() {
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
// 升级时同步更新描边
if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) {
flashSprite = this.node.getComponentInChildren(FlashSprite);
}
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
}
}
/** 攻击力提升特效 */
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;
}
/** 冰冻特效 */
in_iced(t: number = 1) {
this.spawnTimedFx("game/skill/buff/iced", this.node, t);
}
/** 击晕特效 */
in_stun(t: number = 1) {
this.spawnTimedFx("game/skill/buff/stun", this.node, t);
}
/** 技能提示 */
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);
}
/** 技能提示 */
public skill_name(value: string = "", s_uuid: number = 1001, triggerType: string = "", y: number = 50) {
let pos = v3(0, 60);
pos.y = pos.y + y;
Tooltip.load(pos, TooltipTypes.skill, value, s_uuid, this.node, 1, this.model?.fac ?? FacSet.MON, triggerType);
}
/** 血量提示(伤害数字) */
private hp_tip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 0, debuffIcon: string = "") {
let x = this.node.position.x;
// 获取怪物高度的一半,定位到中心点
let halfHeight = 0;
const transform = this.node.getComponent(UITransform);
if (transform) {
halfHeight = transform.height / 2;
}
// 起点设为怪物中心偏下位置,使其在血条下方
let ny = this.node.position.y + halfHeight - 15;
let pos = v3(x, ny, 0);
Tooltip.load(pos, type, value, s_uuid, this.node.parent, 1, this.model?.fac ?? FacSet.MON, "", debuffIcon);
}
/** 护盾减免提示 */
shield_tip(absorbed: number) {
this.hp_tip(TooltipTypes.shield, NumberFormatter.formatNumber(Math.max(0, Math.floor(absorbed))));
}
public playBuff(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 playOther(anm: string = "") {
if (anm === "") return;
var path = "game/skill/ready/" + anm;
// 以自身当前位置为基准,挂载到父节点,即使自身节点销毁动画也能继续播放
this.spawnAnimEndFx(path, this.node.parent, this.node.position);
}
public playEnd(anm: string = "") {
if (anm === "") return;
var path = "game/skill/end/" + anm;
this.spawnAnimEndFx(path, this.node, undefined);
}
public playAllTime(anm: string = "") {
if (anm === "") return;
var path = "game/skill/buff/" + anm;
// 常驻特效直接创建节点,不挂载生命周期销毁组件,随父节点(this.node)一起销毁
this.createFxNode(path, this.node, undefined);
}
/** 治疗特效 */
private heathed() {
this.spawnAnimEndFx("game/skill/buff/heathed", this.node, undefined);
}
private deaded() {
this.spawnAnimEndFx("game/skill/end/dead", this.node.parent, this.node.position);
}
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.setPosition(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 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;
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();
}
}
/** 护盾常驻特效节点,避免重复创建 */
private shieldFxNode: Node | null = null;
add_shield(shield: number) {
// 护盾数据更新由 Model 层处理,这里只负责视图表现
if (this.model && this.model.shield > 0) {
this.topBar.shieldShow(this.model.shield);
this.showShieldFx();
}
}
/** 显示护盾常驻特效(动态加载并挂载到 this.node 下) */
private showShieldFx() {
if (this.shieldFxNode && this.shieldFxNode.isValid) return;
this.shieldFxNode = this.createFxNode("game/skill/buff/shielded", this.node, undefined);
}
/** 隐藏护盾常驻特效 */
private hideShieldFx() {
if (this.shieldFxNode && this.shieldFxNode.isValid) {
this.shieldFxNode.destroy();
}
this.shieldFxNode = null;
}
health(hp: number = 0) {
// ✅ 仅显示特效和提示,不调用 hp_show()
if (hp <= 20) return;
this.heathed();
this.hp_tip(TooltipTypes.health, hp.toFixed(0));
this.lastBarUpdateTime = Date.now() / 1000;
}
alive() {
// 重置复活标记 - 必须最先重置,否则status_change会被拦截
this.model.is_reviving = false;
this.model.is_dead = false
this.model.is_count_dead = false
this.deadCD = 0;
this.hideShieldFx();
// 恢复碰撞体
const collider = this.node.getComponent(Collider2D);
if (collider) {
collider.enabled = true;
}
// 恢复UI
this.topBar.show();
this.status_change("idle");
// 【新增】仅英雄阵营派发复活成功事件,供卡牌技能(HeroCall 类型)监听
// 统一在此派发可覆盖两条复活路径:复活技能触发 + 关卡战斗准备阶段恢复
if (this.model && this.model.fac === FacSet.HERO && this.ent) {
oops.message.dispatchEvent(GameEvent.ReviveSuccess, { eid: this.ent.eid });
}
}
/**
* 死亡视图表现
* 由 HeroAtkSystem 调用,只负责视觉效果和事件通知
*/
do_dead() {
// 添加安全检查
if (!this.model) return;
this.isCastingMax = false;
// 防止重复触发
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;
}
// 如果角色带有死亡触发技能,则由 SCastSystem 播放 playReady("dead")
if (this.model && this.model.runtime_dead && this.model.runtime_dead.length > 0) {
// SCastSystem will handle the "dead" ready animation
} else {
// 播放默认死亡特效
this.deaded();
}
}
realDead() {
// 🔥 修复:添加model安全检查,防止实体销毁过程中的空指针异常
if (!this.model) {
mLogger.warn(this.debugMode, 'HeroViewComp', "[HeroViewComp] realDead called but model is null, skipping");
return;
}
// 隐藏UI
this.topBar.hide();
// 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞
const collider = this.node.getComponent(Collider2D);
if (collider) {
collider.enabled = false;
}
// 根据阵营决定飞出方向:英雄向左(负),怪物向右(正)
let isHero = this.model.fac === FacSet.HERO;
let dirX = isHero ? -800 : 800;
// 死亡往后飞出屏幕动画,结束后统一销毁实体
// 英雄为真实死亡:快照已在 doDead 阶段写入 smc.mission.dead_heroes,此处直接销毁
tween(this.node)
.by(0.5, { position: v3(dirX, 700, 0) }, { easing: "quadOut" })
.call(() => {
this.ent.destroy();
})
.start();
}
/** 受击表现(伤害数字、受击动画、击退) */
do_atked(damage: number, isCrit: boolean, s_uuid: number, knockbackExtra: number = 0) {
// 受到攻击时更新最后更新时间
this.topBar.activateTopBar();
this.lastBarUpdateTime = Date.now() / 1000;
if (damage <= 0) return;
// 播放受击音效
if (isCrit) {
oops.audio.playEffect("music/Critical");
} else {
oops.audio.playEffect("music/Hit");
}
// 视图层表现
let SConf = SkillSet[s_uuid]
const hitAnm = SConf?.DAnm || "atked";
this.back(knockbackExtra)
this.in_atked(hitAnm, this.model.fac == FacSet.HERO ? 1 : -1);
this.showDamage(damage, isCrit);
}
//后退
back(distance: number = 0) {
// 击退位移由 MoveSystem 统一处理,视图层只负责触发,不直接操作 position
// 具体击退速度由 HeroAtkSystem 在伤害结算后写入 MoveComp.knockbackVelocity
}
// 伤害计算和战斗逻辑已迁移到 HeroBattleSystem
playSkillAnm(act: string = "") {
mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] act' + act,)
if (act === "") return;
switch (act) {
case "max":
this.isCastingMax = true;
this.as.max()
break
case "atk":
this.as.atk()
break
case "buff":
this.as.buff()
break
}
}
/** 显示伤害数字 */
showDamage(damage: number, isCrit: boolean, debuffIcon: string = "") {
this.damageQueue.push({
damage,
isCrit,
debuffIcon,
});
}
/** 处理伤害队列 */
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.debuffIcon);
// 设置延时处理下一个伤
this.scheduleOnce(() => {
this.isProcessingDamage = false;
}, this.damageInterval);
}
/** 立即显示伤害效果 */
private showDamageImmediate(damage: number, isCrit: boolean, debuffIcon: string = "") {
if (!this.model) return;
const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage)));
this.topBar.hpShow();
if (isCrit) {
this.hp_tip(TooltipTypes.crit, damageText);
} else {
this.hp_tip(TooltipTypes.life, damageText, 1001, 0, debuffIcon);
}
}
reset() {
this.isCastingMax = false;
this.hideShieldFx();
// 清理残留的定时器和缓动
this.unscheduleAllCallbacks();
Tween.stopAllByTarget(this.node);
this.topBar.reset();
// 清理碰撞器事件监听
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();
// }
}
}