|
|
|
|
import type { AbilityContext } from 'db://assets/mx/module/battle/combat/AbilityContext';
|
|
|
|
|
import type { BattleServices } from 'db://assets/mx/module/battle/combat/BattleServices';
|
|
|
|
|
import type { EntityId, World } from 'db://assets/mx/module/ecs';
|
|
|
|
|
import type { ProjectBattleHandle } from '../runtime/BattleRuntime';
|
|
|
|
|
import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../view/IBattleMonsterView';
|
|
|
|
|
import {
|
|
|
|
|
AttrId,
|
|
|
|
|
AttributeComp,
|
|
|
|
|
CombatComp,
|
|
|
|
|
HealthComp,
|
|
|
|
|
MovementComp,
|
|
|
|
|
TeamComp,
|
|
|
|
|
} from 'db://assets/mx/module/battle/ecs/Components';
|
|
|
|
|
import { VAR_LAST_DAMAGE_AMOUNT } from 'db://assets/mx/module/battle/combat/DamagePipeline';
|
|
|
|
|
import { EffectRegistry } from 'db://assets/mx/module/battle/registry/EffectRegistry';
|
|
|
|
|
import { BattleMapSpec } from '../map/BattleMapSpec';
|
|
|
|
|
import { applySkillTableKnockback } from './AtkHookEffects';
|
|
|
|
|
import { applySkillAtkBuffBoardEffect } from './SkillAtkBuffBoardBridge';
|
|
|
|
|
import { getMonsterStatusText, STATUS_TEXT_HP_CAP } from '../view/MonsterStatusText';
|
|
|
|
|
import { getAllySkillExtraHost } from '../presentation/AllySkillExtraHost';
|
|
|
|
|
|
|
|
|
|
const SKILL_ATK_BUFF_FLAG = '__mx_battle_skillAtkBuffRegistered__';
|
|
|
|
|
const MOD_SPEED = 'skill_atk_buff_speed';
|
|
|
|
|
const MOD_ASPD = 'skill_atk_buff_aspd';
|
|
|
|
|
const MOD_RANGE = 'skill_atk_buff_range';
|
|
|
|
|
|
|
|
|
|
/** 单条 Skill.atk_buff 执行上下文 */
|
|
|
|
|
export interface SkillAtkBuffHitOpts {
|
|
|
|
|
/** 本次命中实际伤害(击退等判定) */
|
|
|
|
|
lastDamage?: number;
|
|
|
|
|
/** 来自被动 result:DoT 用 passive_ 前缀,不回触发 condition */
|
|
|
|
|
fromPassiveResult?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 注册 Skill 表 atk_buff 效果 */
|
|
|
|
|
export function registerSkillAtkBuffEffects(): void {
|
|
|
|
|
const g = globalThis as unknown as Record<string, boolean | undefined>;
|
|
|
|
|
if (g[SKILL_ATK_BUFF_FLAG]) return;
|
|
|
|
|
g[SKILL_ATK_BUFF_FLAG] = true;
|
|
|
|
|
|
|
|
|
|
EffectRegistry.register('ApplySkillAtkBuff', (services, ctx, params) => {
|
|
|
|
|
const buff1 = String(params.buff1 ?? '');
|
|
|
|
|
const buff2 = String(params.buff2 ?? '');
|
|
|
|
|
const opts: SkillAtkBuffHitOpts = { lastDamage: Number(params.lastDamage) || 0 };
|
|
|
|
|
const targets = ctx.targets;
|
|
|
|
|
for (let i = 0; i < targets.length; i++) {
|
|
|
|
|
SkillAtkBuffExecutor.applyPair(services, ctx, targets[i], buff1, buff2, opts);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Skill.atk_buff1/2 执行器(配表语义见 AutoTable ITableSkill) */
|
|
|
|
|
export class SkillAtkBuffExecutor {
|
|
|
|
|
/** 命中后依次执行 buff1、buff2 */
|
|
|
|
|
static applyPair(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
buff1: string,
|
|
|
|
|
buff2: string,
|
|
|
|
|
opts?: SkillAtkBuffHitOpts,
|
|
|
|
|
): void {
|
|
|
|
|
if (!services.world.isAlive(target)) return;
|
|
|
|
|
this.applyRaw(services, ctx, target, buff1, opts);
|
|
|
|
|
this.applyRaw(services, ctx, target, buff2, opts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 召唤出生:仅移速/血量(type 3/4) */
|
|
|
|
|
static applyOnSpawn(services: BattleServices, entity: EntityId, raw: string): void {
|
|
|
|
|
const parts = this._parse(raw);
|
|
|
|
|
if (!parts.length) return;
|
|
|
|
|
const kind = parts[0];
|
|
|
|
|
if (kind === 3 || kind === 4) {
|
|
|
|
|
this.applyRaw(services, null, entity, raw, {});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (kind > 0) {
|
|
|
|
|
console.log('[SkillAtkBuff] 召唤出生仅支持 type 3/4');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 武器被动 atk_buff type 11 的射程像素(≤16 按格)。
|
|
|
|
|
* 计入 deriveTowerSetupStats,部署时不再走 modifier,避免 getAttr 加两次。
|
|
|
|
|
*/
|
|
|
|
|
static loadoutRangeAddPx(buff1: string, buff2: string): number {
|
|
|
|
|
return this._rangeAddFromRaw(buff1) + this._rangeAddFromRaw(buff2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 清掉 skill_atk_buff_range,避免刷新时残留旧的 +1 格 */
|
|
|
|
|
static clearRangeModifiers(services: BattleServices, entity: EntityId): void {
|
|
|
|
|
services.attributes.removeModifiersByGroup(entity, MOD_RANGE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 部署时静态 buff;type 11 已计入 setup.range,此处跳过 */
|
|
|
|
|
static applyLoadoutBuffs(services: BattleServices, entity: EntityId, buff1: string, buff2: string): void {
|
|
|
|
|
this._applyLoadoutRaw(services, entity, buff1);
|
|
|
|
|
this._applyLoadoutRaw(services, entity, buff2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _applyLoadoutRaw(services: BattleServices, entity: EntityId, raw: string): void {
|
|
|
|
|
const parts = this._parse(raw);
|
|
|
|
|
if (!parts.length) return;
|
|
|
|
|
// 11 射程已计入 setup;14 点燃仅命中施加
|
|
|
|
|
if (parts[0] === 11 || parts[0] === 14) return;
|
|
|
|
|
this._applyKind(services, null, entity, parts, {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _rangeAddFromRaw(raw: string): number {
|
|
|
|
|
const parts = this._parse(raw);
|
|
|
|
|
if (parts[0] !== 11) return 0;
|
|
|
|
|
const addVal = Math.floor(parts[1] || 0);
|
|
|
|
|
if (addVal === 0) return 0;
|
|
|
|
|
return Math.abs(addVal) <= 16 ? addVal * BattleMapSpec.CELL_SIZE : addVal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static applyRaw(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
raw: string,
|
|
|
|
|
opts?: SkillAtkBuffHitOpts,
|
|
|
|
|
): void {
|
|
|
|
|
const parts = this._parse(raw);
|
|
|
|
|
if (!parts.length) return;
|
|
|
|
|
const kind = parts[0] || 0;
|
|
|
|
|
if (!this._roll(services, ctx, this._chanceWanForKind(kind, parts))) return;
|
|
|
|
|
this._applyKind(services, ctx, target, parts, opts ?? {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 无概率字段的类型恒为 100% */
|
|
|
|
|
private static _chanceWanForKind(kind: number, parts: number[]): number {
|
|
|
|
|
if (kind === 5 || kind === 9 || kind === 10 || kind === 11 || kind === 14) return 10000;
|
|
|
|
|
return parts[1] ?? 10000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _parse(raw: string): number[] {
|
|
|
|
|
const s = String(raw ?? '').trim();
|
|
|
|
|
if (!s) return [];
|
|
|
|
|
return s.split(',').map((x) => Number(String(x).trim()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _roll(services: BattleServices, ctx: AbilityContext | null, chanceWan: number): boolean {
|
|
|
|
|
const wan = Math.max(0, Number(chanceWan) || 0);
|
|
|
|
|
if (wan >= 10000) return true;
|
|
|
|
|
const rng = ctx?.rng ?? services.rng;
|
|
|
|
|
return rng.next() * 10000 < wan;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _applyKind(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
parts: number[],
|
|
|
|
|
opts: SkillAtkBuffHitOpts,
|
|
|
|
|
): void {
|
|
|
|
|
const kind = parts[0] || 0;
|
|
|
|
|
switch (kind) {
|
|
|
|
|
case 1:
|
|
|
|
|
this._stun(services, ctx, target, parts);
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
this._knockback(services, ctx, target, parts, opts);
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
this._moveSpeed(services, target, parts[2] ?? 0, parts[3] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 4:
|
|
|
|
|
this._maxHp(services, target, parts[2] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 5:
|
|
|
|
|
this._peaches(services, ctx, parts[1] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 6:
|
|
|
|
|
this._attackSpeed(services, target, parts[2] ?? 0, parts[3] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 7:
|
|
|
|
|
this._lowerLevel(services, ctx, target, parts[2] ?? 1, parts[3] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 8:
|
|
|
|
|
this._confusion(services, ctx, target, parts[2] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 9:
|
|
|
|
|
this._blockTile(services, ctx);
|
|
|
|
|
break;
|
|
|
|
|
case 10:
|
|
|
|
|
this._heal(services, ctx, target, parts[1] ?? 0);
|
|
|
|
|
break;
|
|
|
|
|
case 11:
|
|
|
|
|
this._attackRange(services, target, parts[1] ?? 0, parts[2] ?? -1);
|
|
|
|
|
break;
|
|
|
|
|
case 14:
|
|
|
|
|
this._ignite(services, ctx, target, parts, opts);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.log('[SkillAtkBuff] 未支持的类型', kind, parts.join(','));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 点燃:立即首跳,再按间隔跳至结束。
|
|
|
|
|
* @param services 战斗服务
|
|
|
|
|
* @param ctx 施法上下文(缺则跳过)
|
|
|
|
|
* @param target 目标
|
|
|
|
|
* @param parts 14,间隔秒,伤害万分比,持续秒
|
|
|
|
|
* @param opts 命中选项
|
|
|
|
|
*/
|
|
|
|
|
private static _ignite(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
parts: number[],
|
|
|
|
|
opts: SkillAtkBuffHitOpts,
|
|
|
|
|
): void {
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
console.log('[SkillAtkBuff] 点燃需命中上下文', parts.join(','));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const interval = Number(parts[1]);
|
|
|
|
|
const coeffWan = Number(parts[2]);
|
|
|
|
|
const duration = Number(parts[3]);
|
|
|
|
|
if (!(interval > 0) || !(coeffWan > 0) || !(duration > 0)) {
|
|
|
|
|
console.log('[SkillAtkBuff] 点燃参数无效(间隔,系数万分比,持续)', parts.join(','));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!services.config.getBuff('debuff_1')) {
|
|
|
|
|
console.log('[SkillAtkBuff] 缺 buff debuff_1');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const scale = coeffWan / 10000;
|
|
|
|
|
const caster = ctx.caster;
|
|
|
|
|
const fromPassive = opts.fromPassiveResult === true;
|
|
|
|
|
const damageType = fromPassive ? 'passive_burn' : 'burn_dot';
|
|
|
|
|
services.buffs.add(target, 'debuff_1', caster, 1, { duration });
|
|
|
|
|
const endAt = (services.clock.time ?? 0) + duration;
|
|
|
|
|
const tick = (): void => {
|
|
|
|
|
if (!services.world.isAlive(caster) || !services.world.isAlive(target)) return;
|
|
|
|
|
if ((services.clock.time ?? 0) > endAt) return;
|
|
|
|
|
services.damage.dealToTarget(caster, target, {
|
|
|
|
|
scale,
|
|
|
|
|
allowCrit: false,
|
|
|
|
|
damageType,
|
|
|
|
|
fromPassiveResult: fromPassive,
|
|
|
|
|
}, ctx);
|
|
|
|
|
if ((services.clock.time ?? 0) + interval <= endAt) {
|
|
|
|
|
services.clock.schedule(interval, tick);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
tick();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _stun(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
parts: number[],
|
|
|
|
|
): void {
|
|
|
|
|
const duration = Math.max(0, Number(parts[2]) || 0);
|
|
|
|
|
if (!(duration > 0)) return;
|
|
|
|
|
if (!services.config.getBuff('debuff_4')) return;
|
|
|
|
|
const caster = ctx?.caster ?? target;
|
|
|
|
|
services.buffs.add(target, 'debuff_4', caster, 1, { duration });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _knockback(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
parts: number[],
|
|
|
|
|
opts: SkillAtkBuffHitOpts,
|
|
|
|
|
): void {
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
const pixels = Math.max(0, Number(parts[2]) || 0);
|
|
|
|
|
if (!(pixels > 0)) return;
|
|
|
|
|
const lastDamage = opts.lastDamage ?? ctx.vars.get(VAR_LAST_DAMAGE_AMOUNT) ?? 0;
|
|
|
|
|
if (!(lastDamage > 0)) return;
|
|
|
|
|
applySkillTableKnockback(services, ctx, target, pixels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _moveSpeed(services: BattleServices, target: EntityId, bonusWan: number, durationSec: number): void {
|
|
|
|
|
if (bonusWan === 0) return;
|
|
|
|
|
this._ensureAttr(target, services);
|
|
|
|
|
const mul = 1 + bonusWan / 10000;
|
|
|
|
|
services.attributes.addModifier(target, {
|
|
|
|
|
attr: 'move_speed_mul',
|
|
|
|
|
op: 'Mul',
|
|
|
|
|
value: mul,
|
|
|
|
|
source: MOD_SPEED,
|
|
|
|
|
group: MOD_SPEED,
|
|
|
|
|
remain: durationSec > 0 ? durationSec : undefined,
|
|
|
|
|
});
|
|
|
|
|
const move = services.world.getComponent(target, MovementComp);
|
|
|
|
|
if (move) {
|
|
|
|
|
const base = move.baseSpeed > 0 ? move.baseSpeed : move.speed;
|
|
|
|
|
move.speed = Math.max(0, base * mul);
|
|
|
|
|
}
|
|
|
|
|
getMonsterStatusText().play(target, bonusWan > 0 ? '加速' : '减速');
|
|
|
|
|
if (bonusWan > 0 && durationSec > 0) {
|
|
|
|
|
getAllySkillExtraHost().playSpeedFx(target, durationSec);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 配表血量上限:maxHp ×(1+万分比/10000),当前血量按原比例同步。
|
|
|
|
|
* 允许时播 `血量上限` 并飘「血上限」,不播 `怪物加血`;出生加成可关。
|
|
|
|
|
* @param world 实体世界
|
|
|
|
|
* @param target 目标
|
|
|
|
|
* @param coeffWan 上限系数万分比(2000=+20%)
|
|
|
|
|
* @param battle 战斗句柄(用于刷新血条/特效)
|
|
|
|
|
* @param playHeal 是否播血上限表现
|
|
|
|
|
*/
|
|
|
|
|
static applyMaxHpCoeff(world: World, target: EntityId, coeffWan: number, battle?: ProjectBattleHandle | null, playHeal = true): void {
|
|
|
|
|
if (coeffWan === 0) return;
|
|
|
|
|
const health = world.getComponent(target, HealthComp);
|
|
|
|
|
if (!health || !(health.maxHp > 0)) return;
|
|
|
|
|
const oldHp = health.hp;
|
|
|
|
|
const oldMax = health.maxHp;
|
|
|
|
|
const newMax = Math.max(1, Math.floor(oldMax * (1 + coeffWan / 10000)));
|
|
|
|
|
const ratio = oldHp / oldMax;
|
|
|
|
|
const newHp = Math.max(1, Math.min(newMax, Math.floor(newMax * ratio)));
|
|
|
|
|
health.maxHp = newMax;
|
|
|
|
|
health.hp = newHp;
|
|
|
|
|
this._presentHp(target, newHp, newMax, battle, playHeal, STATUS_TEXT_HP_CAP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 刷新血条;加血播 `怪物加血`,血上限播 `血量上限`(不叠加加血)。
|
|
|
|
|
* @param entity 目标
|
|
|
|
|
* @param newHp 变更后当前血量
|
|
|
|
|
* @param newMax 变更后上限
|
|
|
|
|
* @param battle 战斗句柄;缺省用 CurentBattle
|
|
|
|
|
* @param playFx 是否播特效
|
|
|
|
|
* @param statusText 飘字文案;缺省「加血」
|
|
|
|
|
*/
|
|
|
|
|
private static _presentHp(entity: EntityId, newHp: number, newMax: number, battle?: ProjectBattleHandle | null, playFx = true, statusText = '加血'): void {
|
|
|
|
|
const handle = battle ?? (gg.game.CurentBattle as ProjectBattleHandle | null | undefined);
|
|
|
|
|
const node = handle?.getMonsterPresenter?.()?.getEntityNodeMap().get(entity);
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null;
|
|
|
|
|
view?.syncMxHp?.(newHp, newMax);
|
|
|
|
|
if (!playFx) return;
|
|
|
|
|
if (statusText === STATUS_TEXT_HP_CAP) {
|
|
|
|
|
if (!view?.playHpCapEffect) {
|
|
|
|
|
console.warn('[SkillAtkBuff] 缺 playHpCapEffect');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
view.playHpCapEffect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
view?.playHealEffect?.(statusText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _maxHp(services: BattleServices, target: EntityId, coeffWan: number): void {
|
|
|
|
|
this.applyMaxHpCoeff(services.world, target, coeffWan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _peaches(services: BattleServices, ctx: AbilityContext | null, count: number): void {
|
|
|
|
|
const n = Math.max(0, Math.floor(Number(count) || 0));
|
|
|
|
|
if (!(n > 0)) return;
|
|
|
|
|
const ownerId = ctx ? this._ownerId(services, ctx.caster) : 0;
|
|
|
|
|
applySkillAtkBuffBoardEffect(services, {
|
|
|
|
|
kind: 5,
|
|
|
|
|
ownerPlayerId: ownerId,
|
|
|
|
|
peachCount: n,
|
|
|
|
|
target: ctx?.caster,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 攻速万分比走 aspd_add(与攻速符/被动同一通道);负值降攻速。
|
|
|
|
|
* 普攻间隔读 getFinalAttackSpeed,写 AttrId.AttackSpeed Mul 不会生效。
|
|
|
|
|
* @param services 战斗服务
|
|
|
|
|
* @param target 目标塔
|
|
|
|
|
* @param bonusWan 攻速加成万分比(-5000=×0.5)
|
|
|
|
|
* @param durationSec 持续秒;≤0 永久
|
|
|
|
|
*/
|
|
|
|
|
private static _attackSpeed(services: BattleServices, target: EntityId, bonusWan: number, durationSec: number): void {
|
|
|
|
|
if (bonusWan === 0) return;
|
|
|
|
|
this._ensureAttr(target, services);
|
|
|
|
|
services.attributes.addModifier(target, {
|
|
|
|
|
attr: 'aspd_add',
|
|
|
|
|
op: 'Add',
|
|
|
|
|
value: bonusWan,
|
|
|
|
|
source: MOD_ASPD,
|
|
|
|
|
group: MOD_ASPD,
|
|
|
|
|
remain: durationSec > 0 ? durationSec : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _lowerLevel(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
delta: number,
|
|
|
|
|
durationSec: number,
|
|
|
|
|
): void {
|
|
|
|
|
const d = Math.max(1, Math.floor(Number(delta) || 1));
|
|
|
|
|
const ownerId = ctx ? this._ownerId(services, ctx.caster) : 0;
|
|
|
|
|
applySkillAtkBuffBoardEffect(services, {
|
|
|
|
|
kind: 7,
|
|
|
|
|
ownerPlayerId: ownerId,
|
|
|
|
|
target,
|
|
|
|
|
levelDelta: -d,
|
|
|
|
|
durationSec,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _confusion(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
durationSec: number,
|
|
|
|
|
): void {
|
|
|
|
|
const duration = Math.max(0.05, Number(durationSec) || 0);
|
|
|
|
|
if (!services.config.getBuff('debuff_8')) return;
|
|
|
|
|
const caster = ctx?.caster ?? target;
|
|
|
|
|
services.buffs.add(target, 'debuff_8', caster, 1, { duration });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _blockTile(services: BattleServices, ctx: AbilityContext | null): void {
|
|
|
|
|
const ownerId = ctx ? this._ownerId(services, ctx.caster) : 0;
|
|
|
|
|
applySkillAtkBuffBoardEffect(services, { kind: 9, ownerPlayerId: ownerId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _heal(
|
|
|
|
|
services: BattleServices,
|
|
|
|
|
ctx: AbilityContext | null,
|
|
|
|
|
target: EntityId,
|
|
|
|
|
healWan: number,
|
|
|
|
|
): void {
|
|
|
|
|
const wan = Math.max(0, Number(healWan) || 0);
|
|
|
|
|
if (!(wan > 0)) return;
|
|
|
|
|
const health = services.world.getComponent(target, HealthComp);
|
|
|
|
|
if (!health || !(health.maxHp > 0)) return;
|
|
|
|
|
const add = Math.max(1, Math.floor(health.maxHp * wan / 10000));
|
|
|
|
|
health.hp = Math.min(health.maxHp, health.hp + add);
|
|
|
|
|
this._presentHp(target, health.hp, health.maxHp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 攻击距离:只走 modifier。禁止再改 CombatComp.range,否则 sync 进 base 后 getAttr 会加两次。
|
|
|
|
|
* @param services 战斗服务
|
|
|
|
|
* @param target 受益实体
|
|
|
|
|
* @param addRaw ≤16 按格,否则按像素
|
|
|
|
|
* @param durationSec -1 本场永久
|
|
|
|
|
*/
|
|
|
|
|
private static _attackRange(services: BattleServices, target: EntityId, addRaw: number, durationSec: number): void {
|
|
|
|
|
const addVal = Math.floor(Number(addRaw) || 0);
|
|
|
|
|
if (addVal === 0) return;
|
|
|
|
|
const addPx = Math.abs(addVal) <= 16 ? addVal * BattleMapSpec.CELL_SIZE : addVal;
|
|
|
|
|
this._ensureAttr(target, services);
|
|
|
|
|
services.attributes.addModifier(target, {
|
|
|
|
|
attr: AttrId.Range,
|
|
|
|
|
op: 'Add',
|
|
|
|
|
value: addPx,
|
|
|
|
|
source: MOD_RANGE,
|
|
|
|
|
group: MOD_RANGE,
|
|
|
|
|
remain: durationSec < 0 ? undefined : (durationSec > 0 ? durationSec : undefined),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _ensureAttr(target: EntityId, services: BattleServices): void {
|
|
|
|
|
if (!services.world.getComponent(target, AttributeComp)) {
|
|
|
|
|
services.world.addComponent(target, new AttributeComp());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _ownerId(services: BattleServices, entity: EntityId): number {
|
|
|
|
|
const team = services.world.getComponent(entity, TeamComp);
|
|
|
|
|
return team?.ownerId ?? team?.team ?? 0;
|
|
|
|
|
}
|
|
|
|
|
}
|