1. 新增技能飞行距离配置项,支持自定义直线技能飞行距离 2. 调整英雄后退击退范围从30改为20 3. 重构击退逻辑,统一敌我双方的边界钳制处理 4. 优化Boss数值配置,固定基础面板为初始英雄2倍强度 5. 修正技能6006的动画资源引用为atk1 6. 调整近战英雄默认停火距离从80改为60 7. 更新注释文档与配置注释,优化代码可读性
527 lines
18 KiB
TypeScript
527 lines
18 KiB
TypeScript
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 { 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!;
|
||
|
||
// ==================== 直接访问 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 = "";
|
||
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);
|
||
this.model.dirty_shield = false;
|
||
}
|
||
}
|
||
|
||
public cd_show() {
|
||
return;
|
||
}
|
||
|
||
/** 升级特效 */
|
||
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) {
|
||
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);
|
||
}
|
||
|
||
/** 护盾减免提示 */
|
||
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();
|
||
}
|
||
}
|
||
add_shield(shield: number) {
|
||
// 护盾数据更新由 Model 层处理,这里只负责视图表现
|
||
if (this.model && this.model.shield > 0) this.topBar.shieldShow(this.model.shield);
|
||
}
|
||
|
||
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;
|
||
|
||
// 恢复碰撞体
|
||
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;
|
||
|
||
// 防止重复触发
|
||
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);
|
||
}
|
||
|
||
private isBackingUp: boolean = false; // 🔥 添加后退状态标记
|
||
|
||
//后退
|
||
back(distance: number = 0) {
|
||
// 🔥 防止重复调用后退动画
|
||
if (this.isBackingUp) return;
|
||
this.isBackingUp = true; // 🔥 设置后退状态
|
||
|
||
// 基础击退距离 + 额外距离强化,双方阵营统一
|
||
const dist = FightSet.BACK_RANG + distance;
|
||
const isHero = this.model.fac == FacSet.HERO;
|
||
// 边界钳制:HERO 不越过 BoxSet.LETF_END,MON 不越过 BoxSet.RIGHT_END
|
||
const tx = isHero
|
||
? Math.max(BoxSet.LETF_END, this.node.position.x - dist)
|
||
: Math.min(BoxSet.RIGHT_END, this.node.position.x + dist);
|
||
|
||
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.topBar.hpShow();
|
||
if (isCrit) {
|
||
this.hp_tip(TooltipTypes.crit, damageText);
|
||
} else {
|
||
this.hp_tip(TooltipTypes.life, damageText);
|
||
}
|
||
}
|
||
reset() {
|
||
// 清理残留的定时器和缓动
|
||
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();
|
||
// }
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|