M0 回合制战斗壳层:接口与单局状态、调试入口、暂停联动

This commit is contained in:
huangjun
2026-09-01 10:20:02 +08:00
parent fce331da84
commit 8413c2688b
82 changed files with 12032 additions and 8 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bdc9ce5dfa33f4f409f98053881b767a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
namespace SepCore.CustomComponent
{
/// <summary>
/// 本局共享随机数接口。
/// 同一单局的地图生成、敌人 AI、逃跑和敌人掉落依次消费同一个实例;
/// 战斗不能根据 EncounterId、回合数或当前时间重新播种。
/// </summary>
public interface IRunRandomSource
{
/// <summary>
/// 返回 [minInclusive, maxExclusive) 范围内的随机整数。
/// </summary>
int NextInt(int minInclusive, int maxExclusive);
/// <summary>
/// 以千分比概率判定是否成功;RollPermille(0) 必失败,RollPermille(1000) 必成功。
/// </summary>
bool RollPermille(int successPermille);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1e17f39494674cf08295b96df17642e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
using UnityGameFramework.Runtime;
namespace SepCore.CustomComponent
{
/// <summary>
/// 单局共享随机组件。
/// 种子由局外(战备/部署界面玩家输入)带入,进入单局时初始化;
/// 地图生成、物资点、敌人 AI、逃跑判定和敌人掉落依次消费同一个随机源,
/// 战斗只沿用该随机源,不创建战斗私有随机源。
/// 对应 Docs/GameDesign/03_RunExploration.md 的随机生成器约定。
/// </summary>
public class RandomComponent : GameFrameworkComponent
{
private int _seed;
private IRunRandomSource _random;
/// <summary>
/// 获取本局随机种子;未开始单局时为 0。
/// </summary>
public int Seed => _seed;
/// <summary>
/// 获取本局共享随机源;未开始单局时为 null。
/// </summary>
public IRunRandomSource Random => _random;
/// <summary>
/// 开始一局新的单局:使用局外带入的种子初始化共享随机源。
/// </summary>
/// <param name="seed">本局随机种子。</param>
public void BeginRun(int seed)
{
_seed = seed;
_random = new RunRandomSource(seed);
}
/// <summary>
/// 结束当前单局,清空种子与随机源。
/// </summary>
public void EndRun()
{
_seed = 0;
_random = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 373a779ff43a4289b8e8cb23459d7bb3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using System;
namespace SepCore.CustomComponent
{
/// <summary>
/// 以 System.Random 实现的本局共享随机源。
/// 由 TurnBattleComponent 以单局 seed 创建,供地图生成、敌人 AI、逃跑与掉落共用。
/// EditMode 测试使用可注入的序列随机源,不依赖本类。
/// </summary>
public sealed class RunRandomSource : IRunRandomSource
{
private readonly Random _random;
/// <summary>
/// 使用指定种子创建随机源。
/// </summary>
public RunRandomSource(int seed)
{
_random = new Random(seed);
}
public int NextInt(int minInclusive, int maxExclusive)
{
return _random.Next(minInclusive, maxExclusive);
}
public bool RollPermille(int successPermille)
{
return NextInt(0, 1000) < successPermille;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4dc1aacc472441329e01cf158f17981e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2569cd06130964d66b109493dabdfc77
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,112 @@
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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0146e261d98d40109183900a1d033fc9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,64 @@
namespace SepCore.CustomComponent
{
/// <summary>
/// 单局临时角色状态。
/// 数值由单局快照构建器在开战前从角色与装备结算得到;战斗期间只读,战斗结束回写当前 HP/MP。
/// </summary>
public sealed class RunPlayerState
{
/// <summary>
/// 角色配置标识。
/// </summary>
public int CharacterId;
/// <summary>
/// 当前战备中的顺序,1 开始。
/// </summary>
public int PartyOrder;
/// <summary>
/// 当前 HP。
/// </summary>
public int CurrentHp;
/// <summary>
/// 当前 MP。
/// </summary>
public int CurrentMp;
/// <summary>
/// 已结算装备加成的最终 HP 上限。
/// </summary>
public int MaxHp;
/// <summary>
/// 已结算装备加成的最终 MP 上限。
/// </summary>
public int MaxMp;
/// <summary>
/// 已结算装备加成的最终攻击。
/// </summary>
public int Atk;
/// <summary>
/// 已结算装备加成的最终魔力。
/// </summary>
public int Mat;
/// <summary>
/// 已结算装备加成的最终速度。
/// </summary>
public int Speed;
/// <summary>
/// 普通攻击行动配置 ID。
/// </summary>
public int AttackActionId;
/// <summary>
/// 该角色首版唯一技能行动配置 ID。
/// </summary>
public int SkillActionId;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3caaef559cc641769b9ede0b4d033532
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace SepCore.CustomComponent
{
/// <summary>
/// 单局战斗组件。
/// 负责本局临时角色状态、计时、探索暂停标志和战斗占用的最小状态。
/// 本局种子与共享随机源由独立的 RandomComponent 持有,战斗只沿用,不创建私有随机源。
/// 战斗协调(构建请求、打开 UI、回写结果)由独立的 RunBattleCoordinator 负责。
/// </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;
/// <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>
/// 开始一局新的单局:重置全部临时状态。
/// </summary>
public void BeginRun()
{
_players.Clear();
_elapsedMs = 0;
_timerPaused = false;
_explorationPaused = false;
_battleActive = false;
}
/// <summary>
/// 结束当前单局,清空全部临时状态。
/// </summary>
public void EndRun()
{
_players.Clear();
_elapsedMs = 0;
_timerPaused = false;
_explorationPaused = false;
_battleActive = false;
}
/// <summary>
/// 用指定状态替换本局临时角色列表,保持战备顺序。
/// </summary>
/// <param name="players">新的临时角色状态,可为空。</param>
public void ReplacePlayers(IEnumerable<RunPlayerState> players)
{
_players.Clear();
if (players != null)
{
_players.AddRange(players);
}
}
/// <summary>
/// 预留战斗占用,拒绝同一帧或未结束战斗的重复进入。
/// </summary>
/// <returns>预留成功返回 true;已存在战斗占用时返回 false。</returns>
public bool TryReserveBattle()
{
if (_battleActive)
{
return false;
}
_battleActive = true;
return true;
}
/// <summary>
/// 释放战斗占用。
/// </summary>
public void ReleaseBattle()
{
_battleActive = false;
}
/// <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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ceac378d38794bafb31a203753565121
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: