3 Commits

Author SHA1 Message Date
pan
fb299f6328 feat(missionCard): 实现卡牌使用后自动补卡功能
1. 新增GameEvent.CardUsed事件用于通知卡牌使用完成
2. 在各类卡牌使用逻辑中派发CardUsed事件
3. 为MissionCardComp添加CardUsed事件监听与补卡回调
4. 新增buildSingleHeroCard和buildSingleSkillCard方法用于生成单张补卡
5. 调整技能卡使用弹窗逻辑,改为由外部控制显隐
2026-07-21 16:38:52 +08:00
pan
4f95accfbf style(hero): 调整英雄血条UI参数与显示逻辑
1. 调整hp_height和boss_hp_height参数,优化血条显示高度
2. 修复top_node血条方向随对象池复用导致反转的问题
3. 更新血条预制体的尺寸、样式与贴图配置
2026-07-21 16:38:43 +08:00
pan
5d55f3bdb1 style: 格式化代码并修复代码风格问题
本次提交主要对HeroAttrsComp和HeroViewComp两个文件进行了代码风格统一优化:
1. 统一变量声明的空格规范,修复类型标注前后的空格问题
2. 修正函数参数默认值的空格格式
3. 调整空行和注释的排版一致性
4. 修复字符串类型标注的大小写问题
5. 新增等级变更脏标记和对应的UI显示逻辑
6. 优化了部分条件判断和逻辑代码的可读性
2026-07-21 16:33:52 +08:00
7 changed files with 327 additions and 193 deletions

View File

@@ -859,7 +859,7 @@
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 40, "width": 40,
"height": 7 "height": 11
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
@@ -1037,7 +1037,7 @@
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 140, "width": 140,
"height": 30 "height": 43.333333333333336
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
@@ -1162,7 +1162,7 @@
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 40, "width": 40,
"height": 7 "height": 11
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
@@ -1912,8 +1912,8 @@
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 52, "width": 80,
"height": 75 "height": 100
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
@@ -1943,18 +1943,18 @@
"_dstBlendFactor": 4, "_dstBlendFactor": 4,
"_color": { "_color": {
"__type__": "cc.Color", "__type__": "cc.Color",
"r": 255, "r": 253,
"g": 255, "g": 213,
"b": 255, "b": 0,
"a": 255 "a": 255
}, },
"_spriteFrame": { "_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@5c7a0", "__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@79d6f",
"__expectedType__": "cc.SpriteFrame" "__expectedType__": "cc.SpriteFrame"
}, },
"_type": 0, "_type": 0,
"_fillType": 0, "_fillType": 0,
"_sizeMode": 1, "_sizeMode": 0,
"_fillCenter": { "_fillCenter": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
"x": 0, "x": 0,
@@ -1964,7 +1964,10 @@
"_fillRange": 0, "_fillRange": 0,
"_isTrimmedMode": true, "_isTrimmedMode": true,
"_useGrayscale": false, "_useGrayscale": false,
"_atlas": null, "_atlas": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": "" "_id": ""
}, },
{ {
@@ -2007,8 +2010,8 @@
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": -0.103,
"y": 0, "y": -0.601,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -2048,7 +2051,7 @@
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 4.38, "width": 7.29,
"height": 40 "height": 40
}, },
"_anchorPoint": { "_anchorPoint": {
@@ -2087,8 +2090,8 @@
"_string": "1", "_string": "1",
"_horizontalAlign": 1, "_horizontalAlign": 1,
"_verticalAlign": 1, "_verticalAlign": 1,
"_actualFontSize": 15, "_actualFontSize": 25,
"_fontSize": 15, "_fontSize": 25,
"_fontFamily": "Arial", "_fontFamily": "Arial",
"_lineHeight": 40, "_lineHeight": 40,
"_overflow": 0, "_overflow": 0,
@@ -2100,7 +2103,7 @@
"_isSystemFontUsed": false, "_isSystemFontUsed": false,
"_spacingX": 0, "_spacingX": 0,
"_isItalic": false, "_isItalic": false,
"_isBold": false, "_isBold": true,
"_isUnderline": false, "_isUnderline": false,
"_underlineHeight": 2, "_underlineHeight": 2,
"_cacheMode": 0, "_cacheMode": 0,
@@ -2161,7 +2164,7 @@
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 10, "width": 10,
"height": 10 "height": 13
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",

View File

@@ -22,6 +22,8 @@ export enum GameEvent {
CardRefresh = "CardRefresh", CardRefresh = "CardRefresh",
/** 单张卡牌被点击选中payload 为被点击的 CardComp 实例,用于其他卡牌联动隐藏 call_btn */ /** 单张卡牌被点击选中payload 为被点击的 CardComp 实例,用于其他卡牌联动隐藏 call_btn */
CardSelected = "CardSelected", CardSelected = "CardSelected",
/** 单张卡牌被成功使用payload 为该卡槽组件实例 CardComp / SCardComp用于 MissionCardComp 补卡) */
CardUsed = "CardUsed",
UseHeroCard = "UseHeroCard", UseHeroCard = "UseHeroCard",
UseSkillCard = "UseSkillCard", UseSkillCard = "UseSkillCard",
UseSpecialCard = "UseSpecialCard", UseSpecialCard = "UseSpecialCard",

View File

@@ -13,7 +13,7 @@ export class HeroAttrsComp extends ecs.Comp {
private static readonly percentRateThreshold = 1; private static readonly percentRateThreshold = 1;
private static readonly minAttackCd = 0.05; private static readonly minAttackCd = 0.05;
Ebus:any=null! Ebus: any = null!
// ==================== 角色基础信息 ==================== // ==================== 角色基础信息 ====================
hero_uuid: number = 1001; hero_uuid: number = 1001;
hero_name: string = "hero"; hero_name: string = "hero";
@@ -42,7 +42,7 @@ export class HeroAttrsComp extends ecs.Comp {
[SkillTriggerType.FEnd]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[]; [SkillTriggerType.FEnd]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Atking]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[]; [SkillTriggerType.Atking]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Atked]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[]; [SkillTriggerType.Atked]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Revive]?: {s_uuid: number, r_num: number, upr: number}; [SkillTriggerType.Revive]?: { s_uuid: number, r_num: number, upr: number };
// ==================== 特殊属性 ==================== // ==================== 特殊属性 ====================
critical: number = 0; // 暴击率 critical: number = 0; // 暴击率
@@ -64,20 +64,21 @@ export class HeroAttrsComp extends ecs.Comp {
frost_end_time: number = 0; frost_end_time: number = 0;
stun_end_time: number = 0; stun_end_time: number = 0;
boom: boolean = false; // 自爆怪 boom: boolean = false; // 自爆怪
// ==================== 脏标签标记 ==================== // ==================== 脏标签标记 ====================
dirty_hp: boolean = false; // 血量变更标记 dirty_hp: boolean = false; // 血量变更标记
dirty_shield: boolean = false; // 护盾变更标记 dirty_shield: boolean = false; // 护盾变更标记
dirty_lv: boolean = false; // 等级变更标记
// ==================== 技能距离缓存 ==================== // ==================== 技能距离缓存 ====================
maxSkillDistance: number = 0; // 最远技能攻击距离缓存受MP影响 maxSkillDistance: number = 0; // 最远技能攻击距离缓存受MP影响
minSkillDistance: number = 0; // 最近技能攻击距离缓存不受MP影响用于停止位置判断 minSkillDistance: number = 0; // 最近技能攻击距离缓存不受MP影响用于停止位置判断
// ==================== 阵型位置 ==================== // ==================== 阵型位置 ====================
posIndex: number = -1; posIndex: number = -1;
// ==================== 标记状态 ==================== // ==================== 标记状态 ====================
is_dead: boolean = false; is_dead: boolean = false;
is_count_dead: boolean = false; is_count_dead: boolean = false;
@@ -92,10 +93,10 @@ export class HeroAttrsComp extends ecs.Comp {
// ==================== 计数统计 ==================== // ==================== 计数统计 ====================
atk_count: number = 0; // 攻击次数 atk_count: number = 0; // 攻击次数
atked_count: number = 0; // 被攻击次数 atked_count: number = 0; // 被攻击次数
killed_count:number=0; killed_count: number = 0;
combat_target_eid: number = -1; combat_target_eid: number = -1;
enemy_in_cast_range: boolean = false; enemy_in_cast_range: boolean = false;
start(){ start() {
} }
// ==================== BUFF 系统初始化 ==================== // ==================== BUFF 系统初始化 ====================
/** /**
@@ -108,7 +109,7 @@ export class HeroAttrsComp extends ecs.Comp {
} }
/*******************基础属性管理********************/ /*******************基础属性管理********************/
add_hp(value:number){ add_hp(value: number) {
const oldHp = this.hp; const oldHp = this.hp;
let addValue = value; let addValue = value;
this.hp += addValue; this.hp += addValue;
@@ -119,7 +120,7 @@ export class HeroAttrsComp extends ecs.Comp {
} }
return addValue; return addValue;
} }
add_shield(value:number){ add_shield(value: number) {
const oldShield = this.shield; const oldShield = this.shield;
const addValue = Math.max(0, Math.floor(value)); const addValue = Math.max(0, Math.floor(value));
if (addValue <= 0) return; if (addValue <= 0) return;
@@ -131,15 +132,15 @@ export class HeroAttrsComp extends ecs.Comp {
mLogger.log(this.debugMode, 'HeroAttrs', ` 护盾次数变更: ${this.hero_name}, 变化=${addValue}, ${Math.floor(oldShield)} -> ${Math.floor(this.shield)}`); mLogger.log(this.debugMode, 'HeroAttrs', ` 护盾次数变更: ${this.hero_name}, 变化=${addValue}, ${Math.floor(oldShield)} -> ${Math.floor(this.shield)}`);
} }
} }
add_hp_max(value:number){ add_hp_max(value: number) {
this.hp_max+=value this.hp_max += value
this.hp+=value this.hp += value
this.dirty_hp = true; // ✅ 仅标记需要更新 this.dirty_hp = true; // ✅ 仅标记需要更新
return value return value
} }
add_ap(value:number){ add_ap(value: number) {
this.ap +=value this.ap += value
return value return value
} }
@@ -151,7 +152,7 @@ export class HeroAttrsComp extends ecs.Comp {
add_special_attr(attr_type: Attrs, value: number) { add_special_attr(attr_type: Attrs, value: number) {
// 利用枚举值(字符串)与类属性名一致的特性,动态访问并累加属性 // 利用枚举值(字符串)与类属性名一致的特性,动态访问并累加属性
const key = attr_type as keyof this; const key = attr_type as keyof this;
// 确保目标属性存在且类型为数字,避免运行时错误 // 确保目标属性存在且类型为数字,避免运行时错误
if (typeof this[key] === 'number') { if (typeof this[key] === 'number') {
(this as any)[key] += value; (this as any)[key] += value;
@@ -163,15 +164,15 @@ export class HeroAttrsComp extends ecs.Comp {
} }
toFrost(time: number=1) { toFrost(time: number = 1) {
const frostTime = FightSet.FROST_TIME * time; const frostTime = FightSet.FROST_TIME * time;
this.frost_end_time = Math.max(this.frost_end_time, frostTime); this.frost_end_time = Math.max(this.frost_end_time, frostTime);
} }
toStun(time: number=1) { toStun(time: number = 1) {
const stunTime = FightSet.STUN_TIME * time; const stunTime = FightSet.STUN_TIME * time;
this.stun_end_time = Math.max(this.stun_end_time, stunTime); this.stun_end_time = Math.max(this.stun_end_time, stunTime);
// 击晕时 CD 清零 // 击晕时 CD 清零
for (const key in this.skills) { for (const key in this.skills) {
const skill = this.skills[key]; const skill = this.skills[key];
@@ -180,11 +181,11 @@ export class HeroAttrsComp extends ecs.Comp {
} }
} }
} }
updateCD(dt: number){ updateCD(dt: number) {
// 如果处于冰冻状态,则技能 CD 暂停刷新 // 如果处于冰冻状态,则技能 CD 暂停刷新
if (this.isFrost()) return; if (this.isFrost()) return;
// 如果处于击晕状态,则技能 CD 暂停刷新(且保持清零状态) // 如果处于击晕状态,则技能 CD 暂停刷新(且保持清零状态)
if (this.isStun()) { if (this.isStun()) {
for (const key in this.skills) { for (const key in this.skills) {
@@ -195,7 +196,7 @@ export class HeroAttrsComp extends ecs.Comp {
} }
return; return;
} }
for (const key in this.skills) { for (const key in this.skills) {
const skill = this.skills[key]; const skill = this.skills[key];
if (!skill) continue; if (!skill) continue;
@@ -344,7 +345,7 @@ export class HeroAttrsComp extends ecs.Comp {
this.maxSkillDistance = maxRange; this.maxSkillDistance = maxRange;
this.minSkillDistance = minRange; this.minSkillDistance = minRange;
} }
/** /**
* 获取缓存的最远技能攻击距离 * 获取缓存的最远技能攻击距离
* @returns 最远攻击距离 * @returns 最远攻击距离
@@ -352,7 +353,7 @@ export class HeroAttrsComp extends ecs.Comp {
public getCachedMaxSkillDistance(): number { public getCachedMaxSkillDistance(): number {
return this.maxSkillDistance; return this.maxSkillDistance;
} }
/** /**
* 获取缓存的最近技能攻击距离 * 获取缓存的最近技能攻击距离
* @returns 最近攻击距离 * @returns 最近攻击距离
@@ -376,7 +377,7 @@ export class HeroAttrsComp extends ecs.Comp {
this.speed = 100; this.speed = 100;
this.dis = 100; this.dis = 100;
this.shield = 0; this.shield = 0;
// 重置新增属性 // 重置新增属性
this.skills = {}; this.skills = {};
this.call = undefined; this.call = undefined;
@@ -401,16 +402,16 @@ export class HeroAttrsComp extends ecs.Comp {
this.puncture_chance = 0; this.puncture_chance = 0;
this.wfuny = 0; this.wfuny = 0;
this.boom = false; this.boom = false;
this.frost_end_time = 0; this.frost_end_time = 0;
this.stun_end_time = 0; this.stun_end_time = 0;
// 重置技能距离缓存 // 重置技能距离缓存
this.maxSkillDistance = 0; this.maxSkillDistance = 0;
this.minSkillDistance = 0; this.minSkillDistance = 0;
this.posIndex = -1; this.posIndex = -1;
this.is_dead = false; this.is_dead = false;
this.is_count_dead = false; this.is_count_dead = false;
this.is_atking = false; this.is_atking = false;
@@ -420,37 +421,38 @@ export class HeroAttrsComp extends ecs.Comp {
this.is_friend = false; this.is_friend = false;
this.is_kalami = false; this.is_kalami = false;
this.is_reviving = false; this.is_reviving = false;
this.atk_count = 0; this.atk_count = 0;
this.atked_count = 0; this.atked_count = 0;
this.killed_count =0; this.killed_count = 0;
this.combat_target_eid = -1; this.combat_target_eid = -1;
this.enemy_in_cast_range = false; this.enemy_in_cast_range = false;
// 重置脏标签 // 重置脏标签
this.dirty_hp = false; this.dirty_hp = false;
this.dirty_shield = false; this.dirty_shield = false;
this.dirty_lv = false;
} }
} }
@ecs.register('HeroBuffSystem') @ecs.register('HeroBuffSystem')
export class HeroBuffSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { export class HeroBuffSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
private timer =new Timer(0.1) private timer = new Timer(0.1)
filter(): ecs.IMatcher { filter(): ecs.IMatcher {
return ecs.allOf(HeroAttrsComp); return ecs.allOf(HeroAttrsComp);
} }
update(e: ecs.Entity): void { update(e: ecs.Entity): void {
if(this.timer.update(this.dt)){ if (this.timer.update(this.dt)) {
const attrsComp = e.get(HeroAttrsComp); const attrsComp = e.get(HeroAttrsComp);
if(attrsComp.frost_end_time > 0){ if (attrsComp.frost_end_time > 0) {
attrsComp.frost_end_time -= 0.1; attrsComp.frost_end_time -= 0.1;
if(attrsComp.frost_end_time <= 0){ if (attrsComp.frost_end_time <= 0) {
attrsComp.frost_end_time = 0; attrsComp.frost_end_time = 0;
} }
} }
if(attrsComp.stun_end_time > 0){ if (attrsComp.stun_end_time > 0) {
attrsComp.stun_end_time -= 0.1; attrsComp.stun_end_time -= 0.1;
if(attrsComp.stun_end_time <= 0){ if (attrsComp.stun_end_time <= 0) {
attrsComp.stun_end_time = 0; attrsComp.stun_end_time = 0;
} }
} }
@@ -459,5 +461,5 @@ export class HeroBuffSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
} }
} }

View File

@@ -1,11 +1,11 @@
import { Vec3, _decorator , v3,Collider2D,Contact2DType,Label ,Node,Prefab,instantiate,ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Tween, Color, BoxCollider2D, UITransform, UIOpacity, NodeEventType} from "cc"; import { Vec3, _decorator, v3, Collider2D, Contact2DType, Label, Node, Prefab, instantiate, ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Tween, Color, BoxCollider2D, UITransform, UIOpacity, NodeEventType } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { mLogger } from "../common/Logger"; import { mLogger } from "../common/Logger";
import { HeroSpine } from "./HeroSpine"; import { HeroSpine } from "./HeroSpine";
import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet"; import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet";
import { smc } from "../common/SingletonModuleComp"; import { smc } from "../common/SingletonModuleComp";
import { SkillSet,} from "../common/config/SkillSet"; import { SkillSet, } from "../common/config/SkillSet";
import { HeroInfo } from "../common/config/heroSet"; import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops"; import { oops } from "db://oops-framework/core/Oops";
import { UIID } from "../common/config/GameUIConfig"; import { UIID } from "../common/config/GameUIConfig";
@@ -32,20 +32,21 @@ export class HeroViewComp extends CCComp {
// ==================== View 层属性(表现相关)==================== // ==================== View 层属性(表现相关)====================
as: HeroSpine = null! as: HeroSpine = null!
status:String = "" status: String = ""
scale: number = 1; // 显示方向 scale: number = 1; // 显示方向
box_group:number = BoxSet.HERO; // 碰撞组 box_group: number = BoxSet.HERO; // 碰撞组
realDeadTime:number=0.1 realDeadTime: number = 0.1
deadCD:number=0 deadCD: number = 0
monDeadTime:number=0.1 monDeadTime: number = 0.1
hp_height:number=70 hp_height: number = 90
boss_hp_height:number=120 boss_hp_height: number = 160
// 血条显示相关 // 血条显示相关
lastBarUpdateTime:number = 0; // 最后一次血条/蓝条/护盾更新时间 lastBarUpdateTime: number = 0; // 最后一次血条/蓝条/护盾更新时间
// ==================== UI 节点引用 ==================== // ==================== UI 节点引用 ====================
private top_node: Node = null!; private top_node: Node = null!;
private topOpacity: UIOpacity = null!; private topOpacity: UIOpacity = null!;
private topBasePos: Vec3 = v3(); private topBasePos: Vec3 = v3();
private lvLabel: Label | null = null; // 等级显示文本
private readonly barIdleOpacity: number = 153; private readonly barIdleOpacity: number = 153;
private readonly barActiveOpacity: number = 255; private readonly barActiveOpacity: number = 255;
private readonly idleOpacityDelay: number = 0.25; private readonly idleOpacityDelay: number = 0.25;
@@ -73,34 +74,34 @@ export class HeroViewComp extends CCComp {
onLoad() { onLoad() {
this.as = this.getComponent(HeroSpine); this.as = this.getComponent(HeroSpine);
const collider = this.node.getComponent(BoxCollider2D); const collider = this.node.getComponent(BoxCollider2D);
this.scheduleOnce(()=>{ this.scheduleOnce(() => {
if (collider) { if (collider) {
collider.enabled = true; // 先禁 collider.enabled = true; // 先禁
collider.group = this.box_group; // 设置为英雄组 collider.group = this.box_group; // 设置为英雄组
} }
},0.1) }, 0.1)
// let anm = this.node.getChildByName("anm") // let anm = this.node.getChildByName("anm")
// anm.setScale(anm.scale.x*0.8,anm.scale.y*0.8); // anm.setScale(anm.scale.x*0.8,anm.scale.y*0.8);
// 绑定点击事件,点击打开英雄信息面板弹窗 // 绑定点击事件,点击打开英雄信息面板弹窗
this.node.on(NodeEventType.TOUCH_END, this.onHeroClicked, this); this.node.on(NodeEventType.TOUCH_END, this.onHeroClicked, this);
} }
private onHeroClicked() { private onHeroClicked() {
if (!this.model) return; if (!this.model) return;
if (this.model.fac !== FacSet.HERO) return; if (this.model.fac !== FacSet.HERO) return;
oops.audio.playEffect("music/button"); oops.audio.playEffect("music/button");
const eid = this.ent?.eid; const eid = this.ent?.eid;
if (!eid) return; if (!eid) return;
oops.gui.remove(UIID.HInfo); oops.gui.remove(UIID.HInfo);
oops.gui.open(UIID.HInfo, { eid: eid }); oops.gui.open(UIID.HInfo, { eid: eid });
} }
/** 视图层逻辑代码分离演示 */ /** 视图层逻辑代码分离演示 */
start () { start() {
this.init(); this.init();
} }
@@ -110,25 +111,29 @@ export class HeroViewComp extends CCComp {
this.deadCD = 0; this.deadCD = 0;
this.lastBarUpdateTime = 0; this.lastBarUpdateTime = 0;
this.status_change("idle"); this.status_change("idle");
// 初始化 UI 节点 // 初始化 UI 节点
this.initUINodes(); this.initUINodes();
/** 方向 */ /** 方向 */
this.node.setScale(this.scale*Math.abs(this.node.scale.x), 1*this.node.scale.y); // 确保 scale.x 为正后再乘方向 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); // top_node 取 abs 避免对象池复用时 scale 累乘导致血条方向反转
this.top_node.setScale(this.scale * Math.abs(this.top_node.scale.x), 1 * this.top_node.scale.y);
/* 显示角色血*/ /* 显示角色血*/
this.top_node.getChildByName("hp").active = true; this.top_node.getChildByName("hp").active = true;
this.top_node.getChildByName("cd").active = false; this.top_node.getChildByName("cd").active = false;
this.top_node.getChildByName("shield").active = false; this.top_node.getChildByName("shield").active = false;
this.top_node.getChildByName("lv").active = false; this.top_node.getChildByName("lv").active = true;
this.top_node.active = true; this.top_node.active = true;
this.setTopBarOpacity(false); this.setTopBarOpacity(false);
// 初始化等级显示
this.lv_show();
// 🔥 重置血条 UI 显示状态 // 🔥 重置血条 UI 显示状态
if (this.model) { if (this.model) {
this.hp_show(); this.hp_show();
// 根据英雄模型中的等级数据设置描边 // 根据英雄模型中的等级数据设置描边
// 注意HeroViewComp 是挂在 node 上的,而 FlashSprite 可能挂在子节点(如 Sprite/anm 节点) // 注意HeroViewComp 是挂在 node 上的,而 FlashSprite 可能挂在子节点(如 Sprite/anm 节点)
let flashSprite = this.node.getComponent(FlashSprite); let flashSprite = this.node.getComponent(FlashSprite);
@@ -136,53 +141,59 @@ export class HeroViewComp extends CCComp {
// 如果当前节点没有,尝试在所有子节点中查找 // 如果当前节点没有,尝试在所有子节点中查找
flashSprite = this.node.getComponentInChildren(FlashSprite); flashSprite = this.node.getComponentInChildren(FlashSprite);
} }
if (flashSprite) { if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv); flashSprite.setOutlineByLevel(this.model.lv);
} }
} }
} }
/** 初始化 UI 节点引用 */ /** 初始化 UI 节点引用 */
private initUINodes() { private initUINodes() {
this.top_node = this.node.getChildByName("top"); this.top_node = this.node.getChildByName("top");
this.topOpacity = this.top_node.getComponent(UIOpacity) || this.top_node.addComponent(UIOpacity); this.topOpacity = this.top_node.getComponent(UIOpacity) || this.top_node.addComponent(UIOpacity);
this.top_node.setPosition(0, this.hp_height, 0); this.top_node.setPosition(0, this.hp_height, 0);
if(this.model.is_boss) this.top_node.setPosition(0, this.boss_hp_height, 0); if (this.model.is_boss) this.top_node.setPosition(0, this.boss_hp_height, 0);
this.topBasePos = this.top_node.position.clone(); this.topBasePos = this.top_node.position.clone();
const hpNode = this.top_node.getChildByName("hp"); const hpNode = this.top_node.getChildByName("hp");
if(this.model.fac==FacSet.HERO){ if (this.model.fac == FacSet.HERO) {
hpNode.getChildByName("Bar").getComponent(Sprite).color=new Color("#2ECC71") hpNode.getChildByName("Bar").getComponent(Sprite).color = new Color("#2ECC71")
} }
// 确保血条等UI始终在最上层显示 // 确保血条等UI始终在最上层显示
this.top_node.setSiblingIndex(999); this.top_node.setSiblingIndex(999);
// 缓存等级显示 Labellv 节点下的 Label 子节点)
const lvNode = this.top_node.getChildByName("lv");
if (lvNode) {
this.lvLabel = lvNode.getChildByName("Label")?.getComponent(Label) ?? null;
}
} }
/** /**
* View 层每帧更新 * View 层每帧更新
* 注意:数据更新逻辑已移到 HeroAttrSystem这里只负责显示 * 注意:数据更新逻辑已移到 HeroAttrSystem这里只负责显示
*/ */
update(dt: number){ update(dt: number) {
if(!smc.mission.play ) return; if (!smc.mission.play) return;
if(smc.mission.pause) return if (smc.mission.pause) return
// 🔥 修复添加安全检查防止在实体销毁过程中访问null的model // 🔥 修复添加安全检查防止在实体销毁过程中访问null的model
if(!this.ent) return; if (!this.ent) return;
if (!this.model) return; if (!this.model) return;
this.processDamageQueue(); this.processDamageQueue();
if(this.model.is_dead){ if (this.model.is_dead) {
this.deadCD+=dt this.deadCD += dt
if(this.deadCD>=this.realDeadTime){ if (this.deadCD >= this.realDeadTime) {
this.deadCD=0 this.deadCD = 0
this.realDead() this.realDead()
} }
return return
} ; };
// ✅ 按需更新 UI脏标签模式- 只在属性变化时更新 // ✅ 按需更新 UI脏标签模式- 只在属性变化时更新
if (this.model.dirty_hp) { if (this.model.dirty_hp) {
@@ -195,10 +206,15 @@ export class HeroViewComp extends CCComp {
this.show_shield(this.model.shield); this.show_shield(this.model.shield);
this.model.dirty_shield = false; this.model.dirty_shield = false;
} }
if (this.model.dirty_lv) {
this.lv_show();
this.model.dirty_lv = false;
}
} }
public cd_show(){ public cd_show() {
return; return;
} }
/** 显示护盾 */ /** 显示护盾 */
@@ -206,15 +222,15 @@ export class HeroViewComp extends CCComp {
this.lastBarUpdateTime = Date.now() / 1000; this.lastBarUpdateTime = Date.now() / 1000;
this.node.getChildByName("shielded").active = shield > 0; this.node.getChildByName("shielded").active = shield > 0;
} }
/** 显示血量 */ /** 显示血量 */
private hp_show() { private hp_show() {
this.lastBarUpdateTime = Date.now() / 1000; this.lastBarUpdateTime = Date.now() / 1000;
// 不再基于血量是否满来决定显示状态,只更新进度条 // 不再基于血量是否满来决定显示状态,只更新进度条
let hp=this.model.hp; let hp = this.model.hp;
let hp_max=this.model.hp_max; let hp_max = this.model.hp_max;
// mLogger.log(this.debugMode, 'HeroViewComp', "hp_show",hp,hp_max) // mLogger.log(this.debugMode, 'HeroViewComp', "hp_show",hp,hp_max)
let targetProgress = hp_max > 0 ? hp / hp_max : 0; let targetProgress = hp_max > 0 ? hp / hp_max : 0;
targetProgress = clamp(targetProgress, 0, 1); targetProgress = clamp(targetProgress, 0, 1);
let hpNode = this.top_node.getChildByName("hp"); let hpNode = this.top_node.getChildByName("hp");
@@ -237,6 +253,12 @@ export class HeroViewComp extends CCComp {
return this.model.hp >= this.model.hp_max; return this.model.hp >= this.model.hp_max;
} }
/** 更新等级显示 */
private lv_show() {
if (!this.lvLabel || !this.model) return;
this.lvLabel.string = String(this.model.lv);
}
private setTopBarOpacity(isActive: boolean) { private setTopBarOpacity(isActive: boolean) {
if (!this.top_node || !this.top_node.isValid) return; if (!this.top_node || !this.top_node.isValid) return;
if (this.topOpacity) { if (this.topOpacity) {
@@ -269,13 +291,13 @@ export class HeroViewComp extends CCComp {
}) })
.start(); .start();
} }
/** 升级特效 */ /** 升级特效 */
public lv_up() { public lv_up() {
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0); this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
// 升级时同步更新描边 // 升级时同步更新描边
if (this.model) { if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite); let flashSprite = this.node.getComponent(FlashSprite);
@@ -287,12 +309,12 @@ export class HeroViewComp extends CCComp {
} }
} }
} }
/** 攻击力提升特效 */ /** 攻击力提升特效 */
private ap_up() { private ap_up() {
this.spawnTimedFx("game/skill/buff/buff_apup", this.node, 1.0); this.spawnTimedFx("game/skill/buff/buff_apup", this.node, 1.0);
} }
/** 受击特效 */ /** 受击特效 */
private in_atked(anm: string = "atked", scale: number = 1) { private in_atked(anm: string = "atked", scale: number = 1) {
@@ -304,24 +326,24 @@ export class HeroViewComp extends CCComp {
// node.setPosition(this.node.position.x, this.node.position.y+50, this.node.position.z); // node.setPosition(this.node.position.x, this.node.position.y+50, this.node.position.z);
// node.parent = this.node.parent; // node.parent = this.node.parent;
} }
/** 冰冻特效 */ /** 冰冻特效 */
in_iced(t: number = 1) { in_iced(t: number = 1) {
this.spawnTimedFx("game/skill/buff/iced", this.node, t); this.spawnTimedFx("game/skill/buff/iced", this.node, t);
} }
/** 击晕特效 */ /** 击晕特效 */
in_stun(t: number = 1) { in_stun(t: number = 1) {
this.spawnTimedFx("game/skill/buff/stun", this.node, t); this.spawnTimedFx("game/skill/buff/stun", this.node, t);
} }
/** 技能提示 */ /** 技能提示 */
private tooltip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 50) { private tooltip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 50) {
let pos = v3(0, 60); let pos = v3(0, 60);
pos.y = pos.y + y; pos.y = pos.y + y;
Tooltip.load(pos, type, value, s_uuid, this.node); Tooltip.load(pos, type, value, s_uuid, this.node);
} }
/** 技能提示 */ /** 技能提示 */
public skill_name(value: string = "", s_uuid: number = 1001, triggerType: string = "", y: number = 50) { public skill_name(value: string = "", s_uuid: number = 1001, triggerType: string = "", y: number = 50) {
let pos = v3(0, 60); let pos = v3(0, 60);
pos.y = pos.y + y; pos.y = pos.y + y;
@@ -336,7 +358,7 @@ export class HeroViewComp extends CCComp {
if (transform) { if (transform) {
halfHeight = transform.height / 2; halfHeight = transform.height / 2;
} }
// 起点设为怪物中心偏下位置,使其在血条下方 // 起点设为怪物中心偏下位置,使其在血条下方
let ny = this.node.position.y + halfHeight - 15; let ny = this.node.position.y + halfHeight - 15;
let pos = v3(x, ny, 0); let pos = v3(x, ny, 0);
@@ -347,30 +369,30 @@ export class HeroViewComp extends CCComp {
shield_tip(absorbed: number) { shield_tip(absorbed: number) {
this.hp_tip(TooltipTypes.life, NumberFormatter.formatNumber(Math.max(0, Math.floor(absorbed)))); this.hp_tip(TooltipTypes.life, NumberFormatter.formatNumber(Math.max(0, Math.floor(absorbed))));
} }
public playBuff(anm: string = ""){ public playBuff(anm: string = "") {
if(anm==="") return; if (anm === "") return;
var path = "game/skill/buff/" + anm; var path = "game/skill/buff/" + anm;
this.spawnTimedFx(path, this.node, this.effectLifeTime); this.spawnTimedFx(path, this.node, this.effectLifeTime);
} }
public playReady(anm: string = ""){ public playReady(anm: string = "") {
if(anm==="") return; if (anm === "") return;
var path = "game/skill/ready/" + anm; var path = "game/skill/ready/" + anm;
this.spawnAnimEndFx(path, this.node, undefined); this.spawnAnimEndFx(path, this.node, undefined);
} }
public playOther(anm: string = ""){ public playOther(anm: string = "") {
if(anm==="") return; if (anm === "") return;
var path = "game/skill/ready/" + anm; var path = "game/skill/ready/" + anm;
// 以自身当前位置为基准,挂载到父节点,即使自身节点销毁动画也能继续播放 // 以自身当前位置为基准,挂载到父节点,即使自身节点销毁动画也能继续播放
this.spawnAnimEndFx(path, this.node.parent, this.node.position); this.spawnAnimEndFx(path, this.node.parent, this.node.position);
} }
public playEnd(anm: string = ""){ public playEnd(anm: string = "") {
if(anm==="") return; if (anm === "") return;
var path = "game/skill/end/" + anm; var path = "game/skill/end/" + anm;
this.spawnAnimEndFx(path, this.node, undefined); this.spawnAnimEndFx(path, this.node, undefined);
} }
public playAllTime(anm: string = ""){ public playAllTime(anm: string = "") {
if(anm==="") return; if (anm === "") return;
var path = "game/skill/buff/" + anm; var path = "game/skill/buff/" + anm;
// 常驻特效直接创建节点,不挂载生命周期销毁组件,随父节点(this.node)一起销毁 // 常驻特效直接创建节点,不挂载生命周期销毁组件,随父节点(this.node)一起销毁
this.createFxNode(path, this.node, undefined); this.createFxNode(path, this.node, undefined);
@@ -379,7 +401,7 @@ export class HeroViewComp extends CCComp {
private heathed() { private heathed() {
this.spawnAnimEndFx("game/skill/buff/heathed", this.node, undefined); this.spawnAnimEndFx("game/skill/buff/heathed", this.node, undefined);
} }
private deaded(){ private deaded() {
this.spawnAnimEndFx("game/skill/end/dead", this.node.parent, this.node.position); this.spawnAnimEndFx("game/skill/end/dead", this.node.parent, this.node.position);
} }
private createFxNode(path: string, parent: Node | null, worldPos?: Vec3): Node | null { private createFxNode(path: string, parent: Node | null, worldPos?: Vec3): Node | null {
@@ -409,51 +431,51 @@ export class HeroViewComp extends CCComp {
} }
// 注意BaseUp 逻辑已移到 HeroAttrSystem.update() // 注意BaseUp 逻辑已移到 HeroAttrSystem.update()
// 注意updateTemporaryBuffsDebuffs 逻辑已移到 HeroAttrSystem.update() // 注意updateTemporaryBuffsDebuffs 逻辑已移到 HeroAttrSystem.update()
get isActive() { get isActive() {
return this.ent.has(HeroViewComp) && this.node?.isValid; return this.ent.has(HeroViewComp) && this.node?.isValid;
} }
/** 状态切换(动画) */ /** 状态切换(动画) */
status_change(type:string){ status_change(type: string) {
if(this.status === type) return; if (this.status === type) return;
this.status = type; this.status = type;
if(this.model.is_dead || this.model.is_reviving) return if (this.model.is_dead || this.model.is_reviving) return
if(type === "idle"){ if (type === "idle") {
this.as.idle(); this.as.idle();
} else if(type === "move"){ } else if (type === "move") {
this.as.move(); this.as.move();
} }
} }
add_shield(shield:number){ add_shield(shield: number) {
// 护盾数据更新由 Model 层处理,这里只负责视图表现 // 护盾数据更新由 Model 层处理,这里只负责视图表现
if(this.model && this.model.shield>0) this.show_shield(this.model.shield); if (this.model && this.model.shield > 0) this.show_shield(this.model.shield);
} }
health(hp: number = 0) { health(hp: number = 0) {
// ✅ 仅显示特效和提示,不调用 hp_show() // ✅ 仅显示特效和提示,不调用 hp_show()
if(hp<=20) return; if (hp <= 20) return;
this.heathed(); this.heathed();
this.hp_tip(TooltipTypes.health, hp.toFixed(0)); this.hp_tip(TooltipTypes.health, hp.toFixed(0));
this.lastBarUpdateTime = Date.now() / 1000; this.lastBarUpdateTime = Date.now() / 1000;
} }
alive(){
alive() {
// 重置复活标记 - 必须最先重置否则status_change会被拦截 // 重置复活标记 - 必须最先重置否则status_change会被拦截
this.model.is_reviving = false; this.model.is_reviving = false;
this.model.is_dead=false this.model.is_dead = false
this.model.is_count_dead=false this.model.is_count_dead = false
this.deadCD = 0; this.deadCD = 0;
// 恢复碰撞体 // 恢复碰撞体
const collider = this.node.getComponent(Collider2D); const collider = this.node.getComponent(Collider2D);
if (collider) { if (collider) {
collider.enabled = true; collider.enabled = true;
} }
// 恢复UI // 恢复UI
this.top_node.active = true; this.top_node.active = true;
this.status_change("idle"); this.status_change("idle");
// 【新增】仅英雄阵营派发复活成功事件供卡牌技能HeroCall 类型)监听 // 【新增】仅英雄阵营派发复活成功事件供卡牌技能HeroCall 类型)监听
@@ -462,26 +484,26 @@ export class HeroViewComp extends CCComp {
oops.message.dispatchEvent(GameEvent.ReviveSuccess, { eid: this.ent.eid }); oops.message.dispatchEvent(GameEvent.ReviveSuccess, { eid: this.ent.eid });
} }
} }
/** /**
* 死亡视图表现 * 死亡视图表现
* 由 HeroAtkSystem 调用,只负责视觉效果和事件通知 * 由 HeroAtkSystem 调用,只负责视觉效果和事件通知
*/ */
do_dead(){ do_dead() {
// 添加安全检查 // 添加安全检查
if (!this.model) return; if (!this.model) return;
// 防止重复触发 // 防止重复触发
if(this.model.is_count_dead) return; if (this.model.is_count_dead) return;
this.model.is_count_dead = true; // 防止重复触发,必须存在防止重复调用 this.model.is_count_dead = true; // 防止重复触发,必须存在防止重复调用
// 怪物使用0.5秒死亡时间英雄使用realDeadTime // 怪物使用0.5秒死亡时间英雄使用realDeadTime
if(this.model.fac === FacSet.MON){ if (this.model.fac === FacSet.MON) {
this.realDeadTime = this.monDeadTime; this.realDeadTime = this.monDeadTime;
} }
// 如果角色带有死亡触发技能,则由 SCastSystem 播放 playReady("dead") // 如果角色带有死亡触发技能,则由 SCastSystem 播放 playReady("dead")
if (this.model && this.model.dead && this.model.dead.length > 0) { if (this.model && this.model.dead && this.model.dead.length > 0) {
// SCastSystem will handle the "dead" ready animation // SCastSystem will handle the "dead" ready animation
@@ -490,26 +512,26 @@ export class HeroViewComp extends CCComp {
this.deaded(); this.deaded();
} }
} }
realDead(){ realDead() {
// 🔥 修复添加model安全检查防止实体销毁过程中的空指针异常 // 🔥 修复添加model安全检查防止实体销毁过程中的空指针异常
if (!this.model) { if (!this.model) {
mLogger.warn(this.debugMode, 'HeroViewComp', "[HeroViewComp] realDead called but model is null, skipping"); mLogger.warn(this.debugMode, 'HeroViewComp', "[HeroViewComp] realDead called but model is null, skipping");
return; return;
} }
// 隐藏UI // 隐藏UI
this.top_node.active = false; this.top_node.active = false;
// 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞 // 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞
const collider = this.node.getComponent(Collider2D); const collider = this.node.getComponent(Collider2D);
if (collider) { if (collider) {
collider.enabled = false; collider.enabled = false;
} }
// 根据阵营决定飞出方向:英雄向左(负),怪物向右(正) // 根据阵营决定飞出方向:英雄向左(负),怪物向右(正)
let isHero = this.model.fac === FacSet.HERO; let isHero = this.model.fac === FacSet.HERO;
let dirX = isHero ? -800 : 800; let dirX = isHero ? -800 : 800;
// 死亡往后飞出屏幕动画 // 死亡往后飞出屏幕动画
tween(this.node) tween(this.node)
.by(0.5, { position: v3(dirX, 700, 0) }, { easing: "quadOut" }) .by(0.5, { position: v3(dirX, 700, 0) }, { easing: "quadOut" })
@@ -524,55 +546,55 @@ export class HeroViewComp extends CCComp {
}) })
.start(); .start();
} }
do_atked(damage:number,isCrit:boolean,s_uuid:number,isBack:boolean=false){ do_atked(damage: number, isCrit: boolean, s_uuid: number, isBack: boolean = false) {
// 受到攻击时更新最后更新时间 // 受到攻击时更新最后更新时间
this.activateTopBar(); this.activateTopBar();
this.lastBarUpdateTime = Date.now() / 1000; this.lastBarUpdateTime = Date.now() / 1000;
if (damage <= 0) return; if (damage <= 0) return;
// 播放受击音效 // 播放受击音效
if (isCrit) { if (isCrit) {
oops.audio.playEffect("music/Critical"); oops.audio.playEffect("music/Critical");
} else { } else {
oops.audio.playEffect("music/Hit"); oops.audio.playEffect("music/Hit");
} }
// 视图层表现 // 视图层表现
let SConf=SkillSet[s_uuid] let SConf = SkillSet[s_uuid]
const hitAnm = SConf?.DAnm|| "atked"; const hitAnm = SConf?.DAnm || "atked";
if (isBack) this.back() if (isBack) this.back()
this.in_atked(hitAnm, this.model.fac==FacSet.HERO?1:-1); this.in_atked(hitAnm, this.model.fac == FacSet.HERO ? 1 : -1);
this.showDamage(damage, isCrit); this.showDamage(damage, isCrit);
} }
private isBackingUp: boolean = false; // 🔥 添加后退状态标记 private isBackingUp: boolean = false; // 🔥 添加后退状态标记
//后退 //后退
back(distance: number = 0){ back(distance: number = 0) {
// 🔥 防止重复调用后退动画 // 🔥 防止重复调用后退动画
if (this.isBackingUp) return; if (this.isBackingUp) return;
this.isBackingUp = true; // 🔥 设置后退状态 this.isBackingUp = true; // 🔥 设置后退状态
if(this.model.fac==FacSet.MON) { if (this.model.fac == FacSet.MON) {
// 基础击退距离,加上额外的距离强化 // 基础击退距离,加上额外的距离强化
let dist = FightSet.BACK_RANG + distance; let dist = FightSet.BACK_RANG + distance;
let tx=this.node.position.x + dist; let tx = this.node.position.x + dist;
if(tx > 320) tx=320; if (tx > 320) tx = 320;
tween(this.node) tween(this.node)
.to(0.1, { position:v3(tx,this.node.position.y,0)}) .to(0.1, { position: v3(tx, this.node.position.y, 0) })
.call(() => { .call(() => {
this.isBackingUp = false; // 🔥 动画完成后重置状态 this.isBackingUp = false; // 🔥 动画完成后重置状态
}) })
.start(); .start();
} }
if(this.model.fac==FacSet.HERO) { if (this.model.fac == FacSet.HERO) {
let dist = 5 + distance; let dist = 5 + distance;
let tx=this.node.position.x - dist; let tx = this.node.position.x - dist;
if(tx < -320) tx=-320; if (tx < -320) tx = -320;
tween(this.node) tween(this.node)
.to(0.1, { position:v3(tx,this.node.position.y,0)}) .to(0.1, { position: v3(tx, this.node.position.y, 0) })
.call(() => { .call(() => {
this.isBackingUp = false; // 🔥 动画完成后重置状态 this.isBackingUp = false; // 🔥 动画完成后重置状态
}) })
@@ -581,11 +603,11 @@ export class HeroViewComp extends CCComp {
} }
// 伤害计算和战斗逻辑已迁移到 HeroBattleSystem // 伤害计算和战斗逻辑已迁移到 HeroBattleSystem
playSkillAnm(act:string="") { playSkillAnm(act: string = "") {
mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] act'+act,) mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] act' + act,)
if (act==="") return; if (act === "") return;
switch(act){ switch (act) {
case "max": case "max":
this.as.max() this.as.max()
break break
@@ -611,12 +633,12 @@ export class HeroViewComp extends CCComp {
/** 处理伤害队列 */ /** 处理伤害队列 */
private processDamageQueue() { private processDamageQueue() {
if (this.isProcessingDamage || this.damageQueue.length === 0) return; if (this.isProcessingDamage || this.damageQueue.length === 0) return;
this.isProcessingDamage = true; this.isProcessingDamage = true;
const damageInfo = this.damageQueue.shift()!; const damageInfo = this.damageQueue.shift()!;
this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit); this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit);
// 设置延时处理下一个伤 // 设置延时处理下一个伤
this.scheduleOnce(() => { this.scheduleOnce(() => {
this.isProcessingDamage = false; this.isProcessingDamage = false;
@@ -626,7 +648,7 @@ export class HeroViewComp extends CCComp {
/** 立即显示伤害效果 */ /** 立即显示伤害效果 */
private showDamageImmediate(damage: number, isCrit: boolean) { private showDamageImmediate(damage: number, isCrit: boolean) {
if (!this.model) return; if (!this.model) return;
const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage))); const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage)));
this.hp_show(); this.hp_show();
if (isCrit) { if (isCrit) {
@@ -648,18 +670,18 @@ export class HeroViewComp extends CCComp {
const collider = this.getComponent(Collider2D); const collider = this.getComponent(Collider2D);
if (collider) { if (collider) {
collider.off(Contact2DType.BEGIN_CONTACT); collider.off(Contact2DType.BEGIN_CONTACT);
} }
this.deadCD=0 this.deadCD = 0
this.lastBarUpdateTime=0 this.lastBarUpdateTime = 0
this.unschedule(this.restoreBarIdleOpacity); this.unschedule(this.restoreBarIdleOpacity);
// 清理伤害队列 // 清理伤害队列
this.damageQueue.length = 0; this.damageQueue.length = 0;
this.isProcessingDamage = false; this.isProcessingDamage = false;
// 解绑点击事件 // 解绑点击事件
this.node.off(NodeEventType.TOUCH_END, this.onHeroClicked, this); this.node.off(NodeEventType.TOUCH_END, this.onHeroClicked, this);
// 节点生命周期由 Monster 对象池管理,此处不再销毁 // 节点生命周期由 Monster 对象池管理,此处不再销毁
// if (this.node && this.node.isValid) { // if (this.node && this.node.isValid) {
// this.node.destroy(); // this.node.destroy();

View File

@@ -347,18 +347,26 @@ export class CardComp extends CCComp {
/** /**
* 根据卡牌类型分发对应的游戏效果事件。 * 根据卡牌类型分发对应的游戏效果事件。
* - 英雄卡 → CallHero * - 英雄卡 → CallHero
* - 技能卡 → UseSkillCard
* - 特殊升级 / 特殊刷新 → UseSpecialCard * - 特殊升级 / 特殊刷新 → UseSpecialCard
* *
* 卡牌使用成功后会派发 CardUsed 事件(携带本卡槽组件),
* 由 MissionCardComp 接收并立即补充一张新卡到该槽位。
* SpecialRefresh 例外:其效果本身会重填所有槽位,无需重复补卡。
*
* @param payload 被使用的卡牌数据 * @param payload 被使用的卡牌数据
*/ */
private executeCardEffectEntry(payload: CardConfig) { private executeCardEffectEntry(payload: CardConfig) {
switch (payload.type) { switch (payload.type) {
case CardType.Hero: case CardType.Hero:
oops.message.dispatchEvent(GameEvent.CallHero, payload); oops.message.dispatchEvent(GameEvent.CallHero, payload);
oops.message.dispatchEvent(GameEvent.CardUsed, this);
break; break;
case CardType.SpecialUpgrade: case CardType.SpecialUpgrade:
oops.message.dispatchEvent(GameEvent.UseSpecialCard, payload);
oops.message.dispatchEvent(GameEvent.CardUsed, this);
break;
case CardType.SpecialRefresh: case CardType.SpecialRefresh:
// 刷新卡效果tryRefreshHeroCards会重填全部 3 槽,此处不再补卡
oops.message.dispatchEvent(GameEvent.UseSpecialCard, payload); oops.message.dispatchEvent(GameEvent.UseSpecialCard, payload);
break; break;
} }

View File

@@ -287,6 +287,7 @@ export class MissionCardComp extends CCComp {
oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this); oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, this); oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this); oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
oops.message.on(GameEvent.CardUsed, this.onCardUsed, this);
/** 按钮触控事件:抽卡 */ /** 按钮触控事件:抽卡 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this); this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
@@ -431,10 +432,9 @@ export class MissionCardComp extends CCComp {
} }
private onUseSkillCard(event: string, args: any) { private onUseSkillCard(event: string, args: any) {
// 选择技能后关闭弹窗 // 购买技能卡后不再关闭弹窗:技能卡池与英雄卡池保持一致,
if (this.skill_card_node && this.skill_card_node.isValid) { // 由 onCardUsed 立即向被使用的槽位补充一张新技能卡。
this.skill_card_node.active = false; // 弹窗由外部(按钮/阶段切换)控制显隐。
}
// 修复:独立判断 guide2 的关闭 和 guide3 的开启 // 修复:独立判断 guide2 的关闭 和 guide3 的开启
if (!smc.finish_guides.includes(2)) { if (!smc.finish_guides.includes(2)) {
@@ -460,6 +460,7 @@ export class MissionCardComp extends CCComp {
oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this); oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.off(GameEvent.UseSpecialCard, this.onUseSpecialCard, this); oops.message.off(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this); oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
oops.message.off(GameEvent.CardUsed, this.onCardUsed, this);
if (this.cards_chou && this.cards_chou.isValid) { if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this); this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this); this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
@@ -572,6 +573,47 @@ export class MissionCardComp extends CCComp {
}); });
} }
/**
* 单张卡牌被成功使用后的补卡回调:
* - payload 为该卡槽组件实例CardComp 或 SCardComp
* - 命中英雄卡槽 → 调用 buildSingleHeroCard 补一张英雄/升级卡。
* - 命中技能卡槽 → 调用 buildSingleSkillCard 补一张技能卡。
* - 未命中任何槽位(如已销毁/未缓存)→ 忽略。
*
* Why: 购买一张卡后立即补充新卡,避免出现空槽,提升抽卡体验;
* 英雄卡池与技能卡池遵循同一逻辑。
*/
private onCardUsed(event: string, args: any) {
const source = args;
if (!source) return;
const heroIdx = this.cardComps.findIndex(c => c === source);
if (heroIdx >= 0) {
const card = this.buildSingleHeroCard();
if (card) {
this.cardComps[heroIdx].applyDrawCard(card);
}
mLogger.log(this.debugMode, "MissionCardComp", "refill hero slot", {
index: heroIdx,
uuid: card?.uuid ?? 0
});
return;
}
const skillIdx = this.skillCardComps.findIndex(c => c === source);
if (skillIdx >= 0) {
const card = this.buildSingleSkillCard();
if (card) {
this.skillCardComps[skillIdx].applyDrawCard(card);
}
mLogger.log(this.debugMode, "MissionCardComp", "refill skill slot", {
index: skillIdx,
uuid: card?.uuid ?? 0
});
return;
}
}
// ======================== 按钮触控回调 ======================== // ======================== 按钮触控回调 ========================
/** 抽卡按钮按下反馈 */ /** 抽卡按钮按下反馈 */
@@ -957,6 +999,58 @@ export class MissionCardComp extends CCComp {
return filled; return filled;
} }
/**
* 构建单张英雄池卡牌,用于购买后补卡。
*
* 规则与 buildDrawCards 保持一致(不含"必出轮询"逻辑):
* 1. 满员且有可升级英雄 → 仅从升级卡池按权重抽 1 张。
* 2. 否则从 升级卡 + 未召唤英雄卡 的混合池按权重抽 1 张。
*
* @returns 卡牌数据;池为空时返回 null
*/
private buildSingleHeroCard(): CardConfig | null {
const upgradeCards = this.buildHeroUpgradeCards();
const aliveHeroCount = this.getAliveHeroCount();
const heroMax = this.getMissionHeroMaxNum();
if (aliveHeroCount >= heroMax && upgradeCards.length > 0) {
return this.weightedPick(upgradeCards);
}
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
const aliveHeroUuids = this.getAliveHeroUuids();
const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid));
const mixedPool: CardConfig[] = [...upgradeCards, ...availableHeroCards];
if (mixedPool.length === 0) return null;
return this.weightedPick(mixedPool);
}
/**
* 构建单张技能卡,用于购买后补卡。
* 规则与 buildSkillDrawCards 对齐(按当前 wave 过滤)。
*
* @returns 卡牌数据;池为空时返回 null
*/
private buildSingleSkillCard(): CardConfig | null {
const currentWave = this.getCurrentWave();
const cards = drawCardsByRule(1, {
count: 1,
type: CardType.Skill,
wave: currentWave,
unique: true
});
if (cards.length > 0) return cards[0];
// 兜底:放宽 wave 限制再尝试
const fallback = drawCardsByRule(1, {
count: 1,
type: CardType.Skill,
unique: true
});
return fallback[0] ?? null;
}
private tryRefreshHeroCards(heroType?: HType): boolean { private tryRefreshHeroCards(heroType?: HType): boolean {
const cards = drawCardsByRule(1, { const cards = drawCardsByRule(1, {
count: 3, count: 3,
@@ -1199,6 +1293,7 @@ export class MissionCardComp extends CCComp {
} }
model.updateSkillDistanceCache(); model.updateSkillDistanceCache();
model.dirty_hp = true; model.dirty_hp = true;
model.dirty_lv = true;
oops.message.dispatchEvent(GameEvent.HeroLvUp, { oops.message.dispatchEvent(GameEvent.HeroLvUp, {
uuid: model.hero_uuid, uuid: model.hero_uuid,
lv: nextLv lv: nextLv

View File

@@ -109,6 +109,8 @@ export class SCardComp extends CCComp {
this.clearAfterUse(); this.clearAfterUse();
this.isUsing = false; this.isUsing = false;
oops.message.dispatchEvent(GameEvent.UseSkillCard, used); oops.message.dispatchEvent(GameEvent.UseSkillCard, used);
// 派发 CardUsed 让 MissionCardComp 立即补一张新技能卡到本槽位
oops.message.dispatchEvent(GameEvent.CardUsed, this);
}); });
return used; return used;
} }