import { Vec3 } from 'cc'; import type { AbilityContext } from 'db://assets/mx/module/battle/combat/AbilityContext'; import type { BattleServices } from 'db://assets/mx/module/battle/combat/BattleServices'; import type { Cell } from 'db://assets/mx/module/battle/battlefield/Cell'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { BattleEventFactory } from 'db://assets/mx/module/battle/events/BattleEvents'; import { UnitFactory } from 'db://assets/mx/module/battle/unit/UnitFactory'; import { HealthComp, MovementComp, TagComp, TeamComp, TransformComp, UnitMetaComp, } from 'db://assets/mx/module/battle/ecs/Components'; import { BattleTeamId } from 'db://assets/mx/module/battle/ecs/BattleTeamId'; import { EntityTag } from 'db://assets/mx/module/battle/ecs/EntityTag'; import { EffectRegistry } from 'db://assets/mx/module/battle/registry/EffectRegistry'; import { parseMonsterUnitDefId } from '../config/MonsterHpCalculator'; import { TableBattleConfigProvider } from '../config/TableBattleConfigProvider'; import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager'; import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor'; import { resolveSummonJumpJoin } from './YingCampSummonHost'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import { YingCampAnim, YING_HOLD_BEFORE_JUMP_SEC, YING_SPAWN_GROW_SEC } from '../view/YingCampAnim'; const SUMMON_FLAG = '__mx_battle_skillSummonRegistered__'; /** 出怪类型(atk_para 第 4 段) */ export const enum SkillSummonSpawnMode { /** 施法处出现后跳入进路(杨戬哮天犬) */ Camp = 1, /** 施法者自身周围出现 */ AroundSelf = 2, } /** 预兆播放点上偏(像素) */ const CASTER_SUMMON_LIFT_PX = 45; /** 出现相对默认缩放 */ const CASTER_SUMMON_SCALE_FROM = 0; /** 停顿时相对默认缩放 */ const CASTER_SUMMON_SCALE_HOLD = 0.5; /** Skill.atk_type=8 召唤参数 */ export interface SkillSummonParams { /** 怪物 id;-1=当前波小怪 */ monsterId: number; /** 单次施法召唤数量 */ count: number; /** 同批召唤间隔(秒);0=同时 */ intervalSec: number; /** 出怪类型:1 营地,2 自身周围 */ spawnMode: number; /** Skill.atk_buff1 */ buff1: string; /** Skill.atk_buff2 */ buff2: string; /** Skill.id */ skillId: number; } /** 自身周围偏移:四向后对角 */ const AROUND_DIR: ReadonlyArray = [ [0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [1, -1], [-1, 1], [-1, -1], ]; /** 注册 Skill 表召唤效果(atk_type=8) */ export function registerSkillSummonEffects(): void { const g = globalThis as unknown as Record; if (g[SUMMON_FLAG]) return; g[SUMMON_FLAG] = true; EffectRegistry.register('SummonFromSkill', (services, ctx, params) => { SkillTableSummonExecutor.execute(services, ctx, { monsterId: Math.floor(Number(params.monsterId) || 0), count: Math.max(1, Math.floor(Number(params.count) || 1)), intervalSec: Math.max(0, Number(params.intervalSec) || 0), spawnMode: Math.floor(Number(params.spawnMode) || 0), buff1: String(params.buff1 ?? ''), buff2: String(params.buff2 ?? ''), skillId: Math.floor(Number(params.skillId) || 0), }); }); } /** Skill 表 atk_type=8 召唤 */ export class SkillTableSummonExecutor { /** * 执行一次 Skill 表召唤。 * @param services 战斗服务 * @param ctx 技能上下文 * @param cfg 召唤参数 */ static execute(services: BattleServices, ctx: AbilityContext, cfg: SkillSummonParams): void { if (!services.world.isAlive(ctx.caster)) return; if (cfg.spawnMode !== SkillSummonSpawnMode.Camp && cfg.spawnMode !== SkillSummonSpawnMode.AroundSelf) { console.log('[SkillTableSummon] 出怪类型须为1施法处跳入或2自身周围', cfg.skillId, cfg.spawnMode); return; } const casterTeam = services.world.getComponent(ctx.caster, TeamComp); const ownerPlayerId = casterTeam?.ownerId ?? 0; const casterIsEnemy = BattleTeamId.isEnemyTeam(casterTeam?.team ?? 0); const job = new SkillSummonJob(services, ctx.caster, ownerPlayerId, casterIsEnemy, cfg); job.start(); } /** * 施法者格及周围可行走格,不足则重叠在原点。 * @param services 战斗服务 * @param caster 施法者 * @param count 需要的格数 */ static pickAroundCells(services: BattleServices, caster: EntityId, count: number): Cell[] { const tf = services.world.getComponent(caster, TransformComp); if (!tf) { console.log('[SkillTableSummon] 施法者无 Transform,无法自身周围召唤', caster); return []; } const origin: Cell = { row: tf.row, col: tf.col }; const picked: Cell[] = []; SkillTableSummonExecutor._tryAddCell(services, picked, origin, count); for (let i = 0; i < AROUND_DIR.length && picked.length < count; i++) { const d = AROUND_DIR[i]; SkillTableSummonExecutor._tryAddCell(services, picked, { row: origin.row + d[0], col: origin.col + d[1] }, count); } if (picked.length <= 0) { console.log('[SkillTableSummon] 自身周围无可行走格', caster, origin.row, origin.col); return []; } if (picked.length < count) { console.log('[SkillTableSummon] 自身周围可行走格不足,重叠出生', caster, picked.length, count); while (picked.length < count) { picked.push({ row: picked[0].row, col: picked[0].col }); } } return picked; } /** * @param services 战斗服务 * @param out 已选格 * @param cell 候选格 * @param limit 上限 */ private static _tryAddCell(services: BattleServices, out: Cell[], cell: Cell, limit: number): void { if (out.length >= limit) return; if (!services.grid.isWalkable(cell)) return; out.push({ row: cell.row, col: cell.col }); } } /** 一次施法的分批召唤(间隔回调用类方法,禁止函数内嵌套) */ class SkillSummonJob { private _left: number; private _aroundIndex = 0; private readonly _aroundCells: Cell[] = []; /** * @param _services 战斗服务 * @param _caster 施法者 * @param _ownerPlayerId 所属玩家 * @param _casterIsEnemy 施法者是否敌方 * @param _cfg 召唤参数 */ constructor( private readonly _services: BattleServices, private readonly _caster: EntityId, private readonly _ownerPlayerId: number, private readonly _casterIsEnemy: boolean, private readonly _cfg: SkillSummonParams, ) { this._left = _cfg.count; if (_cfg.spawnMode === SkillSummonSpawnMode.AroundSelf) { const cells = SkillTableSummonExecutor.pickAroundCells(_services, _caster, _cfg.count); for (let i = 0; i < cells.length; i++) { this._aroundCells.push(cells[i]); } } } /** 立即召唤;有间隔则后续由时钟回调 */ start(): void { if (this._cfg.intervalSec <= 0) { while (this._left > 0) { this._spawnOnce(); } return; } this._spawnOnce(); if (this._left > 0) { this._services.clock.schedule(this._cfg.intervalSec, this._onInterval); } } /** 间隔到点再招一只 */ private readonly _onInterval = (): void => { if (this._left <= 0) return; this._spawnOnce(); if (this._left > 0) { this._services.clock.schedule(this._cfg.intervalSec, this._onInterval); } }; /** 按出怪类型招一只 */ private _spawnOnce(): void { if (this._left <= 0) return; this._left -= 1; if (this._cfg.spawnMode === SkillSummonSpawnMode.Camp) { this._spawnAtCamp(); return; } this._spawnAroundSelf(); } /** 施法处出现后跳入横向进路,沿路攻向对方营地 */ private _spawnAtCamp(): void { const monsterId = resolveSummonMonsterId(this._services, this._ownerPlayerId, this._cfg.monsterId); if (monsterId <= 0) return; const row = MonsterDataManager.getMonsterDataById(monsterId); if (!row) { console.log('[SkillTableSummon] 怪物表缺失', monsterId, this._cfg.skillId); return; } const tf = this._services.world.getComponent(this._caster, TransformComp); if (!tf) { console.log('[SkillTableSummon] 施法者无 Transform,无法施法处召唤', this._caster); return; } const from: Cell = { row: tf.row, col: tf.col }; const spawnPort = this._ownerPlayerId === 0 ? 2 : 1; const join = resolveSummonJumpJoin(this._services.grid, this._services.pathfinder, this._services.topology, from, spawnPort); if (!join) { console.log('[SkillTableSummon] 无跳出落点', from, spawnPort, this._cfg.skillId); return; } const waveInfo = resolveWaveSmallMonster(this._services, this._ownerPlayerId); const difficultyRow = resolveSummonDifficultyRow(this._services, this._ownerPlayerId, waveInfo); const config = gg.battle.config as TableBattleConfigProvider; const unitDefId = config.ensureSpawnMonsterDef(monsterId, spawnPort, difficultyRow); const targetPlayerId = this._ownerPlayerId === 0 ? 1 : 0; const start = this._resolveCasterAppearWorld(); if (start) YingCampAnim.setPendingStart(start, { scaleFrom: CASTER_SUMMON_SCALE_FROM, scaleHold: CASTER_SUMMON_SCALE_HOLD, growSec: YING_SPAWN_GROW_SEC, holdSec: YING_HOLD_BEFORE_JUMP_SEC, }); else console.log('[SkillTableSummon] 施法处世界坐标缺失,怪会从格心起跳', this._caster); const entity = this._services.spawner.spawnExtraEnemyAtCell( unitDefId, spawnPort, targetPlayerId, from, undefined, join, ); YingCampAnim.clearPendingStart(); if (entity == null) return; applySpawnBuffs(this._services, entity, this._cfg.buff1); applySpawnBuffs(this._services, entity, this._cfg.buff2); } /** 预兆播放点上偏,与 showeffect 挂点一致 */ private _resolveCasterAppearWorld(): Vec3 | null { const center = SkillPresentationEnv.getCompositionWorldCenter(this._caster); if (!center) return null; return new Vec3(center.x, center.y + CASTER_SUMMON_LIFT_PX, center.z); } /** 在施法者周围格生成,阵营与施法者一致 */ private _spawnAroundSelf(): void { if (!this._services.world.isAlive(this._caster)) { console.log('[SkillTableSummon] 施法者已消失,停止自身周围召唤', this._cfg.skillId); this._left = 0; return; } const cell = this._aroundCells[this._aroundIndex]; this._aroundIndex += 1; if (!cell) { console.log('[SkillTableSummon] 自身周围出生格缺失', this._cfg.skillId); return; } const monsterId = resolveSummonMonsterId(this._services, this._ownerPlayerId, this._cfg.monsterId); if (monsterId <= 0) return; const row = MonsterDataManager.getMonsterDataById(monsterId); if (!row) { console.log('[SkillTableSummon] 怪物表缺失', monsterId, this._cfg.skillId); return; } const waveInfo = resolveWaveSmallMonster(this._services, this._ownerPlayerId); const spawnPort = this._resolveAroundSpawnPort(); const difficultyRow = resolveSummonDifficultyRow(this._services, this._ownerPlayerId, waveInfo); const config = gg.battle.config as TableBattleConfigProvider; const unitDefId = config.ensureSpawnMonsterDef(monsterId, spawnPort, difficultyRow); const def = this._services.config.getUnit(unitDefId); if (!def) { console.log('[SkillTableSummon] unitDef 缺失', unitDefId); return; } const entity = UnitFactory.create(this._services, def, this._ownerPlayerId, cell, [cell]); if (!this._services.world.isAlive(entity)) return; this._applyCasterTeam(entity); const meta = this._services.world.getComponent(entity, UnitMetaComp); if (meta) { meta.spawnPoint = spawnPort; meta.glowShadow = true; } const move = this._services.world.getComponent(entity, MovementComp); if (move) { move.presentLocked = true; move.spawnProtected = true; const w = this._services.grid.cellToWorld(cell); move.worldX = w.x; move.worldY = w.y; } const hp = this._services.world.getComponent(entity, HealthComp)?.maxHp ?? 1; this._services.events.emit(BattleEventFactory.unitSpawned(entity, unitDefId, 0, hp), true); if (move) { this._services.movement.assignPath(entity); } applySpawnBuffs(this._services, entity, this._cfg.buff1); applySpawnBuffs(this._services, entity, this._cfg.buff2); } /** * 自身周围召唤的出怪口:优先施法者,否则按所属玩家营地口。 */ private _resolveAroundSpawnPort(): number { const casterMeta = this._services.world.getComponent(this._caster, UnitMetaComp); const casterPort = Math.floor(Number(casterMeta?.spawnPoint) || 0); if (casterPort > 0) return casterPort; return this._ownerPlayerId === 0 ? 2 : 1; } /** * 召唤物阵营与施法者对齐。 * @param entity 召唤实体 */ private _applyCasterTeam(entity: EntityId): void { const team = this._services.world.getComponent(entity, TeamComp); if (!team) return; if (this._casterIsEnemy) { team.team = BattleTeamId.enemyForPlayer(this._ownerPlayerId); team.ownerId = this._ownerPlayerId; return; } team.team = this._ownerPlayerId; team.ownerId = this._ownerPlayerId; const tagComp = this._services.world.getComponent(entity, TagComp); if (tagComp) { tagComp.tags = tagComp.tags.filter((t) => t !== EntityTag.Enemy); } } } function resolveSummonMonsterId(services: BattleServices, ownerPlayerId: number, rawId: number): number { if (rawId !== -1) return Math.floor(rawId); const wave = resolveWaveSmallMonster(services, ownerPlayerId); return wave?.monsterId ?? 0; } /** * 召唤物血量用的 Monster1 行:当前波小怪段,否则本波首段。 * @param services 战斗服务 * @param ownerPlayerId 所属玩家 * @param waveInfo 当前波小怪信息 */ function resolveSummonDifficultyRow( services: BattleServices, ownerPlayerId: number, waveInfo: { monsterId: number; difficultyRow: number } | null, ): number { if (waveInfo) return Math.max(1, waveInfo.difficultyRow); const fromWave = readCurrentWaveDifficultyRow(services, ownerPlayerId); if (fromWave > 0) return fromWave; console.log('[SkillTableSummon] 无法解析当前波次难度行,血量按第 1 波'); return 1; } /** * 当前波次 Monster1 行(与波次怪 unitDef 一致)。 * @param services 战斗服务 * @param ownerPlayerId 所属玩家 */ function readCurrentWaveDifficultyRow(services: BattleServices, ownerPlayerId: number): number { const waves = services.config.getLevel(services.levelId)?.waves; if (!waves?.length) return 0; let wi = services.spawner.waveIndex; if (wi < 0) wi = 0; if (wi >= waves.length) wi = waves.length - 1; const wave = waves[wi]; const spawnPort = ownerPlayerId === 0 ? 2 : 1; let portPlan = null as (typeof wave.ports)[number] | null; for (let i = 0; i < (wave.ports?.length ?? 0); i++) { if (wave.ports[i].port === spawnPort) { portPlan = wave.ports[i]; break; } } if (!portPlan) portPlan = wave.ports?.[0] ?? null; const seg = portPlan?.segments?.[0]; const fromSeg = Math.max(0, Math.floor(Number(seg?.difficultyRow) || 0)); if (fromSeg > 0) return fromSeg; const parsed = seg ? parseMonsterUnitDefId(seg.unitDefId) : null; return Math.max(0, parsed?.difficultyRow ?? 0); } function resolveWaveSmallMonster( services: BattleServices, ownerPlayerId: number, ): { monsterId: number; difficultyRow: number } | null { const level = services.config.getLevel(services.levelId); const waves = level?.waves; if (!waves?.length) { console.log('[SkillTableSummon] 无波次配置'); return null; } let wi = services.spawner.waveIndex; if (wi < 0) wi = 0; if (wi >= waves.length) wi = waves.length - 1; const wave = waves[wi]; const spawnPort = ownerPlayerId === 0 ? 2 : 1; let portPlan = null as (typeof wave.ports)[number] | null; for (let i = 0; i < (wave.ports?.length ?? 0); i++) { if (wave.ports[i].port === spawnPort) { portPlan = wave.ports[i]; break; } } if (!portPlan) portPlan = wave.ports?.[0] ?? null; if (!portPlan?.segments?.length) return null; for (let s = 0; s < portPlan.segments.length; s++) { const seg = portPlan.segments[s]; const parsed = parseMonsterUnitDefId(seg.unitDefId); if (!parsed) continue; const row = MonsterDataManager.getMonsterDataById(parsed.monsterId); if (row?.type === MonsterType.SmallMonster || row?.type === MonsterType.SummonMonster) { return { monsterId: parsed.monsterId, difficultyRow: Math.max(1, Number(seg.difficultyRow) || parsed.difficultyRow || 1), }; } } return null; } /** 出生时应用 atk_buff:3=移速、4=血量 */ function applySpawnBuffs(services: BattleServices, entity: EntityId, raw: string): void { SkillAtkBuffExecutor.applyOnSpawn(services, entity, raw); }