M1 回合制战斗架构收敛与 1v1 攻击闭环:BattleRuntime 内核、组件唯一入口、UI 数据驱动与 EditMode 测试
- 删除中间契约(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 纯逻辑用例全部通过
This commit is contained in:
@@ -1,112 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.Battle;
|
||||
using SepCore.Definition;
|
||||
using SepCore.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace SepCore.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 单局战斗协调器(普通类,不挂场景)。
|
||||
/// 负责从单局状态构建请求、预留战斗占用、暂停/恢复探索更新与计时、打开/关闭战斗 UI;
|
||||
/// 后续里程碑在此创建并校验 BattleController,并消费 BattleResult 生成 BattleReturnPlan。
|
||||
/// 不负责回合排序、效果计算和 UI 控件状态。
|
||||
/// </summary>
|
||||
public sealed class RunBattleCoordinator
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试开始一场战斗。
|
||||
/// 战斗占用已激活时拒绝;请求构建失败时释放占用且不暂停地图。
|
||||
/// </summary>
|
||||
/// <param name="encounter">探索层创建的遭遇输入。</param>
|
||||
/// <returns>是否成功开始。</returns>
|
||||
public bool TryStartBattle(BattleEncounter encounter)
|
||||
{
|
||||
if (encounter == null)
|
||||
{
|
||||
Log.Error("Can not start battle with null encounter.");
|
||||
return false;
|
||||
}
|
||||
|
||||
TurnBattleComponent runBattle = GameEntry.TurnBattle;
|
||||
if (runBattle == null || runBattle.IsBattleActive)
|
||||
{
|
||||
Log.Warning("Can not start battle because battle occupancy is already active.");
|
||||
return false;
|
||||
}
|
||||
|
||||
BattleStartRequest request = BuildStartRequest(encounter);
|
||||
if (request == null)
|
||||
{
|
||||
Log.Warning("Can not start battle because start request is invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 预留战斗占用并暂停探索更新与单局计时;
|
||||
// 后续里程碑在此创建并校验 BattleController,失败则释放占用且不暂停地图
|
||||
runBattle.TryReserveBattle();
|
||||
runBattle.SetExplorationPaused(true);
|
||||
runBattle.SetTimerPaused(true);
|
||||
|
||||
GameEntry.UI.OpenUIForm(UIFormType.BattleForm);
|
||||
Log.Info("Battle started with encounter '{0}'.", encounter.EncounterId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭调试壳层并恢复探索更新与单局计时。
|
||||
/// M0 调试入口专用;M7 起由 ApplyResult 按战斗结果统一回写与恢复。
|
||||
/// </summary>
|
||||
public void EndDebugBattle()
|
||||
{
|
||||
TurnBattleComponent runBattle = GameEntry.TurnBattle;
|
||||
if (runBattle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UGuiForm form = GameEntry.UI.GetUIForm(UIFormType.BattleForm);
|
||||
if (form != null)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(form);
|
||||
}
|
||||
|
||||
runBattle.SetExplorationPaused(false);
|
||||
runBattle.SetTimerPaused(false);
|
||||
runBattle.ReleaseBattle();
|
||||
Log.Info("Debug battle closed.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从单局临时角色状态与共享随机源构建启动请求。
|
||||
/// 请求创建后视为只读;构建失败返回 null,不消耗随机数。
|
||||
/// </summary>
|
||||
private static BattleStartRequest BuildStartRequest(BattleEncounter encounter)
|
||||
{
|
||||
IRunRandomSource random = GameEntry.Random != null ? GameEntry.Random.Random : null;
|
||||
if (random == null)
|
||||
{
|
||||
Log.Warning("Can not start battle because run random source is not initialized.");
|
||||
return null;
|
||||
}
|
||||
|
||||
List<BattlePlayerInput> players = new List<BattlePlayerInput>();
|
||||
foreach (RunPlayerState state in GameEntry.TurnBattle.Players)
|
||||
{
|
||||
players.Add(new BattlePlayerInput(state.CharacterId, state.PartyOrder,
|
||||
state.CurrentHp, state.CurrentMp, state.MaxHp, state.MaxMp,
|
||||
state.Atk, state.Mat, state.Speed,
|
||||
state.AttackActionId, state.SkillActionId));
|
||||
}
|
||||
|
||||
if (players.Count == 0)
|
||||
{
|
||||
Log.Warning("Can not start battle because no run player state exists.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BattleStartRequest(encounter.EncounterId, players, encounter.EnemyPartyConfigId,
|
||||
encounter.IsPreemptive, random);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0146e261d98d40109183900a1d033fc9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,14 +1,20 @@
|
||||
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 持有,战斗只沿用,不创建私有随机源。
|
||||
/// 战斗协调(构建请求、打开 UI、回写结果)由独立的 RunBattleCoordinator 负责。
|
||||
/// </summary>
|
||||
public class TurnBattleComponent : GameFrameworkComponent
|
||||
{
|
||||
@@ -17,6 +23,16 @@ namespace SepCore.CustomComponent
|
||||
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>
|
||||
/// 获取本局临时角色状态列表(只读)。
|
||||
@@ -45,16 +61,17 @@ namespace SepCore.CustomComponent
|
||||
/// </summary>
|
||||
public bool IsBattleActive => _battleActive;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前唯一的战斗运行时;仅战斗期间非 null。
|
||||
/// </summary>
|
||||
internal BattleRuntime Runtime => _runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 开始一局新的单局:重置全部临时状态。
|
||||
/// </summary>
|
||||
public void BeginRun()
|
||||
{
|
||||
_players.Clear();
|
||||
_elapsedMs = 0;
|
||||
_timerPaused = false;
|
||||
_explorationPaused = false;
|
||||
_battleActive = false;
|
||||
ResetRunState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,11 +79,7 @@ namespace SepCore.CustomComponent
|
||||
/// </summary>
|
||||
public void EndRun()
|
||||
{
|
||||
_players.Clear();
|
||||
_elapsedMs = 0;
|
||||
_timerPaused = false;
|
||||
_explorationPaused = false;
|
||||
_battleActive = false;
|
||||
ResetRunState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -83,26 +96,142 @@ namespace SepCore.CustomComponent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预留战斗占用,拒绝同一帧或未结束战斗的重复进入。
|
||||
/// 尝试开始一场战斗,是战斗模块的唯一外部入口。
|
||||
/// 校验通过后预留战斗占用、暂停探索更新与单局计时并打开 BattleForm;
|
||||
/// 校验失败不消耗随机数、不修改单局状态。
|
||||
/// 战斗完成时通过 onCompleted 一次性回调返回 BattleResult;调试入口可不提供回调。
|
||||
/// </summary>
|
||||
/// <returns>预留成功返回 true;已存在战斗占用时返回 false。</returns>
|
||||
public bool TryReserveBattle()
|
||||
/// <param name="encounter">探索层创建的遭遇输入。</param>
|
||||
/// <param name="onCompleted">战斗结束的一次性完成回调,可为 null。</param>
|
||||
/// <returns>是否成功开始。</returns>
|
||||
public bool TryStartBattle(BattleEncounter encounter, Action<BattleResult> onCompleted)
|
||||
{
|
||||
if (_battleActive)
|
||||
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>
|
||||
public void ReleaseBattle()
|
||||
/// <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>
|
||||
@@ -132,5 +261,90 @@ namespace SepCore.CustomComponent
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user