feat(skill): add round end skill trigger system

新增回合结束(FEnd)类型的技能触发机制,包括:
1. 新增SkillTriggerType.FEnd枚举与相关配置映射
2. 为英雄、怪物属性组件添加回合结束技能的运行时解析与存储
3. 实现关卡节间流转时触发全队英雄回合结束技能的逻辑
4. 完善战力计算、技能图标检索等配套支持逻辑
This commit is contained in:
panFD
2026-09-14 16:59:47 +08:00
parent c97c3f36c5
commit 3bbf4a7eb6
6 changed files with 43 additions and 3 deletions

View File

@@ -199,6 +199,7 @@ export enum SkillTriggerType {
Call = "call", // 召唤后触发 降临 Call = "call", // 召唤后触发 降临
Dead = "dead", // 死亡后触发 遗志 Dead = "dead", // 死亡后触发 遗志
FStart = "fstart", // 战斗开始 先手 FStart = "fstart", // 战斗开始 先手
FEnd = "fend", // 回合结束 收官
Field = "field", // 驻场 光环 Field = "field", // 驻场 光环
Atking = "atking", // 攻击后触发 追击 Atking = "atking", // 攻击后触发 追击
Atked = "atked", // 受击后触发 反击 Atked = "atked", // 受击后触发 反击
@@ -209,6 +210,7 @@ export const SkillTriggerName = {
[SkillTriggerType.Call]: "召唤", [SkillTriggerType.Call]: "召唤",
[SkillTriggerType.Dead]: "死亡后", [SkillTriggerType.Dead]: "死亡后",
[SkillTriggerType.FStart]: "回合开始", [SkillTriggerType.FStart]: "回合开始",
[SkillTriggerType.FEnd]: "回合结束",
[SkillTriggerType.Field]: "光环", [SkillTriggerType.Field]: "光环",
[SkillTriggerType.Atking]: "攻击", [SkillTriggerType.Atking]: "攻击",
[SkillTriggerType.Atked]: "受伤", [SkillTriggerType.Atked]: "受伤",
@@ -219,6 +221,7 @@ export const SkillTriggerDesc = {
[SkillTriggerType.Call]: "召唤时", [SkillTriggerType.Call]: "召唤时",
[SkillTriggerType.Dead]: "死亡时", [SkillTriggerType.Dead]: "死亡时",
[SkillTriggerType.FStart]: "战斗开始时", [SkillTriggerType.FStart]: "战斗开始时",
[SkillTriggerType.FEnd]: "回合结束时",
[SkillTriggerType.Field]: "光环", [SkillTriggerType.Field]: "光环",
[SkillTriggerType.Atking]: "攻击n次", [SkillTriggerType.Atking]: "攻击n次",
[SkillTriggerType.Atked]: "受伤n次", [SkillTriggerType.Atked]: "受伤n次",
@@ -351,7 +354,7 @@ export function getHeroSkillIcon(hero: heroInfo | undefined, skillUuid: number,
return undefined; return undefined;
} }
// 自动定位:再各触发组,最后光环 // 自动定位:再各触发组,最后光环
for (const type of [SkillTriggerType.Call, SkillTriggerType.Dead, SkillTriggerType.FStart, SkillTriggerType.Atking, SkillTriggerType.Atked]) { for (const type of [SkillTriggerType.Call, SkillTriggerType.Dead, SkillTriggerType.FStart, SkillTriggerType.FEnd, SkillTriggerType.Atking, SkillTriggerType.Atked]) {
const grouped = hero[type] as TriggerGrouped | undefined; const grouped = hero[type] as TriggerGrouped | undefined;
const entry = grouped?.[skillUuid]?.[0]; const entry = grouped?.[skillUuid]?.[0];
if (entry) return resolveTriggerSkillIcon(entry, skillUuid); if (entry) return resolveTriggerSkillIcon(entry, skillUuid);
@@ -395,6 +398,7 @@ export interface heroInfo {
[SkillTriggerType.Call]?: TriggerGrouped; // 召唤后触发的技能配置(按 s_uuid 分组,每组按 lv 升序) [SkillTriggerType.Call]?: TriggerGrouped; // 召唤后触发的技能配置(按 s_uuid 分组,每组按 lv 升序)
[SkillTriggerType.Dead]?: TriggerGrouped; // 死亡后触发的技能配置 [SkillTriggerType.Dead]?: TriggerGrouped; // 死亡后触发的技能配置
[SkillTriggerType.FStart]?: TriggerGrouped; // 战斗开始时释放的技能配置 [SkillTriggerType.FStart]?: TriggerGrouped; // 战斗开始时释放的技能配置
[SkillTriggerType.FEnd]?: TriggerGrouped; // 回合结束时释放的技能配置
[SkillTriggerType.Field]?: LvFieldEntry[]; // 驻场光环(按 lv 升序,运行时取 ≤英雄等级 的最大档) [SkillTriggerType.Field]?: LvFieldEntry[]; // 驻场光环(按 lv 升序,运行时取 ≤英雄等级 的最大档)
[SkillTriggerType.Atking]?: TriggerGrouped; // 普通攻击后触发的技能配置,s_uuid: 技能id, t_num: 触发所需的普攻次数 [SkillTriggerType.Atking]?: TriggerGrouped; // 普通攻击后触发的技能配置,s_uuid: 技能id, t_num: 触发所需的普攻次数
[SkillTriggerType.Atked]?: TriggerGrouped; // 受击后触发的技能配置,s_uuid: 技能id, t_num: 触发所需的受击次数 [SkillTriggerType.Atked]?: TriggerGrouped; // 受击后触发的技能配置,s_uuid: 技能id, t_num: 触发所需的受击次数
@@ -1235,6 +1239,7 @@ export const HERO_POWER_WEIGHTS = {
W_atking: 1.0, W_atking: 1.0,
W_atked: 0.8, W_atked: 0.8,
W_fstart: 1.0, W_fstart: 1.0,
W_fend: 1.0,
W_dead: 0.6, W_dead: 0.6,
W_field_alive: 1.0, W_field_alive: 1.0,
W_revive: 1.0, W_revive: 1.0,
@@ -1407,6 +1412,7 @@ function getTriggerCount(
case SkillTriggerType.Atked: case SkillTriggerType.Atked:
return getHitCount(seconds) / t_num; return getHitCount(seconds) / t_num;
case SkillTriggerType.FStart: case SkillTriggerType.FStart:
case SkillTriggerType.FEnd:
return getWaveCount(seconds); return getWaveCount(seconds);
case SkillTriggerType.Dead: case SkillTriggerType.Dead:
// 死亡触发次数 = 回合数 × (1 + 最大复活次数),复活从 dead 配置中提取 // 死亡触发次数 = 回合数 × (1 + 最大复活次数),复活从 dead 配置中提取
@@ -1548,6 +1554,7 @@ export interface HeroPowerBreakdown {
atking: number; atking: number;
atked: number; atked: number;
fstart: number; fstart: number;
fend: number;
dead: number; dead: number;
field: number; field: number;
revive: number; revive: number;
@@ -1596,6 +1603,9 @@ function calcTriggerPowerDetailed(
case SkillTriggerType.FStart: case SkillTriggerType.FStart:
weight = weights.W_fstart; weight = weights.W_fstart;
break; break;
case SkillTriggerType.FEnd:
weight = weights.W_fend;
break;
case SkillTriggerType.Dead: case SkillTriggerType.Dead:
weight = weights.W_dead; weight = weights.W_dead;
break; break;
@@ -1693,6 +1703,7 @@ export function calcHeroPowerDetailed(hero: heroInfo, targetLv: number = 1, seco
const atking = hero.atking ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.atking, targetLv), hero, targetLv, SkillTriggerType.Atking, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; const atking = hero.atking ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.atking, targetLv), hero, targetLv, SkillTriggerType.Atking, seconds) : { total: 0, items: [] as TriggerSkillEval[] };
const atked = hero.atked ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.atked, targetLv), hero, targetLv, SkillTriggerType.Atked, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; const atked = hero.atked ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.atked, targetLv), hero, targetLv, SkillTriggerType.Atked, seconds) : { total: 0, items: [] as TriggerSkillEval[] };
const fstart = hero.fstart ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.fstart, targetLv), hero, targetLv, SkillTriggerType.FStart, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; const fstart = hero.fstart ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.fstart, targetLv), hero, targetLv, SkillTriggerType.FStart, seconds) : { total: 0, items: [] as TriggerSkillEval[] };
const fend = hero.fend ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.fend, targetLv), hero, targetLv, SkillTriggerType.FEnd, seconds) : { total: 0, items: [] as TriggerSkillEval[] };
const dead = hero.dead ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.dead, targetLv), hero, targetLv, SkillTriggerType.Dead, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; const dead = hero.dead ? calcTriggerPowerDetailed(resolveTriggerByLv(hero.dead, targetLv), hero, targetLv, SkillTriggerType.Dead, seconds) : { total: 0, items: [] as TriggerSkillEval[] };
const fieldEntries = resolveFieldByLv(hero.field, targetLv); const fieldEntries = resolveFieldByLv(hero.field, targetLv);
const field = fieldEntries.length > 0 ? calcFieldPowerDetailed(fieldEntries, hero, targetLv, seconds) : { total: 0, items: [] as FieldSkillEval[] }; const field = fieldEntries.length > 0 ? calcFieldPowerDetailed(fieldEntries, hero, targetLv, seconds) : { total: 0, items: [] as FieldSkillEval[] };
@@ -1708,7 +1719,7 @@ export function calcHeroPowerDetailed(hero: heroInfo, targetLv: number = 1, seco
} }
} }
const total = hpValue + dpsValue + atking.total + atked.total + fstart.total + dead.total + field.total + reviveValue; const total = hpValue + dpsValue + atking.total + atked.total + fstart.total + fend.total + dead.total + field.total + reviveValue;
return { return {
total, total,
@@ -1718,11 +1729,12 @@ export function calcHeroPowerDetailed(hero: heroInfo, targetLv: number = 1, seco
atking: atking.total, atking: atking.total,
atked: atked.total, atked: atked.total,
fstart: fstart.total, fstart: fstart.total,
fend: fend.total,
dead: dead.total, dead: dead.total,
field: field.total, field: field.total,
revive: reviveValue, revive: reviveValue,
}, },
triggerSkills: [...atking.items, ...atked.items, ...fstart.items, ...dead.items], triggerSkills: [...atking.items, ...atked.items, ...fstart.items, ...fend.items, ...dead.items],
fieldSkills: field.items, fieldSkills: field.items,
normalAttack, normalAttack,
}; };

View File

@@ -177,6 +177,7 @@ export class Hero extends ecs.Entity {
model.runtime_call = resolveTriggerByLv(model.call, hero_lv); model.runtime_call = resolveTriggerByLv(model.call, hero_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, hero_lv); model.runtime_dead = resolveTriggerByLv(model.dead, hero_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, hero_lv); model.runtime_fstart = resolveTriggerByLv(model.fstart, hero_lv);
model.runtime_fend = resolveTriggerByLv(model.fend, hero_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, hero_lv); model.runtime_atking = resolveTriggerByLv(model.atking, hero_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, hero_lv); model.runtime_atked = resolveTriggerByLv(model.atked, hero_lv);
model.runtime_field = resolveFieldByLv(model.field, hero_lv); model.runtime_field = resolveFieldByLv(model.field, hero_lv);

View File

@@ -53,6 +53,7 @@ export class HeroAttrsComp extends ecs.Comp {
[SkillTriggerType.Call]?: TriggerGrouped; [SkillTriggerType.Call]?: TriggerGrouped;
[SkillTriggerType.Dead]?: TriggerGrouped; [SkillTriggerType.Dead]?: TriggerGrouped;
[SkillTriggerType.FStart]?: TriggerGrouped; [SkillTriggerType.FStart]?: TriggerGrouped;
[SkillTriggerType.FEnd]?: TriggerGrouped;
[SkillTriggerType.Atking]?: TriggerGrouped; [SkillTriggerType.Atking]?: TriggerGrouped;
[SkillTriggerType.Atked]?: TriggerGrouped; [SkillTriggerType.Atked]?: TriggerGrouped;
/** 驻场光环(按 lv 解锁,存活期间全局生效) */ /** 驻场光环(按 lv 解锁,存活期间全局生效) */
@@ -71,6 +72,8 @@ export class HeroAttrsComp extends ecs.Comp {
runtime_dead?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[]; runtime_dead?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的战前触发条目 */ /** 当前等级下生效的战前触发条目 */
runtime_fstart?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[]; runtime_fstart?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的回合结束触发条目 */
runtime_fend?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的驻场光环词条列表 */ /** 当前等级下生效的驻场光环词条列表 */
runtime_field?: FieldEntry[]; runtime_field?: FieldEntry[];
@@ -725,6 +728,7 @@ export class HeroAttrsComp extends ecs.Comp {
this.call = undefined; this.call = undefined;
this.dead = undefined; this.dead = undefined;
this.fstart = undefined; this.fstart = undefined;
this.fend = undefined;
this.atking = undefined; this.atking = undefined;
this.atked = undefined; this.atked = undefined;
this.field = undefined; this.field = undefined;
@@ -734,6 +738,7 @@ export class HeroAttrsComp extends ecs.Comp {
this.runtime_call = undefined; this.runtime_call = undefined;
this.runtime_dead = undefined; this.runtime_dead = undefined;
this.runtime_fstart = undefined; this.runtime_fstart = undefined;
this.runtime_fend = undefined;
this.runtime_field = undefined; this.runtime_field = undefined;
this.critical = 0; this.critical = 0;
this.critical_res = 0; this.critical_res = 0;

View File

@@ -241,6 +241,7 @@ export class Monster extends ecs.Entity {
model.runtime_call = resolveTriggerByLv(model.call, mon_lv); model.runtime_call = resolveTriggerByLv(model.call, mon_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, mon_lv); model.runtime_dead = resolveTriggerByLv(model.dead, mon_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, mon_lv); model.runtime_fstart = resolveTriggerByLv(model.fstart, mon_lv);
model.runtime_fend = resolveTriggerByLv(model.fend, mon_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, mon_lv); model.runtime_atking = resolveTriggerByLv(model.atking, mon_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, mon_lv); model.runtime_atked = resolveTriggerByLv(model.atked, mon_lv);
// 复活已纳入 dead 触发组,无需单独解析 revive // 复活已纳入 dead 触发组,无需单独解析 revive

View File

@@ -44,6 +44,9 @@ export class SkillTriggerHelper {
case SkillTriggerType.FStart: case SkillTriggerType.FStart:
this.handleArrayTrigger(model.runtime_fstart, model, view, type); this.handleArrayTrigger(model.runtime_fstart, model, view, type);
break; break;
case SkillTriggerType.FEnd:
this.handleArrayTrigger(model.runtime_fend, model, view, type);
break;
case SkillTriggerType.Atking: case SkillTriggerType.Atking:
this.handleAtking(model, view); this.handleAtking(model, view);
break; break;

View File

@@ -565,6 +565,10 @@ export class MissionComp extends CCComp {
// 通关判定:Boss 节(末节)胜利即视为本关通关 // 通关判定:Boss 节(末节)胜利即视为本关通关
smc.vmdata.scores.passed_wave_20 = true; smc.vmdata.scores.passed_wave_20 = true;
// 触发回合结束技能(fend):仅节间流转时触发;
// 末节结算/失败时 pause 已置位,跳过避免结算瞬间白播施法表现
this.triggerHeroWaveEndSkills();
} }
// 战斗结束阶段,按 FightSet.WAVE_HEAL_RATE 恢复所有英雄血量 // 战斗结束阶段,按 FightSet.WAVE_HEAL_RATE 恢复所有英雄血量
@@ -676,6 +680,20 @@ export class MissionComp extends CCComp {
}); });
} }
/**
* 触发英雄的回合结束技能(fend)
* 每回合(节)切入 BattleEnd 时对全队存活英雄各触发 1 次(无光环加成),
* 先于节间回血 healAllHeroes 结算,保证治疗/护盾类 fend 能吃到节末血线
*/
private triggerHeroWaveEndSkills() {
ecs.query(this.heroAttrsMatcher).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view || attrs.is_dead || attrs.fac !== FacSet.HERO) return;
SkillTriggerHelper.trigger(SkillTriggerType.FEnd, attrs, view);
});
}
/** /**
* 战斗结束阶段治疗所有英雄(节间回血),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例 * 战斗结束阶段治疗所有英雄(节间回血),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例
*/ */