feat(skill): add timed buff support for skills

1. 新增timed_buff_id配置项到SkillConfig和SkillOverrides接口
2. 实现计时buff逻辑:配置该id时通过buffManager添加限时buff,跳过永久buff逻辑
3. 优化代码格式和空行规范,修复部分代码细节问题
This commit is contained in:
pan
2026-07-23 16:25:08 +08:00
parent 9a5093d2a4
commit 2381812d06
3 changed files with 9619 additions and 9324 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -154,6 +154,7 @@ export interface SkillConfig {
stun?: number, // 额外击晕概率 stun?: number, // 额外击晕概率
bck?: number, // 额外击退概率 bck?: number, // 额外击退概率
buff_type?: Attrs, // Buff 类型 (单一职责) buff_type?: Attrs, // Buff 类型 (单一职责)
timed_buff_id?: number, // 计时性 buff 配置 id指向 BuffList存在时走 buffManager 而非永久 add_*
call_hero?: number, // 召唤技能召唤英雄id(可选) call_hero?: number, // 召唤技能召唤英雄id(可选)
is_accel?: boolean, // 是否逐渐加速飞行 is_accel?: boolean, // 是否逐渐加速飞行
info: string, // 技能描述 info: string, // 技能描述
@@ -170,6 +171,7 @@ export interface SkillOverrides {
stun?: number; stun?: number;
bck?: number; bck?: number;
buff_type?: Attrs; buff_type?: Attrs;
timed_buff_id?: number;
call_hero?: number; call_hero?: number;
is_accel?: boolean; is_accel?: boolean;
} }

View File

@@ -14,6 +14,7 @@ import { SkillTriggerType } from "../common/config/heroSet";
import { SkillTriggerHelper } from "./SkillTriggerHelper"; import { SkillTriggerHelper } from "./SkillTriggerHelper";
import { MissionEconomy } from "../map/MissionEconomy"; import { MissionEconomy } from "../map/MissionEconomy";
import { MissionHeroComp } from "../map/MissionHeroComp"; import { MissionHeroComp } from "../map/MissionHeroComp";
import { buffManager } from "./BuffManager";
/** /**
* ==================== 自动施法系统 ==================== * ==================== 自动施法系统 ====================
@@ -44,18 +45,18 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
oops.message.off(GameEvent.TriggerSkill, this.onTriggerSkill, this); oops.message.off(GameEvent.TriggerSkill, this.onTriggerSkill, this);
} }
private onTriggerSkill(event: string, args: { private onTriggerSkill(event: string, args: {
s_uuid: number, s_uuid: number,
heroAttrs?: HeroAttrsComp, heroAttrs?: HeroAttrsComp,
heroView?: HeroViewComp, heroView?: HeroViewComp,
triggerType?: string, triggerType?: string,
isCardSkill?: boolean, isCardSkill?: boolean,
card_lv?: number, card_lv?: number,
targetPos?: Vec3, targetPos?: Vec3,
overrides?: any overrides?: any
}) { }) {
if (!args || !args.s_uuid) return; if (!args || !args.s_uuid) return;
// 卡牌技能直接触发 // 卡牌技能直接触发
if (args.isCardSkill) { if (args.isCardSkill) {
this.forceCastCardSkill(args.s_uuid, args.card_lv || 1, args.targetPos || new Vec3(FightSet.CSKILL_START_X, FightSet.CSKILL_START_Y, 0), args.overrides); this.forceCastCardSkill(args.s_uuid, args.card_lv || 1, args.targetPos || new Vec3(FightSet.CSKILL_START_X, FightSet.CSKILL_START_Y, 0), args.overrides);
@@ -94,12 +95,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 构造一个模拟的 HeroAttrsComp 用于数值计算,只包含基础卡牌伤害计算所需的属性 // 构造一个模拟的 HeroAttrsComp 用于数值计算,只包含基础卡牌伤害计算所需的属性
const mockAttrs = new HeroAttrsComp(); const mockAttrs = new HeroAttrsComp();
// 动态计算卡牌的虚拟攻击力: // 动态计算卡牌的虚拟攻击力:
// 1. 根据卡牌等级给予基础成长(同英雄升级公式,基准设为 100 // 1. 根据卡牌等级给予基础成长(同英雄升级公式,基准设为 100
let baseAp = 100 * Math.pow(FightSet.HERO_LV_MULTIPLIER, cardLv - 1); let baseAp = 100 * Math.pow(FightSet.HERO_LV_MULTIPLIER, cardLv - 1);
let highestAp = baseAp; let highestAp = baseAp;
// 2. 获取场上最高攻击力的英雄,保证后期奶量/增益绝对够用 // 2. 获取场上最高攻击力的英雄,保证后期奶量/增益绝对够用
for (const eid of smc.mission.heroGrid) { for (const eid of smc.mission.heroGrid) {
if (eid >= 0) { if (eid >= 0) {
@@ -113,7 +114,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
} }
} }
mockAttrs.ap = highestAp; mockAttrs.ap = highestAp;
mockAttrs.critical = 0; mockAttrs.critical = 0;
mockAttrs.freeze_chance = 0; mockAttrs.freeze_chance = 0;
mockAttrs.stun_chance = 0; mockAttrs.stun_chance = 0;
@@ -121,7 +122,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
mockAttrs.fac = FacSet.HERO; mockAttrs.fac = FacSet.HERO;
mockAttrs.type = HType.Long; // 假定为远程,拥有较长索敌范围 mockAttrs.type = HType.Long; // 假定为远程,拥有较长索敌范围
mockAttrs.dis = 2000; // 给予全屏以上的索敌范围 mockAttrs.dis = 2000; // 给予全屏以上的索敌范围
let targetPos: Vec3 | null = null; let targetPos: Vec3 | null = null;
if (!isFriendly) { if (!isFriendly) {
// 伪造一个 view 供找敌逻辑使用,位置为 spawnPos // 伪造一个 view 供找敌逻辑使用,位置为 spawnPos
@@ -140,7 +141,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
return; return;
} }
} }
console.log("[SCastSystem] forceCastCardSkill: casting skill", s_uuid, "castTimes", castTimes, "targetPos", targetPos); console.log("[SCastSystem] forceCastCardSkill: casting skill", s_uuid, "castTimes", castTimes, "targetPos", targetPos);
for (let i = 0; i < castTimes; i++) { for (let i = 0; i < castTimes; i++) {
if (isFriendly) { if (isFriendly) {
@@ -165,7 +166,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const skill = ecs.getEntity<Skill>(Skill); const skill = ecs.getEntity<Skill>(Skill);
const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex); const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex);
// 伪造一个简单的 heroView 供 Skill 初始化使用,只包含方向信息 // 伪造一个简单的 heroView 供 Skill 初始化使用,只包含方向信息
const mockView = { const mockView = {
node: { scale: new Vec3(1, 1, 1), position: actualStartPos }, node: { scale: new Vec3(1, 1, 1), position: actualStartPos },
@@ -192,7 +193,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private isOutOfBattleBounds(x: number): boolean { private isOutOfBattleBounds(x: number): boolean {
return x < BoxSet.LETF_END || x > BoxSet.RIGHT_END; return x < BoxSet.LETF_END || x > BoxSet.RIGHT_END;
} }
/** 系统过滤器:仅处理英雄实体 */ /** 系统过滤器:仅处理英雄实体 */
filter(): ecs.IMatcher { filter(): ecs.IMatcher {
return this.getHeroMatcher(); return this.getHeroMatcher();
@@ -205,9 +206,9 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
* 3. 选取本帧可施放技能并执行施法 * 3. 选取本帧可施放技能并执行施法
*/ */
update(e: ecs.Entity): void { update(e: ecs.Entity): void {
if(!smc.mission.play ) return; if (!smc.mission.play) return;
if(smc.mission.pause) return if (smc.mission.pause) return
if(!smc.mission.in_fight) return if (!smc.mission.in_fight) return
const heroAttrs = e.get(HeroAttrsComp); const heroAttrs = e.get(HeroAttrsComp);
const heroView = e.get(HeroViewComp); const heroView = e.get(HeroViewComp);
if (!heroAttrs || !heroView || !heroView.node) return; if (!heroAttrs || !heroView || !heroView.node) return;
@@ -231,7 +232,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
heroView.playReady("yellow"); heroView.playReady("yellow");
} else if (triggerType === 'dead') { } else if (triggerType === 'dead') {
heroView.playOther("dead"); heroView.playOther("dead");
}else{ } else {
heroView.playOther('yellow') heroView.playOther('yellow')
} }
@@ -273,11 +274,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001]; const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001];
const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0))); const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0)));
const castTimes = 1 + cNum; const castTimes = 1 + cNum;
let val="" let val = ""
if(castTimes >1){ if (castTimes > 1) {
val = "*"+castTimes.toString val = "*" + castTimes.toString
} }
heroView.skill_name(val,s_uuid,triggerType) heroView.skill_name(val, s_uuid, triggerType)
for (let i = 0; i < castTimes; i++) { for (let i = 0; i < castTimes; i++) {
if (!heroView.node || !heroView.node.isValid) return; if (!heroView.node || !heroView.node.isValid) return;
if (isFriendly) { if (isFriendly) {
@@ -340,12 +341,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const skillLv = castPlan.skillLv; const skillLv = castPlan.skillLv;
const overrides = castPlan.overrides; const overrides = castPlan.overrides;
let config = SkillSet[s_uuid]; let config = SkillSet[s_uuid];
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid]:SkillUpList[1001]; const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001];
const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0))); const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0)));
if (!config) return; if (!config) return;
config = mergeSkillParams(config, overrides); config = mergeSkillParams(config, overrides);
//播放前摇技能动画 //播放前摇技能动画
heroView.playReady(config.readyAnm); heroView.playReady(config.readyAnm);
//播放角色攻击动画 //播放角色攻击动画
@@ -363,7 +364,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。 // 注意:这里仍然是基于时间的延迟,受帧率波动影响。
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。 // 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
const delay = config.ready > 0 ? config.ready : FightSet.SKILL_CAST_DELAY; const delay = config.ready > 0 ? config.ready : FightSet.SKILL_CAST_DELAY;
heroView.scheduleOnce(() => { heroView.scheduleOnce(() => {
if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return; if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return;
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return; if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
@@ -407,7 +408,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
* 创建技能实体(投射物/范围体等)。 * 创建技能实体(投射物/范围体等)。
* 仅用于对敌伤害技能的实体化表现与碰撞伤害分发。 * 仅用于对敌伤害技能的实体化表现与碰撞伤害分发。
*/ */
private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp,cAttrsComp: HeroAttrsComp, targetPos: Vec3, castIndex: number = 0, overrides?: SkillOverrides) { private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3, castIndex: number = 0, overrides?: SkillOverrides) {
if (!caster.node || !caster.node.isValid) return; if (!caster.node || !caster.node.isValid) return;
const parent = caster.node.parent; const parent = caster.node.parent;
if (!parent) return; if (!parent) return;
@@ -443,12 +444,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private applyFriendlySkillEffects(_s_uuid: number, _skillLv: number, config: SkillConfig, _heroView: HeroViewComp, _cAttrsComp: HeroAttrsComp, targets: HeroViewComp[], _targetPos: Vec3 | null, isCardSkill: boolean = false) { private applyFriendlySkillEffects(_s_uuid: number, _skillLv: number, config: SkillConfig, _heroView: HeroViewComp, _cAttrsComp: HeroAttrsComp, targets: HeroViewComp[], _targetPos: Vec3 | null, isCardSkill: boolean = false) {
const kind = config.kind ?? SkillKind.Support; const kind = config.kind ?? SkillKind.Support;
const sUp = SkillUpList[_s_uuid] ?? SkillUpList[1001]; const sUp = SkillUpList[_s_uuid] ?? SkillUpList[1001];
const sAp =config.ap+sUp.ap*_skillLv; const sAp = config.ap + sUp.ap * _skillLv;
const sHit=config.hit_count+sUp.hit_count*_skillLv; const sHit = config.hit_count + sUp.hit_count * _skillLv;
const applyTargets = kind === SkillKind.Heal const applyTargets = kind === SkillKind.Heal
? this.pickHealTargetsByMostMissingHp(targets, sHit) ? this.pickHealTargetsByMostMissingHp(targets, sHit)
: this.pickRandomFriendlyTargets(targets, sHit); : this.pickRandomFriendlyTargets(targets, sHit);
for (const target of applyTargets) { for (const target of applyTargets) {
this.applyActualFriendlyEffect(target, kind, sAp, _cAttrsComp, config, sUp, _skillLv); this.applyActualFriendlyEffect(target, kind, sAp, _cAttrsComp, config, sUp, _skillLv);
} }
@@ -460,13 +461,13 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if (!target.ent) return; if (!target.ent) return;
const model = target.ent.get(HeroAttrsComp); const model = target.ent.get(HeroAttrsComp);
if (!model || model.is_dead) return; if (!model || model.is_dead) return;
if (config.endAnm && config.endAnm !== "") { if (config.endAnm && config.endAnm !== "") {
target.playEnd(config.endAnm); target.playEnd(config.endAnm);
} }
if (kind === SkillKind.Heal && sAp !== 0) { if (kind === SkillKind.Heal && sAp !== 0) {
const addHp = Math.floor(sAp*_cAttrsComp.ap/100); const addHp = Math.floor(sAp * _cAttrsComp.ap / 100);
model.add_hp(addHp); model.add_hp(addHp);
target.health(addHp); target.health(addHp);
if (_cAttrsComp.fac === FacSet.HERO) { if (_cAttrsComp.fac === FacSet.HERO) {
@@ -483,20 +484,32 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
MissionEconomy.addCoin(addGold); MissionEconomy.addCoin(addGold);
} }
} }
if (config.buff_type !== undefined) { if (config.buff_type !== undefined) {
// 计时性 buff配置了 timed_buff_id 时走 buffManager 限时路径,
// 同一技能不应既永久加成又限时加成,故提前 return 跳过下方永久 add_* 逻辑
if (config.timed_buff_id) {
buffManager.applyBuff(
target.ent,
config.timed_buff_id,
_cAttrsComp.hero_uuid,
_cAttrsComp.ap
);
return;
}
const baseValue = config.ap; const baseValue = config.ap;
let upgradeValue = 0; let upgradeValue = 0;
// 根据 buff 类型选择对应的升级加成 // 根据 buff 类型选择对应的升级加成
if (config.buff_type === Attrs.ap) upgradeValue = sUp.buff_ap || 0; if (config.buff_type === Attrs.ap) upgradeValue = sUp.buff_ap || 0;
else if (config.buff_type === Attrs.hp_max) upgradeValue = sUp.buff_hp || 0; else if (config.buff_type === Attrs.hp_max) upgradeValue = sUp.buff_hp || 0;
else if (config.buff_type === Attrs.critical) upgradeValue = sUp.crt || 0; else if (config.buff_type === Attrs.critical) upgradeValue = sUp.crt || 0;
// 如果后续有冰冻、击晕等,在这里加上对应的 sUp 字段即可,如 sUp.frz / sUp.stun // 如果后续有冰冻、击晕等,在这里加上对应的 sUp 字段即可,如 sUp.frz / sUp.stun
const totalBuffValue = baseValue + upgradeValue; const totalBuffValue = baseValue + upgradeValue;
switch (config.buff_type){ switch (config.buff_type) {
case Attrs.ap: case Attrs.ap:
model.add_ap(totalBuffValue); model.add_ap(totalBuffValue);
break; break;
@@ -507,7 +520,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 除了 hp_max 和 ap其他固定属性走统一的 add_special_attr 方法 // 除了 hp_max 和 ap其他固定属性走统一的 add_special_attr 方法
model.add_special_attr(config.buff_type, totalBuffValue); model.add_special_attr(config.buff_type, totalBuffValue);
break; break;
} }
} }
} }
@@ -569,11 +582,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] { private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] {
const eids: number[] = []; const eids: number[] = [];
const grid = fac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid; const grid = fac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid;
for (const eid of grid) { for (const eid of grid) {
if (eid >= 0) { if (eid >= 0) {
if (!includeSelf && typeof selfEid === "number" && eid === selfEid) continue; if (!includeSelf && typeof selfEid === "number" && eid === selfEid) continue;
const entity = ecs.getEntityByEid(eid); const entity = ecs.getEntityByEid(eid);
if (entity) { if (entity) {
const model = entity.get(HeroAttrsComp); const model = entity.get(HeroAttrsComp);
@@ -645,7 +658,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 怪物找英雄 // 怪物找英雄
for (let col = 0; col < 2; col++) { for (let col = 0; col < 2; col++) {
// 列内顺序:怪物配置优先中路 // 列内顺序:怪物配置优先中路
const rowOrder = [1, myRow, myRow === 0 ? 2 : 0]; const rowOrder = [1, myRow, myRow === 0 ? 2 : 0];
for (const row of rowOrder) { for (const row of rowOrder) {
const idx = col * 3 + row; const idx = col * 3 + row;
const eid = smc.mission.heroGrid[idx]; const eid = smc.mission.heroGrid[idx];
@@ -741,10 +754,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
halfHeight = 40; // 如果没有 UITransform给一个默认高度偏移 halfHeight = 40; // 如果没有 UITransform给一个默认高度偏移
} }
} }
const pos = target.node.position.clone(); const pos = target.node.position.clone();
pos.y += halfHeight; pos.y += halfHeight;
// 至于最终投射物是否要飞出屏幕(例如线性弹道延长至 +-500由 SMoveSystem 统一处理 // 至于最终投射物是否要飞出屏幕(例如线性弹道延长至 +-500由 SMoveSystem 统一处理
return pos; return pos;
} }