Files
pixelheros/assets/script/game/hero/SkillTriggerHelper.ts
pan be6d941b90 feat(skill): add trigger probability and pity system for attack/attacked triggers
1. 新增技能触发概率配置与保底叠加机制,仅对攻击/受击触发生效
2. 前端编辑器新增t_chance概率与pity_add保底增量配置项
3. 技能描述文案支持展示触发概率
4. 运行时新增概率判定与保底缓存逻辑,兼容存量配置
2026-09-21 15:25:02 +08:00

229 lines
10 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 { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { ResolvedTriggerEntry, SkillTriggerType } from "../common/config/heroSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { FacSet } from "../common/config/GameSet";
import { FieldSkillType, SkillOverrides } from "../common/config/SkillSet";
import { smc } from "../common/SingletonModuleComp";
import { FieldSkillHelper } from "./FieldSkillHelper";
/**
* 特殊触发技能的统一中介者 (Mediator)
*
* 核心职责:
* 1. 统一管理英雄/怪物的各类生命周期触发技能(召唤、死亡、战前、战后、攻受击)。
* 2. 集中处理触发次数的计算(如:局内羁绊加成、局外天赋加成)。
* 3. 作为唯一出口,通过事件总线向 SCastSystem 派发 GameEvent.TriggerSkill 事件。
*
* 优势:
* - 极大地解耦了具体业务组件(Hero/Mon/HeroAtkSystem/MissionComp)与技能系统。
* - 避免了原先散落在各处的判定逻辑和冗余的事件派发代码。
*/
export class SkillTriggerHelper {
/**
* 统一的技能触发入口
* 根据传入的触发类型,自动分发到对应的处理函数中执行判定和加成逻辑。
*
* @param type 触发类型枚举 (SkillTriggerType)
* @param model 施法者的属性数据组件 (HeroAttrsComp)
* @param view 施法者的视图表现组件 (HeroViewComp)
*/
public static trigger(type: SkillTriggerType, model: HeroAttrsComp, view: HeroViewComp) {
// 健壮性检查:若实体或节点已被销毁,直接阻断,防止空指针报错
if (!model || !view || !view.node || !view.node.isValid) return;
switch (type) {
case SkillTriggerType.Call:
this.handleCall(model, view);
break;
case SkillTriggerType.Dead:
this.handleDead(model, view);
break;
case SkillTriggerType.FStart:
this.handleArrayTrigger(model.runtime_fstart, model, view, type);
break;
case SkillTriggerType.FEnd:
this.handleArrayTrigger(model.runtime_fend, model, view, type);
break;
case SkillTriggerType.Atking:
this.handleAtking(model, view);
break;
case SkillTriggerType.Atked:
this.handleAtked(model, view);
break;
}
}
/**
* 处理召唤(落地)触发技能
* 支持受【召唤强化羁绊 (SummonCount)】的多次触发加成。
*/
private static handleCall(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.runtime_call || model.runtime_call.length === 0) return;
let triggerCount = 1;
// 仅英雄享受加成,怪物始终只触发 1 次
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.SummonCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount)); // 确保最少触发 1 次
for (let i = 0; i < triggerCount; i++) {
this.dispatchArray(model.runtime_call, model, view, SkillTriggerType.Call);
}
}
/**
* 处理死亡触发技能
* 支持受【亡语强化羁绊 (DeadCount)】的多次触发加成。
*/
private static handleDead(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.runtime_dead || model.runtime_dead.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.DeadCount);
// 【局内战绩评分系统】统计死亡触发技能生效次数(用于局后防守评分结算)
smc.vmdata.scores.dead_trigger_count += model.runtime_dead.length;
}
triggerCount = Math.max(1, Math.floor(triggerCount));
for (let i = 0; i < triggerCount; i++) {
this.dispatchArray(model.runtime_dead, model, view, SkillTriggerType.Dead);
}
}
/**
* 处理攻击附加技能
* 判定逻辑:依据配置中的 t_num(触发所需普攻次数),对比当前的普攻累计次数取模判定。
* 天赋修正:L4 减少 t_num 阈值(下限 1);L5 按概率额外触发 1 次。
*/
private static handleAtking(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.runtime_atking || model.runtime_atking.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.AtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
// L5 天赋:概率额外触发 1 次(与羁绊次数加成独立乘算)
if (model.talent_extra_trigger_chance > 0 && Math.random() * 100 < model.talent_extra_trigger_chance) {
triggerCount += 1;
}
// L4 天赋:减少触发所需次数(下限 1)
const tNumReduce = model.talent_t_num_reduce;
model.runtime_atking.forEach(atkConfig => {
// atk_count 代表已进行的普攻次数。当其余数刚好整除配置阈值时触发。
const tNum = Math.max(1, atkConfig.t_num - tNumReduce);
if (model.atk_count > 0 && model.atk_count % tNum === 0
&& this.rollTriggerChance(model, SkillTriggerType.Atking, atkConfig)) {
for (let i = 0; i < triggerCount; i++) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atking, atkConfig.overrides);
}
}
});
}
/**
* 处理受击附加技能
* 判定逻辑:依据配置中的 t_num(触发所需受击次数),对比当前的受击累计次数取模判定。
* 天赋修正:L4 减少 t_num 阈值(下限 1);L5 按概率额外触发 1 次。
*/
private static handleAtked(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.runtime_atked || model.runtime_atked.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.BeAtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
// L5 天赋:概率额外触发 1 次(与羁绊次数加成独立乘算)
if (model.talent_extra_trigger_chance > 0 && Math.random() * 100 < model.talent_extra_trigger_chance) {
triggerCount += 1;
}
// L4 天赋:减少触发所需次数(下限 1)
const tNumReduce = model.talent_t_num_reduce;
model.runtime_atked.forEach(atkConfig => {
// atked_count 代表已承受的受击次数。当其余数刚好整除配置阈值时触发。
const tNum = Math.max(1, atkConfig.t_num - tNumReduce);
if (model.atked_count > 0 && model.atked_count % tNum === 0
&& this.rollTriggerChance(model, SkillTriggerType.Atked, atkConfig)) {
for (let i = 0; i < triggerCount; i++) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atked, atkConfig.overrides);
}
}
});
}
/**
* 触发概率判定 + 保底叠加(仅 Atking/Atked 消费)
* 次数条件达成后按 t_chance(缺省 100 必触发,兼容存量配置)掷骰;
* 未命中且配置 pity_add > 0 时,为该 s_uuid 累积下次概率增量,形成"越不触发越容易触发"的保底体验;
* 触发成功后清空叠加,最终概率封顶 100。
*
* @param model 施法者属性组件(保底缓存挂载于此)
* @param type 触发类型(决定读/写哪份保底缓存)
* @param config 当前生效的触发条目
* @returns 是否通过概率判定
*/
private static rollTriggerChance(model: HeroAttrsComp, type: SkillTriggerType, config: ResolvedTriggerEntry): boolean {
const pityMap = type === SkillTriggerType.Atking ? model.pity_atking : model.pity_atked;
const chance = Math.min(100, (config.t_chance ?? 100) + (pityMap[config.s_uuid] ?? 0));
if (chance >= 100 || Math.random() * 100 < chance) {
pityMap[config.s_uuid] = 0; // 触发成功:清空保底叠加
return true;
}
const pityAdd = config.pity_add ?? 0;
if (pityAdd > 0) {
pityMap[config.s_uuid] = (pityMap[config.s_uuid] ?? 0) + pityAdd;
}
return false;
}
/**
* 通用的数组型触发器处理(适用于 FStart 等无需额外判定的简单列表触发)
*/
private static handleArrayTrigger(configs: { s_uuid: number, t_num: number, overrides?: SkillOverrides }[] | undefined, model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType) {
if (!configs || configs.length === 0) return;
this.dispatchArray(configs, model, view, type);
}
/**
* 批量派发技能事件
*/
private static dispatchArray(configs: { s_uuid: number, t_num?: number, overrides?: SkillOverrides }[], model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType) {
configs.forEach(config => this.dispatchSingle(config.s_uuid, model, view, type, config.overrides));
}
/**
* 单一技能事件派发底层接口
* 事件发出后,将由 SCastSystem 的 forceCastTriggerSkill 监听拦截并执行无视CD的强制施法
*/
private static dispatchSingle(s_uuid: number, model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType, overrides?: SkillOverrides) {
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: s_uuid,
heroAttrs: model,
heroView: view,
triggerType: type,
overrides: overrides
});
}
/**
* 公共单技能派发(供 HeroAtkSystem 等命中后触发场景调用)。
* 直接复用 TriggerSkill 通道(forceCastTriggerSkill),无视 CD、无施法前摇。
* @param s_uuid 被触发的技能 uuid
* @param model 施法者属性组件
* @param view 施法者视图组件
* @param type 触发类型(语义来源,如 SkillTriggerType.Atking)
* @param overrides 透传给被触发技能的覆写参数
*/
public static dispatchSinglePublic(s_uuid: number, model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType, overrides?: SkillOverrides) {
this.dispatchSingle(s_uuid, model, view, type, overrides);
}
}