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.
 
 
 
 
AdLike2048/assets/script/battle/combat/PassiveSkillRuntime.ts

1855 lines
64 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';
/** 被动 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,
}
/** 攻速 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';
/**
* 是否为被动 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;
}
/** 毒 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;
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;
}
/**
* 从触发目标起跳,向范围内未打过的最近敌蔓延 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,
};
}
/**
* 查找实体绑定下标。
* @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;
}