- BattleRuntime 调度替换为完整规则:速度高优先、同速玩家优先、同阵营同速 PartyOrder 小优先;先制第一轮玩家全先行,第二轮恢复普通排序;RemainingTurnOrder 同步按调度序输出 - TurnBattleComponent 开局行动者为敌人时自动推进,协程等待 UI 监听注册避免先手事件丢失 - 调试入口支持 1v1 / 2v2 / 4v4 与先制开关;新增敌人队伍预设 4005(四普通敌人) - UI:修复 UIForm 复用导致第二次开战按钮监听丢失;EnemySlots/TurnSlots 改为按数量复用 item,图标同一单位不重复加载 - EditMode 新增 10 个调度用例(共 35 个):排序规则、先制两轮、重复敌人分别击杀、4v4 创建、死亡移出候选、固定随机序列复现
361 lines
12 KiB
C#
361 lines
12 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using SepCore.Battle;
|
||
using SepCore.Definition;
|
||
using SepCore.UI;
|
||
using UnityEngine;
|
||
using UnityGameFramework.Runtime;
|
||
|
||
namespace SepCore.CustomComponent
|
||
{
|
||
/// <summary>
|
||
/// 单局战斗组件,也是战斗模块的唯一外部入口。
|
||
/// 负责本局临时角色状态、计时、探索暂停标志、战斗占用和当前唯一的 BattleRuntime。
|
||
/// 外部通过 TryStartBattle 启动战斗、CloseBattle 关闭调试战斗;
|
||
/// 战斗规则(调度、指令校验、效果、敌人决策)在内部共享同一个 BattleRuntime,
|
||
/// 不为模块内部的函数调用建立请求/响应 DTO。
|
||
/// 本局种子与共享随机源由独立的 RandomComponent 持有,战斗只沿用,不创建私有随机源。
|
||
/// </summary>
|
||
public class TurnBattleComponent : GameFrameworkComponent
|
||
{
|
||
private readonly List<RunPlayerState> _players = new List<RunPlayerState>();
|
||
private float _elapsedMs;
|
||
private bool _timerPaused;
|
||
private bool _explorationPaused;
|
||
private bool _battleActive;
|
||
private BattleRuntime _runtime;
|
||
private IBattleConfigProvider _config;
|
||
private Action<BattleResult> _onCompleted;
|
||
private Action<BattleStep> _stepListener;
|
||
private Coroutine _autoAdvanceRoutine;
|
||
|
||
/// <summary>
|
||
/// 敌人自动行动前的间歇时间(秒),用于让 UI 展示行动过程。
|
||
/// </summary>
|
||
private const float AutoAdvanceDelaySeconds = 0.6f;
|
||
|
||
/// <summary>
|
||
/// 获取本局临时角色状态列表(只读)。
|
||
/// </summary>
|
||
public IReadOnlyList<RunPlayerState> Players => _players;
|
||
|
||
/// <summary>
|
||
/// 获取本局已流逝时间(毫秒)。
|
||
/// </summary>
|
||
public long RunElapsedMs => (long)_elapsedMs;
|
||
|
||
/// <summary>
|
||
/// 获取单局计时是否已暂停。
|
||
/// </summary>
|
||
public bool IsTimerPaused => _timerPaused;
|
||
|
||
/// <summary>
|
||
/// 获取探索更新是否已暂停。
|
||
/// 战斗期间为 true,地图更新统一以此作为门禁。
|
||
/// </summary>
|
||
public bool IsExplorationPaused => _explorationPaused;
|
||
|
||
/// <summary>
|
||
/// 获取当前是否存在战斗占用。
|
||
/// 战斗重复进入由此标志拒绝。
|
||
/// </summary>
|
||
public bool IsBattleActive => _battleActive;
|
||
|
||
/// <summary>
|
||
/// 获取当前唯一的战斗运行时;仅战斗期间非 null。
|
||
/// </summary>
|
||
internal BattleRuntime Runtime => _runtime;
|
||
|
||
/// <summary>
|
||
/// 开始一局新的单局:重置全部临时状态。
|
||
/// </summary>
|
||
public void BeginRun()
|
||
{
|
||
ResetRunState();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结束当前单局,清空全部临时状态。
|
||
/// </summary>
|
||
public void EndRun()
|
||
{
|
||
ResetRunState();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用指定状态替换本局临时角色列表,保持战备顺序。
|
||
/// </summary>
|
||
/// <param name="players">新的临时角色状态,可为空。</param>
|
||
public void ReplacePlayers(IEnumerable<RunPlayerState> players)
|
||
{
|
||
_players.Clear();
|
||
if (players != null)
|
||
{
|
||
_players.AddRange(players);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试开始一场战斗,是战斗模块的唯一外部入口。
|
||
/// 校验通过后预留战斗占用、暂停探索更新与单局计时并打开 BattleForm;
|
||
/// 校验失败不消耗随机数、不修改单局状态。
|
||
/// 战斗完成时通过 onCompleted 一次性回调返回 BattleResult;调试入口可不提供回调。
|
||
/// </summary>
|
||
/// <param name="encounter">探索层创建的遭遇输入。</param>
|
||
/// <param name="onCompleted">战斗结束的一次性完成回调,可为 null。</param>
|
||
/// <returns>是否成功开始。</returns>
|
||
public bool TryStartBattle(BattleEncounter encounter, Action<BattleResult> onCompleted)
|
||
{
|
||
if (encounter == null)
|
||
{
|
||
Log.Error("Can not start battle with null encounter.");
|
||
return false;
|
||
}
|
||
|
||
if (_battleActive)
|
||
{
|
||
Log.Warning("Can not start battle because battle occupancy is already active.");
|
||
return false;
|
||
}
|
||
|
||
if (GameEntry.Random == null || GameEntry.Random.Random == null)
|
||
{
|
||
Log.Warning("Can not start battle because run random source is not initialized.");
|
||
return false;
|
||
}
|
||
|
||
if (_players.Count == 0)
|
||
{
|
||
Log.Warning("Can not start battle because no run player state exists.");
|
||
return false;
|
||
}
|
||
|
||
if (GameEntry.Luban == null || !GameEntry.Luban.IsReady)
|
||
{
|
||
Log.Warning("Can not start battle because battle config is not ready.");
|
||
return false;
|
||
}
|
||
|
||
if (_config == null)
|
||
{
|
||
_config = new LubanBattleConfigProvider();
|
||
}
|
||
|
||
BattleRuntime runtime = BattleRuntime.Create(encounter, _players, _config, GameEntry.Random.Random);
|
||
if (runtime == null)
|
||
{
|
||
Log.Warning("Can not start battle because battle runtime can not be created from given encounter/players/config.");
|
||
return false;
|
||
}
|
||
|
||
_runtime = runtime;
|
||
_onCompleted = onCompleted;
|
||
_battleActive = true;
|
||
_explorationPaused = true;
|
||
_timerPaused = true;
|
||
|
||
GameEntry.UI.OpenUIForm(UIFormType.BattleForm);
|
||
Log.Info("Battle started with encounter '{0}'.", encounter.EncounterId);
|
||
|
||
// M2:敌人速度更高时开局行动者为敌人,同样按间歇节奏自动推进
|
||
ScheduleAutoAdvance();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提交当前玩家指令并同步推进单次行动,返回本次推进的行动记录、最新视图和可能的最终结果。
|
||
/// 非法指令不消耗行动机会;玩家行动后若轮到敌人,由组件按间歇节奏自动推进敌人回合,
|
||
/// 每一步通过已注册的推进监听推送给 UI。
|
||
/// 战斗完成后向 onCompleted 一次性回调返回 BattleResult。
|
||
/// </summary>
|
||
/// <param name="command">当前玩家行动者的指令。</param>
|
||
/// <returns>本次玩家行动的推进结果;当前没有进行中的战斗时返回 null。</returns>
|
||
public BattleStep SubmitCommand(BattleCommand command)
|
||
{
|
||
if (_runtime == null || _runtime.IsCompleted)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
BattleStep step = _runtime.SubmitCommand(command);
|
||
if (step.Result != null)
|
||
{
|
||
ApplyResultWriteback(step.Result);
|
||
Action<BattleResult> callback = _onCompleted;
|
||
_onCompleted = null;
|
||
callback?.Invoke(step.Result);
|
||
}
|
||
else
|
||
{
|
||
ScheduleAutoAdvance();
|
||
}
|
||
|
||
return step;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册战斗推进监听;UI 通过它接收组件自动推进(敌人行动)产生的每一步。
|
||
/// </summary>
|
||
/// <param name="listener">推进监听,传入 null 取消注册。</param>
|
||
public void SetStepListener(Action<BattleStep> listener)
|
||
{
|
||
_stepListener = listener;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前战斗的只读视图;没有进行中的战斗时返回 null。
|
||
/// </summary>
|
||
public BattleViewState GetViewState()
|
||
{
|
||
return _runtime != null ? _runtime.BuildViewState() : null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭当前战斗壳层并恢复探索更新与单局计时。
|
||
/// M0 调试入口专用;M5 起由战斗结果统一回写单局状态后关闭。
|
||
/// </summary>
|
||
public void CloseBattle()
|
||
{
|
||
if (!_battleActive)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UGuiForm form = GameEntry.UI.GetUIForm(UIFormType.BattleForm);
|
||
if (form != null)
|
||
{
|
||
GameEntry.UI.CloseUIForm(form);
|
||
}
|
||
|
||
_runtime = null;
|
||
_onCompleted = null;
|
||
StopAutoAdvance();
|
||
_stepListener = null;
|
||
_explorationPaused = false;
|
||
_timerPaused = false;
|
||
_battleActive = false;
|
||
Log.Info("Battle closed.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置探索更新暂停状态。
|
||
/// </summary>
|
||
/// <param name="paused">是否暂停探索更新。</param>
|
||
public void SetExplorationPaused(bool paused)
|
||
{
|
||
_explorationPaused = paused;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置单局计时暂停状态。
|
||
/// </summary>
|
||
/// <param name="paused">是否暂停单局计时。</param>
|
||
public void SetTimerPaused(bool paused)
|
||
{
|
||
_timerPaused = paused;
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (_timerPaused)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_elapsedMs += Time.deltaTime * 1000f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前行动者是敌人且战斗未结束时,启动带间歇的自动推进协程。
|
||
/// </summary>
|
||
private void ScheduleAutoAdvance()
|
||
{
|
||
if (_autoAdvanceRoutine != null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_runtime == null || _runtime.IsCompleted || _runtime.CurrentActor == null ||
|
||
_runtime.CurrentActor.Faction != BattleFaction.Enemy)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_autoAdvanceRoutine = StartCoroutine(AutoAdvanceRoutine());
|
||
}
|
||
|
||
private System.Collections.IEnumerator AutoAdvanceRoutine()
|
||
{
|
||
// 战斗 UI 打开是异步资源加载,等推进监听注册后再推第一步,避免敌人先手事件丢失
|
||
int waitFrames = 0;
|
||
while (_stepListener == null && _runtime != null && !_runtime.IsCompleted && waitFrames++ < 600)
|
||
{
|
||
yield return null;
|
||
}
|
||
|
||
while (_runtime != null && !_runtime.IsCompleted &&
|
||
_runtime.CurrentActor != null && _runtime.CurrentActor.Faction == BattleFaction.Enemy)
|
||
{
|
||
yield return new WaitForSeconds(AutoAdvanceDelaySeconds);
|
||
|
||
BattleStep step = _runtime.AdvanceEnemyTurn();
|
||
if (step.Result != null)
|
||
{
|
||
ApplyResultWriteback(step.Result);
|
||
Action<BattleResult> callback = _onCompleted;
|
||
_onCompleted = null;
|
||
callback?.Invoke(step.Result);
|
||
}
|
||
|
||
_stepListener?.Invoke(step);
|
||
}
|
||
|
||
_autoAdvanceRoutine = null;
|
||
}
|
||
|
||
private void StopAutoAdvance()
|
||
{
|
||
if (_autoAdvanceRoutine != null)
|
||
{
|
||
StopCoroutine(_autoAdvanceRoutine);
|
||
_autoAdvanceRoutine = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 非 TotalDefeat 结果把玩家战后 HP/MP 回写到单局临时状态。
|
||
/// 首版保留原始战斗值;M5 按结束矩阵统一应用阵亡恢复 1/1 等规则。
|
||
/// </summary>
|
||
private void ApplyResultWriteback(BattleResult result)
|
||
{
|
||
if (result.Outcome == BattleOutcome.TotalDefeat)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (BattlePlayerResult playerResult in result.Players)
|
||
{
|
||
RunPlayerState state = _players.Find(player => player.CharacterId == playerResult.CharacterId);
|
||
if (state != null)
|
||
{
|
||
state.CurrentHp = playerResult.CurrentHp;
|
||
state.CurrentMp = playerResult.CurrentMp;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ResetRunState()
|
||
{
|
||
_players.Clear();
|
||
_elapsedMs = 0;
|
||
_timerPaused = false;
|
||
_explorationPaused = false;
|
||
_battleActive = false;
|
||
_runtime = null;
|
||
_onCompleted = null;
|
||
StopAutoAdvance();
|
||
_stepListener = null;
|
||
}
|
||
}
|
||
}
|