|
|
|
|
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<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) 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);
|
|
|
|
|
}
|