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/SkillTableSummonExecutor.ts

551 lines
22 KiB

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 type { GridMap } from 'db://assets/mx/module/battle/battlefield/GridMap';
import type { MapTopology } from 'db://assets/mx/module/battle/battlefield/MapTopology';
import type { Pathfinder } from 'db://assets/mx/module/battle/battlefield/Pathfinder';
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
import { SummonJumpPending, SUMMON_JUMP_GROW_SEC, SUMMON_JUMP_HOLD_SEC } from '../view/SummonJumpPending';
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<readonly [number, number]> = [
[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<string, boolean | undefined>;
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) SummonJumpPending.setPendingStart(start, {
scaleFrom: CASTER_SUMMON_SCALE_FROM,
scaleHold: CASTER_SUMMON_SCALE_HOLD,
growSec: SUMMON_JUMP_GROW_SEC,
holdSec: SUMMON_JUMP_HOLD_SEC,
});
else console.log('[SkillTableSummon] 施法处世界坐标缺失,怪会从格心起跳', this._caster);
const entity = this._services.spawner.spawnExtraEnemyAtCell(
unitDefId,
spawnPort,
targetPlayerId,
from,
undefined,
join,
);
SummonJumpPending.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);
}
/** 横向最近进路格(不含出怪口);没有或与口同行则用普通怪跳跃落点。 */
export function resolveSummonJumpJoin(grid: GridMap, pathfinder: Pathfinder, topology: MapTopology, from: Cell, spawnPort: number): Cell | null {
const spawn = topology.getSpawnCell(spawnPort);
const sameRowAsPortal = !!spawn && from.row === spawn.row;
const campAreaId = topology.getSpawnPortCampArea(spawnPort);
if (!sameRowAsPortal && campAreaId !== undefined) {
const hz = findHorizontalSummonJoin(grid, pathfinder, from, campAreaId, spawn);
if (hz && !isSpawnPortalCell(hz, spawn)) return hz;
}
const land = resolvePortalJumpLand(pathfinder, topology, spawnPort);
if (!land || isSpawnPortalCell(land, spawn)) {
console.log('[SkillTableSummon] 出怪口不可作跳跃落点', spawnPort, from);
return null;
}
return land;
}
/**
* 同行最近可达进路格;起点本身可走且可达则直接用。
* @param grid 地图
* @param pathfinder 寻路
* @param from 起点
* @param campAreaId 阵营区域
* @param spawn 出怪口格(排除)
*/
function findHorizontalSummonJoin(grid: GridMap, pathfinder: Pathfinder, from: Cell, campAreaId: number, spawn?: Cell | null): Cell | null {
if (grid.isWalkable(from) && !isSpawnPortalCell(from, spawn)) {
const selfCost = pathfinder.getPathCost(from, campAreaId);
if (Number.isFinite(selfCost)) return { row: from.row, col: from.col };
}
let best: Cell | null = null;
let bestDist = Number.POSITIVE_INFINITY;
let bestCost = Number.POSITIVE_INFINITY;
for (let c = 0; c < grid.cols; c++) {
if (c === from.col) continue;
const cell = { row: from.row, col: c };
if (isSpawnPortalCell(cell, spawn)) continue;
if (!grid.isWalkable(cell)) continue;
const cost = pathfinder.getPathCost(cell, campAreaId);
if (!Number.isFinite(cost)) continue;
const dist = Math.abs(c - from.col);
if (dist < bestDist || (dist === bestDist && cost < bestCost)) {
best = cell;
bestDist = dist;
bestCost = cost;
}
}
return best;
}
/** 普通怪出怪口跳跃落点(进路第一格,不含出怪口本身)。 */
function resolvePortalJumpLand(pathfinder: Pathfinder, topology: MapTopology, spawnPort: number): Cell | null {
const spawn = topology.getSpawnCell(spawnPort);
if (!spawn) return null;
const campAreaId = topology.getSpawnPortCampArea(spawnPort);
if (campAreaId === undefined) {
console.log('[SkillTableSummon] 出怪口无阵营区域,无法取跳跃落点', spawnPort);
return null;
}
const target = pathfinder.shortestTargetCell(spawn, campAreaId);
if (!target) {
console.log('[SkillTableSummon] 出怪口无攻击目标,无法取跳跃落点', spawnPort);
return null;
}
const path = pathfinder.buildPathToTarget(spawn, target, campAreaId);
if (path.length <= 1) {
console.log('[SkillTableSummon] 出怪口进路不足一格', spawnPort);
return null;
}
const land = { row: path[1].row, col: path[1].col };
if (isSpawnPortalCell(land, spawn)) {
console.log('[SkillTableSummon] 进路第一格仍是出怪口', spawnPort);
return null;
}
return land;
}
/** 是否出怪口格 */
function isSpawnPortalCell(cell: Cell, spawn: Cell | null | undefined): boolean {
return !!spawn && cell.row === spawn.row && cell.col === spawn.col;
}