- 删除中间契约(BattleStartRequest/BattlePlayerInput/BattleReturnPlan/桥接与策略接口/FlowState),收敛为 BattleViewState/BattleStep/一层 BattleEvent - 新增内部 BattleRuntime/BattleUnit/BattleStatus 共享上下文;TurnBattleComponent 成为唯一外部入口,吸收启动/暂停/回写职责 - 内核支持 1v1 攻击闭环:效果公式、HP/MP 钳制、阵亡、Victory/TotalDefeat、敌人随机决策;敌人行动间歇 0.6s 由组件协程编排并经 StepListener 推送 - UI:玩家卡片/敌人槽/回合槽按战局重建,图标配置驱动(含新增 EnemyConfig.Icon),每轮刷新一次 TurnSlots 且轮内只移动高亮 - 新增 Tests 程序集 + InternalsVisibleTo,25 个 BattleRuntime 纯逻辑用例全部通过
351 lines
12 KiB
C#
351 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);
|
||
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()
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
}
|