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:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Tests")]
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c518c47a9854ba19045abd1f211eb7d
|
||||
guid: cd1500230107441cfa5e86657fa0b9e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -16,11 +16,6 @@ public partial class GameEntry : MonoBehaviour
|
||||
|
||||
public static TurnBattleComponent TurnBattle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单局战斗协调器(普通类,非场景组件)。
|
||||
/// </summary>
|
||||
public static RunBattleCoordinator RunBattle { get; private set; }
|
||||
|
||||
private static void InitCustomComponents()
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
@@ -28,6 +23,5 @@ public partial class GameEntry : MonoBehaviour
|
||||
Save = UnityGameFramework.Runtime.GameEntry.GetComponent<SaveComponent>();
|
||||
Random = UnityGameFramework.Runtime.GameEntry.GetComponent<RandomComponent>();
|
||||
TurnBattle = UnityGameFramework.Runtime.GameEntry.GetComponent<TurnBattleComponent>();
|
||||
RunBattle = new RunBattleCoordinator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个效果对单个目标产生的数值或状态变化。
|
||||
/// </summary>
|
||||
public sealed class BattleActionEffectRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// 目标单位。
|
||||
/// </summary>
|
||||
public readonly int TargetUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算前的 HP。
|
||||
/// </summary>
|
||||
public readonly int BeforeHp;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算后的 HP。
|
||||
/// </summary>
|
||||
public readonly int AfterHp;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算前的 MP。
|
||||
/// </summary>
|
||||
public readonly int BeforeMp;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算后的 MP。
|
||||
/// </summary>
|
||||
public readonly int AfterMp;
|
||||
|
||||
/// <summary>
|
||||
/// 状态变化列表;无状态变化时为空。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattleStatusChangeRecord> StatusChanges;
|
||||
|
||||
public BattleActionEffectRecord(int targetUnitId, int beforeHp, int afterHp, int beforeMp, int afterMp,
|
||||
IReadOnlyList<BattleStatusChangeRecord> statusChanges)
|
||||
{
|
||||
TargetUnitId = targetUnitId;
|
||||
BeforeHp = beforeHp;
|
||||
AfterHp = afterHp;
|
||||
BeforeMp = beforeMp;
|
||||
AfterMp = afterMp;
|
||||
StatusChanges = statusChanges != null ? Copy(statusChanges) : new BattleStatusChangeRecord[0];
|
||||
}
|
||||
|
||||
private static BattleStatusChangeRecord[] Copy(IReadOnlyList<BattleStatusChangeRecord> source)
|
||||
{
|
||||
BattleStatusChangeRecord[] copy = new BattleStatusChangeRecord[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态变化记录。
|
||||
/// </summary>
|
||||
public sealed class BattleStatusChangeRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态类型。
|
||||
/// </summary>
|
||||
public readonly BattleStatusType StatusType;
|
||||
|
||||
/// <summary>
|
||||
/// 变化后的剩余持续次数;状态被移除时为 0。
|
||||
/// </summary>
|
||||
public readonly int RemainingRounds;
|
||||
|
||||
public BattleStatusChangeRecord(BattleStatusType statusType, int remainingRounds)
|
||||
{
|
||||
StatusType = statusType;
|
||||
RemainingRounds = remainingRounds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一次行动产生的有序行动记录。
|
||||
/// 首版 UI 立即消费并显示;未来动画系统可以逐条等待播放,不需要修改战斗规则内核。
|
||||
/// </summary>
|
||||
public sealed class BattleActionRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// 行动者。
|
||||
/// </summary>
|
||||
public readonly int ActorUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 指令类型,对应配表 BattleActionType。
|
||||
/// </summary>
|
||||
public readonly BattleActionType CommandType;
|
||||
|
||||
/// <summary>
|
||||
/// 攻击或技能配置 ID;逃跑为 0。
|
||||
/// </summary>
|
||||
public readonly int ActionConfigId;
|
||||
|
||||
/// <summary>
|
||||
/// 按结算顺序排列的效果记录。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattleActionEffectRecord> Effects;
|
||||
|
||||
public BattleActionRecord(int actorUnitId, BattleActionType commandType, int actionConfigId,
|
||||
IReadOnlyList<BattleActionEffectRecord> effects)
|
||||
{
|
||||
ActorUnitId = actorUnitId;
|
||||
CommandType = commandType;
|
||||
ActionConfigId = actionConfigId;
|
||||
Effects = effects != null ? Copy(effects) : new BattleActionEffectRecord[0];
|
||||
}
|
||||
|
||||
private static BattleActionEffectRecord[] Copy(IReadOnlyList<BattleActionEffectRecord> source)
|
||||
{
|
||||
BattleActionEffectRecord[] copy = new BattleActionEffectRecord[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 战斗推进返回值。
|
||||
/// </summary>
|
||||
public sealed class BattleAdvance
|
||||
{
|
||||
/// <summary>
|
||||
/// 本次推进产生的有序行动记录列表。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattleActionRecord> Records;
|
||||
|
||||
/// <summary>
|
||||
/// 推进结束后的只读战斗快照。
|
||||
/// </summary>
|
||||
public readonly BattleSnapshot Snapshot;
|
||||
|
||||
/// <summary>
|
||||
/// 流程状态。
|
||||
/// </summary>
|
||||
public readonly BattleFlowState FlowState;
|
||||
|
||||
/// <summary>
|
||||
/// 仅在 Completed 时存在,否则为 null。
|
||||
/// </summary>
|
||||
public readonly BattleResult Result;
|
||||
|
||||
public BattleAdvance(IReadOnlyList<BattleActionRecord> records, BattleSnapshot snapshot,
|
||||
BattleFlowState flowState, BattleResult result)
|
||||
{
|
||||
Records = records != null ? Copy(records) : new BattleActionRecord[0];
|
||||
Snapshot = snapshot;
|
||||
FlowState = flowState;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
private static BattleActionRecord[] Copy(IReadOnlyList<BattleActionRecord> source)
|
||||
{
|
||||
BattleActionRecord[] copy = new BattleActionRecord[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 一次效果对单个目标的一层行动记录。
|
||||
/// 表达行动者、行动、目标以及数值或状态变化;
|
||||
/// StatusType 为 None 时表示没有状态变化。
|
||||
/// </summary>
|
||||
public sealed class BattleEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 行动者。
|
||||
/// </summary>
|
||||
public readonly int ActorUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 指令类型,对应配表 BattleActionType。
|
||||
/// </summary>
|
||||
public readonly BattleActionType CommandType;
|
||||
|
||||
/// <summary>
|
||||
/// 攻击或技能配置 ID;逃跑为 0。
|
||||
/// </summary>
|
||||
public readonly int ActionConfigId;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算的目标单位。
|
||||
/// </summary>
|
||||
public readonly int TargetUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算前的 HP。
|
||||
/// </summary>
|
||||
public readonly int BeforeHp;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算后的 HP。
|
||||
/// </summary>
|
||||
public readonly int AfterHp;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算前的 MP。
|
||||
/// </summary>
|
||||
public readonly int BeforeMp;
|
||||
|
||||
/// <summary>
|
||||
/// 效果结算后的 MP。
|
||||
/// </summary>
|
||||
public readonly int AfterMp;
|
||||
|
||||
/// <summary>
|
||||
/// 施加或刷新的状态类型;无状态变化时为 None。
|
||||
/// </summary>
|
||||
public readonly BattleStatusType StatusType;
|
||||
|
||||
/// <summary>
|
||||
/// 变化后的剩余持续次数(行动机会);状态被移除时为 0。
|
||||
/// </summary>
|
||||
public readonly int StatusRemainingRounds;
|
||||
|
||||
public BattleEvent(int actorUnitId, BattleActionType commandType, int actionConfigId,
|
||||
int targetUnitId, int beforeHp, int afterHp, int beforeMp, int afterMp,
|
||||
BattleStatusType statusType, int statusRemainingRounds)
|
||||
{
|
||||
ActorUnitId = actorUnitId;
|
||||
CommandType = commandType;
|
||||
ActionConfigId = actionConfigId;
|
||||
TargetUnitId = targetUnitId;
|
||||
BeforeHp = beforeHp;
|
||||
AfterHp = afterHp;
|
||||
BeforeMp = beforeMp;
|
||||
AfterMp = afterMp;
|
||||
StatusType = statusType;
|
||||
StatusRemainingRounds = statusRemainingRounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84f740882a72430ca27c4d77004257d8
|
||||
guid: 9bd0c7df032194911a2aee0942338d48
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 战斗流程状态。
|
||||
/// </summary>
|
||||
public enum BattleFlowState
|
||||
{
|
||||
/// <summary>
|
||||
/// 等待当前玩家行动者提交指令。
|
||||
/// </summary>
|
||||
AwaitingPlayerCommand,
|
||||
|
||||
/// <summary>
|
||||
/// 战斗完成,结果已产出。
|
||||
/// </summary>
|
||||
Completed,
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家输入快照,是进入本场战斗时的值快照。
|
||||
/// 装备计算由单局角色快照构建器完成;战斗内核不读取角色存档、装备栏或背包。
|
||||
/// </summary>
|
||||
public sealed class BattlePlayerInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色配置标识;同时用于把结果回写到单局角色状态。
|
||||
/// </summary>
|
||||
public readonly int CharacterId;
|
||||
|
||||
/// <summary>
|
||||
/// 当前战备中的顺序。
|
||||
/// </summary>
|
||||
public readonly int PartyOrder;
|
||||
|
||||
/// <summary>
|
||||
/// 单局临时状态中的当前 HP。
|
||||
/// </summary>
|
||||
public readonly int CurrentHp;
|
||||
|
||||
/// <summary>
|
||||
/// 单局临时状态中的当前 MP。
|
||||
/// </summary>
|
||||
public readonly int CurrentMp;
|
||||
|
||||
/// <summary>
|
||||
/// 已结算装备加成的最终 HP 上限。
|
||||
/// </summary>
|
||||
public readonly int MaxHp;
|
||||
|
||||
/// <summary>
|
||||
/// 已结算装备加成的最终 MP 上限。
|
||||
/// </summary>
|
||||
public readonly int MaxMp;
|
||||
|
||||
/// <summary>
|
||||
/// 已结算装备加成的最终攻击。
|
||||
/// </summary>
|
||||
public readonly int Atk;
|
||||
|
||||
/// <summary>
|
||||
/// 已结算装备加成的最终魔力。
|
||||
/// </summary>
|
||||
public readonly int Mat;
|
||||
|
||||
/// <summary>
|
||||
/// 已结算装备加成的最终速度。
|
||||
/// </summary>
|
||||
public readonly int Speed;
|
||||
|
||||
/// <summary>
|
||||
/// 普通攻击行动配置 ID。
|
||||
/// </summary>
|
||||
public readonly int AttackActionId;
|
||||
|
||||
/// <summary>
|
||||
/// 该角色首版唯一技能行动配置 ID。
|
||||
/// </summary>
|
||||
public readonly int SkillActionId;
|
||||
|
||||
public BattlePlayerInput(int characterId, int partyOrder, int currentHp, int currentMp,
|
||||
int maxHp, int maxMp, int atk, int mat, int speed, int attackActionId, int skillActionId)
|
||||
{
|
||||
CharacterId = characterId;
|
||||
PartyOrder = partyOrder;
|
||||
CurrentHp = currentHp;
|
||||
CurrentMp = currentMp;
|
||||
MaxHp = maxHp;
|
||||
MaxMp = maxMp;
|
||||
Atk = atk;
|
||||
Mat = mat;
|
||||
Speed = speed;
|
||||
AttackActionId = attackActionId;
|
||||
SkillActionId = skillActionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 每名参战玩家的战后结果。
|
||||
/// 保留原始战斗值:阵亡者 HP 为 0;恢复 1/1 的规则由 RunBattleCoordinator 统一应用。
|
||||
/// 保留原始战斗值:阵亡者 HP 为 0;恢复 1/1 的规则由 TurnBattleComponent 统一应用。
|
||||
/// </summary>
|
||||
public sealed class BattlePlayerResult
|
||||
{
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 探索恢复计划,由 RunBattleCoordinator 根据战斗结果统一生成,探索层只执行计划。
|
||||
/// 探索层不得再次根据玩家 HP 或逃跑数量重判战斗结果。
|
||||
/// </summary>
|
||||
public sealed class BattleReturnPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// 关联原地图敌人实例。
|
||||
/// </summary>
|
||||
public readonly int EncounterId;
|
||||
|
||||
/// <summary>
|
||||
/// 战斗结果。
|
||||
/// </summary>
|
||||
public readonly BattleOutcome Outcome;
|
||||
|
||||
/// <summary>
|
||||
/// 回写后的玩家临时状态(含 1/1 恢复规则应用后的结果)。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattlePlayerResult> Players;
|
||||
|
||||
/// <summary>
|
||||
/// 是否移除触发敌人(胜利)。
|
||||
/// </summary>
|
||||
public readonly bool RemoveEncounterEnemy;
|
||||
|
||||
/// <summary>
|
||||
/// 是否保留并恢复触发敌人,下次按完整预设重建(逃跑类结果)。
|
||||
/// </summary>
|
||||
public readonly bool ResetEncounterEnemy;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用本局随机源结算掉落(胜利)。
|
||||
/// </summary>
|
||||
public readonly bool ShouldRollDrops;
|
||||
|
||||
/// <summary>
|
||||
/// 逃跑类结果的保护时间(毫秒);其他结果为 0。
|
||||
/// </summary>
|
||||
public readonly int ProtectionMs;
|
||||
|
||||
/// <summary>
|
||||
/// 是否恢复探索更新。
|
||||
/// </summary>
|
||||
public readonly bool ShouldResumeExploration;
|
||||
|
||||
/// <summary>
|
||||
/// 是否以单局失败结束,不返回探索。
|
||||
/// </summary>
|
||||
public readonly bool EndsRunAsDefeated;
|
||||
|
||||
public BattleReturnPlan(int encounterId, BattleOutcome outcome,
|
||||
IReadOnlyList<BattlePlayerResult> players, bool removeEncounterEnemy, bool resetEncounterEnemy,
|
||||
bool shouldRollDrops, int protectionMs, bool shouldResumeExploration, bool endsRunAsDefeated)
|
||||
{
|
||||
EncounterId = encounterId;
|
||||
Outcome = outcome;
|
||||
Players = players != null ? Copy(players) : new BattlePlayerResult[0];
|
||||
RemoveEncounterEnemy = removeEncounterEnemy;
|
||||
ResetEncounterEnemy = resetEncounterEnemy;
|
||||
ShouldRollDrops = shouldRollDrops;
|
||||
ProtectionMs = protectionMs;
|
||||
ShouldResumeExploration = shouldResumeExploration;
|
||||
EndsRunAsDefeated = endsRunAsDefeated;
|
||||
}
|
||||
|
||||
private static BattlePlayerResult[] Copy(IReadOnlyList<BattlePlayerResult> source)
|
||||
{
|
||||
BattlePlayerResult[] copy = new BattlePlayerResult[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.CustomComponent;
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前战斗的事实唯一来源,仅战斗期间存在。
|
||||
/// 调度、指令校验、效果、状态和敌人决策共享同一实例,不复制单位、轮次和状态集合;
|
||||
/// 内部不为函数调用建立请求/响应 DTO,玩家与敌人的行动都走同一条执行管线并产生同一组 BattleEvent。
|
||||
/// M1 只支持 1 名玩家与 1 名敌人的普通攻击闭环。
|
||||
/// </summary>
|
||||
internal sealed class BattleRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// 触发本场战斗的遭遇 ID,用于结果回写关联地图敌人。
|
||||
/// </summary>
|
||||
public readonly int EncounterId;
|
||||
|
||||
/// <summary>
|
||||
/// 本场全部单位,创建顺序即稳定显示顺序(玩家在前)。
|
||||
/// </summary>
|
||||
public readonly BattleUnit[] Units;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为本场先制战斗(只影响第一轮;M1 未启用)。
|
||||
/// </summary>
|
||||
public readonly bool IsPreemptive;
|
||||
|
||||
/// <summary>
|
||||
/// 当前轮次,从 1 开始。
|
||||
/// </summary>
|
||||
public int RoundNumber = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 当前行动者;无行动者或战斗完成时为 0。
|
||||
/// </summary>
|
||||
public int CurrentActorUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 本轮已获得行动机会的单位 ID。
|
||||
/// </summary>
|
||||
public readonly HashSet<int> ActedUnitIds = new HashSet<int>();
|
||||
|
||||
/// <summary>
|
||||
/// 本次推进产生的待展示行动记录,推进结束时被 BattleStep 取走。
|
||||
/// </summary>
|
||||
public readonly List<BattleEvent> PendingEvents = new List<BattleEvent>();
|
||||
|
||||
/// <summary>
|
||||
/// 战斗结束后的结果;非 null 即表示战斗已完成。
|
||||
/// </summary>
|
||||
public BattleResult Result;
|
||||
|
||||
private readonly IRunRandomSource _random;
|
||||
private readonly IBattleConfigProvider _config;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已完成。
|
||||
/// </summary>
|
||||
public bool IsCompleted => Result != null;
|
||||
|
||||
/// <summary>
|
||||
/// 当前行动者单位;无行动者时返回 null。
|
||||
/// </summary>
|
||||
public BattleUnit CurrentActor => GetUnit(CurrentActorUnitId);
|
||||
|
||||
private BattleRuntime(int encounterId, BattleUnit[] units, bool isPreemptive,
|
||||
IRunRandomSource random, IBattleConfigProvider config)
|
||||
{
|
||||
EncounterId = encounterId;
|
||||
Units = units;
|
||||
IsPreemptive = isPreemptive;
|
||||
_random = random;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从遭遇、单局玩家状态、配置和本局随机源创建唯一战斗运行时。
|
||||
/// 任一必要输入缺失或配置缺失时返回 null,表示启动失败且不产生任何副作用。
|
||||
/// </summary>
|
||||
public static BattleRuntime Create(BattleEncounter encounter, IReadOnlyList<RunPlayerState> players,
|
||||
IBattleConfigProvider config, IRunRandomSource random)
|
||||
{
|
||||
if (encounter == null || players == null || players.Count == 0 || config == null || random == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
EnemyPartyConfig party = config.GetEnemyParty(encounter.EnemyPartyConfigId);
|
||||
if (party == null || party.EnemyIds == null || party.EnemyIds.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<BattleUnit> units = new List<BattleUnit>(players.Count + party.EnemyIds.Count);
|
||||
int nextId = 1;
|
||||
|
||||
foreach (RunPlayerState state in players)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
BattleUnit unit = new BattleUnit(nextId++, BattleFaction.Player, state.CharacterId, state.PartyOrder,
|
||||
state.CurrentHp, state.MaxHp, state.CurrentMp, state.MaxMp,
|
||||
state.Atk, state.Mat, state.Speed);
|
||||
if (state.AttackActionId != 0)
|
||||
{
|
||||
unit.ActionIds.Add(state.AttackActionId);
|
||||
}
|
||||
|
||||
if (state.SkillActionId != 0)
|
||||
{
|
||||
unit.ActionIds.Add(state.SkillActionId);
|
||||
}
|
||||
|
||||
units.Add(unit);
|
||||
}
|
||||
|
||||
int enemyOrder = 1;
|
||||
foreach (int enemyId in party.EnemyIds)
|
||||
{
|
||||
EnemyConfig enemy = config.GetEnemy(enemyId);
|
||||
if (enemy == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
BattleUnit unit = new BattleUnit(nextId++, BattleFaction.Enemy, enemy.Id, enemyOrder++,
|
||||
enemy.MaxHp, enemy.MaxHp, enemy.MaxMp, enemy.MaxMp, enemy.Atk, enemy.Mat, enemy.Speed);
|
||||
if (enemy.ActionIds != null)
|
||||
{
|
||||
unit.ActionIds.AddRange(enemy.ActionIds);
|
||||
}
|
||||
|
||||
units.Add(unit);
|
||||
}
|
||||
|
||||
BattleRuntime runtime = new BattleRuntime(encounter.EncounterId, units.ToArray(),
|
||||
encounter.IsPreemptive, random, config);
|
||||
runtime.SelectNextActor();
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按运行时 ID 获取单位;不存在时返回 null。
|
||||
/// </summary>
|
||||
public BattleUnit GetUnit(int unitId)
|
||||
{
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (unit.UnitId == unitId)
|
||||
{
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单位是否仍在战斗中(未阵亡且未逃跑)。
|
||||
/// </summary>
|
||||
public static bool IsActive(BattleUnit unit)
|
||||
{
|
||||
return unit != null && !unit.IsDefeated && !unit.IsEscaped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验并执行当前玩家指令(单次行动)。
|
||||
/// 非法指令不消耗 MP、行动机会或随机数;玩家行动后仅选择下一个行动者,
|
||||
/// 不自动推进敌人回合——间歇节奏由组件在外部编排。
|
||||
/// </summary>
|
||||
public BattleStep SubmitCommand(BattleCommand command)
|
||||
{
|
||||
if (!ValidatePlayerCommand(command))
|
||||
{
|
||||
return new BattleStep(new BattleEvent[0], BuildViewState(), null);
|
||||
}
|
||||
|
||||
PendingEvents.Clear();
|
||||
|
||||
BattleUnit actor = GetUnit(command.ActorUnitId);
|
||||
ExecuteAction(actor, command.CommandType, command.ActionConfigId, command.TargetUnitIds);
|
||||
MarkActed(command.ActorUnitId);
|
||||
CheckBattleEnd();
|
||||
|
||||
if (!IsCompleted)
|
||||
{
|
||||
SelectNextActor();
|
||||
}
|
||||
|
||||
return new BattleStep(DrainEvents(), BuildViewState(), Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推进当前敌人行动者的自动回合(单次行动)。
|
||||
/// 当前行动者不是敌人、没有行动者或战斗已完成时返回无变化步骤;
|
||||
/// 与玩家指令共用同一执行管线并产生同一组 BattleEvent。
|
||||
/// </summary>
|
||||
public BattleStep AdvanceEnemyTurn()
|
||||
{
|
||||
BattleUnit actor = CurrentActor;
|
||||
if (actor == null || actor.Faction != BattleFaction.Enemy || IsCompleted)
|
||||
{
|
||||
return new BattleStep(new BattleEvent[0], BuildViewState(), null);
|
||||
}
|
||||
|
||||
PendingEvents.Clear();
|
||||
|
||||
ExecuteEnemyTurn(actor);
|
||||
MarkActed(actor.UnitId);
|
||||
CheckBattleEnd();
|
||||
|
||||
if (!IsCompleted)
|
||||
{
|
||||
SelectNextActor();
|
||||
}
|
||||
|
||||
return new BattleStep(DrainEvents(), BuildViewState(), Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前只读视图。
|
||||
/// </summary>
|
||||
public BattleViewState BuildViewState()
|
||||
{
|
||||
List<BattleUnitView> unitViews = new List<BattleUnitView>(Units.Length);
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
List<BattleStatusView> statusViews = new List<BattleStatusView>(unit.Statuses.Count);
|
||||
foreach (BattleStatus status in unit.Statuses)
|
||||
{
|
||||
statusViews.Add(new BattleStatusView(status.Type, status.RemainingRounds));
|
||||
}
|
||||
|
||||
unitViews.Add(new BattleUnitView(unit.UnitId, unit.Faction, unit.ConfigId, unit.PartyOrder,
|
||||
unit.CurrentHp, unit.MaxHp, unit.CurrentMp, unit.MaxMp, unit.Speed,
|
||||
unit.IsDefeated, unit.IsEscaped, statusViews));
|
||||
}
|
||||
|
||||
List<int> remainingOrder = new List<int>();
|
||||
if (!IsCompleted)
|
||||
{
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (IsActive(unit) && !ActedUnitIds.Contains(unit.UnitId))
|
||||
{
|
||||
remainingOrder.Add(unit.UnitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<int> availableActions = new List<int>();
|
||||
BattleUnit actor = CurrentActor;
|
||||
if (actor != null && actor.Faction == BattleFaction.Player)
|
||||
{
|
||||
availableActions.AddRange(actor.ActionIds);
|
||||
}
|
||||
|
||||
return new BattleViewState(RoundNumber, CurrentActorUnitId, unitViews, remainingOrder, availableActions);
|
||||
}
|
||||
|
||||
private bool ValidatePlayerCommand(BattleCommand command)
|
||||
{
|
||||
if (command == null || IsCompleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (command.ActorUnitId != CurrentActorUnitId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BattleUnit actor = GetUnit(command.ActorUnitId);
|
||||
if (actor == null || actor.Faction != BattleFaction.Player)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (command.CommandType != BattleActionType.Attack)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BattleActionConfig action = _config.GetAction(command.ActionConfigId);
|
||||
if (action == null || action.ActionType != BattleActionType.Attack)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actor.CurrentMp < action.MpCost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryResolvePlayerTargets(command, actor, action);
|
||||
}
|
||||
|
||||
private bool TryResolvePlayerTargets(BattleCommand command, BattleUnit actor, BattleActionConfig action)
|
||||
{
|
||||
if (action.TargetType != BattleTargetType.SingleEnemy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (command.TargetUnitIds == null || command.TargetUnitIds.Count != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BattleUnit target = GetUnit(command.TargetUnitIds[0]);
|
||||
return target != null && target.Faction != actor.Faction && IsActive(target);
|
||||
}
|
||||
|
||||
private void ExecuteAction(BattleUnit actor, BattleActionType commandType, int actionConfigId,
|
||||
IReadOnlyList<int> targetUnitIds)
|
||||
{
|
||||
BattleActionConfig action = _config.GetAction(actionConfigId);
|
||||
if (action == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.MpCost > 0 && actor.CurrentMp >= action.MpCost)
|
||||
{
|
||||
actor.CurrentMp -= action.MpCost;
|
||||
}
|
||||
|
||||
foreach (int targetUnitId in targetUnitIds)
|
||||
{
|
||||
BattleUnit target = GetUnit(targetUnitId);
|
||||
if (target == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (BattleEffect effect in action.Effects)
|
||||
{
|
||||
ApplyEffect(actor, target, commandType, actionConfigId, effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffect(BattleUnit actor, BattleUnit target, BattleActionType commandType,
|
||||
int actionConfigId, BattleEffect effect)
|
||||
{
|
||||
int sourceValue = GetStatValue(actor, effect.SourceStat);
|
||||
int change = effect.FlatValue + sourceValue * effect.SourceScalePermille / 1000;
|
||||
|
||||
int beforeHp = target.CurrentHp;
|
||||
int beforeMp = target.CurrentMp;
|
||||
|
||||
switch (effect.TargetStat)
|
||||
{
|
||||
case BattleStatType.HP:
|
||||
target.CurrentHp = Clamp(target.CurrentHp + change, 0, target.MaxHp);
|
||||
if (target.CurrentHp <= 0)
|
||||
{
|
||||
target.CurrentHp = 0;
|
||||
target.IsDefeated = true;
|
||||
}
|
||||
|
||||
break;
|
||||
case BattleStatType.MP:
|
||||
target.CurrentMp = Clamp(target.CurrentMp + change, 0, target.MaxMp);
|
||||
break;
|
||||
default:
|
||||
// M1 不处理速度/上限等目标属性修改与状态施加
|
||||
break;
|
||||
}
|
||||
|
||||
PendingEvents.Add(new BattleEvent(actor.UnitId, commandType, actionConfigId, target.UnitId,
|
||||
beforeHp, target.CurrentHp, beforeMp, target.CurrentMp,
|
||||
BattleStatusType.None, 0));
|
||||
}
|
||||
|
||||
private static int GetStatValue(BattleUnit unit, BattleStatType stat)
|
||||
{
|
||||
switch (stat)
|
||||
{
|
||||
case BattleStatType.HP:
|
||||
return unit.CurrentHp;
|
||||
case BattleStatType.MP:
|
||||
return unit.CurrentMp;
|
||||
case BattleStatType.MaxHP:
|
||||
return unit.MaxHp;
|
||||
case BattleStatType.MaxMP:
|
||||
return unit.MaxMp;
|
||||
case BattleStatType.ATK:
|
||||
return unit.Atk;
|
||||
case BattleStatType.MAT:
|
||||
return unit.Mat;
|
||||
case BattleStatType.Speed:
|
||||
return unit.Speed;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int Clamp(int value, int min, int max)
|
||||
{
|
||||
return value < min ? min : (value > max ? max : value);
|
||||
}
|
||||
|
||||
private void MarkActed(int unitId)
|
||||
{
|
||||
ActedUnitIds.Add(unitId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择本轮下一个未行动的活跃单位;全部行动过则进入下一轮。
|
||||
/// M1 按单位创建顺序(玩家在前)轮流,每轮每个单位最多行动一次;
|
||||
/// M2 起由完整调度规则(速度、阵营、队伍顺序、先制)替换。
|
||||
/// </summary>
|
||||
private void SelectNextActor()
|
||||
{
|
||||
if (IsCompleted)
|
||||
{
|
||||
CurrentActorUnitId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
BattleUnit next = FindNextActor();
|
||||
if (next == null)
|
||||
{
|
||||
RoundNumber++;
|
||||
ActedUnitIds.Clear();
|
||||
next = FindNextActor();
|
||||
}
|
||||
|
||||
CurrentActorUnitId = next != null ? next.UnitId : 0;
|
||||
}
|
||||
|
||||
private BattleUnit FindNextActor()
|
||||
{
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (IsActive(unit) && !ActedUnitIds.Contains(unit.UnitId))
|
||||
{
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 敌人随机决策并执行:从 MP 足够的行动中等概率选择,单体行动从合法目标中等概率选择;
|
||||
/// 没有可执行行动或合法目标时跳过本次机会。
|
||||
/// </summary>
|
||||
private void ExecuteEnemyTurn(BattleUnit enemy)
|
||||
{
|
||||
List<int> usableActions = new List<int>();
|
||||
foreach (int actionId in enemy.ActionIds)
|
||||
{
|
||||
BattleActionConfig action = _config.GetAction(actionId);
|
||||
if (action != null && enemy.CurrentMp >= action.MpCost)
|
||||
{
|
||||
usableActions.Add(actionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (usableActions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int chosenActionId = PickOne(usableActions);
|
||||
BattleActionConfig chosen = _config.GetAction(chosenActionId);
|
||||
|
||||
List<int> targets = new List<int>();
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (unit.Faction != enemy.Faction && IsActive(unit))
|
||||
{
|
||||
targets.Add(unit.UnitId);
|
||||
}
|
||||
}
|
||||
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int targetUnitId = PickOne(targets);
|
||||
ExecuteAction(enemy, chosen.ActionType, chosenActionId, new[] { targetUnitId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等概率选一;只有一项时直接返回,不消费随机数。
|
||||
/// </summary>
|
||||
private int PickOne(IReadOnlyList<int> candidates)
|
||||
{
|
||||
if (candidates.Count == 1)
|
||||
{
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
return candidates[_random.NextInt(0, candidates.Count)];
|
||||
}
|
||||
|
||||
private void CheckBattleEnd()
|
||||
{
|
||||
if (Result != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool allEnemiesDefeated = true;
|
||||
bool allPlayersDefeated = true;
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (IsActive(unit))
|
||||
{
|
||||
if (unit.Faction == BattleFaction.Enemy)
|
||||
{
|
||||
allEnemiesDefeated = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
allPlayersDefeated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BattleOutcome outcome;
|
||||
if (allEnemiesDefeated)
|
||||
{
|
||||
outcome = BattleOutcome.Victory;
|
||||
}
|
||||
else if (allPlayersDefeated)
|
||||
{
|
||||
outcome = BattleOutcome.TotalDefeat;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<BattlePlayerResult> players = new List<BattlePlayerResult>();
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (unit.Faction == BattleFaction.Player)
|
||||
{
|
||||
players.Add(new BattlePlayerResult(unit.ConfigId, unit.CurrentHp, unit.CurrentMp,
|
||||
unit.IsDefeated, unit.IsEscaped));
|
||||
}
|
||||
}
|
||||
|
||||
Result = new BattleResult(EncounterId, outcome, players);
|
||||
CurrentActorUnitId = 0;
|
||||
}
|
||||
|
||||
private List<BattleEvent> DrainEvents()
|
||||
{
|
||||
List<BattleEvent> drained = new List<BattleEvent>(PendingEvents);
|
||||
PendingEvents.Clear();
|
||||
return drained;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec4ca0d279034f4a939fd35aedab383e
|
||||
guid: 11f1778a3f4e34da691bcac2656ed3a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5bfa0a211d34ba6874d7d0b276192a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,58 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.CustomComponent;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 战斗启动请求,创建后视为只读。
|
||||
/// 战斗开始失败时不得消耗随机数、修改单局状态或暂停状态计数。
|
||||
/// </summary>
|
||||
public sealed class BattleStartRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于将结果关联回地图敌人实例。
|
||||
/// </summary>
|
||||
public readonly int EncounterId;
|
||||
|
||||
/// <summary>
|
||||
/// 1 到 4 个玩家输入,保持战备顺序。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattlePlayerInput> Players;
|
||||
|
||||
/// <summary>
|
||||
/// 用于按队伍预设创建 1 到 4 个敌人运行时单位。
|
||||
/// </summary>
|
||||
public readonly int EnemyPartyConfigId;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用第一轮玩家先制。
|
||||
/// </summary>
|
||||
public readonly bool IsPreemptive;
|
||||
|
||||
/// <summary>
|
||||
/// 当前单局唯一的随机源实例,来自 RandomComponent,不创建战斗私有随机源。
|
||||
/// </summary>
|
||||
public readonly IRunRandomSource Random;
|
||||
|
||||
public BattleStartRequest(int encounterId, IReadOnlyList<BattlePlayerInput> players,
|
||||
int enemyPartyConfigId, bool isPreemptive, IRunRandomSource random)
|
||||
{
|
||||
EncounterId = encounterId;
|
||||
Players = players != null ? Copy(players) : new BattlePlayerInput[0];
|
||||
EnemyPartyConfigId = enemyPartyConfigId;
|
||||
IsPreemptive = isPreemptive;
|
||||
Random = random;
|
||||
}
|
||||
|
||||
private static BattlePlayerInput[] Copy(IReadOnlyList<BattlePlayerInput> source)
|
||||
{
|
||||
BattlePlayerInput[] copy = new BattlePlayerInput[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 040ffec92b064e3fbd9d9a8db9b39291
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 单场战斗中的状态,只存在于 BattleRuntime。
|
||||
/// 同类状态不叠层,重复施加保留较长的剩余行动机会次数。
|
||||
/// </summary>
|
||||
internal sealed class BattleStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态类型。
|
||||
/// </summary>
|
||||
public BattleStatusType Type;
|
||||
|
||||
/// <summary>
|
||||
/// 剩余持续次数(行动机会)。
|
||||
/// </summary>
|
||||
public int RemainingRounds;
|
||||
|
||||
public BattleStatus(BattleStatusType type, int remainingRounds)
|
||||
{
|
||||
Type = type;
|
||||
RemainingRounds = remainingRounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bef66af163534a79b4b14ba27ed80665
|
||||
guid: 5d551f82833074a5fa669f05ba7249eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 一次同步推进的返回值,是 UI 读取行动记录与最新视图的唯一来源。
|
||||
/// 不保存独立流程状态:Result 非 null 即表示战斗已完成。
|
||||
/// </summary>
|
||||
public sealed class BattleStep
|
||||
{
|
||||
/// <summary>
|
||||
/// 本次推进产生的有序行动记录列表。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattleEvent> Events;
|
||||
|
||||
/// <summary>
|
||||
/// 推进结束后的只读战斗视图。
|
||||
/// </summary>
|
||||
public readonly BattleViewState View;
|
||||
|
||||
/// <summary>
|
||||
/// 仅在战斗完成时存在,否则为 null。
|
||||
/// </summary>
|
||||
public readonly BattleResult Result;
|
||||
|
||||
public BattleStep(IReadOnlyList<BattleEvent> events, BattleViewState view, BattleResult result)
|
||||
{
|
||||
Events = events != null ? Copy(events) : new BattleEvent[0];
|
||||
View = view;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
private static BattleEvent[] Copy(IReadOnlyList<BattleEvent> source)
|
||||
{
|
||||
BattleEvent[] copy = new BattleEvent[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 993a7eab17ef14d69b2ca67b770c05da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 单场战斗的运行时单位,仅 BattleRuntime 及其协作者读写。
|
||||
/// UI 与探索层通过 BattleUnitView 读取,不直接持有本类型。
|
||||
/// </summary>
|
||||
internal sealed class BattleUnit
|
||||
{
|
||||
/// <summary>
|
||||
/// 本场战斗单位的唯一运行时标识;重复配置的敌人拥有不同 ID。
|
||||
/// </summary>
|
||||
public readonly int UnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 阵营。
|
||||
/// </summary>
|
||||
public readonly BattleFaction Faction;
|
||||
|
||||
/// <summary>
|
||||
/// 配置标识:玩家为 CharacterId,敌人为 EnemyConfigId。
|
||||
/// </summary>
|
||||
public readonly int ConfigId;
|
||||
|
||||
/// <summary>
|
||||
/// 同阵营同速度的最终并列规则顺序。
|
||||
/// </summary>
|
||||
public readonly int PartyOrder;
|
||||
|
||||
/// <summary>
|
||||
/// HP 上限。
|
||||
/// </summary>
|
||||
public readonly int MaxHp;
|
||||
|
||||
/// <summary>
|
||||
/// MP 上限。
|
||||
/// </summary>
|
||||
public readonly int MaxMp;
|
||||
|
||||
/// <summary>
|
||||
/// 攻击。
|
||||
/// </summary>
|
||||
public readonly int Atk;
|
||||
|
||||
/// <summary>
|
||||
/// 魔力。
|
||||
/// </summary>
|
||||
public readonly int Mat;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 HP。
|
||||
/// </summary>
|
||||
public int CurrentHp;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 MP。
|
||||
/// </summary>
|
||||
public int CurrentMp;
|
||||
|
||||
/// <summary>
|
||||
/// 当前速度;可被战斗内效果修改。
|
||||
/// </summary>
|
||||
public int Speed;
|
||||
|
||||
/// <summary>
|
||||
/// 是否阵亡。
|
||||
/// </summary>
|
||||
public bool IsDefeated;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已逃跑。
|
||||
/// </summary>
|
||||
public bool IsEscaped;
|
||||
|
||||
/// <summary>
|
||||
/// 当前剩余状态。
|
||||
/// </summary>
|
||||
public readonly List<BattleStatus> Statuses = new List<BattleStatus>();
|
||||
|
||||
/// <summary>
|
||||
/// 该单位可用的行动配置 ID。
|
||||
/// </summary>
|
||||
public readonly List<int> ActionIds = new List<int>();
|
||||
|
||||
public BattleUnit(int unitId, BattleFaction faction, int configId, int partyOrder,
|
||||
int currentHp, int maxHp, int currentMp, int maxMp, int atk, int mat, int speed)
|
||||
{
|
||||
UnitId = unitId;
|
||||
Faction = faction;
|
||||
ConfigId = configId;
|
||||
PartyOrder = partyOrder;
|
||||
CurrentHp = currentHp;
|
||||
MaxHp = maxHp;
|
||||
CurrentMp = currentMp;
|
||||
MaxMp = maxMp;
|
||||
Atk = atk;
|
||||
Mat = mat;
|
||||
Speed = speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 794735c8736d646c19d9275131444dce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+28
-63
@@ -4,9 +4,9 @@ using SepCore.Definition;
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 单位剩余状态快照。
|
||||
/// 单个战斗状态的只读视图。
|
||||
/// </summary>
|
||||
public sealed class BattleStatusSnapshot
|
||||
public sealed class BattleStatusView
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态类型。
|
||||
@@ -18,7 +18,7 @@ namespace SepCore.Battle
|
||||
/// </summary>
|
||||
public readonly int RemainingRounds;
|
||||
|
||||
public BattleStatusSnapshot(BattleStatusType statusType, int remainingRounds)
|
||||
public BattleStatusView(BattleStatusType statusType, int remainingRounds)
|
||||
{
|
||||
StatusType = statusType;
|
||||
RemainingRounds = remainingRounds;
|
||||
@@ -27,8 +27,9 @@ namespace SepCore.Battle
|
||||
|
||||
/// <summary>
|
||||
/// 单个战斗单位的只读视图。
|
||||
/// 内部调度与效果结算不得读取本视图;视图只在外部边界按需生成。
|
||||
/// </summary>
|
||||
public sealed class BattleUnitSnapshot
|
||||
public sealed class BattleUnitView
|
||||
{
|
||||
/// <summary>
|
||||
/// 本场战斗单位的唯一运行时标识;重复配置的敌人拥有不同 ID。
|
||||
@@ -46,7 +47,7 @@ namespace SepCore.Battle
|
||||
public readonly int ConfigId;
|
||||
|
||||
/// <summary>
|
||||
/// 同阵营同速度的最终并列规则顺序。
|
||||
/// 同阵营同速度的最终并列规则顺序,也是显示顺序。
|
||||
/// </summary>
|
||||
public readonly int PartyOrder;
|
||||
|
||||
@@ -70,16 +71,6 @@ namespace SepCore.Battle
|
||||
/// </summary>
|
||||
public readonly int MaxMp;
|
||||
|
||||
/// <summary>
|
||||
/// 攻击。
|
||||
/// </summary>
|
||||
public readonly int Atk;
|
||||
|
||||
/// <summary>
|
||||
/// 魔力。
|
||||
/// </summary>
|
||||
public readonly int Mat;
|
||||
|
||||
/// <summary>
|
||||
/// 当前速度。
|
||||
/// </summary>
|
||||
@@ -96,19 +87,13 @@ namespace SepCore.Battle
|
||||
public readonly bool IsEscaped;
|
||||
|
||||
/// <summary>
|
||||
/// 剩余状态列表。
|
||||
/// 当前剩余状态列表。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattleStatusSnapshot> Statuses;
|
||||
public readonly IReadOnlyList<BattleStatusView> Statuses;
|
||||
|
||||
/// <summary>
|
||||
/// 可用行动配置 ID 列表。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<int> AvailableActionIds;
|
||||
|
||||
public BattleUnitSnapshot(int battleUnitId, BattleFaction faction, int configId, int partyOrder,
|
||||
int currentHp, int maxHp, int currentMp, int maxMp, int atk, int mat, int speed,
|
||||
bool isDefeated, bool isEscaped, IReadOnlyList<BattleStatusSnapshot> statuses,
|
||||
IReadOnlyList<int> availableActionIds)
|
||||
public BattleUnitView(int battleUnitId, BattleFaction faction, int configId, int partyOrder,
|
||||
int currentHp, int maxHp, int currentMp, int maxMp, int speed,
|
||||
bool isDefeated, bool isEscaped, IReadOnlyList<BattleStatusView> statuses)
|
||||
{
|
||||
BattleUnitId = battleUnitId;
|
||||
Faction = faction;
|
||||
@@ -118,29 +103,15 @@ namespace SepCore.Battle
|
||||
MaxHp = maxHp;
|
||||
CurrentMp = currentMp;
|
||||
MaxMp = maxMp;
|
||||
Atk = atk;
|
||||
Mat = mat;
|
||||
Speed = speed;
|
||||
IsDefeated = isDefeated;
|
||||
IsEscaped = isEscaped;
|
||||
Statuses = statuses != null ? Copy(statuses) : new BattleStatusSnapshot[0];
|
||||
AvailableActionIds = availableActionIds != null ? Copy(availableActionIds) : new int[0];
|
||||
Statuses = statuses != null ? Copy(statuses) : new BattleStatusView[0];
|
||||
}
|
||||
|
||||
private static BattleStatusSnapshot[] Copy(IReadOnlyList<BattleStatusSnapshot> source)
|
||||
private static BattleStatusView[] Copy(IReadOnlyList<BattleStatusView> source)
|
||||
{
|
||||
BattleStatusSnapshot[] copy = new BattleStatusSnapshot[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static int[] Copy(IReadOnlyList<int> source)
|
||||
{
|
||||
int[] copy = new int[source.Count];
|
||||
BattleStatusView[] copy = new BattleStatusView[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
@@ -151,21 +122,16 @@ namespace SepCore.Battle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只读战斗快照,是 UI 和敌人策略唯一允许读取的战斗状态。
|
||||
/// 每次推进都创建逻辑上的只读快照,不把可修改的内部列表或战斗单位引用暴露给调用方。
|
||||
/// 当前战斗的只读视图,是 UI 唯一允许读取的战斗状态。
|
||||
/// 每次推进后在外部边界生成,不把内部可变对象暴露给 UI 或探索层。
|
||||
/// </summary>
|
||||
public sealed class BattleSnapshot
|
||||
public sealed class BattleViewState
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前轮次,从 1 开始。
|
||||
/// </summary>
|
||||
public readonly int RoundNumber;
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否为先制战斗第一轮。
|
||||
/// </summary>
|
||||
public readonly bool IsPreemptiveRound;
|
||||
|
||||
/// <summary>
|
||||
/// 当前行动者;战斗完成时为 0。
|
||||
/// </summary>
|
||||
@@ -174,7 +140,7 @@ namespace SepCore.Battle
|
||||
/// <summary>
|
||||
/// 全部单位的只读视图,保持稳定显示顺序。
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<BattleUnitSnapshot> Units;
|
||||
public readonly IReadOnlyList<BattleUnitView> Units;
|
||||
|
||||
/// <summary>
|
||||
/// 本轮从当前行动者开始的剩余运行时单位 ID;速度变化后重建。
|
||||
@@ -182,25 +148,24 @@ namespace SepCore.Battle
|
||||
public readonly IReadOnlyList<int> RemainingTurnOrder;
|
||||
|
||||
/// <summary>
|
||||
/// 流程状态。
|
||||
/// 当前玩家可用的行动配置 ID;当前行动者不是玩家或战斗完成时为空。
|
||||
/// </summary>
|
||||
public readonly BattleFlowState FlowState;
|
||||
public readonly IReadOnlyList<int> AvailableActionIds;
|
||||
|
||||
public BattleSnapshot(int roundNumber, bool isPreemptiveRound, int currentActorUnitId,
|
||||
IReadOnlyList<BattleUnitSnapshot> units, IReadOnlyList<int> remainingTurnOrder,
|
||||
BattleFlowState flowState)
|
||||
public BattleViewState(int roundNumber, int currentActorUnitId,
|
||||
IReadOnlyList<BattleUnitView> units, IReadOnlyList<int> remainingTurnOrder,
|
||||
IReadOnlyList<int> availableActionIds)
|
||||
{
|
||||
RoundNumber = roundNumber;
|
||||
IsPreemptiveRound = isPreemptiveRound;
|
||||
CurrentActorUnitId = currentActorUnitId;
|
||||
Units = units != null ? Copy(units) : new BattleUnitSnapshot[0];
|
||||
Units = units != null ? Copy(units) : new BattleUnitView[0];
|
||||
RemainingTurnOrder = remainingTurnOrder != null ? Copy(remainingTurnOrder) : new int[0];
|
||||
FlowState = flowState;
|
||||
AvailableActionIds = availableActionIds != null ? Copy(availableActionIds) : new int[0];
|
||||
}
|
||||
|
||||
private static BattleUnitSnapshot[] Copy(IReadOnlyList<BattleUnitSnapshot> source)
|
||||
private static BattleUnitView[] Copy(IReadOnlyList<BattleUnitView> source)
|
||||
{
|
||||
BattleUnitSnapshot[] copy = new BattleUnitSnapshot[source.Count];
|
||||
BattleUnitView[] copy = new BattleUnitView[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
copy[i] = source[i];
|
||||
@@ -220,4 +185,4 @@ namespace SepCore.Battle
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6be1facf77c6042c0be34019eb5f255e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
using SepCore.CustomComponent;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人策略接口。
|
||||
/// 策略只读取快照并返回指令,不直接修改 BattleSession。
|
||||
/// 当前没有可执行行动时,本次行动机会记为跳过,不能形成无限循环。
|
||||
/// </summary>
|
||||
public interface IEnemyBattlePolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// 为当前敌人行动者决定指令。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">只读战斗快照。</param>
|
||||
/// <param name="actorUnitId">当前获得行动机会的敌人单位。</param>
|
||||
/// <param name="random">本局共享随机源。</param>
|
||||
BattleCommand Decide(BattleSnapshot snapshot, int actorUnitId, IRunRandomSource random);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e803cf69950a4fb9b0b8d63603f99a4b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 探索层恢复接口。
|
||||
/// 碰撞方主动把 BattleEncounter 交给 RunBattleCoordinator;协调器不扫描场景寻找敌人。
|
||||
/// ApplyBattleReturn 必须按 EncounterId 精确操作触发敌人,不能按 EnemyConfigId 批量处理同类敌人。
|
||||
/// </summary>
|
||||
public interface IExplorationBattleBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行探索恢复计划。
|
||||
/// </summary>
|
||||
/// <param name="plan">由 RunBattleCoordinator 生成的恢复计划。</param>
|
||||
void ApplyBattleReturn(BattleReturnPlan plan);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf022ae8498b418784037549ce415e3a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using SepCore.CustomComponent;
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// 战斗配置的运行时适配器,隔离战斗规则与 GameEntry.Luban。
|
||||
/// 配置缺失返回 null,由启动校验决定失败;正常行动不重复扫描全表完整性。
|
||||
/// EditMode 测试使用内存配置,不使用本类。
|
||||
/// </summary>
|
||||
internal sealed class LubanBattleConfigProvider : IBattleConfigProvider
|
||||
{
|
||||
public EnemyPartyConfig GetEnemyParty(int id)
|
||||
{
|
||||
return SafeGet(() => GameEntry.Luban.Get<EnemyPartyConfig>(id));
|
||||
}
|
||||
|
||||
public EnemyConfig GetEnemy(int id)
|
||||
{
|
||||
return SafeGet(() => GameEntry.Luban.Get<EnemyConfig>(id));
|
||||
}
|
||||
|
||||
public BattleActionConfig GetAction(int id)
|
||||
{
|
||||
return SafeGet(() => GameEntry.Luban.Get<BattleActionConfig>(id));
|
||||
}
|
||||
|
||||
public GlobalConfig GetGlobal()
|
||||
{
|
||||
return SafeGet(() => GameEntry.Luban.Global != null ? GameEntry.Luban.Global.Data : null);
|
||||
}
|
||||
|
||||
private static T SafeGet<T>(System.Func<T> getter) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return GameEntry.Luban != null && GameEntry.Luban.IsReady ? getter() : null;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50fd8ca181e304ffc8bc130beabf9177
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace SepCore.Debugger
|
||||
/// <summary>
|
||||
/// 战斗壳层调试入口(仅供 Editor/Development Build,正式构建不注册)。
|
||||
/// 模拟探索层:缺少单局状态时用配表角色与固定种子初始化临时状态,
|
||||
/// 然后通过 RunBattleCoordinator 以固定调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
|
||||
/// 然后通过 TurnBattleComponent 以固定调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
|
||||
/// </summary>
|
||||
public class BattleDebuggerWindow : IDebuggerWindow
|
||||
{
|
||||
@@ -49,7 +49,7 @@ namespace SepCore.Debugger
|
||||
|
||||
if (GUILayout.Button("Close", GUILayout.Height(30)))
|
||||
{
|
||||
GameEntry.RunBattle.EndDebugBattle();
|
||||
GameEntry.TurnBattle.CloseBattle();
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
@@ -81,8 +81,8 @@ namespace SepCore.Debugger
|
||||
{
|
||||
InitDebugRunStateIfNeeded();
|
||||
|
||||
// 固定调试遭遇:EncounterId=1,敌人队伍预设 1,普通战斗
|
||||
bool started = GameEntry.RunBattle.TryStartBattle(new BattleEncounter(1, 1, false));
|
||||
// 固定调试遭遇:EncounterId=1,敌人队伍预设 4001(单敌人),普通战斗
|
||||
bool started = GameEntry.TurnBattle.TryStartBattle(new BattleEncounter(1, 4001, false), null);
|
||||
if (!started)
|
||||
{
|
||||
Log.Warning("Battle debug shell can not start, maybe a battle is already active.");
|
||||
@@ -112,9 +112,21 @@ namespace SepCore.Debugger
|
||||
return players;
|
||||
}
|
||||
|
||||
// M1 为 1v1:只取第一个新存档角色作为我方单位
|
||||
if (global.NewGameCharacterIds == null || global.NewGameCharacterIds.Count == 0)
|
||||
{
|
||||
Log.Warning("Can not build debug players without new game character ids.");
|
||||
return players;
|
||||
}
|
||||
|
||||
int order = 1;
|
||||
foreach (int characterId in global.NewGameCharacterIds)
|
||||
{
|
||||
if (order > 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(characterId);
|
||||
if (config == null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user