Files
pixelheros/assets/script/game/hero/HeroViewComp.ts
panw 33e549d22c fix: 修复技能冷却时间初始化和治疗特效调用错误
- 将技能初始化时的冷却时间设为0,避免首次使用时需要等待完整冷却
- 修正HeroViewComp中health方法错误调用heathed属性而非方法的问题
2026-03-24 14:29:54 +08:00

584 lines
20 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, NumberFormatter, TooltipTypes } from "../common/config/GameSet";
import { smc } from "../common/SingletonModuleComp";
import { SkillSet,} from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { Tooltip } from "../skill/Tooltip";
import { timedCom } from "../skill/timedCom";
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.getChildByName("lv").active = this.model.fac==FacSet.HERO;
this.top_node.getChildByName("lv").getChildByName("lv2").active=this.model.lv >= 2
this.top_node.getChildByName("lv").getChildByName("lv3").active=this.model.lv >= 3
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;
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.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.getDisplaySkillCdProgress();
}
/** 显示护盾 */
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;
}
/** 冰冻特效 */
in_iced(t: number = 1) {
this.spawnTimedFx("game/skill/buff/iced", 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);
}
/** 血量提示(伤害数字) */
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, 1, this.model?.fac ?? FacSet.MON);
}
/** 护盾吸收提示 */
shield_tip(absorbed: number) {
this.hp_tip(TooltipTypes.life, NumberFormatter.formatNumber(Math.max(0, Math.floor(absorbed))));
}
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 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();
}
}
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<=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.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 = SConf?.DAnm|| "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;
const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage)));
this.hp_show();
if (isCrit) {
this.hp_tip(TooltipTypes.crit, damageText);
} else {
this.hp_tip(TooltipTypes.life, damageText);
}
}
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();
// }
}
}