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

473 lines
18 KiB

import type { EntityId } from 'db://assets/mx/module/ecs';
import {
BattlePhase,
BattleTeamId,
BattleEventFactory,
HealthComp,
MovementComp,
TeamComp,
TransformComp,
UnitMetaComp,
isBajiaoBlowRetreating,
isYanSoulClaimed,
type BattleSession,
type Cell,
} from 'db://assets/mx/module/battle';
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
import { MonsterType } from '../../manager/MonsterDataManager';
import {
resolveCarriedBaseThunderConfig,
type BattleBuffBaseThunderAdjust,
} from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
import { schedulePresentation } from '../skill/BattlePresentationClock';
import { isMechanismUnit } from '../mechanism/MechanismServices';
import type { ProjectBattleHandle } from '../runtime/BattleRuntime';
import type { BattleGameBaseThunderPresenter } from '../../ui/BattleGame/BattleGameBaseThunderPresenter';
import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea';
/** 御雷珠伤害类型 */
const BASE_THUNDER_DAMAGE_TYPE = 'base_thunder';
/** 无施法者占位 */
const BASE_THUNDER_CASTER = 0 as EntityId;
/** 飘字展示后再击杀(逻辑秒) */
const BASE_THUNDER_KILL_DELAY_SEC = 0.4;
/** 与 Pathfinder 正交一步代价一致,用于距离场→格数 */
const PATH_ORTH_STEP = 1000;
/** 沿进路排序的目标条目 */
type ThunderTargetEntry = {
/** 受击实体 */
entityId: EntityId;
/** 在生效格序列中的索引(越小越靠前) */
cellOrder: number;
};
/** 单玩家御雷珠状态 */
type PlayerThunderState = {
/** 防守方 playerId */
playerId: number;
/** 汇总配置 */
config: BattleBuffBaseThunderAdjust;
/** 本局已触发次数 */
usedTriggers: number;
};
/**
* 被动 functiontype=20:进攻各方军营的敌军接近终点时降雷。
* 本机/AI 各读 CarryStore,次数独立。
*/
export class BattleBuffBaseThunderHost {
private _battle: ProjectBattleHandle | null = null;
private _presenter: BattleGameBaseThunderPresenter | null = null;
private readonly _states: PlayerThunderState[] = [];
/** 当前一轮落雷进行中 */
private _striking = false;
/** 延迟击杀调度世代(unbind 时递增以作废) */
private _killScheduleGen = 0;
/** 绑定本局与表现层 */
bind(battle: ProjectBattleHandle, presenter: BattleGameBaseThunderPresenter | null): void {
const sameBattle = this._battle === battle;
if (!sameBattle) {
this.unbind();
}
this._battle = battle;
this._presenter = presenter;
this._rebuildStates(sameBattle);
}
/**
* AI 挂载后按 CarryStore 重扫(保留本局已触发次数)。
* @param battle 可选;开战时 Host 可能尚未 bind,传入则一并绑定
*/
refreshCarry(battle?: ProjectBattleHandle): void {
const b = battle ?? this._battle;
if (!b) {
console.log('[BaseThunder] refreshCarry 时尚未 bind');
return;
}
this._battle = b;
this._rebuildStates(true);
}
/**
* 该玩家御雷珠单局次数是否已用尽。
* @param playerId 防守方
*/
isTriggersExhausted(playerId: number): boolean {
for (let i = 0; i < this._states.length; i++) {
const state = this._states[i];
if (state.playerId !== playerId) continue;
return state.usedTriggers >= state.config.maxTriggers;
}
return false;
}
/** 解除绑定 */
unbind(): void {
this._battle = null;
this._presenter = null;
this._states.length = 0;
this._striking = false;
this._killScheduleGen++;
}
/**
* 重建各方神雷状态。
* @param keepUsed 是否保留已触发次数
*/
private _rebuildStates(keepUsed = false): void {
const prev: PlayerThunderState[] = [];
if (keepUsed) {
for (let i = 0; i < this._states.length; i++) {
prev.push(this._states[i]);
}
}
this._states.length = 0;
this._tryPush(0, prev);
this._tryPush(AI_PLAYER_ID, prev);
}
/**
* 若该玩家携带有效御雷珠则记入。
* @param playerId 槽位
* @param prev 重建前状态(用于保留次数)
*/
private _tryPush(playerId: number, prev: readonly PlayerThunderState[] = []): void {
const passives = BattleBuffCarryStore.getPassives(playerId);
const adjust = resolveCarriedBaseThunderConfig(passives);
if (!adjust.active || adjust.maxTriggers <= 0 || adjust.triggerCells <= 0 || adjust.effectCells <= 0) {
if (adjust.active) {
console.log('[BaseThunder] 御雷珠配置无效,未启用', playerId, adjust);
}
return;
}
if (adjust.smallDmgWan <= 0 || adjust.bossDmgWan <= 0) {
console.log('[BaseThunder] 御雷珠伤害比例无效', playerId, adjust);
return;
}
let usedTriggers = 0;
for (let i = 0; i < prev.length; i++) {
if (prev[i].playerId === playerId) {
usedTriggers = prev[i].usedTriggers;
break;
}
}
this._states.push({ playerId, config: adjust, usedTriggers });
}
/** 帧更新:扫描触发条件 */
tick(_dt: number): void {
if (this._striking || !this._battle || this._states.length <= 0) return;
if (this._battle.session.phase !== BattlePhase.COMBAT) return;
for (let i = 0; i < this._states.length; i++) {
const state = this._states[i];
if (state.usedTriggers >= state.config.maxTriggers) continue;
const anchor = this._findTriggerAnchor(state.playerId, state.config.triggerCells);
if (anchor == null) continue;
const effectCells = this._collectEffectCells(anchor, state.config.effectCells);
if (effectCells.length <= 0) continue;
const targets = this._collectTargets(state.playerId, effectCells);
if (targets.length <= 0) continue;
state.usedTriggers++;
this._striking = true;
const config = state.config;
const presenter = this._presenter;
if (presenter) {
presenter.playStrike(
targets,
(entityId) => this._applyDamage(state.playerId, config, entityId),
() => {
this._striking = false;
},
);
return;
}
for (let t = 0; t < targets.length; t++) {
this._applyDamage(state.playerId, config, targets[t]);
}
this._striking = false;
return;
}
}
/**
* 找最靠近终点、且处于触发距离内的进攻指定玩家的小怪/Boss/召唤怪。
* @param ownerPlayerId 防守方
* @param triggerCells 触发距离
*/
private _findTriggerAnchor(ownerPlayerId: number, triggerCells: number): EntityId | null {
const battle = this._battle;
if (!battle) return null;
const session = battle.session;
const thresholdCells = Math.max(1, Math.floor(triggerCells));
let bestId: EntityId | null = null;
let bestRemaining = Number.POSITIVE_INFINITY;
session.world.forEachAlive((entityId) => {
if (!this._isThunderCandidate(battle, ownerPlayerId, entityId)) return;
const move = session.world.getComponent(entityId, MovementComp);
if (!move || move.spawnProtected || move.presentLocked || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
const remainingCells = this._getRemainingCellsToAttackTarget(session, entityId);
if (!Number.isFinite(remainingCells) || remainingCells > thresholdCells) return;
if (remainingCells < bestRemaining) {
bestRemaining = remainingCells;
bestId = entityId;
}
});
return bestId;
}
/**
* 实体沿进路至攻击终点的剩余格数。
* 取「未走完路径步数」与「距离场格数」的较小值,避免 path 过长或重寻路导致漏触发。
*/
private _getRemainingCellsToAttackTarget(session: BattleSession, entityId: EntityId): number {
const world = session.world;
const move = world.getComponent(entityId, MovementComp);
const tf = world.getComponent(entityId, TransformComp);
if (!move || !tf) return Number.POSITIVE_INFINITY;
if (move.reachedGoal) return 0;
let best = Number.POSITIVE_INFINITY;
const pathSteps = move.path.length - move.pathIndex;
if (pathSteps > 0) best = pathSteps;
const campAreaId = this._resolveEntityCampAreaId(session, entityId);
const cost = session.pathfinder.getPathCost({ row: tf.row, col: tf.col }, campAreaId);
if (Number.isFinite(cost) && cost >= 0) {
const costCells = Math.max(0, Math.ceil(cost / PATH_ORTH_STEP));
if (costCells < best) best = costCells;
}
if (Number.isFinite(best)) return best;
const forward = this._buildPathToAttackTarget(session, entityId);
if (forward.length <= 0) return 0;
return forward.length;
}
/** 从实体当前格起,沿进路至攻击目标的路径格(不含起点) */
private _buildPathToAttackTarget(session: BattleSession, entityId: EntityId): Cell[] {
const world = session.world;
const tf = world.getComponent(entityId, TransformComp);
if (!tf) return [];
const move = world.getComponent(entityId, MovementComp);
const out: Cell[] = [];
if (move && move.pathIndex < move.path.length) {
for (let i = move.pathIndex; i < move.path.length; i++) {
const c = move.path[i];
const last = out.length > 0 ? out[out.length - 1] : tf;
if (last.row === c.row && last.col === c.col) continue;
out.push({ row: c.row, col: c.col });
}
}
if (out.length > 0) return out;
const campAreaId = this._resolveEntityCampAreaId(session, entityId);
const from = { row: tf.row, col: tf.col };
const target = session.pathfinder.shortestTargetCell(from, campAreaId);
if (!target) return [];
const fullPath = session.pathfinder.buildPathToTarget(from, target, campAreaId);
if (fullPath.length <= 1) return [];
for (let i = 1; i < fullPath.length; i++) {
const c = fullPath[i];
const last = out.length > 0 ? out[out.length - 1] : from;
if (last.row === c.row && last.col === c.col) continue;
out.push({ row: c.row, col: c.col });
}
return out;
}
/** 实体出怪口所属阵营区域(与 MovementSystem.assignPath 一致) */
private _resolveEntityCampAreaId(session: BattleSession, entityId: EntityId): number | undefined {
const meta = session.world.getComponent(entityId, UnitMetaComp);
const port = meta?.spawnPoint ?? 0;
if (port < 1) {
console.log('[BaseThunder] 实体缺少有效 spawnPoint', entityId);
return undefined;
}
const area = session.topology.getSpawnPortCampArea(port);
if (area === undefined) {
console.log('[BaseThunder] 出怪口缺少阵营区域', port, entityId);
}
return area;
}
/** 实体出怪口格(与 MovementSystem 一致) */
private _resolveEntitySpawnCell(session: BattleSession, entityId: EntityId): Cell | null {
const meta = session.world.getComponent(entityId, UnitMetaComp);
const port = meta?.spawnPoint ?? 0;
if (port < 1) {
console.log('[BaseThunder] 实体缺少有效 spawnPoint', entityId);
return null;
}
const spawn = session.topology.getSpawnCell(port);
if (!spawn) {
console.log('[BaseThunder] 出怪口格缺失', port, entityId);
}
return spawn ?? null;
}
/**
* 从锚点格起沿进路向出怪口后退收集生效格(含锚点格,共 effectCells 格)。
* @param anchorId 锚点
* @param effectCells 生效格数
*/
private _collectEffectCells(anchorId: EntityId, effectCells: number): Cell[] {
const battle = this._battle;
const count = Math.max(1, Math.floor(effectCells));
if (!battle) return [];
const session = battle.session;
const world = session.world;
const tf = world.getComponent(anchorId, TransformComp);
if (!tf) return [];
const from = { row: tf.row, col: tf.col };
const meta = world.getComponent(anchorId, UnitMetaComp);
const port = meta?.spawnPoint ?? 0;
const spawn = this._resolveEntitySpawnCell(session, anchorId);
if (!spawn || port < 1) return [{ ...from }];
const retreatSteps = count - 1;
const path = session.pathfinder.buildRetreatPath(from, retreatSteps, spawn, port);
if (path.length <= 0) return [{ ...from }];
if (path.length <= count) return path;
return path.slice(0, count);
}
/**
* 收集生效格上的小怪/Boss;排序为锚点格(近终点)优先,再沿后退方向。
* @param ownerPlayerId 防守方
* @param effectCells 生效格
*/
private _collectTargets(ownerPlayerId: number, effectCells: Cell[]): EntityId[] {
const battle = this._battle;
if (!battle) return [];
const session = battle.session;
const entries: ThunderTargetEntry[] = [];
session.world.forEachAlive((entityId) => {
if (!this._isThunderCandidate(battle, ownerPlayerId, entityId)) return;
const tf = session.world.getComponent(entityId, TransformComp);
const move = session.world.getComponent(entityId, MovementComp);
let cellOrder = -1;
if (move) {
const cell = session.grid.worldToCell(move.worldX, move.worldY);
cellOrder = this._indexOfCell(effectCells, cell.row, cell.col);
}
if (cellOrder < 0 && tf) {
cellOrder = this._indexOfCell(effectCells, tf.row, tf.col);
}
if (cellOrder < 0) return;
entries.push({ entityId, cellOrder });
});
entries.sort((a, b) => {
if (a.cellOrder !== b.cellOrder) return a.cellOrder - b.cellOrder;
return a.entityId - b.entityId;
});
const out: EntityId[] = [];
for (let i = 0; i < entries.length; i++) {
out.push(entries[i].entityId);
}
return out;
}
/** 格在序列中的索引;-1 表示不在序列内 */
private _indexOfCell(cells: Cell[], row: number, col: number): number {
for (let i = 0; i < cells.length; i++) {
if (cells[i].row === row && cells[i].col === col) return i;
}
return -1;
}
/**
* 是否可被御雷珠选中的进攻指定玩家的小怪/Boss/召唤怪。
* @param battle 战斗
* @param ownerPlayerId 防守方
* @param entityId 实体
*/
private _isThunderCandidate(battle: ProjectBattleHandle, ownerPlayerId: number, entityId: EntityId): boolean {
const session = battle.session;
const world = session.world;
if (!world.isAlive(entityId)) return false;
if (isMechanismUnit(session, entityId)) return false;
const team = world.getComponent(entityId, TeamComp);
if (!team || !BattleTeamId.isEnemyTeam(team.team)) return false;
if (Math.max(0, Math.floor(Number(team.ownerId) || 0)) !== ownerPlayerId) return false;
const row = battle.resolveMonsterRow(entityId);
if (!row) return false;
const type = Math.floor(Number(row.type));
return type === MonsterType.SmallMonster
|| type === MonsterType.Boss
|| type === MonsterType.SummonMonster;
}
/**
* 对单目标结算伤害(走正常掉血/击杀流程)。
* @param ownerPlayerId 防守方
* @param config 配置
* @param entityId 目标
*/
private _applyDamage(ownerPlayerId: number, config: BattleBuffBaseThunderAdjust, entityId: EntityId): void {
const battle = this._battle;
if (!battle || !config) return;
const session = battle.session;
const world = session.world;
if (!world.isAlive(entityId)) return;
if (!this._isThunderCandidate(battle, ownerPlayerId, entityId)) return;
const hp = world.getComponent(entityId, HealthComp);
if (!hp || hp.maxHp <= 0) return;
const move = world.getComponent(entityId, MovementComp);
if (move?.spawnProtected || move?.presentLocked || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
const monsterRow = battle.resolveMonsterRow(entityId);
const isBoss = monsterRow != null && Math.floor(Number(monsterRow.type)) === MonsterType.Boss;
const wan = isBoss ? config.bossDmgWan : config.smallDmgWan;
const amount = Math.max(1, Math.floor(hp.maxHp * wan / 10000));
const hpBefore = hp.hp;
session.damage.apply(BASE_THUNDER_CASTER, entityId, amount, BASE_THUNDER_DAMAGE_TYPE, undefined, false);
session.events.flush();
session.events.emit(
BattleEventFactory.hitAfter(BASE_THUNDER_CASTER, entityId, amount, false, undefined, BASE_THUNDER_DAMAGE_TYPE),
true,
);
if (hp.hp <= 0) {
this._scheduleKill(entityId, hpBefore);
}
}
/** 飘字后再击杀,避免产桃/死亡特效抢在伤害数字前 */
private _scheduleKill(entityId: EntityId, hpBefore: number): void {
const battle = this._battle;
if (!battle) return;
const scheduleGen = this._killScheduleGen;
schedulePresentation(BASE_THUNDER_KILL_DELAY_SEC, () => {
if (scheduleGen !== this._killScheduleGen || !this._battle) return;
const session = this._battle.session;
const world = session.world;
if (!world.isAlive(entityId)) return;
const hpNow = world.getComponent(entityId, HealthComp);
if (!hpNow || hpNow.hp > 0) return;
session.lifecycle.kill(entityId, BASE_THUNDER_CASTER, hpBefore);
});
}
}
let _host: BattleBuffBaseThunderHost | null = null;
/** 全局御雷珠被动 Host(单局复用) */
export function getBattleBuffBaseThunderHost(): BattleBuffBaseThunderHost {
if (!_host) _host = new BattleBuffBaseThunderHost();
return _host;
}