You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1931 lines
68 KiB
1931 lines
68 KiB
import type { BattleSession } from 'db://assets/mx/module/battle';
|
|
import { BattleEventFactory, BattleTeamId } from 'db://assets/mx/module/battle';
|
|
import { createAbilityContext } from 'db://assets/mx/module/battle/combat/AbilityContext';
|
|
import { VAR_LAST_DAMAGE_AMOUNT } from 'db://assets/mx/module/battle/combat/DamagePipeline';
|
|
import {
|
|
AttrId,
|
|
AttributeComp,
|
|
HealthComp,
|
|
MovementComp,
|
|
TeamComp,
|
|
TransformComp,
|
|
isBajiaoBlowRetreating,
|
|
isYanSoulClaimed,
|
|
} from 'db://assets/mx/module/battle/ecs/Components';
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
import { applySkillTableKnockback } from './AtkHookEffects';
|
|
import { applySkillAtkBuffBoardEffect } from './SkillAtkBuffBoardBridge';
|
|
import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor';
|
|
|
|
/** 被动 result 10 覆盖普攻(由装载服务实现,避免本文件再引用 Loadout) */
|
|
export interface IPassiveAtkOverrideHost {
|
|
/**
|
|
* 覆盖宿主普攻 atk_type / atk_para 并重建 Ability。
|
|
* @param session 战斗会话
|
|
* @param entityId 触发宿主
|
|
* @param atkType 新 atk_type
|
|
* @param atkPara 新 atk_para
|
|
* @param sourceSkillId 被动 Skill.id
|
|
*/
|
|
applyNormalAtkOverride(session: BattleSession, entityId: EntityId, atkType: string, atkPara: string, sourceSkillId: number): void;
|
|
}
|
|
|
|
/** bindFromLoadout 只需武器被动槽 */
|
|
interface PassiveLoadoutBind {
|
|
/** 武器技能 Skill.id */
|
|
weaponSkillId: number;
|
|
/** 武器技能是否为主动 */
|
|
weaponSkillActive: boolean;
|
|
}
|
|
|
|
/** 被动触发条件类型 */
|
|
export enum PassiveConditionKind {
|
|
/** 造成伤害触发,参数:概率万分比 */
|
|
OnDealDamage = 1,
|
|
/** 默认生效(部署时触发一次) */
|
|
Always = 2,
|
|
/** 连续攻击同一单位 */
|
|
SameTarget = 3,
|
|
/** 首次命中某单位 */
|
|
FirstHitTarget = 4,
|
|
/** 击杀,参数:概率万分比 */
|
|
OnKill = 5,
|
|
/** 技能释放次数(含普攻 AbilityCast),参数:次数 */
|
|
SkillCastCount = 6,
|
|
}
|
|
|
|
/** 被动触发结果类型 */
|
|
export enum PassiveResultKind {
|
|
/** 剑阵:半径像素,间隔秒,伤害系数万分比,持续秒 */
|
|
SwordArray = 1,
|
|
/** 提升攻速:加成万分比,最大层数,持续秒 */
|
|
Aspd = 2,
|
|
/** 提升射程:倍数 */
|
|
RangeMul = 3,
|
|
/** 击退:距离像素 */
|
|
Knockback = 4,
|
|
/** 添加桃子:数量 */
|
|
Peach = 5,
|
|
/** 眩晕:时长秒 */
|
|
Stun = 6,
|
|
/** 爆炸火球多向穿透:最大数量(角=360/数量),伤害系数万分比,飞行速度像素/秒,飞行时间秒 */
|
|
HomingDragon = 7,
|
|
/** 上毒:间隔秒,伤害系数万分比,持续秒 */
|
|
Poison = 8,
|
|
/** 直线火龙:飞行速度像素/秒,伤害系数万分比,持续秒 */
|
|
LineDragon = 9,
|
|
/** 修改普攻:覆盖宿主普攻 Skill.atk_type / atk_para */
|
|
ModifyAttack = 10,
|
|
/** 直线剑气:数量,间隔秒,伤害系数万分比 */
|
|
SwordWave = 11,
|
|
/** 闪电链:蔓延范围像素,蔓延数量,伤害系数万分比 */
|
|
LightningChain = 12,
|
|
/** 爆炸:圆形半径像素,伤害系数万分比 */
|
|
Explosion = 14,
|
|
}
|
|
|
|
/** 攻速 modifier group 前缀 */
|
|
const ASPD_GROUP = 'passive_aspd_';
|
|
/** 射程 modifier group 前缀 */
|
|
const RANGE_GROUP = 'passive_range_';
|
|
/** 毒跳伤害类型(不回触发 condition) */
|
|
const POISON_DMG = 'passive_poison';
|
|
/** 剑阵跳伤类型(不回触发 condition) */
|
|
const SWORD_ARRAY_DMG = 'passive_sword_array';
|
|
/** 爆炸火球伤害类型(不回触发 condition) */
|
|
const HOMING_DRAGON_DMG = 'passive_homing_dragon';
|
|
/** 直线火龙伤害类型(不回触发 condition) */
|
|
const LINE_DRAGON_DMG = 'passive_line_dragon';
|
|
/** 直线剑气伤害类型(不回触发 condition) */
|
|
const SWORD_WAVE_DMG = 'passive_sword_wave';
|
|
/** 闪电链伤害类型(不回触发 condition) */
|
|
const LIGHTNING_CHAIN_DMG = 'passive_lightning_chain';
|
|
/** 爆炸伤害类型(不回触发 condition) */
|
|
const EXPLOSION_DMG = 'passive_explosion';
|
|
|
|
/**
|
|
* 是否为被动 result 造成的伤害(不回触发任何 condition)。
|
|
* @param damageType 伤害类型(约定 `passive_` 前缀)
|
|
*/
|
|
export function isPassiveResultDamage(damageType?: string): boolean {
|
|
return !!damageType && damageType.indexOf('passive_') === 0;
|
|
}
|
|
|
|
/** 剑阵表现事件名 */
|
|
export const SWORD_ARRAY_EVENT = 'SwordArrayApplied';
|
|
/** 爆炸火球表现事件名 */
|
|
export const HOMING_DRAGON_EVENT = 'HomingDragonApplied';
|
|
/** 直线火龙表现事件名 */
|
|
export const LINE_DRAGON_EVENT = 'LineDragonApplied';
|
|
/** 直线剑气表现事件名 */
|
|
export const SWORD_WAVE_EVENT = 'SwordWaveApplied';
|
|
/** 闪电链表现事件名 */
|
|
export const LIGHTNING_CHAIN_EVENT = 'LightningChainApplied';
|
|
/** 直线火龙预制体宽 100,命中半长(再加半格占地) */
|
|
const LINE_DRAGON_HALF_LEN = 50;
|
|
/** 直线火龙预制体高 30,命中半宽(再加半格占地) */
|
|
const LINE_DRAGON_HALF_WID = 15;
|
|
/** 剑气飞行速度(配表无速度段;与直线火龙常见 400 同量级) */
|
|
const SWORD_WAVE_SPEED = 400;
|
|
/** 轩辕剑刀光动画 */
|
|
const SWORD_WAVE_ANIM_BLUE = '剑气蓝';
|
|
/** 吴钩双剑刀光动画 */
|
|
const SWORD_WAVE_ANIM_RED = '剑气红';
|
|
/** 玉帝刀光碰撞盒宽 100,命中半长 */
|
|
const SWORD_WAVE_HALF_LEN = 50;
|
|
/** 玉帝刀光碰撞盒高 200,命中半宽 */
|
|
const SWORD_WAVE_HALF_WID = 100;
|
|
/** 出界判定外扩(半长 + 一格) */
|
|
const SWORD_WAVE_BOUND_PAD = SWORD_WAVE_HALF_LEN + 80;
|
|
/** 电母「普通雷电」资源时长 */
|
|
const LIGHTNING_BOLT_ANIM_SEC = 0.5333;
|
|
/** 相对资源原速(与老项目传导倍率一致) */
|
|
const LIGHTNING_ANIM_SPEED = 2;
|
|
/** 两跳间隔(逻辑秒) */
|
|
const LIGHTNING_HOP_SEC = LIGHTNING_BOLT_ANIM_SEC / LIGHTNING_ANIM_SPEED;
|
|
|
|
/** 剑阵表现 cue */
|
|
export interface SwordArrayCue {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 落点目标(可 0) */
|
|
target: EntityId;
|
|
/** 持续秒 */
|
|
duration: number;
|
|
/** 半径像素(表现按预制体边长缩放) */
|
|
radius: number;
|
|
}
|
|
|
|
/** 爆炸火球一枚(径向直线) */
|
|
export interface HomingDragonMissile {
|
|
/** 方向单位 X */
|
|
ux: number;
|
|
/** 方向单位 Y */
|
|
uy: number;
|
|
/** 终点逻辑 X */
|
|
destX: number;
|
|
/** 终点逻辑 Y */
|
|
destY: number;
|
|
/** 飞行秒 */
|
|
duration: number;
|
|
}
|
|
|
|
/** 爆炸火球表现 cue */
|
|
export interface HomingDragonCue {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 触发生成点实体(击杀目标等,可已死) */
|
|
originTarget: EntityId;
|
|
/** 生成点逻辑 X */
|
|
originX: number;
|
|
/** 生成点逻辑 Y */
|
|
originY: number;
|
|
/** 各枚火球 */
|
|
missiles: HomingDragonMissile[];
|
|
}
|
|
|
|
/** 直线火龙表现 cue */
|
|
export interface LineDragonCue {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 触发朝向目标 */
|
|
target: EntityId;
|
|
/** 起点逻辑 X(施法者) */
|
|
originX: number;
|
|
/** 起点逻辑 Y */
|
|
originY: number;
|
|
/** 终点逻辑 X */
|
|
destX: number;
|
|
/** 终点逻辑 Y */
|
|
destY: number;
|
|
/** 飞行秒 */
|
|
duration: number;
|
|
}
|
|
|
|
/** 直线剑气表现 cue(玉帝刀光) */
|
|
export interface SwordWaveCue {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 触发朝向目标 */
|
|
target: EntityId;
|
|
/** 起点逻辑 X */
|
|
originX: number;
|
|
/** 起点逻辑 Y */
|
|
originY: number;
|
|
/** 方向单位 X */
|
|
ux: number;
|
|
/** 方向单位 Y */
|
|
uy: number;
|
|
/** 飞行速度像素/秒 */
|
|
speed: number;
|
|
/** Spine 动画:轩辕剑「剑气蓝」/ 吴钩双剑「剑气红」 */
|
|
animation: string;
|
|
}
|
|
|
|
/** 闪电链一跳表现 cue(电母-大招 / 电母-大招受击) */
|
|
export interface LightningChainCue {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 本跳起点实体(可已死) */
|
|
from: EntityId;
|
|
/** 本跳落点实体 */
|
|
to: EntityId;
|
|
/** 起点逻辑 X */
|
|
fromX: number;
|
|
/** 起点逻辑 Y */
|
|
fromY: number;
|
|
/** 落点逻辑 X */
|
|
toX: number;
|
|
/** 落点逻辑 Y */
|
|
toY: number;
|
|
/** 是否拉电弧(落点与起点重合则否) */
|
|
showBolt: boolean;
|
|
}
|
|
|
|
/** 单条被动绑定 */
|
|
export interface PassiveSkillBind {
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** condition 首段类型 */
|
|
conditionKind: number;
|
|
/** condition 参数(不含类型) */
|
|
conditionParams: number[];
|
|
/** result 首段类型 */
|
|
resultKind: number;
|
|
/** result 参数(不含类型) */
|
|
resultParams: number[];
|
|
/** 冷却剩余(秒);0=可触发 */
|
|
cdLeft: number;
|
|
/** 配表冷却(秒) */
|
|
cd: number;
|
|
/** 上次命中目标(condition 3) */
|
|
lastTarget: EntityId;
|
|
/** 已首次命中过的目标(condition 4) */
|
|
hitTargets: EntityId[];
|
|
/** 已累计释放次数(condition 6) */
|
|
castCount: number;
|
|
/** 攻速当前层数(result 2) */
|
|
stackCount: number;
|
|
/** 攻速剩余持续(秒) */
|
|
stackLeft: number;
|
|
/** Skill.atk_buff1 原文(空=无) */
|
|
atkBuff1: string;
|
|
/** Skill.atk_buff2 原文(空=无) */
|
|
atkBuff2: string;
|
|
}
|
|
|
|
/** 毒 DoT */
|
|
interface PoisonDot {
|
|
/** 施毒者 */
|
|
caster: EntityId;
|
|
/** 中毒目标 */
|
|
target: EntityId;
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** 跳伤间隔(秒) */
|
|
interval: number;
|
|
/** 伤害倍率(攻击力) */
|
|
scale: number;
|
|
/** 剩余持续(秒) */
|
|
left: number;
|
|
/** 距下次跳伤(秒) */
|
|
wait: number;
|
|
}
|
|
|
|
/** 剑阵持续伤害区 */
|
|
interface SwordArrayZone {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** 圆心逻辑 X */
|
|
x: number;
|
|
/** 圆心逻辑 Y */
|
|
y: number;
|
|
/** 半径像素 */
|
|
radius: number;
|
|
/** 跳伤间隔(秒) */
|
|
interval: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 剩余持续(秒) */
|
|
left: number;
|
|
/** 距下次跳伤(秒) */
|
|
wait: number;
|
|
}
|
|
|
|
/** 直线穿透火球/火龙在飞 */
|
|
interface LineDragonFlight {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** 当前逻辑 X */
|
|
x: number;
|
|
/** 当前逻辑 Y */
|
|
y: number;
|
|
/** 方向单位 X */
|
|
ux: number;
|
|
/** 方向单位 Y */
|
|
uy: number;
|
|
/** 飞行速度像素/秒 */
|
|
speed: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 剩余飞行(秒) */
|
|
left: number;
|
|
/** 伤害类型(不回触发 condition) */
|
|
dmgType: string;
|
|
/** 已命中实体(每目标一次) */
|
|
hitIds: EntityId[];
|
|
}
|
|
|
|
/** 待按间隔放出的剑气 */
|
|
interface SwordWavePending {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 朝向参考目标 */
|
|
target: EntityId;
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** 起点逻辑 X */
|
|
originX: number;
|
|
/** 起点逻辑 Y */
|
|
originY: number;
|
|
/** 方向单位 X */
|
|
ux: number;
|
|
/** 方向单位 Y */
|
|
uy: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 距放出(秒) */
|
|
wait: number;
|
|
}
|
|
|
|
/** 直线剑气在飞(穿出地图边界后回收) */
|
|
interface SwordWaveFlight {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** 当前逻辑 X */
|
|
x: number;
|
|
/** 当前逻辑 Y */
|
|
y: number;
|
|
/** 方向单位 X */
|
|
ux: number;
|
|
/** 方向单位 Y */
|
|
uy: number;
|
|
/** 飞行速度像素/秒 */
|
|
speed: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 已命中实体(每目标一次) */
|
|
hitIds: EntityId[];
|
|
}
|
|
|
|
/** 进行中的闪电链 */
|
|
interface LightningChainRun {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** Skill.id */
|
|
skillId: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 蔓延半径像素 */
|
|
range: number;
|
|
/** 剩余可打目标数 */
|
|
remain: number;
|
|
/** 距结算当前 pending(秒) */
|
|
wait: number;
|
|
/** 待结算伤害目标(0=无) */
|
|
pending: EntityId;
|
|
/** 当前搜索原点逻辑 X */
|
|
fromX: number;
|
|
/** 当前搜索原点逻辑 Y */
|
|
fromY: number;
|
|
/** 上一跳起点实体(表现用) */
|
|
fromId: EntityId;
|
|
/** 已蔓延实体(不重复) */
|
|
visited: EntityId[];
|
|
}
|
|
|
|
/**
|
|
* 实体被动技能运行时(Skill.type=2)。
|
|
* 逻辑 result 在此执行;需表现的类型发 cue 或告警。
|
|
*/
|
|
export class PassiveSkillHost {
|
|
private readonly _binds: { entityId: EntityId; list: PassiveSkillBind[] }[] = [];
|
|
private readonly _dots: PoisonDot[] = [];
|
|
private readonly _zones: SwordArrayZone[] = [];
|
|
private readonly _lineDragons: LineDragonFlight[] = [];
|
|
private readonly _wavePending: SwordWavePending[] = [];
|
|
private readonly _waves: SwordWaveFlight[] = [];
|
|
private readonly _chains: LightningChainRun[] = [];
|
|
private _session: BattleSession | null = null;
|
|
/** result 结算中(含其产生的伤害/击杀),禁止再判任何 condition */
|
|
private _resultGate = 0;
|
|
/** 普攻覆盖(装载服务注册) */
|
|
private _atkOverride: IPassiveAtkOverrideHost | null = null;
|
|
|
|
/**
|
|
* 注册/清除普攻覆盖器。
|
|
* @param host 装载服务;null 清除
|
|
*/
|
|
bindAtkOverride(host: IPassiveAtkOverrideHost | null): void {
|
|
this._atkOverride = host;
|
|
}
|
|
|
|
/** 清空全部绑定(开局 / 销毁) */
|
|
clear(): void {
|
|
this._binds.length = 0;
|
|
this._dots.length = 0;
|
|
this._zones.length = 0;
|
|
this._lineDragons.length = 0;
|
|
this._wavePending.length = 0;
|
|
this._waves.length = 0;
|
|
this._chains.length = 0;
|
|
this._session = null;
|
|
this._atkOverride = null;
|
|
}
|
|
|
|
/**
|
|
* 卸下实体被动并清属性/毒。
|
|
* @param entityId 塔实体
|
|
*/
|
|
unbind(entityId: EntityId): void {
|
|
this._stripMods(entityId);
|
|
this._removeDotsByCaster(entityId);
|
|
this._removeZonesByCaster(entityId);
|
|
this._removeWavesByCaster(entityId);
|
|
this._removeChainsByCaster(entityId);
|
|
const idx = this._indexOf(entityId);
|
|
if (idx >= 0) this._binds.splice(idx, 1);
|
|
}
|
|
|
|
/**
|
|
* 按武器被动装载;condition=2 在此触发一次。
|
|
* @param session 战斗会话
|
|
* @param entityId 塔实体
|
|
* @param loadout 三槽装载
|
|
*/
|
|
bindFromLoadout(session: BattleSession, entityId: EntityId, loadout: PassiveLoadoutBind): void {
|
|
this._session = session;
|
|
this.unbind(entityId);
|
|
const list: PassiveSkillBind[] = [];
|
|
if (loadout.weaponSkillId > 0 && !loadout.weaponSkillActive) {
|
|
const bind = this._parseSkill(loadout.weaponSkillId);
|
|
if (bind) list.push(bind);
|
|
}
|
|
if (list.length === 0) return;
|
|
this._binds.push({ entityId, list });
|
|
this._fireAlways(session, entityId, list);
|
|
}
|
|
|
|
/**
|
|
* 查询实体已绑定被动。
|
|
* @param entityId 塔实体
|
|
*/
|
|
getBinds(entityId: EntityId): readonly PassiveSkillBind[] {
|
|
const idx = this._indexOf(entityId);
|
|
return idx >= 0 ? this._binds[idx].list : [];
|
|
}
|
|
|
|
/**
|
|
* 推进冷却、攻速层数、毒跳、剑阵与火球。
|
|
* @param dt 逻辑秒
|
|
*/
|
|
tick(dt: number): void {
|
|
for (let i = 0; i < this._binds.length; i++) {
|
|
const entityId = this._binds[i].entityId;
|
|
const list = this._binds[i].list;
|
|
for (let j = 0; j < list.length; j++) {
|
|
const b = list[j];
|
|
if (b.cdLeft > 0) b.cdLeft = Math.max(0, b.cdLeft - dt);
|
|
if (b.stackLeft > 0) {
|
|
b.stackLeft = Math.max(0, b.stackLeft - dt);
|
|
if (b.stackLeft <= 0) {
|
|
b.stackCount = 0;
|
|
this._session?.attributes.removeModifiersByGroup(entityId, ASPD_GROUP + b.skillId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this._tickDots(dt);
|
|
this._tickZones(dt);
|
|
this._tickLineDragons(dt);
|
|
this._tickSwordWavePending(dt);
|
|
this._tickSwordWaves(dt);
|
|
this._tickLightningChains(dt);
|
|
}
|
|
|
|
/**
|
|
* 目标消失时清状态,避免实体 id 复用误触发。
|
|
* @param target 消失的实体
|
|
*/
|
|
forgetTarget(target: EntityId): void {
|
|
if (!(target > 0)) return;
|
|
for (let i = 0; i < this._binds.length; i++) {
|
|
const list = this._binds[i].list;
|
|
for (let j = 0; j < list.length; j++) {
|
|
const b = list[j];
|
|
if (b.lastTarget === target) b.lastTarget = 0;
|
|
this._removeHit(b, target);
|
|
}
|
|
}
|
|
for (let i = this._dots.length - 1; i >= 0; i--) {
|
|
if (this._dots[i].target === target) this._dots.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 造成伤害后尝试 condition 1/4。
|
|
* @param session 战斗会话
|
|
* @param caster 伤害来源
|
|
* @param target 受击目标
|
|
* @param amount 本次伤害量
|
|
*/
|
|
onDealDamage(session: BattleSession, caster: EntityId, target: EntityId, amount: number): boolean {
|
|
if (this._resultGate > 0) return false;
|
|
const idx = this._indexOf(caster);
|
|
if (idx < 0) return false;
|
|
const list = this._binds[idx].list;
|
|
let fired = false;
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (this._tryOnHit(session, caster, target, list[i], amount)) fired = true;
|
|
}
|
|
return fired;
|
|
}
|
|
|
|
/**
|
|
* 击杀后尝试 condition 5。
|
|
* @param session 战斗会话
|
|
* @param caster 击杀者
|
|
* @param target 死者
|
|
*/
|
|
onKill(session: BattleSession, caster: EntityId, target: EntityId): boolean {
|
|
if (this._resultGate > 0) return false;
|
|
const idx = this._indexOf(caster);
|
|
if (idx < 0) return false;
|
|
const list = this._binds[idx].list;
|
|
let fired = false;
|
|
for (let i = 0; i < list.length; i++) {
|
|
const b = list[i];
|
|
if (b.conditionKind !== PassiveConditionKind.OnKill) continue;
|
|
if (b.cdLeft > 0) continue;
|
|
if (!this._rollWan(session, b.conditionParams[0], b.skillId)) continue;
|
|
this._fire(session, caster, target, b, 0);
|
|
fired = true;
|
|
}
|
|
return fired;
|
|
}
|
|
|
|
/**
|
|
* 技能释放后尝试 condition 3/6(按主目标,一击一次)。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 主目标(可 0)
|
|
*/
|
|
onAbilityCast(session: BattleSession, caster: EntityId, target: EntityId): boolean {
|
|
const idx = this._indexOf(caster);
|
|
if (idx < 0) return false;
|
|
const list = this._binds[idx].list;
|
|
let fired = false;
|
|
for (let i = 0; i < list.length; i++) {
|
|
const b = list[i];
|
|
if (b.conditionKind === PassiveConditionKind.SameTarget) {
|
|
if (this._trySameTarget(session, caster, target, b)) fired = true;
|
|
continue;
|
|
}
|
|
if (b.conditionKind !== PassiveConditionKind.SkillCastCount) continue;
|
|
if (b.cdLeft > 0) continue;
|
|
const need = b.conditionParams[0];
|
|
if (!(need > 0)) {
|
|
console.log(`[PassiveSkill] condition 6 次数无效 skill=${b.skillId}`);
|
|
continue;
|
|
}
|
|
b.castCount += 1;
|
|
if (b.castCount < need) continue;
|
|
b.castCount = 0;
|
|
this._fire(session, caster, target, b, 0);
|
|
fired = true;
|
|
}
|
|
return fired;
|
|
}
|
|
|
|
/** 部署时触发 condition 2 */
|
|
private _fireAlways(session: BattleSession, entityId: EntityId, list: PassiveSkillBind[]): void {
|
|
for (let i = 0; i < list.length; i++) {
|
|
const b = list[i];
|
|
if (b.conditionKind !== PassiveConditionKind.Always) continue;
|
|
this._fire(session, entityId, 0, b, 0);
|
|
}
|
|
}
|
|
|
|
/** 命中判定 condition 1/4 */
|
|
private _tryOnHit(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, amount: number): boolean {
|
|
const kind = bind.conditionKind;
|
|
if (kind === PassiveConditionKind.OnDealDamage) {
|
|
if (bind.cdLeft > 0) return false;
|
|
if (!this._rollWan(session, bind.conditionParams[0], bind.skillId)) return false;
|
|
this._fire(session, caster, target, bind, amount);
|
|
return true;
|
|
}
|
|
if (kind === PassiveConditionKind.FirstHitTarget) {
|
|
if (!(target > 0) || this._hasHit(bind, target)) return false;
|
|
this._markHit(bind, target);
|
|
if (bind.cdLeft > 0) return false;
|
|
this._fire(session, caster, target, bind, amount);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* condition 3:连续出手主目标相同则叠层。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 主目标
|
|
* @param bind 绑定
|
|
*/
|
|
private _trySameTarget(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind): boolean {
|
|
const same = bind.lastTarget === target && target > 0;
|
|
if (target > 0) bind.lastTarget = target;
|
|
if (!same || bind.cdLeft > 0) return false;
|
|
this._fire(session, caster, target, bind, 0);
|
|
return true;
|
|
}
|
|
|
|
/** 先进入冷却再执行 result;result 期间抬闸,其伤害/击杀不再判 condition */
|
|
private _fire(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, lastDamage: number): void {
|
|
bind.cdLeft = bind.cd > 0 ? bind.cd : 0;
|
|
this._beginResultGate();
|
|
try {
|
|
this._applyResult(session, caster, target, bind, lastDamage);
|
|
} finally {
|
|
this._endResultGate();
|
|
}
|
|
}
|
|
|
|
/** result 结算开始 */
|
|
private _beginResultGate(): void {
|
|
this._resultGate += 1;
|
|
}
|
|
|
|
/** result 结算结束 */
|
|
private _endResultGate(): void {
|
|
this._resultGate = Math.max(0, this._resultGate - 1);
|
|
}
|
|
|
|
/**
|
|
* result 产生的伤害。fromPassiveResult 跳过入口钩子,闸内不回触发 condition。
|
|
* @param session 战斗会话
|
|
* @param caster 伤害来源
|
|
* @param target 受击目标
|
|
* @param skillId 被动 Skill.id
|
|
* @param scale 伤害倍率
|
|
* @param damageType `passive_` 前缀类型
|
|
*/
|
|
private _dealResultDamage(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, scale: number, damageType: string): void {
|
|
this._beginResultGate();
|
|
try {
|
|
const ctx = createAbilityContext(session, caster, `passive_${skillId}`, [target]);
|
|
session.damage.dealToTarget(caster, target, {
|
|
scale,
|
|
allowCrit: false,
|
|
damageType,
|
|
fromPassiveResult: true,
|
|
}, ctx);
|
|
session.events.flush();
|
|
} finally {
|
|
this._endResultGate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 万分比掷骰;缺参则失败并告警。
|
|
* @param session 战斗会话
|
|
* @param wan 万分比
|
|
* @param skillId 用于告警
|
|
*/
|
|
private _rollWan(session: BattleSession, wan: number, skillId: number): boolean {
|
|
if (!(wan >= 0)) {
|
|
console.log(`[PassiveSkill] 概率无效 skill=${skillId} wan=${wan}`);
|
|
return false;
|
|
}
|
|
if (wan >= 10000) return true;
|
|
return session.rng.next() * 10000 < wan;
|
|
}
|
|
|
|
/** 是否已首次命中过该目标 */
|
|
private _hasHit(bind: PassiveSkillBind, target: EntityId): boolean {
|
|
const ids = bind.hitTargets;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
if (ids[i] === target) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 记录首次命中 */
|
|
private _markHit(bind: PassiveSkillBind, target: EntityId): void {
|
|
bind.hitTargets.push(target);
|
|
}
|
|
|
|
/** 目标消失时移除首次命中记录 */
|
|
private _removeHit(bind: PassiveSkillBind, target: EntityId): void {
|
|
const ids = bind.hitTargets;
|
|
for (let i = ids.length - 1; i >= 0; i--) {
|
|
if (ids[i] === target) ids.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 按 result 类型执行逻辑效果 */
|
|
private _applyResult(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, lastDamage: number): void {
|
|
const kind = bind.resultKind;
|
|
const p = bind.resultParams;
|
|
switch (kind) {
|
|
case PassiveResultKind.Aspd:
|
|
this._applyAspd(session, caster, bind, p);
|
|
return;
|
|
case PassiveResultKind.RangeMul:
|
|
this._applyRangeMul(session, caster, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.Knockback:
|
|
this._applyKnockback(session, caster, target, lastDamage, p, bind.skillId);
|
|
return;
|
|
case PassiveResultKind.Peach:
|
|
this._applyPeach(session, caster, p, bind.skillId);
|
|
return;
|
|
case PassiveResultKind.Stun:
|
|
this._applyStun(session, caster, target, p, bind.skillId);
|
|
return;
|
|
case PassiveResultKind.Poison:
|
|
this._applyPoison(caster, target, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.SwordArray:
|
|
this._applySwordArray(session, caster, target, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.HomingDragon:
|
|
this._applyHomingDragon(session, caster, target, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.LineDragon:
|
|
this._applyLineDragon(session, caster, target, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.ModifyAttack:
|
|
this._applyModifyAttack(session, caster, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.SwordWave:
|
|
this._applySwordWave(session, caster, target, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.LightningChain:
|
|
this._applyLightningChain(session, caster, target, bind.skillId, p);
|
|
return;
|
|
case PassiveResultKind.Explosion:
|
|
this._applyExplosion(session, caster, target, bind, p);
|
|
return;
|
|
default:
|
|
console.log(`[PassiveSkill] 未知 result skill=${bind.skillId} kind=${kind}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 覆盖宿主普攻 atk_type / atk_para。
|
|
* result 参数:atk_type, atk_para...
|
|
* @param session 战斗会话
|
|
* @param caster 触发宿主
|
|
* @param skillId 被动 Skill.id
|
|
* @param p atk_type 与 atk_para 各段
|
|
*/
|
|
private _applyModifyAttack(session: BattleSession, caster: EntityId, skillId: number, p: number[]): void {
|
|
const atkType = Math.floor(p[0] || 0);
|
|
if (!(atkType > 0)) {
|
|
console.log(`[PassiveSkill] result 10 缺 atk_type skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
if (p.length < 2) {
|
|
console.log(`[PassiveSkill] result 10 缺 atk_para skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
if (!this._atkOverride) {
|
|
console.log(`[PassiveSkill] result 10 无装载覆盖器 skill=${skillId}`);
|
|
return;
|
|
}
|
|
this._atkOverride.applyNormalAtkOverride(session, caster, String(atkType), p.slice(1).join(','), skillId);
|
|
}
|
|
|
|
/**
|
|
* 攻速:层数叠加,每触发刷新持续。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param bind 绑定
|
|
* @param p 加成万分比, 最大层数, 持续秒
|
|
*/
|
|
private _applyAspd(session: BattleSession, caster: EntityId, bind: PassiveSkillBind, p: number[]): void {
|
|
const bonusWan = p[0];
|
|
const maxStack = p[1];
|
|
const dur = p[2];
|
|
if (!(bonusWan > 0) || !(maxStack > 0) || !(dur > 0)) {
|
|
console.log(`[PassiveSkill] result 2 参数无效 skill=${bind.skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
this._ensureAttr(session, caster);
|
|
bind.stackCount = Math.min(Math.floor(maxStack), bind.stackCount + 1);
|
|
bind.stackLeft = dur;
|
|
session.attributes.addModifier(caster, {
|
|
attr: 'aspd_add',
|
|
op: 'Add',
|
|
value: bonusWan * bind.stackCount,
|
|
source: ASPD_GROUP + bind.skillId,
|
|
group: ASPD_GROUP + bind.skillId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 射程乘倍(永久,同 skill 覆盖)。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param skillId Skill.id
|
|
* @param p 倍数
|
|
*/
|
|
private _applyRangeMul(session: BattleSession, caster: EntityId, skillId: number, p: number[]): void {
|
|
const mul = p[0];
|
|
if (!(mul > 0)) {
|
|
console.log(`[PassiveSkill] result 3 倍数无效 skill=${skillId} mul=${mul}`);
|
|
return;
|
|
}
|
|
this._ensureAttr(session, caster);
|
|
session.attributes.addModifier(caster, {
|
|
attr: AttrId.Range,
|
|
op: 'Mul',
|
|
value: mul,
|
|
source: RANGE_GROUP + skillId,
|
|
group: RANGE_GROUP + skillId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 击退当前目标。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 目标
|
|
* @param lastDamage 本次伤害
|
|
* @param p 距离像素
|
|
* @param skillId Skill.id
|
|
*/
|
|
private _applyKnockback(session: BattleSession, caster: EntityId, target: EntityId, lastDamage: number, p: number[], skillId: number): void {
|
|
const pixels = p[0];
|
|
if (!(pixels > 0)) {
|
|
console.log(`[PassiveSkill] result 4 距离无效 skill=${skillId} px=${pixels}`);
|
|
return;
|
|
}
|
|
if (!(target > 0) || !session.world.isAlive(target)) return;
|
|
if (!(lastDamage > 0)) {
|
|
console.log(`[PassiveSkill] result 4 无伤害 skill=${skillId}`);
|
|
return;
|
|
}
|
|
const ctx = createAbilityContext(session, caster, `passive_${skillId}`, [target]);
|
|
ctx.vars.set(VAR_LAST_DAMAGE_AMOUNT, lastDamage);
|
|
applySkillTableKnockback(session, ctx, target, pixels);
|
|
}
|
|
|
|
/**
|
|
* 给施法者玩家加桃子。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param p 数量
|
|
* @param skillId Skill.id
|
|
*/
|
|
private _applyPeach(session: BattleSession, caster: EntityId, p: number[], skillId: number): void {
|
|
const n = Math.floor(p[0]);
|
|
if (!(n > 0)) {
|
|
console.log(`[PassiveSkill] result 5 数量无效 skill=${skillId} n=${p[0]}`);
|
|
return;
|
|
}
|
|
const team = session.world.getComponent(caster, TeamComp);
|
|
applySkillAtkBuffBoardEffect(session, {
|
|
kind: 5,
|
|
ownerPlayerId: team?.ownerId ?? team?.team ?? 0,
|
|
peachCount: n,
|
|
target: caster,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 眩晕目标(debuff_4)。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 目标
|
|
* @param p 时长秒
|
|
* @param skillId Skill.id
|
|
*/
|
|
private _applyStun(session: BattleSession, caster: EntityId, target: EntityId, p: number[], skillId: number): void {
|
|
const dur = p[0];
|
|
if (!(dur > 0)) {
|
|
console.log(`[PassiveSkill] result 6 时长无效 skill=${skillId} dur=${dur}`);
|
|
return;
|
|
}
|
|
if (!(target > 0) || !session.world.isAlive(target)) return;
|
|
if (!session.config.getBuff('debuff_4')) {
|
|
console.log(`[PassiveSkill] 缺 buff debuff_4 skill=${skillId}`);
|
|
return;
|
|
}
|
|
session.buffs.add(target, 'debuff_4', caster, 1, { duration: dur });
|
|
}
|
|
|
|
/**
|
|
* 上毒:立即首跳,再按间隔跳至结束。
|
|
* @param caster 施法者
|
|
* @param target 目标
|
|
* @param skillId Skill.id
|
|
* @param p 间隔秒, 系数万分比, 持续秒
|
|
*/
|
|
private _applyPoison(caster: EntityId, target: EntityId, skillId: number, p: number[]): void {
|
|
const interval = p[0];
|
|
const coeffWan = p[1];
|
|
const dur = p[2];
|
|
if (!(interval > 0) || !(coeffWan > 0) || !(dur > 0)) {
|
|
console.log(`[PassiveSkill] result 8 参数无效 skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
if (!(target > 0)) return;
|
|
const scale = coeffWan / 10000;
|
|
for (let i = 0; i < this._dots.length; i++) {
|
|
const d = this._dots[i];
|
|
if (d.caster === caster && d.target === target && d.skillId === skillId) {
|
|
d.interval = interval;
|
|
d.scale = scale;
|
|
d.left = dur;
|
|
d.wait = 0;
|
|
return;
|
|
}
|
|
}
|
|
this._dots.push({ caster, target, skillId, interval, scale, left: dur, wait: 0 });
|
|
}
|
|
|
|
/**
|
|
* 剑阵:落点半径内周期伤害,并发表现 cue。同塔同技能只留一团,再次触发刷新。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 落点目标
|
|
* @param skillId Skill.id
|
|
* @param p 半径像素, 间隔秒, 系数万分比, 持续秒
|
|
*/
|
|
private _applySwordArray(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void {
|
|
const radius = p[0];
|
|
const interval = p[1];
|
|
const coeffWan = p[2];
|
|
const dur = p[3];
|
|
if (!(radius > 0) || !(interval > 0) || !(coeffWan > 0) || !(dur > 0)) {
|
|
console.log(`[PassiveSkill] result 1 参数无效 skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
const pos = this._logicPos(session, target > 0 ? target : caster);
|
|
if (!pos) {
|
|
console.log(`[PassiveSkill] result 1 无落点 skill=${skillId}`);
|
|
return;
|
|
}
|
|
const scale = coeffWan / 10000;
|
|
const existed = this._findZone(caster, skillId);
|
|
if (existed) {
|
|
existed.x = pos.x;
|
|
existed.y = pos.y;
|
|
existed.radius = radius;
|
|
existed.interval = interval;
|
|
existed.scale = scale;
|
|
existed.left = dur;
|
|
existed.wait = 0;
|
|
} else {
|
|
this._zones.push({
|
|
caster,
|
|
skillId,
|
|
x: pos.x,
|
|
y: pos.y,
|
|
radius,
|
|
interval,
|
|
scale,
|
|
left: dur,
|
|
wait: 0,
|
|
});
|
|
}
|
|
const cue: SwordArrayCue = { caster, target, duration: dur, radius };
|
|
session.events.emit(BattleEventFactory.custom(SWORD_ARRAY_EVENT, cue));
|
|
}
|
|
|
|
/**
|
|
* 爆炸火球:击杀点按 360/数量 均分方向直线穿透,飞满配表时间后结束。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param originTarget 触发落点实体
|
|
* @param skillId Skill.id
|
|
* @param p 最大数量, 系数万分比, 飞行速度像素/秒, 飞行时间秒
|
|
*/
|
|
private _applyHomingDragon(session: BattleSession, caster: EntityId, originTarget: EntityId, skillId: number, p: number[]): void {
|
|
const maxCount = Math.floor(p[0]);
|
|
const coeffWan = p[1];
|
|
const speed = p[2];
|
|
const dur = p[3];
|
|
if (!(maxCount > 0) || !(coeffWan > 0) || !(speed > 0) || !(dur > 0)) {
|
|
console.log(`[PassiveSkill] result 7 参数无效 skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
const origin = this._logicPos(session, originTarget > 0 ? originTarget : caster)
|
|
?? this._logicPos(session, caster);
|
|
if (!origin) {
|
|
console.log(`[PassiveSkill] result 7 无落点 skill=${skillId}`);
|
|
return;
|
|
}
|
|
const scale = coeffWan / 10000;
|
|
const travel = speed * dur;
|
|
const stepRad = (Math.PI * 2) / maxCount;
|
|
const missiles: HomingDragonMissile[] = [];
|
|
for (let i = 0; i < maxCount; i++) {
|
|
const rad = i * stepRad;
|
|
const ux = Math.cos(rad);
|
|
const uy = Math.sin(rad);
|
|
const flight: LineDragonFlight = {
|
|
caster,
|
|
skillId,
|
|
x: origin.x,
|
|
y: origin.y,
|
|
ux,
|
|
uy,
|
|
speed,
|
|
scale,
|
|
left: dur,
|
|
dmgType: HOMING_DRAGON_DMG,
|
|
hitIds: [],
|
|
};
|
|
this._lineDragons.push(flight);
|
|
this._hitLineDragon(session, flight, 0);
|
|
missiles.push({
|
|
ux,
|
|
uy,
|
|
destX: origin.x + ux * travel,
|
|
destY: origin.y + uy * travel,
|
|
duration: dur,
|
|
});
|
|
}
|
|
const cue: HomingDragonCue = {
|
|
caster,
|
|
originTarget,
|
|
originX: origin.x,
|
|
originY: origin.y,
|
|
missiles,
|
|
};
|
|
session.events.emit(BattleEventFactory.custom(HOMING_DRAGON_EVENT, cue));
|
|
}
|
|
|
|
/**
|
|
* 直线火龙:从施法者沿触发目标方向匀速飞 duration 秒,穿透且每敌一次。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 触发朝向目标
|
|
* @param skillId Skill.id
|
|
* @param p 飞行速度像素/秒, 系数万分比, 持续秒
|
|
*/
|
|
private _applyLineDragon(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void {
|
|
const speed = p[0];
|
|
const coeffWan = p[1];
|
|
const dur = p[2];
|
|
if (!(speed > 0) || !(coeffWan > 0) || !(dur > 0)) {
|
|
console.log(`[PassiveSkill] result 9 参数无效 skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
const origin = this._logicPos(session, caster);
|
|
if (!origin) {
|
|
console.log(`[PassiveSkill] result 9 无起点 skill=${skillId}`);
|
|
return;
|
|
}
|
|
const aim = this._logicPos(session, target);
|
|
if (!aim) {
|
|
console.log(`[PassiveSkill] result 9 无朝向 skill=${skillId} target=${target}`);
|
|
return;
|
|
}
|
|
const dx = aim.x - origin.x;
|
|
const dy = aim.y - origin.y;
|
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
if (!(len > 1e-4)) {
|
|
console.log(`[PassiveSkill] result 9 朝向为零 skill=${skillId}`);
|
|
return;
|
|
}
|
|
const ux = dx / len;
|
|
const uy = dy / len;
|
|
const travel = speed * dur;
|
|
const flight: LineDragonFlight = {
|
|
caster,
|
|
skillId,
|
|
x: origin.x,
|
|
y: origin.y,
|
|
ux,
|
|
uy,
|
|
speed,
|
|
scale: coeffWan / 10000,
|
|
left: dur,
|
|
dmgType: LINE_DRAGON_DMG,
|
|
hitIds: [],
|
|
};
|
|
this._lineDragons.push(flight);
|
|
this._hitLineDragon(session, flight, 0);
|
|
const cue: LineDragonCue = {
|
|
caster,
|
|
target,
|
|
originX: origin.x,
|
|
originY: origin.y,
|
|
destX: origin.x + ux * travel,
|
|
destY: origin.y + uy * travel,
|
|
duration: dur,
|
|
};
|
|
session.events.emit(BattleEventFactory.custom(LINE_DRAGON_EVENT, cue));
|
|
}
|
|
|
|
/**
|
|
* 玉帝刀光动画:轩辕剑蓝、吴钩双剑红。
|
|
* @param skillId 被动 Skill.id
|
|
*/
|
|
private _swordWaveAnim(skillId: number): string {
|
|
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId);
|
|
const name = String(row?.name ?? '').trim();
|
|
if (name === '吴钩双剑') return SWORD_WAVE_ANIM_RED;
|
|
if (name === '轩辕剑') return SWORD_WAVE_ANIM_BLUE;
|
|
console.log(`[PassiveSkill] result 11 未配置剑气色 skill=${skillId} name=${name}`);
|
|
return SWORD_WAVE_ANIM_BLUE;
|
|
}
|
|
|
|
/**
|
|
* 直线剑气:按间隔连发 count 道,优先各朝不同敌人;敌数不足则循环复用。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 触发主目标(优先作为第一道朝向)
|
|
* @param skillId Skill.id
|
|
* @param p 数量, 间隔秒, 系数万分比
|
|
*/
|
|
private _applySwordWave(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void {
|
|
const count = Math.floor(p[0]);
|
|
const interval = p[1];
|
|
const coeffWan = p[2];
|
|
if (!(count > 0) || !(interval >= 0) || !(coeffWan > 0)) {
|
|
console.log(`[PassiveSkill] result 11 参数无效 skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
const origin = this._logicPos(session, caster);
|
|
if (!origin) {
|
|
console.log(`[PassiveSkill] result 11 无起点 skill=${skillId}`);
|
|
return;
|
|
}
|
|
const picks: EntityId[] = [];
|
|
this._collectHostileIds(session, caster, picks);
|
|
if (picks.length === 0) {
|
|
console.log(`[PassiveSkill] result 11 无敌可朝向 skill=${skillId}`);
|
|
return;
|
|
}
|
|
// 触发主目标优先,其余打乱;敌数不足时按序循环复用
|
|
const ordered: EntityId[] = [];
|
|
if (target > 0) {
|
|
for (let i = 0; i < picks.length; i++) {
|
|
if (picks[i] === target) {
|
|
ordered.push(target);
|
|
picks[i] = picks[picks.length - 1];
|
|
picks.length -= 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this._shuffleIds(session, picks);
|
|
for (let i = 0; i < picks.length; i++) ordered.push(picks[i]);
|
|
const scale = coeffWan / 10000;
|
|
let spawned = 0;
|
|
for (let i = 0; i < count; i++) {
|
|
let ux = 0;
|
|
let uy = 0;
|
|
let tid: EntityId = 0;
|
|
let ok = false;
|
|
for (let k = 0; k < ordered.length; k++) {
|
|
tid = ordered[(i + k) % ordered.length];
|
|
const aim = this._logicPos(session, tid);
|
|
if (!aim) continue;
|
|
const dx = aim.x - origin.x;
|
|
const dy = aim.y - origin.y;
|
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
if (!(len > 1e-4)) continue;
|
|
ux = dx / len;
|
|
uy = dy / len;
|
|
ok = true;
|
|
break;
|
|
}
|
|
if (!ok) break;
|
|
this._wavePending.push({
|
|
caster,
|
|
target: tid,
|
|
skillId,
|
|
originX: origin.x,
|
|
originY: origin.y,
|
|
ux,
|
|
uy,
|
|
scale,
|
|
wait: spawned * interval,
|
|
});
|
|
spawned += 1;
|
|
}
|
|
if (spawned <= 0) {
|
|
console.log(`[PassiveSkill] result 11 无有效朝向 skill=${skillId}`);
|
|
return;
|
|
}
|
|
this._tickSwordWavePending(0);
|
|
}
|
|
|
|
/**
|
|
* 收集可被火龙索敌的敌对存活单位。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param out 输出 id
|
|
*/
|
|
private _collectHostileIds(session: BattleSession, caster: EntityId, out: EntityId[]): void {
|
|
const casterTeam = session.world.getComponent(caster, TeamComp);
|
|
session.world.view(HealthComp).forEach((id, hp) => {
|
|
if (id === caster) return;
|
|
if (!session.world.isAlive(id)) return;
|
|
if (!(hp.hp > 0)) return;
|
|
const move = session.world.getComponent(id, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) return;
|
|
out.push(id);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 原地打乱 id(session.rng)。
|
|
* @param session 战斗会话
|
|
* @param ids 列表
|
|
*/
|
|
private _shuffleIds(session: BattleSession, ids: EntityId[]): void {
|
|
for (let i = ids.length - 1; i > 0; i--) {
|
|
const j = Math.floor(session.rng.next() * (i + 1));
|
|
const tmp = ids[i];
|
|
ids[i] = ids[j];
|
|
ids[j] = tmp;
|
|
}
|
|
}
|
|
|
|
/** 推进直线火球/火龙位移与穿透命中 */
|
|
private _tickLineDragons(dt: number): void {
|
|
const session = this._session;
|
|
if (!session) return;
|
|
for (let i = this._lineDragons.length - 1; i >= 0; i--) {
|
|
const d = this._lineDragons[i];
|
|
const step = d.speed * dt;
|
|
d.x += d.ux * step;
|
|
d.y += d.uy * step;
|
|
d.left -= dt;
|
|
this._hitLineDragon(session, d, step * 0.5);
|
|
if (d.left <= 0) this._lineDragons.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 推进待放出剑气 */
|
|
private _tickSwordWavePending(dt: number): void {
|
|
const session = this._session;
|
|
if (!session) return;
|
|
for (let i = this._wavePending.length - 1; i >= 0; i--) {
|
|
const p = this._wavePending[i];
|
|
p.wait -= dt;
|
|
if (p.wait > 0) continue;
|
|
this._wavePending.splice(i, 1);
|
|
this._spawnSwordWave(session, p);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 放出一道剑气并通知表现。
|
|
* @param session 战斗会话
|
|
* @param p 待放参数
|
|
*/
|
|
private _spawnSwordWave(session: BattleSession, p: SwordWavePending): void {
|
|
if (!session.world.isAlive(p.caster)) return;
|
|
const flight: SwordWaveFlight = {
|
|
caster: p.caster,
|
|
skillId: p.skillId,
|
|
x: p.originX,
|
|
y: p.originY,
|
|
ux: p.ux,
|
|
uy: p.uy,
|
|
speed: SWORD_WAVE_SPEED,
|
|
scale: p.scale,
|
|
hitIds: [],
|
|
};
|
|
this._waves.push(flight);
|
|
this._hitSwordWave(session, flight);
|
|
const cue: SwordWaveCue = {
|
|
caster: p.caster,
|
|
target: p.target,
|
|
originX: p.originX,
|
|
originY: p.originY,
|
|
ux: p.ux,
|
|
uy: p.uy,
|
|
speed: SWORD_WAVE_SPEED,
|
|
animation: this._swordWaveAnim(p.skillId),
|
|
};
|
|
session.events.emit(BattleEventFactory.custom(SWORD_WAVE_EVENT, cue));
|
|
}
|
|
|
|
/** 推进剑气位移、穿透命中与出界回收 */
|
|
private _tickSwordWaves(dt: number): void {
|
|
const session = this._session;
|
|
if (!session) return;
|
|
for (let i = this._waves.length - 1; i >= 0; i--) {
|
|
const w = this._waves[i];
|
|
const step = w.speed * dt;
|
|
w.x += w.ux * step;
|
|
w.y += w.uy * step;
|
|
this._hitSwordWave(session, w);
|
|
if (this._isSwordWaveOut(session, w)) this._waves.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 剑气是否已飞出地图外扩边界。
|
|
* @param session 战斗会话
|
|
* @param w 在飞剑气
|
|
*/
|
|
private _isSwordWaveOut(session: BattleSession, w: SwordWaveFlight): boolean {
|
|
const grid = session.grid;
|
|
const maxX = grid.cols * grid.cellSize;
|
|
const maxY = grid.rows * grid.cellSize;
|
|
const pad = SWORD_WAVE_BOUND_PAD;
|
|
return w.x < -pad || w.y < -pad || w.x > maxX + pad || w.y > maxY + pad;
|
|
}
|
|
|
|
/**
|
|
* 剑气当前体盒内敌方各伤一次。
|
|
* @param session 战斗会话
|
|
* @param w 在飞剑气
|
|
*/
|
|
private _hitSwordWave(session: BattleSession, w: SwordWaveFlight): void {
|
|
if (!session.world.isAlive(w.caster)) return;
|
|
const casterTeam = session.world.getComponent(w.caster, TeamComp);
|
|
const cell = session.grid.worldToCell(w.x, w.y);
|
|
const cellSize = Math.max(1, session.grid.cellSize);
|
|
const radiusCells = Math.max(SWORD_WAVE_HALF_LEN, SWORD_WAVE_HALF_WID) / cellSize + 2;
|
|
const ids = session.spatial.queryRadius(cell, radiusCells);
|
|
session.events.flush();
|
|
this._beginResultGate();
|
|
try {
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (id === w.caster) continue;
|
|
if (this._swordWaveHit(w, id)) continue;
|
|
if (!session.world.isAlive(id)) continue;
|
|
const hp = session.world.getComponent(id, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) continue;
|
|
const move = session.world.getComponent(id, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue;
|
|
const pos = this._logicPos(session, id);
|
|
if (!pos) continue;
|
|
const rx = pos.x - w.x;
|
|
const ry = pos.y - w.y;
|
|
const lx = rx * w.ux + ry * w.uy;
|
|
const ly = rx * w.uy - ry * w.ux;
|
|
if (Math.abs(lx) > SWORD_WAVE_HALF_LEN || Math.abs(ly) > SWORD_WAVE_HALF_WID) continue;
|
|
w.hitIds.push(id);
|
|
this._dealResultDamage(session, w.caster, id, w.skillId, w.scale, SWORD_WAVE_DMG);
|
|
}
|
|
session.events.flush();
|
|
} finally {
|
|
this._endResultGate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 该剑气是否已命中过该实体。
|
|
* @param w 在飞剑气
|
|
* @param id 实体
|
|
*/
|
|
private _swordWaveHit(w: SwordWaveFlight, id: EntityId): boolean {
|
|
const ids = w.hitIds;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
if (ids[i] === id) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 爆炸:以触发目标为圆心圈伤;行上 atk_buff 非空时对命中目标施加。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 触发落点
|
|
* @param bind 被动绑定
|
|
* @param p 半径像素, 伤害万分比
|
|
*/
|
|
private _applyExplosion(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, p: number[]): void {
|
|
const radius = p[0];
|
|
const coeffWan = p[1];
|
|
if (!(radius > 0) || !(coeffWan > 0)) {
|
|
console.log(`[PassiveSkill] result 14 参数无效 skill=${bind.skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
const origin = this._logicPos(session, target > 0 ? target : caster)
|
|
?? this._logicPos(session, caster);
|
|
if (!origin) {
|
|
console.log(`[PassiveSkill] result 14 无落点 skill=${bind.skillId}`);
|
|
return;
|
|
}
|
|
const scale = coeffWan / 10000;
|
|
const casterTeam = session.world.getComponent(caster, TeamComp);
|
|
const cell = session.grid.worldToCell(origin.x, origin.y);
|
|
const radiusCells = radius / Math.max(1, session.grid.cellSize);
|
|
const ids = session.spatial.queryRadius(cell, radiusCells + 1);
|
|
const r2 = radius * radius;
|
|
const hitIds: EntityId[] = [];
|
|
this._beginResultGate();
|
|
try {
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (id === caster) continue;
|
|
if (!session.world.isAlive(id)) continue;
|
|
const hp = session.world.getComponent(id, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) continue;
|
|
const move = session.world.getComponent(id, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue;
|
|
const pos = this._logicPos(session, id);
|
|
if (!pos) continue;
|
|
const dx = pos.x - origin.x;
|
|
const dy = pos.y - origin.y;
|
|
if (dx * dx + dy * dy > r2) continue;
|
|
this._dealResultDamage(session, caster, id, bind.skillId, scale, EXPLOSION_DMG);
|
|
hitIds.push(id);
|
|
}
|
|
session.events.flush();
|
|
} finally {
|
|
this._endResultGate();
|
|
}
|
|
const buff1 = String(bind.atkBuff1 ?? '').trim();
|
|
const buff2 = String(bind.atkBuff2 ?? '').trim();
|
|
if (!buff1 && !buff2) return;
|
|
const abilityCtx = createAbilityContext(session, caster, `passive_${bind.skillId}`, hitIds);
|
|
for (let i = 0; i < hitIds.length; i++) {
|
|
const id = hitIds[i];
|
|
if (!session.world.isAlive(id)) continue;
|
|
SkillAtkBuffExecutor.applyPair(session, abilityCtx, id, buff1, buff2, { fromPassiveResult: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从触发目标起跳,向范围内未打过的最近敌蔓延 hop 次。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param target 触发目标
|
|
* @param skillId Skill.id
|
|
* @param p 蔓延范围像素, 蔓延数量, 系数万分比
|
|
*/
|
|
private _applyLightningChain(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void {
|
|
const range = p[0];
|
|
const count = Math.floor(p[1]);
|
|
const coeffWan = p[2];
|
|
if (!(range > 0) || !(count > 0) || !(coeffWan > 0)) {
|
|
console.log(`[PassiveSkill] result 12 参数无效 skill=${skillId}`, p.join(','));
|
|
return;
|
|
}
|
|
const origin = this._logicPos(session, target);
|
|
if (!origin) {
|
|
console.log(`[PassiveSkill] result 12 无起点 skill=${skillId} target=${target}`);
|
|
return;
|
|
}
|
|
this._chains.push({
|
|
caster,
|
|
skillId,
|
|
scale: coeffWan / 10000,
|
|
range,
|
|
remain: count,
|
|
wait: 0,
|
|
pending: 0,
|
|
fromX: origin.x,
|
|
fromY: origin.y,
|
|
fromId: target,
|
|
visited: [],
|
|
});
|
|
this._tickLightningChains(0);
|
|
}
|
|
|
|
/** 推进闪电链 hop:先结算上一跳伤害,再选下一目标。 */
|
|
private _tickLightningChains(dt: number): void {
|
|
const session = this._session;
|
|
if (!session) return;
|
|
for (let i = this._chains.length - 1; i >= 0; i--) {
|
|
const run = this._chains[i];
|
|
run.wait -= dt;
|
|
let alive = true;
|
|
while (alive && run.wait <= 0) {
|
|
alive = this._stepLightningHop(session, run);
|
|
}
|
|
if (!alive) this._chains.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 结算 pending 并尝试放出下一跳。
|
|
* @param session 战斗会话
|
|
* @param run 进行中的链
|
|
* @returns 链是否继续
|
|
*/
|
|
private _stepLightningHop(session: BattleSession, run: LightningChainRun): boolean {
|
|
if (run.pending > 0) {
|
|
this._dealLightningHop(session, run, run.pending);
|
|
run.pending = 0;
|
|
}
|
|
if (run.remain <= 0) return false;
|
|
if (!session.world.isAlive(run.caster)) return false;
|
|
const next = this._pickLightningHop(session, run);
|
|
if (next === 0) return false;
|
|
const aim = this._logicPos(session, next);
|
|
if (!aim) return false;
|
|
run.visited.push(next);
|
|
run.remain -= 1;
|
|
const dx = aim.x - run.fromX;
|
|
const dy = aim.y - run.fromY;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const showBolt = dist > 1;
|
|
const cue: LightningChainCue = {
|
|
caster: run.caster,
|
|
from: run.fromId,
|
|
to: next,
|
|
fromX: run.fromX,
|
|
fromY: run.fromY,
|
|
toX: aim.x,
|
|
toY: aim.y,
|
|
showBolt,
|
|
};
|
|
session.events.emit(BattleEventFactory.custom(LIGHTNING_CHAIN_EVENT, cue));
|
|
run.pending = next;
|
|
run.fromId = next;
|
|
run.fromX = aim.x;
|
|
run.fromY = aim.y;
|
|
run.wait = showBolt ? LIGHTNING_HOP_SEC : 0;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 范围内未访问的最近敌;没有则结束(不重复打已蔓延目标)。
|
|
* @param session 战斗会话
|
|
* @param run 进行中的链
|
|
*/
|
|
private _pickLightningHop(session: BattleSession, run: LightningChainRun): EntityId {
|
|
const ids: EntityId[] = [];
|
|
this._collectHostileIds(session, run.caster, ids);
|
|
const r2 = run.range * run.range;
|
|
let best: EntityId = 0;
|
|
let bestD2 = r2;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (this._chainVisited(run, id)) continue;
|
|
const pos = this._logicPos(session, id);
|
|
if (!pos) continue;
|
|
const dx = pos.x - run.fromX;
|
|
const dy = pos.y - run.fromY;
|
|
const d2 = dx * dx + dy * dy;
|
|
if (d2 > r2) continue;
|
|
if (best === 0 || d2 < bestD2) {
|
|
best = id;
|
|
bestD2 = d2;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/**
|
|
* 该链是否已蔓延过该实体。
|
|
* @param run 进行中的链
|
|
* @param id 实体
|
|
*/
|
|
private _chainVisited(run: LightningChainRun, id: EntityId): boolean {
|
|
const ids = run.visited;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
if (ids[i] === id) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 对一跳目标结算伤害。
|
|
* @param session 战斗会话
|
|
* @param run 进行中的链
|
|
* @param target 落点
|
|
*/
|
|
private _dealLightningHop(session: BattleSession, run: LightningChainRun, target: EntityId): void {
|
|
if (!session.world.isAlive(run.caster) || !session.world.isAlive(target)) return;
|
|
const hp = session.world.getComponent(target, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) return;
|
|
this._dealResultDamage(session, run.caster, target, run.skillId, run.scale, LIGHTNING_CHAIN_DMG);
|
|
}
|
|
|
|
/**
|
|
* 直线火龙当前体盒内敌方各伤一次(穿透,每敌一次)。
|
|
* 占地按半格放大,与 atk_type 12 散射箭一致,避免只打点心漏掉同线怪。
|
|
* @param session 战斗会话
|
|
* @param d 在飞火龙
|
|
* @param extraLen 本帧步进半长,防穿透漏检
|
|
*/
|
|
private _hitLineDragon(session: BattleSession, d: LineDragonFlight, extraLen: number): void {
|
|
if (!session.world.isAlive(d.caster)) return;
|
|
const casterTeam = session.world.getComponent(d.caster, TeamComp);
|
|
const cell = session.grid.worldToCell(d.x, d.y);
|
|
const cellSize = Math.max(1, session.grid.cellSize);
|
|
const halfBody = cellSize * 0.5;
|
|
const radiusCells = (LINE_DRAGON_HALF_LEN + extraLen + halfBody) / cellSize + 2;
|
|
const ids = session.spatial.queryRadius(cell, radiusCells);
|
|
session.events.flush();
|
|
this._beginResultGate();
|
|
try {
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (id === d.caster) continue;
|
|
if (this._lineDragonHit(d, id)) continue;
|
|
if (!session.world.isAlive(id)) continue;
|
|
const hp = session.world.getComponent(id, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) continue;
|
|
const move = session.world.getComponent(id, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue;
|
|
const pos = this._logicPos(session, id);
|
|
if (!pos) continue;
|
|
const rx = pos.x - d.x;
|
|
const ry = pos.y - d.y;
|
|
const lx = rx * d.ux + ry * d.uy;
|
|
const ly = rx * d.uy - ry * d.ux;
|
|
if (Math.abs(lx) > LINE_DRAGON_HALF_LEN + extraLen + halfBody) continue;
|
|
if (Math.abs(ly) > LINE_DRAGON_HALF_WID + halfBody) continue;
|
|
d.hitIds.push(id);
|
|
this._dealResultDamage(session, d.caster, id, d.skillId, d.scale, d.dmgType);
|
|
}
|
|
session.events.flush();
|
|
} finally {
|
|
this._endResultGate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 该火龙是否已命中过该实体。
|
|
* @param d 在飞火龙
|
|
* @param id 实体
|
|
*/
|
|
private _lineDragonHit(d: LineDragonFlight, id: EntityId): boolean {
|
|
const ids = d.hitIds;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
if (ids[i] === id) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 推进毒跳 */
|
|
private _tickDots(dt: number): void {
|
|
const session = this._session;
|
|
if (!session) return;
|
|
for (let i = this._dots.length - 1; i >= 0; i--) {
|
|
const d = this._dots[i];
|
|
if (!session.world.isAlive(d.caster) || !session.world.isAlive(d.target)) {
|
|
this._dots.splice(i, 1);
|
|
continue;
|
|
}
|
|
d.left -= dt;
|
|
d.wait -= dt;
|
|
if (d.wait <= 0) {
|
|
this._dealResultDamage(session, d.caster, d.target, d.skillId, d.scale, POISON_DMG);
|
|
d.wait += d.interval;
|
|
}
|
|
if (d.left <= 0) this._removeDot(d);
|
|
}
|
|
}
|
|
|
|
/** 推进剑阵跳伤 */
|
|
private _tickZones(dt: number): void {
|
|
const session = this._session;
|
|
if (!session) return;
|
|
for (let i = this._zones.length - 1; i >= 0; i--) {
|
|
const z = this._zones[i];
|
|
if (!session.world.isAlive(z.caster)) {
|
|
this._zones.splice(i, 1);
|
|
continue;
|
|
}
|
|
z.left -= dt;
|
|
z.wait -= dt;
|
|
if (z.wait <= 0) {
|
|
this._pulseZone(session, z);
|
|
z.wait += z.interval;
|
|
}
|
|
if (z.left <= 0) this._removeZone(z);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 剑阵一跳:半径内敌方造成伤害。
|
|
* @param session 战斗会话
|
|
* @param zone 剑阵区
|
|
*/
|
|
private _pulseZone(session: BattleSession, zone: SwordArrayZone): void {
|
|
session.events.flush();
|
|
const casterTeam = session.world.getComponent(zone.caster, TeamComp);
|
|
const cell = session.grid.worldToCell(zone.x, zone.y);
|
|
const radiusCells = zone.radius / Math.max(1, session.grid.cellSize);
|
|
const ids = session.spatial.queryRadius(cell, radiusCells + 1);
|
|
const r2 = zone.radius * zone.radius;
|
|
this._beginResultGate();
|
|
try {
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (id === zone.caster) continue;
|
|
if (!session.world.isAlive(id)) continue;
|
|
const hp = session.world.getComponent(id, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) continue;
|
|
const move = session.world.getComponent(id, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue;
|
|
const pos = this._logicPos(session, id);
|
|
if (!pos) continue;
|
|
const dx = pos.x - zone.x;
|
|
const dy = pos.y - zone.y;
|
|
if (dx * dx + dy * dy > r2) continue;
|
|
this._dealResultDamage(session, zone.caster, id, zone.skillId, zone.scale, SWORD_ARRAY_DMG);
|
|
}
|
|
session.events.flush();
|
|
} finally {
|
|
this._endResultGate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 实体逻辑世界坐标。
|
|
* @param session 战斗会话
|
|
* @param entity 实体
|
|
*/
|
|
private _logicPos(session: BattleSession, entity: EntityId): { x: number; y: number } | null {
|
|
const move = session.world.getComponent(entity, MovementComp);
|
|
if (move && (move.worldX !== 0 || move.worldY !== 0)) {
|
|
return { x: move.worldX, y: move.worldY };
|
|
}
|
|
const tf = session.world.getComponent(entity, TransformComp);
|
|
if (!tf) return null;
|
|
return session.grid.cellToWorld({ row: tf.row, col: tf.col });
|
|
}
|
|
|
|
/**
|
|
* 按引用移除剑阵区。
|
|
* @param zone 剑阵区
|
|
*/
|
|
private _removeZone(zone: SwordArrayZone): void {
|
|
for (let i = this._zones.length - 1; i >= 0; i--) {
|
|
if (this._zones[i] === zone) {
|
|
this._zones.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 按引用移除毒跳(伤害回调可能已 splice,避免错删)。
|
|
* @param dot 毒跳
|
|
*/
|
|
private _removeDot(dot: PoisonDot): void {
|
|
for (let i = this._dots.length - 1; i >= 0; i--) {
|
|
if (this._dots[i] === dot) {
|
|
this._dots.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 卸下时清攻速/射程 modifier */
|
|
private _stripMods(entityId: EntityId): void {
|
|
const session = this._session;
|
|
if (!session || !session.world.isAlive(entityId)) return;
|
|
const idx = this._indexOf(entityId);
|
|
if (idx < 0) return;
|
|
const list = this._binds[idx].list;
|
|
for (let i = 0; i < list.length; i++) {
|
|
const id = list[i].skillId;
|
|
session.attributes.removeModifiersByGroup(entityId, ASPD_GROUP + id);
|
|
session.attributes.removeModifiersByGroup(entityId, RANGE_GROUP + id);
|
|
}
|
|
}
|
|
|
|
/** 卸下施法者时清其毒 */
|
|
private _removeDotsByCaster(caster: EntityId): void {
|
|
for (let i = this._dots.length - 1; i >= 0; i--) {
|
|
if (this._dots[i].caster === caster) this._dots.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 同施法者同技能的已有剑阵。
|
|
* @param caster 施法者
|
|
* @param skillId Skill.id
|
|
*/
|
|
private _findZone(caster: EntityId, skillId: number): SwordArrayZone | null {
|
|
for (let i = 0; i < this._zones.length; i++) {
|
|
const z = this._zones[i];
|
|
if (z.caster === caster && z.skillId === skillId) return z;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 卸下施法者时清其剑阵 */
|
|
private _removeZonesByCaster(caster: EntityId): void {
|
|
for (let i = this._zones.length - 1; i >= 0; i--) {
|
|
if (this._zones[i].caster === caster) this._zones.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 卸下施法者时清其剑气 */
|
|
private _removeWavesByCaster(caster: EntityId): void {
|
|
for (let i = this._wavePending.length - 1; i >= 0; i--) {
|
|
if (this._wavePending[i].caster === caster) this._wavePending.splice(i, 1);
|
|
}
|
|
for (let i = this._waves.length - 1; i >= 0; i--) {
|
|
if (this._waves[i].caster === caster) this._waves.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 卸下施法者时清其闪电链 */
|
|
private _removeChainsByCaster(caster: EntityId): void {
|
|
for (let i = this._chains.length - 1; i >= 0; i--) {
|
|
if (this._chains[i].caster === caster) this._chains.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 确保 AttributeComp 存在。
|
|
* @param session 战斗会话
|
|
* @param entity 实体
|
|
*/
|
|
private _ensureAttr(session: BattleSession, entity: EntityId): void {
|
|
if (!session.world.getComponent(entity, AttributeComp)) {
|
|
session.world.addComponent(entity, new AttributeComp());
|
|
}
|
|
session.attributes.syncCombatToAttributes(entity);
|
|
}
|
|
|
|
/**
|
|
* 解析 Skill.condition / result。
|
|
* @param skillId Skill.id
|
|
*/
|
|
private _parseSkill(skillId: number): PassiveSkillBind | null {
|
|
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId);
|
|
if (!row) {
|
|
console.log(`[PassiveSkill] 缺表 Skill.id=${skillId}`);
|
|
return null;
|
|
}
|
|
if (Number(row.type) !== 2) return null;
|
|
const cond = String(row.condition ?? '').trim();
|
|
const result = String(row.result ?? '').trim();
|
|
if (!cond || !result) {
|
|
console.log(`[PassiveSkill] condition/result 为空 Skill.id=${skillId}`);
|
|
return null;
|
|
}
|
|
const condParts = cond.split(',').map(Number);
|
|
const resultParts = result.split(',').map(Number);
|
|
const conditionKind = condParts[0] || 0;
|
|
if (conditionKind < 1 || conditionKind > 6) {
|
|
console.log(`[PassiveSkill] 未知 condition skill=${skillId} kind=${conditionKind}`);
|
|
}
|
|
return {
|
|
skillId,
|
|
conditionKind,
|
|
conditionParams: condParts.slice(1),
|
|
resultKind: resultParts[0] || 0,
|
|
resultParams: resultParts.slice(1),
|
|
cdLeft: 0,
|
|
cd: Number(row.cd) || 0,
|
|
lastTarget: 0,
|
|
hitTargets: [],
|
|
castCount: 0,
|
|
stackCount: 0,
|
|
stackLeft: 0,
|
|
atkBuff1: String(row.atk_buff1 ?? ''),
|
|
atkBuff2: String(row.atk_buff2 ?? ''),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 查找实体绑定下标。
|
|
* @param entityId 塔实体
|
|
*/
|
|
private _indexOf(entityId: EntityId): number {
|
|
for (let i = 0; i < this._binds.length; i++) {
|
|
if (this._binds[i].entityId === entityId) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
let _host: PassiveSkillHost | null = null;
|
|
|
|
/** 全局被动宿主 */
|
|
export function getPassiveSkillHost(): PassiveSkillHost {
|
|
if (!_host) _host = new PassiveSkillHost();
|
|
return _host;
|
|
}
|
|
|
|
/** 测试用重置 */
|
|
export function resetPassiveSkillHostForTests(): void {
|
|
_host = null;
|
|
}
|
|
|