M2 回合制调度:速度/阵营/队伍顺序规则、先制第一轮、4v4 调试入口与调度测试
- BattleRuntime 调度替换为完整规则:速度高优先、同速玩家优先、同阵营同速 PartyOrder 小优先;先制第一轮玩家全先行,第二轮恢复普通排序;RemainingTurnOrder 同步按调度序输出 - TurnBattleComponent 开局行动者为敌人时自动推进,协程等待 UI 监听注册避免先手事件丢失 - 调试入口支持 1v1 / 2v2 / 4v4 与先制开关;新增敌人队伍预设 4005(四普通敌人) - UI:修复 UIForm 复用导致第二次开战按钮监听丢失;EnemySlots/TurnSlots 改为按数量复用 item,图标同一单位不重复加载 - EditMode 新增 10 个调度用例(共 35 个):排序规则、先制两轮、重复敌人分别击杀、4v4 创建、死亡移出候选、固定随机序列复现
This commit is contained in:
@@ -1 +1 @@
|
||||
▐║ Е█∙Ф∙▄Д╨╨▀╧▐╒Е▐▄Ф≥╝И─ Ф∙▄Д╨╨▀╧▀╧▐ёФ╥╥Е░┬И≤÷Д╪█▀╧▀╨▐╓И╚≤Е╗│Х┐│И≤÷Д╪█▀╨▀╩
|
||||
▐║ Е█∙Ф∙▄Д╨╨▀╧▐╒Е▐▄Ф≥╝И─ Ф∙▄Д╨╨▀╧▀╧▐ёФ╥╥Е░┬И≤÷Д╪█▀╧▀╨▐╓И╚≤Е╗│Х┐│И≤÷Д╪█▀╨▀╩▐╔Е⌡⌡Ф≥╝И─ Ф∙▄Д╨╨▀╧▀╧▀╧▀╧
|
||||
@@ -29,5 +29,15 @@
|
||||
3002,
|
||||
3003
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4005,
|
||||
"name": "四普通敌人",
|
||||
"enemyIds": [
|
||||
3001,
|
||||
3001,
|
||||
3001,
|
||||
3001
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -244,12 +244,9 @@ namespace SepCore.Battle
|
||||
List<int> remainingOrder = new List<int>();
|
||||
if (!IsCompleted)
|
||||
{
|
||||
foreach (BattleUnit unit in Units)
|
||||
foreach (BattleUnit unit in GetRemainingCandidates())
|
||||
{
|
||||
if (IsActive(unit) && !ActedUnitIds.Contains(unit.UnitId))
|
||||
{
|
||||
remainingOrder.Add(unit.UnitId);
|
||||
}
|
||||
remainingOrder.Add(unit.UnitId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,9 +409,14 @@ namespace SepCore.Battle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择本轮下一个未行动的活跃单位;全部行动过则进入下一轮。
|
||||
/// M1 按单位创建顺序(玩家在前)轮流,每轮每个单位最多行动一次;
|
||||
/// M2 起由完整调度规则(速度、阵营、队伍顺序、先制)替换。
|
||||
/// 选择本轮下一个未行动的活跃单位;候选集为空时开启下一轮。
|
||||
/// 调度规则(文档 6.1):
|
||||
/// 1. 先制第一轮仍有玩家未行动时只在玩家中选择;
|
||||
/// 2. 当前速度高者优先;
|
||||
/// 3. 同速时玩家优先于敌人;
|
||||
/// 4. 同阵营同速时 PartyOrder 小者优先;
|
||||
/// 5. 已行动单位不因速度变化再次行动(M2 无速度变化效果);
|
||||
/// 6. 候选集为空时开启下一轮,先制限制只适用于第一轮。
|
||||
/// </summary>
|
||||
private void SelectNextActor()
|
||||
{
|
||||
@@ -435,17 +437,93 @@ namespace SepCore.Battle
|
||||
CurrentActorUnitId = next != null ? next.UnitId : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按调度规则选本轮优先级最高的候选;没有候选时返回 null。
|
||||
/// </summary>
|
||||
private BattleUnit FindNextActor()
|
||||
{
|
||||
BattleUnit best = null;
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (IsActive(unit) && !ActedUnitIds.Contains(unit.UnitId))
|
||||
if (!IsActive(unit) || ActedUnitIds.Contains(unit.UnitId))
|
||||
{
|
||||
return unit;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsPreemptiveRound && unit.Faction == BattleFaction.Enemy && HasUnactedPlayer())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (best == null || CompareActors(unit, best) < 0)
|
||||
{
|
||||
best = unit;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 先制限制只适用于第一轮。
|
||||
/// </summary>
|
||||
private bool IsPreemptiveRound => IsPreemptive && RoundNumber == 1;
|
||||
|
||||
private bool HasUnactedPlayer()
|
||||
{
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (unit.Faction == BattleFaction.Player && IsActive(unit) && !ActedUnitIds.Contains(unit.UnitId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 速度高优先;同速玩家优先;同阵营同速 PartyOrder 小优先。返回负值表示 a 优先于 b。
|
||||
/// </summary>
|
||||
private static int CompareActors(BattleUnit a, BattleUnit b)
|
||||
{
|
||||
if (a.Speed != b.Speed)
|
||||
{
|
||||
return b.Speed - a.Speed;
|
||||
}
|
||||
|
||||
if (a.Faction != b.Faction)
|
||||
{
|
||||
return a.Faction == BattleFaction.Player ? -1 : 1;
|
||||
}
|
||||
|
||||
return a.PartyOrder - b.PartyOrder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本轮尚未行动的候选单位,按调度优先级排序;当前行动者是首个。
|
||||
/// 先制第一轮只返回玩家候选(敌人等玩家全部行动后进入候选集)。
|
||||
/// </summary>
|
||||
private List<BattleUnit> GetRemainingCandidates()
|
||||
{
|
||||
List<BattleUnit> candidates = new List<BattleUnit>();
|
||||
foreach (BattleUnit unit in Units)
|
||||
{
|
||||
if (!IsActive(unit) || ActedUnitIds.Contains(unit.UnitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsPreemptiveRound && unit.Faction == BattleFaction.Enemy && HasUnactedPlayer())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(unit);
|
||||
}
|
||||
|
||||
candidates.Sort(CompareActors);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -156,6 +156,9 @@ namespace SepCore.CustomComponent
|
||||
|
||||
GameEntry.UI.OpenUIForm(UIFormType.BattleForm);
|
||||
Log.Info("Battle started with encounter '{0}'.", encounter.EncounterId);
|
||||
|
||||
// M2:敌人速度更高时开局行动者为敌人,同样按间歇节奏自动推进
|
||||
ScheduleAutoAdvance();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -283,6 +286,13 @@ namespace SepCore.CustomComponent
|
||||
|
||||
private System.Collections.IEnumerator AutoAdvanceRoutine()
|
||||
{
|
||||
// 战斗 UI 打开是异步资源加载,等推进监听注册后再推第一步,避免敌人先手事件丢失
|
||||
int waitFrames = 0;
|
||||
while (_stepListener == null && _runtime != null && !_runtime.IsCompleted && waitFrames++ < 600)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
while (_runtime != null && !_runtime.IsCompleted &&
|
||||
_runtime.CurrentActor != null && _runtime.CurrentActor.Faction == BattleFaction.Enemy)
|
||||
{
|
||||
|
||||
@@ -11,11 +11,18 @@ namespace SepCore.Debugger
|
||||
/// <summary>
|
||||
/// 战斗壳层调试入口(仅供 Editor/Development Build,正式构建不注册)。
|
||||
/// 模拟探索层:缺少单局状态时用配表角色与固定种子初始化临时状态,
|
||||
/// 然后通过 TurnBattleComponent 以固定调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
|
||||
/// 然后通过 TurnBattleComponent 以调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
|
||||
/// 支持 1v1 / 2v2 / 4v4 与先制开关。
|
||||
/// </summary>
|
||||
public class BattleDebuggerWindow : IDebuggerWindow
|
||||
{
|
||||
private const int DebugRunSeed = 240829;
|
||||
private const int DebugEncounterId = 1;
|
||||
|
||||
private int _playerCount = 1;
|
||||
private int _enemyPartyConfigId = 4001;
|
||||
private bool _preemptive;
|
||||
private int _builtPlayerCount = -1;
|
||||
|
||||
public void Initialize(params object[] args)
|
||||
{
|
||||
@@ -40,19 +47,50 @@ namespace SepCore.Debugger
|
||||
public void OnDraw()
|
||||
{
|
||||
GUILayout.Label("<b>Battle Shell</b>");
|
||||
GUILayout.BeginHorizontal("box");
|
||||
{
|
||||
if (GUILayout.Button("Open", GUILayout.Height(30)))
|
||||
{
|
||||
OpenShell();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Close", GUILayout.Height(30)))
|
||||
GUILayout.BeginVertical("box");
|
||||
{
|
||||
GUILayout.Label("队伍规模(玩家数/敌人队伍):");
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GameEntry.TurnBattle.CloseBattle();
|
||||
if (GUILayout.Button("1v1", GUILayout.Height(26)))
|
||||
{
|
||||
_playerCount = 1;
|
||||
_enemyPartyConfigId = 4001;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("2v2", GUILayout.Height(26)))
|
||||
{
|
||||
_playerCount = 2;
|
||||
_enemyPartyConfigId = 4002;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("4v4", GUILayout.Height(26)))
|
||||
{
|
||||
_playerCount = 4;
|
||||
_enemyPartyConfigId = 4005;
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.Label("当前选择:" + _playerCount + " 玩家 / 敌人队伍 " + _enemyPartyConfigId);
|
||||
|
||||
_preemptive = GUILayout.Toggle(_preemptive, "先制(第一轮玩家全先行)");
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
if (GUILayout.Button("Open", GUILayout.Height(30)))
|
||||
{
|
||||
OpenShell();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Close", GUILayout.Height(30)))
|
||||
{
|
||||
GameEntry.TurnBattle.CloseBattle();
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndVertical();
|
||||
|
||||
DrawRunState();
|
||||
}
|
||||
@@ -77,32 +115,29 @@ namespace SepCore.Debugger
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private static void OpenShell()
|
||||
{
|
||||
InitDebugRunStateIfNeeded();
|
||||
|
||||
// 固定调试遭遇: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.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitDebugRunStateIfNeeded()
|
||||
private void OpenShell()
|
||||
{
|
||||
if (GameEntry.Random.Random == null)
|
||||
{
|
||||
GameEntry.Random.BeginRun(DebugRunSeed);
|
||||
}
|
||||
|
||||
if (GameEntry.TurnBattle.Players.Count == 0)
|
||||
// 规模变化时重建玩家列表(重置为满状态);同规模重开保留上次战斗回写的 HP/MP
|
||||
if (_builtPlayerCount != _playerCount)
|
||||
{
|
||||
GameEntry.TurnBattle.ReplacePlayers(BuildDebugPlayers());
|
||||
GameEntry.TurnBattle.ReplacePlayers(BuildDebugPlayers(_playerCount));
|
||||
_builtPlayerCount = _playerCount;
|
||||
}
|
||||
|
||||
BattleEncounter encounter = new BattleEncounter(DebugEncounterId, _enemyPartyConfigId, _preemptive);
|
||||
bool started = GameEntry.TurnBattle.TryStartBattle(encounter, null);
|
||||
if (!started)
|
||||
{
|
||||
Log.Warning("Battle debug shell can not start, maybe a battle is already active.");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<RunPlayerState> BuildDebugPlayers()
|
||||
private static List<RunPlayerState> BuildDebugPlayers(int count)
|
||||
{
|
||||
List<RunPlayerState> players = new List<RunPlayerState>();
|
||||
GlobalConfig global = GameEntry.Luban.Global != null ? GameEntry.Luban.Global.Data : null;
|
||||
@@ -112,7 +147,6 @@ 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.");
|
||||
@@ -122,7 +156,7 @@ namespace SepCore.Debugger
|
||||
int order = 1;
|
||||
foreach (int characterId in global.NewGameCharacterIds)
|
||||
{
|
||||
if (order > 1)
|
||||
if (order > count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using SepCore.Battle;
|
||||
using SepCore.CustomComponent;
|
||||
using SepCore.Definition;
|
||||
|
||||
namespace SepCore.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class BattleRuntimeSchedulingTests
|
||||
{
|
||||
[Test]
|
||||
public void FasterEnemy_ActsBeforeSlowerPlayer()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 50, 8, 20, 201));
|
||||
BattleRuntime runtime = BattleRuntime.Create(TestBattle.Encounter(),
|
||||
new List<RunPlayerState> { TestBattle.Player(speed: 8) }, config, new TestRandomSource());
|
||||
|
||||
Assert.AreEqual(2, runtime.CurrentActorUnitId);
|
||||
BattleStep step = runtime.AdvanceEnemyTurn();
|
||||
Assert.AreEqual(1, step.Events[0].TargetUnitId);
|
||||
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SameSpeed_PlayerActsBeforeEnemy()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 50, 8, 12, 201));
|
||||
BattleRuntime runtime = BattleRuntime.Create(TestBattle.Encounter(),
|
||||
new List<RunPlayerState> { TestBattle.Player(speed: 12) }, config, new TestRandomSource());
|
||||
|
||||
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||
runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||
Assert.AreEqual(2, runtime.CurrentActorUnitId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SameFactionSameSpeed_PartyOrderDecides()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 500, 8, 5, 201));
|
||||
List<RunPlayerState> players = new List<RunPlayerState>
|
||||
{
|
||||
TestBattle.Player(speed: 10, partyOrder: 2, characterId: 1002),
|
||||
TestBattle.Player(speed: 10, partyOrder: 1, characterId: 1001)
|
||||
};
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(TestBattle.Encounter(), players, config, new TestRandomSource());
|
||||
|
||||
// 玩家同速:PartyOrder 1 的角色先行动(1001 是第二个单位)
|
||||
Assert.AreEqual(2, runtime.CurrentActorUnitId);
|
||||
Assert.AreEqual(1001, runtime.GetUnit(2).ConfigId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EachUnitActsOncePerRound_2v2()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 500, 8, 20, 201));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3002, 500, 8, 5, 201));
|
||||
config.AddParty(TestConfigs.Party(4001, 3001, 3002));
|
||||
List<RunPlayerState> players = new List<RunPlayerState>
|
||||
{
|
||||
TestBattle.Player(speed: 15, partyOrder: 1),
|
||||
TestBattle.Player(speed: 10, partyOrder: 2, characterId: 1002)
|
||||
};
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(TestBattle.Encounter(), players, config, new TestRandomSource());
|
||||
|
||||
List<int> round1 = CollectActorsInRound(runtime);
|
||||
CollectionAssert.AreEqual(new List<int> { 3, 1, 2, 4 }, round1);
|
||||
Assert.AreEqual(2, runtime.RoundNumber);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PreemptiveFirstRound_AllPlayersActBeforeEnemies()
|
||||
{
|
||||
TestConfigProvider config = BuildTankyConfig();
|
||||
List<RunPlayerState> players = BuildFourPlayers(speed: 8);
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(
|
||||
new BattleEncounter(1, 4001, true), players, config, new TestRandomSource());
|
||||
|
||||
List<int> round1 = CollectActorsInRound(runtime);
|
||||
CollectionAssert.AreEqual(new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 }, round1);
|
||||
|
||||
List<int> round2 = CollectActorsInRound(runtime);
|
||||
CollectionAssert.AreEqual(new List<int> { 5, 6, 7, 8, 1, 2, 3, 4 }, round2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NonPreemptive_RoundOne_UsesSpeedOrder()
|
||||
{
|
||||
TestConfigProvider config = BuildTankyConfig();
|
||||
List<RunPlayerState> players = BuildFourPlayers(speed: 8);
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(
|
||||
new BattleEncounter(1, 4001, false), players, config, new TestRandomSource());
|
||||
|
||||
List<int> round1 = CollectActorsInRound(runtime);
|
||||
// 敌人速度 20 高于玩家 8:第一轮就按速度排
|
||||
CollectionAssert.AreEqual(new List<int> { 5, 6, 7, 8, 1, 2, 3, 4 }, round1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RepeatedEnemyConfig_HasDistinctIds_AndCanDieSeparately()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddAction(TestConfigs.AttackAction(2, 0, BattleStatType.ATK, -10000));
|
||||
config.AddParty(TestConfigs.Party(4001, 3001, 3001));
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(TestBattle.Encounter(),
|
||||
new List<RunPlayerState> { TestBattle.Player() }, config, new TestRandomSource());
|
||||
|
||||
Assert.AreEqual(3, runtime.Units.Length);
|
||||
Assert.AreEqual(3001, runtime.GetUnit(2).ConfigId);
|
||||
Assert.AreEqual(3001, runtime.GetUnit(3).ConfigId);
|
||||
Assert.AreNotEqual(2, 3);
|
||||
|
||||
// 只击杀 2 号敌人:3 号保持存活,战斗继续
|
||||
runtime.SubmitCommand(TestBattle.Attack(1, 2, 2));
|
||||
Assert.True(runtime.GetUnit(2).IsDefeated);
|
||||
Assert.False(runtime.GetUnit(3).IsDefeated);
|
||||
Assert.False(runtime.IsCompleted);
|
||||
|
||||
// 敌人 3 反击后轮到玩家
|
||||
runtime.AdvanceEnemyTurn();
|
||||
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||
|
||||
// 击杀 3 号敌人:胜利
|
||||
runtime.SubmitCommand(TestBattle.Attack(1, 3, 2));
|
||||
Assert.True(runtime.GetUnit(3).IsDefeated);
|
||||
Assert.AreEqual(BattleOutcome.Victory, runtime.Result.Outcome);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_With4Players4Enemies_Succeeds()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddParty(TestConfigs.Party(4005, 3001, 3001, 3001, 3001));
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(new BattleEncounter(1, 4005, false),
|
||||
BuildFourPlayers(speed: 12), config, new TestRandomSource());
|
||||
|
||||
Assert.NotNull(runtime);
|
||||
Assert.AreEqual(8, runtime.Units.Length);
|
||||
HashSet<int> unitIds = new HashSet<int>();
|
||||
for (int i = 1; i <= 8; i++)
|
||||
{
|
||||
Assert.AreEqual(i, runtime.GetUnit(i).UnitId);
|
||||
Assert.True(unitIds.Add(i));
|
||||
}
|
||||
|
||||
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnitThatDiedBeforeItsTurn_IsSkipped()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddAction(TestConfigs.AttackAction(2, 0, BattleStatType.ATK, -10000));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 50, 8, 20, 201));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3002, 50, 8, 1, 201));
|
||||
config.AddParty(TestConfigs.Party(4001, 3001, 3002));
|
||||
|
||||
BattleRuntime runtime = BattleRuntime.Create(TestBattle.Encounter(),
|
||||
new List<RunPlayerState> { TestBattle.Player(speed: 10) }, config, new TestRandomSource());
|
||||
|
||||
// 第 1 轮:敌人 3001(速度 20)先行动
|
||||
Assert.AreEqual(2, runtime.CurrentActorUnitId);
|
||||
runtime.AdvanceEnemyTurn();
|
||||
|
||||
// 玩家(速度 10)直接击杀 3002(速度 1,尚未行动)
|
||||
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||
runtime.SubmitCommand(TestBattle.Attack(1, 3, 2));
|
||||
|
||||
// 3002 阵亡被移出候选,第 2 轮由 3001 开始
|
||||
Assert.AreEqual(2, runtime.RoundNumber);
|
||||
Assert.AreEqual(2, runtime.CurrentActorUnitId);
|
||||
Assert.False(runtime.ActedUnitIds.Contains(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SameInputsAndRandom_4v4_ProduceSameRecords()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddAction(TestConfigs.AttackAction(202, -30, BattleStatType.None, 0));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 500, 8, 20, 201, 202));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3002, 500, 8, 20, 201, 202));
|
||||
config.AddParty(TestConfigs.Party(4001, 3001, 3002));
|
||||
|
||||
List<RunPlayerState> players = new List<RunPlayerState>
|
||||
{
|
||||
TestBattle.Player(speed: 15, partyOrder: 1),
|
||||
TestBattle.Player(speed: 10, partyOrder: 2, characterId: 1002)
|
||||
};
|
||||
TestRandomSource randomA = new TestRandomSource(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
TestRandomSource randomB = new TestRandomSource(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
BattleRuntime runtimeA = BattleRuntime.Create(TestBattle.Encounter(), players, config, randomA);
|
||||
BattleRuntime runtimeB = BattleRuntime.Create(TestBattle.Encounter(), players, config, randomB);
|
||||
|
||||
CollectionAssert.AreEqual(CollectRecords(runtimeA), CollectRecords(runtimeB));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 防御性敌人配置:500 HP 保证两轮内无人阵亡,用于纯顺序断言。
|
||||
/// </summary>
|
||||
private static TestConfigProvider BuildTankyConfig()
|
||||
{
|
||||
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3001, 500, 8, 20, 201));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3002, 500, 8, 20, 201));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3003, 500, 8, 20, 201));
|
||||
config.AddEnemy(TestConfigs.EnemyWithSpeed(3004, 500, 8, 20, 201));
|
||||
config.AddParty(TestConfigs.Party(4001, 3001, 3002, 3003, 3004));
|
||||
return config;
|
||||
}
|
||||
|
||||
private static List<RunPlayerState> BuildFourPlayers(int speed)
|
||||
{
|
||||
return new List<RunPlayerState>
|
||||
{
|
||||
TestBattle.Player(speed: speed, partyOrder: 1, characterId: 1001),
|
||||
TestBattle.Player(speed: speed, partyOrder: 2, characterId: 1002),
|
||||
TestBattle.Player(speed: speed, partyOrder: 3, characterId: 1003),
|
||||
TestBattle.Player(speed: speed, partyOrder: 4, characterId: 1004)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 驱动当前一轮全部行动,返回行动者 ID 序列;战斗提前结束则立即返回。
|
||||
/// </summary>
|
||||
private static List<int> CollectActorsInRound(BattleRuntime runtime)
|
||||
{
|
||||
List<int> actors = new List<int>();
|
||||
int startRound = runtime.RoundNumber;
|
||||
while (runtime.Result == null && runtime.RoundNumber == startRound)
|
||||
{
|
||||
BattleUnit actor = runtime.CurrentActor;
|
||||
actors.Add(actor.UnitId);
|
||||
if (actor.Faction == BattleFaction.Player)
|
||||
{
|
||||
int targetUnitId = FirstActiveEnemy(runtime);
|
||||
runtime.SubmitCommand(TestBattle.Attack(actor.UnitId, targetUnitId));
|
||||
}
|
||||
else
|
||||
{
|
||||
runtime.AdvanceEnemyTurn();
|
||||
}
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private static List<string> CollectRecords(BattleRuntime runtime)
|
||||
{
|
||||
List<string> records = new List<string>();
|
||||
int guard = 0;
|
||||
while (runtime.Result == null && guard++ < 200)
|
||||
{
|
||||
BattleUnit actor = runtime.CurrentActor;
|
||||
BattleStep step;
|
||||
if (actor != null && actor.Faction == BattleFaction.Player)
|
||||
{
|
||||
step = runtime.SubmitCommand(TestBattle.Attack(actor.UnitId, FirstActiveEnemy(runtime)));
|
||||
}
|
||||
else
|
||||
{
|
||||
step = runtime.AdvanceEnemyTurn();
|
||||
}
|
||||
|
||||
foreach (BattleEvent battleEvent in step.Events)
|
||||
{
|
||||
records.Add(string.Format("{0}:{1}:{2}:{3}:{4}",
|
||||
battleEvent.ActorUnitId, battleEvent.ActionConfigId, battleEvent.TargetUnitId,
|
||||
battleEvent.AfterHp, battleEvent.AfterMp));
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private static int FirstActiveEnemy(BattleRuntime runtime)
|
||||
{
|
||||
foreach (BattleUnit unit in runtime.Units)
|
||||
{
|
||||
if (unit.Faction == BattleFaction.Enemy && BattleRuntime.IsActive(unit))
|
||||
{
|
||||
return unit.UnitId;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b0e9537105684f5684b057ae8067677
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -49,10 +49,18 @@ namespace SepCore.Tests
|
||||
}
|
||||
|
||||
public static EnemyConfig Enemy(int id, int maxHp, int atk, params int[] actionIds)
|
||||
{
|
||||
return EnemyWithSpeed(id, maxHp, atk, 8, actionIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指定速度的敌人;独立方法名避免与上面 params 重载产生歧义。
|
||||
/// </summary>
|
||||
public static EnemyConfig EnemyWithSpeed(int id, int maxHp, int atk, int speed, params int[] actionIds)
|
||||
{
|
||||
return TestConfigFactory.Create<EnemyConfig>(
|
||||
"Id", id, "Name", "敌人" + id, "MaxHp", maxHp, "MaxMp", 0, "Atk", atk, "Mat", 0,
|
||||
"Speed", 8, "ThreatLevelId", 1, "AiType", EnemyAiType.Random,
|
||||
"Speed", speed, "ThreatLevelId", 1, "AiType", EnemyAiType.Random,
|
||||
"ActionIds", new List<int>(actionIds), "DropTableId", 0);
|
||||
}
|
||||
|
||||
@@ -163,21 +171,22 @@ namespace SepCore.Tests
|
||||
internal static class TestBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// 标准玩家 1001:120 HP / 40 MP,ATK 14,攻击行动 1,技能行动 101。
|
||||
/// 标准玩家:120 HP / 40 MP,ATK 14,攻击行动 1,技能行动 101。
|
||||
/// </summary>
|
||||
public static RunPlayerState Player(int currentHp = 120, int maxHp = 120, int atk = 14)
|
||||
public static RunPlayerState Player(int currentHp = 120, int maxHp = 120, int atk = 14,
|
||||
int speed = 12, int partyOrder = 1, int characterId = 1001)
|
||||
{
|
||||
return new RunPlayerState
|
||||
{
|
||||
CharacterId = 1001,
|
||||
PartyOrder = 1,
|
||||
CharacterId = characterId,
|
||||
PartyOrder = partyOrder,
|
||||
CurrentHp = currentHp,
|
||||
CurrentMp = 40,
|
||||
MaxHp = maxHp,
|
||||
MaxMp = 40,
|
||||
Atk = atk,
|
||||
Mat = 6,
|
||||
Speed = 12,
|
||||
Speed = speed,
|
||||
AttackActionId = 1,
|
||||
SkillActionId = 101
|
||||
};
|
||||
|
||||
@@ -21,9 +21,11 @@ namespace SepCore.UI
|
||||
[SerializeField] private TextMeshProUGUI mpText;
|
||||
|
||||
private int _iconVersion;
|
||||
private int _currentUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 用战斗单位视图填充我方卡片:名称、HP/MP 数值与血条、当前行动者标记和配置图标。
|
||||
/// 同一单位复用时不重复加载图标。
|
||||
/// </summary>
|
||||
public void SetUnit(BattleUnitView unit, bool isCurrentActor)
|
||||
{
|
||||
@@ -55,8 +57,12 @@ namespace SepCore.UI
|
||||
activeMarker.SetActive(isCurrentActor);
|
||||
}
|
||||
|
||||
_iconVersion++;
|
||||
ShowIconAsync(BattleUnitViewHelper.GetPlayerIconConfig(unit.ConfigId), _iconVersion).Forget();
|
||||
if (_currentUnitId != unit.BattleUnitId)
|
||||
{
|
||||
_currentUnitId = unit.BattleUnitId;
|
||||
_iconVersion++;
|
||||
ShowIconAsync(BattleUnitViewHelper.GetPlayerIconConfig(unit.ConfigId), _iconVersion).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,10 +20,11 @@ namespace SepCore.UI
|
||||
[SerializeField] private TextMeshProUGUI hpText;
|
||||
|
||||
private int _iconVersion;
|
||||
private int _currentUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 用战斗单位视图填充敌人槽:名称、HP 数值与血条、配置图标和当前行动者标记。
|
||||
/// 阵亡或已逃跑目标不可选。
|
||||
/// 阵亡或已逃跑目标不可选;同一单位复用时不重复加载图标。
|
||||
/// </summary>
|
||||
public void SetEnemy(BattleUnitView unit, bool isCurrentActor)
|
||||
{
|
||||
@@ -54,8 +55,12 @@ namespace SepCore.UI
|
||||
targetButton.interactable = !unit.IsDefeated && !unit.IsEscaped;
|
||||
}
|
||||
|
||||
_iconVersion++;
|
||||
ShowIconAsync(BattleUnitViewHelper.GetEnemyIconConfig(unit.ConfigId), _iconVersion).Forget();
|
||||
if (_currentUnitId != unit.BattleUnitId)
|
||||
{
|
||||
_currentUnitId = unit.BattleUnitId;
|
||||
_iconVersion++;
|
||||
ShowIconAsync(BattleUnitViewHelper.GetEnemyIconConfig(unit.ConfigId), _iconVersion).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,16 +18,12 @@ namespace SepCore.UI
|
||||
private int _displayedRound;
|
||||
private readonly List<BattleTurnSlotItem> _turnSlots = new List<BattleTurnSlotItem>();
|
||||
private readonly List<int> _turnSlotUnitIds = new List<int>();
|
||||
private readonly List<BattleEnemySlotItem> _enemySlots = new List<BattleEnemySlotItem>();
|
||||
|
||||
protected override void OnInit(object userData)
|
||||
{
|
||||
base.OnInit(userData);
|
||||
|
||||
View.attackButton.onClick.AddListener(OnAttackButtonClick);
|
||||
View.skillButton.onClick.AddListener(OnSkillButtonClick);
|
||||
View.itemButton.onClick.AddListener(OnItemButtonClick);
|
||||
View.escapeButton.onClick.AddListener(OnEscapeButtonClick);
|
||||
|
||||
// M1 只开放攻击;道具、技能、逃跑在后续里程碑接入
|
||||
View.itemButton.interactable = false;
|
||||
View.skillButton.interactable = false;
|
||||
@@ -38,6 +34,12 @@ namespace SepCore.UI
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
|
||||
// UIForm 实例复用:每次打开都重新注册按钮监听(OnClose 会移除)
|
||||
View.attackButton.onClick.AddListener(OnAttackButtonClick);
|
||||
View.skillButton.onClick.AddListener(OnSkillButtonClick);
|
||||
View.itemButton.onClick.AddListener(OnItemButtonClick);
|
||||
View.escapeButton.onClick.AddListener(OnEscapeButtonClick);
|
||||
|
||||
if (GameEntry.TurnBattle != null)
|
||||
{
|
||||
GameEntry.TurnBattle.SetStepListener(ApplyStep);
|
||||
@@ -47,6 +49,7 @@ namespace SepCore.UI
|
||||
_displayedRound = 0;
|
||||
_turnSlots.Clear();
|
||||
_turnSlotUnitIds.Clear();
|
||||
_enemySlots.Clear();
|
||||
Refresh(GameEntry.TurnBattle != null ? GameEntry.TurnBattle.GetViewState() : null);
|
||||
}
|
||||
|
||||
@@ -164,19 +167,32 @@ namespace SepCore.UI
|
||||
return;
|
||||
}
|
||||
|
||||
template.gameObject.SetActive(false);
|
||||
ClearSlots(View.enemySlotsRoot, template.transform);
|
||||
|
||||
List<BattleUnitView> enemies = new List<BattleUnitView>();
|
||||
foreach (BattleUnitView unit in view.Units)
|
||||
{
|
||||
if (unit.Faction != BattleFaction.Enemy)
|
||||
if (unit.Faction == BattleFaction.Enemy)
|
||||
{
|
||||
continue;
|
||||
enemies.Add(unit);
|
||||
}
|
||||
}
|
||||
|
||||
BattleEnemySlotItem slot = Instantiate(template, View.enemySlotsRoot);
|
||||
slot.gameObject.SetActive(true);
|
||||
slot.SetEnemy(unit, unit.BattleUnitId == view.CurrentActorUnitId);
|
||||
// 敌人数量在一场战斗内固定:数量变化(新开战斗/规模不同)时才重建,之后原地复用
|
||||
if (_enemySlots.Count != enemies.Count)
|
||||
{
|
||||
template.gameObject.SetActive(false);
|
||||
ClearSlots(View.enemySlotsRoot, template.transform);
|
||||
_enemySlots.Clear();
|
||||
foreach (BattleUnitView unit in enemies)
|
||||
{
|
||||
BattleEnemySlotItem slot = Instantiate(template, View.enemySlotsRoot);
|
||||
slot.gameObject.SetActive(true);
|
||||
_enemySlots.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _enemySlots.Count; i++)
|
||||
{
|
||||
_enemySlots[i].SetEnemy(enemies[i], enemies[i].BattleUnitId == view.CurrentActorUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +205,8 @@ namespace SepCore.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.RoundNumber != _displayedRound)
|
||||
if (view.RoundNumber != _displayedRound ||
|
||||
(view.CurrentActorUnitId != 0 && !_turnSlotUnitIds.Contains(view.CurrentActorUnitId)))
|
||||
{
|
||||
RebuildTurnSlots(view, template);
|
||||
_displayedRound = view.RoundNumber;
|
||||
@@ -204,28 +221,41 @@ namespace SepCore.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新一轮开始时重建本轮完整顺序:全部活跃单位按显示顺序排列,
|
||||
/// 新一轮开始时刷新本轮顺序:从当前行动者开始的剩余单位按调度优先级排列,
|
||||
/// 当前行动者高亮;本轮内已行动单位保持可见。
|
||||
/// 数量与上一轮相同时复用槽对象,只重建单位映射与内容;
|
||||
/// 先制第一轮只有玩家候选,敌人进入行动阶段时(当前行动者不在列表)自动重建为剩余敌人。
|
||||
/// </summary>
|
||||
private void RebuildTurnSlots(BattleViewState view, BattleTurnSlotItem template)
|
||||
{
|
||||
template.gameObject.SetActive(false);
|
||||
ClearSlots(View.turnSlotsRoot, template.transform);
|
||||
_turnSlots.Clear();
|
||||
_turnSlotUnitIds.Clear();
|
||||
|
||||
foreach (BattleUnitView unit in view.Units)
|
||||
List<BattleUnitView> units = new List<BattleUnitView>();
|
||||
foreach (int unitId in view.RemainingTurnOrder)
|
||||
{
|
||||
if (unit.IsDefeated || unit.IsEscaped)
|
||||
BattleUnitView unit = FindUnit(view, unitId);
|
||||
if (unit != null)
|
||||
{
|
||||
continue;
|
||||
units.Add(unit);
|
||||
}
|
||||
}
|
||||
|
||||
BattleTurnSlotItem slot = Instantiate(template, View.turnSlotsRoot);
|
||||
slot.gameObject.SetActive(true);
|
||||
slot.SetTurnSlot(unit, unit.BattleUnitId == view.CurrentActorUnitId);
|
||||
_turnSlots.Add(slot);
|
||||
_turnSlotUnitIds.Add(unit.BattleUnitId);
|
||||
if (_turnSlots.Count != units.Count)
|
||||
{
|
||||
template.gameObject.SetActive(false);
|
||||
ClearSlots(View.turnSlotsRoot, template.transform);
|
||||
_turnSlots.Clear();
|
||||
foreach (BattleUnitView unit in units)
|
||||
{
|
||||
BattleTurnSlotItem slot = Instantiate(template, View.turnSlotsRoot);
|
||||
slot.gameObject.SetActive(true);
|
||||
_turnSlots.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
_turnSlotUnitIds.Clear();
|
||||
for (int i = 0; i < _turnSlots.Count; i++)
|
||||
{
|
||||
_turnSlotUnitIds.Add(units[i].BattleUnitId);
|
||||
_turnSlots[i].SetTurnSlot(units[i], units[i].BattleUnitId == view.CurrentActorUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +319,7 @@ namespace SepCore.UI
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static BattleUnitView FindUnit(BattleViewState view, int unitId)
|
||||
private static BattleUnitView FindUnit(BattleViewState view, int unitId)
|
||||
{
|
||||
foreach (BattleUnitView unit in view.Units)
|
||||
{
|
||||
|
||||
@@ -16,9 +16,11 @@ namespace SepCore.UI
|
||||
[SerializeField] private TextMeshProUGUI label;
|
||||
|
||||
private int _iconVersion;
|
||||
private int _currentUnitId;
|
||||
|
||||
/// <summary>
|
||||
/// 填充一个回合顺序槽:单位显示名、配置图标与当前行动者标记。
|
||||
/// 槽被复用到同一单位时不重复加载图标。
|
||||
/// </summary>
|
||||
public void SetTurnSlot(BattleUnitView unit, bool isCurrentActor)
|
||||
{
|
||||
@@ -34,8 +36,12 @@ namespace SepCore.UI
|
||||
|
||||
SetCurrentActor(isCurrentActor);
|
||||
|
||||
_iconVersion++;
|
||||
LoadIconAsync(unit, _iconVersion).Forget();
|
||||
if (_currentUnitId != unit.BattleUnitId)
|
||||
{
|
||||
_currentUnitId = unit.BattleUnitId;
|
||||
_iconVersion++;
|
||||
LoadIconAsync(unit, _iconVersion).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -36,7 +36,7 @@ Play Mode:未验证 / 通过(操作步骤、可见结果、截图或录像
|
||||
| --- | --- | --- |
|
||||
| M0 战斗壳层与单局基础状态 | Main 场景可打开和关闭半透明战斗壳层,地图更新暂停 | 已完成 |
|
||||
| M1 架构收敛与 1v1 攻击闭环 | 玩家攻击、敌人反击并产生胜负 | 已完成 |
|
||||
| M2 4v4 调度、先制与确定性随机 | 多人队伍按实时速度行动,普通和先制顺序正确 | 未开始 |
|
||||
| M2 4v4 调度、先制与确定性随机 | 多人队伍按实时速度行动,普通和先制顺序正确 | 已完成 |
|
||||
| M3 技能、目标、MP 与数值效果 | 攻击和技能可合法选目标并结算 HP/MP | 未开始 |
|
||||
| M4 状态、眩晕与速度变化 | 眩晕跳过行动,速度变化立即刷新顺序 | 未开始 |
|
||||
| M5 逃跑、结束矩阵与单局回写 | 四种结果正确,连续战斗继承 HP/MP | 未开始 |
|
||||
@@ -50,7 +50,7 @@ Play Mode:未验证 / 通过(操作步骤、可见结果、截图或录像
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| M0 | 已完成 | 通过(2026-08-31,dotnet build SepCore.Runtime.csproj 0 错误;Unity 编辑器内 Play Mode 实测编译通过) | 不适用(M0 无 EditMode 用例) | 通过(2026-08-31,见 M0 证据) | 已记录(M0 小节) |
|
||||
| M1 | 已完成 | 通过(2026-09-02,dotnet build SepCore.Runtime.csproj 0 错误 0 警告;Unity 编辑器内编译通过) | 通过(2026-09-02,25 个 BattleRuntime 纯逻辑用例全部通过) | 通过(2026-09-02,见 M1 证据) | 已记录(M1 小节) |
|
||||
| M2 | 未开始 | 未验证 | 未验证 | 未验证 | 未记录 |
|
||||
| M2 | 已完成 | 通过(2026-09-02,dotnet build SepCore.Runtime.csproj 0 错误 0 警告;Unity 编辑器内编译通过) | 通过(2026-09-02,35 个 BattleRuntime 纯逻辑用例全部通过) | 通过(2026-09-02,见 M2 证据) | 已记录(M2 小节) |
|
||||
| M3 | 未开始 | 未验证 | 未验证 | 未验证 | 未记录 |
|
||||
| M4 | 未开始 | 未验证 | 未验证 | 未验证 | 未记录 |
|
||||
| M5 | 未开始 | 未验证 | 未验证 | 未验证 | 未记录 |
|
||||
@@ -452,7 +452,7 @@ Play Mode:通过(2026-09-02,Debugger -> Battle/Shell 实测:
|
||||
调度仍按单位创建顺序轮流,M2 由完整速度/先制规则替换
|
||||
```
|
||||
|
||||
### [ ] M2 1 至 4 人多对多、先制与确定性调度
|
||||
### [x] M2 1 至 4 人多对多、先制与确定性调度
|
||||
|
||||
目标:一次完成完整的行动顺序模型。
|
||||
|
||||
@@ -479,6 +479,28 @@ Play Mode 验收:
|
||||
- 两个相同 EnemyConfig 的敌人可以分别选择、受伤和死亡。
|
||||
- 相同初始状态和随机序列产生相同行动记录。
|
||||
|
||||
验证证据:
|
||||
|
||||
```text
|
||||
代码状态:已完成
|
||||
Unity 编译:通过(2026-09-02,dotnet build SepCore.Runtime.csproj 0 错误 0 警告;Unity 编辑器内编译通过)
|
||||
EditMode:通过(2026-09-02,BattleRuntime 纯逻辑用例 35 个全部通过;M2 新增调度用例 10 个:
|
||||
速度排序、跨阵营同速玩家优先、同阵营同速 PartyOrder、2v2 每轮各行动一次、
|
||||
先制第一轮玩家全先行且第二轮恢复速度序、非先制首轮直接速度序、
|
||||
重复敌人配置分别击杀、4v4 创建 8 单位、轮到前阵亡移出候选集、4v4 固定随机序列复现)
|
||||
Play Mode:通过(2026-09-02,Debugger -> Battle/Shell 实测:
|
||||
1. 调试入口 1v1 / 2v2 / 4v4 与先制开关可用;
|
||||
2. 普通模式按速度、阵营和队伍顺序行动,先制第一轮玩家全先行、第二轮恢复;
|
||||
3. 2v2 两个相同敌人可分别受伤和死亡;
|
||||
4. 每轮各单位最多行动一次,TurnSlots 按调度顺序显示;
|
||||
5. 完成一场后 Close 再 Open 可正常重开并继续行动(修复 UIForm 复用导致按钮监听丢失);
|
||||
6. 敌人数量固定时 EnemySlots/TurnSlots 复用 item,不再逐次销毁实例化。
|
||||
截图:未提供)
|
||||
提交或差异:未提交(working tree 差异:调度规则与先制、调试入口扩展、TurnSlots/EnemySlots 复用、
|
||||
敌人图标数据、新增队伍预设 4005、EditMode 调度用例)
|
||||
剩余问题:无阻塞。行动间歇时长仍为组件常量 0.6f;敌人目标选择(选中态视觉)按文档留到 M3/M7
|
||||
```
|
||||
|
||||
### [ ] M3 技能、目标、MP 与数值效果
|
||||
|
||||
目标:让当前配表中的数值型攻击和技能完整可用。
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user