Compare commits
2
Commits
8413c2688b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
202ab337f9 | ||
|
|
7dcac0ac87 |
Binary file not shown.
@@ -12,7 +12,8 @@
|
|||||||
"actionIds": [
|
"actionIds": [
|
||||||
201
|
201
|
||||||
],
|
],
|
||||||
"dropTableId": 1
|
"dropTableId": 1,
|
||||||
|
"icon": "敌人1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3002,
|
"id": 3002,
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
"actionIds": [
|
"actionIds": [
|
||||||
201
|
201
|
||||||
],
|
],
|
||||||
"dropTableId": 2
|
"dropTableId": 2,
|
||||||
|
"icon": "敌人2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3003,
|
"id": 3003,
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
201,
|
201,
|
||||||
202
|
202
|
||||||
],
|
],
|
||||||
"dropTableId": 3
|
"dropTableId": 3,
|
||||||
|
"icon": "敌人3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1 +1 @@
|
|||||||
▐║ Е█∙Ф∙▄Д╨╨▀╧▐╒Е▐▄Ф≥╝И─ Ф∙▄Д╨╨▀╧▀╧▐ёФ╥╥Е░┬И≤÷Д╪█▀╧▀╨▐╓И╚≤Е╗│Х┐│И≤÷Д╪█▀╨▀╩
|
▐║ Е█∙Ф∙▄Д╨╨▀╧▐╒Е▐▄Ф≥╝И─ Ф∙▄Д╨╨▀╧▀╧▐ёФ╥╥Е░┬И≤÷Д╪█▀╧▀╨▐╓И╚≤Е╗│Х┐│И≤÷Д╪█▀╨▀╩▐╔Е⌡⌡Ф≥╝И─ Ф∙▄Д╨╨▀╧▀╧▀╧▀╧
|
||||||
@@ -29,5 +29,15 @@
|
|||||||
3002,
|
3002,
|
||||||
3003
|
3003
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4005,
|
||||||
|
"name": "四普通敌人",
|
||||||
|
"enemyIds": [
|
||||||
|
3001,
|
||||||
|
3001,
|
||||||
|
3001,
|
||||||
|
3001
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Binary file not shown.
@@ -19,6 +19,21 @@
|
|||||||
"atlasPath": "Characters.png",
|
"atlasPath": "Characters.png",
|
||||||
"spriteIndex": 3
|
"spriteIndex": 3
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "敌人1",
|
||||||
|
"atlasPath": "Monsters.png",
|
||||||
|
"spriteIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "敌人2",
|
||||||
|
"atlasPath": "Monsters.png",
|
||||||
|
"spriteIndex": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "敌人3",
|
||||||
|
"atlasPath": "Monsters.png",
|
||||||
|
"spriteIndex": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "旧硬币",
|
"key": "旧硬币",
|
||||||
"atlasPath": "Items.png",
|
"atlasPath": "Items.png",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public sealed partial class EnemyConfig : Luban.BeanBase
|
|||||||
AiType = (EnemyAiType)_buf.ReadInt();
|
AiType = (EnemyAiType)_buf.ReadInt();
|
||||||
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);ActionIds = new System.Collections.Generic.List<int>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { int _e0; _e0 = _buf.ReadInt(); ActionIds.Add(_e0);}}
|
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);ActionIds = new System.Collections.Generic.List<int>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { int _e0; _e0 = _buf.ReadInt(); ActionIds.Add(_e0);}}
|
||||||
DropTableId = _buf.ReadInt();
|
DropTableId = _buf.ReadInt();
|
||||||
|
Icon = _buf.ReadString();
|
||||||
|
Icon_Ref = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static EnemyConfig DeserializeEnemyConfig(ByteBuf _buf)
|
public static EnemyConfig DeserializeEnemyConfig(ByteBuf _buf)
|
||||||
@@ -78,6 +80,11 @@ public sealed partial class EnemyConfig : Luban.BeanBase
|
|||||||
/// 独立掉落表ID
|
/// 独立掉落表ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly int DropTableId;
|
public readonly int DropTableId;
|
||||||
|
/// <summary>
|
||||||
|
/// 图标资源配置
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Icon;
|
||||||
|
public SpriteConfig Icon_Ref;
|
||||||
|
|
||||||
public const int __ID__ = 1284055242;
|
public const int __ID__ = 1284055242;
|
||||||
public override int GetTypeId() => __ID__;
|
public override int GetTypeId() => __ID__;
|
||||||
@@ -95,6 +102,7 @@ public sealed partial class EnemyConfig : Luban.BeanBase
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Icon_Ref = tables.TbSpriteConfig.GetOrDefault(Icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
@@ -111,6 +119,7 @@ public sealed partial class EnemyConfig : Luban.BeanBase
|
|||||||
+ "aiType:" + AiType + ","
|
+ "aiType:" + AiType + ","
|
||||||
+ "actionIds:" + Luban.StringUtil.CollectionToString(ActionIds) + ","
|
+ "actionIds:" + Luban.StringUtil.CollectionToString(ActionIds) + ","
|
||||||
+ "dropTableId:" + DropTableId + ","
|
+ "dropTableId:" + DropTableId + ","
|
||||||
|
+ "icon:" + Icon + ","
|
||||||
+ "}";
|
+ "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ namespace SepCore.Editor
|
|||||||
i == 0 ? Ink : MutedWhite, TextAlignmentOptions.Center, FontStyles.Bold);
|
i == 0 ? Ink : MutedWhite, TextAlignmentOptions.Center, FontStyles.Bold);
|
||||||
|
|
||||||
BattleTurnSlotItem slot = slotRect.gameObject.AddComponent<BattleTurnSlotItem>();
|
BattleTurnSlotItem slot = slotRect.gameObject.AddComponent<BattleTurnSlotItem>();
|
||||||
slot.ConfigureEditor(marker.gameObject, icon, label);
|
|
||||||
RegisterReference(slot, "turnSlot" + (i + 1));
|
RegisterReference(slot, "turnSlot" + (i + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +291,6 @@ namespace SepCore.Editor
|
|||||||
Image mpFill = CreateBar(cardRect, "MP", contentX, 112f, 242f, Blue, mp, maxMp, out TextMeshProUGUI mpText);
|
Image mpFill = CreateBar(cardRect, "MP", contentX, 112f, 242f, Blue, mp, maxMp, out TextMeshProUGUI mpText);
|
||||||
|
|
||||||
BattleActorCardItem card = cardRect.gameObject.AddComponent<BattleActorCardItem>();
|
BattleActorCardItem card = cardRect.gameObject.AddComponent<BattleActorCardItem>();
|
||||||
card.ConfigureEditor(marker.gameObject, icon, hpFill, mpFill, name, hpText, mpText);
|
|
||||||
RegisterReference(card, "playerCard" + (index + 1));
|
RegisterReference(card, "playerCard" + (index + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +328,6 @@ namespace SepCore.Editor
|
|||||||
Image hpFill = CreateEnemyBar(slotRect, hp[i], maxHp[i], out TextMeshProUGUI hpText);
|
Image hpFill = CreateEnemyBar(slotRect, hp[i], maxHp[i], out TextMeshProUGUI hpText);
|
||||||
|
|
||||||
BattleEnemySlotItem slot = slotRect.gameObject.AddComponent<BattleEnemySlotItem>();
|
BattleEnemySlotItem slot = slotRect.gameObject.AddComponent<BattleEnemySlotItem>();
|
||||||
slot.ConfigureEditor(targetButton, selected.gameObject, icon, hpFill, name, hpText);
|
|
||||||
RegisterReference(slot, "enemySlot" + (i + 1));
|
RegisterReference(slot, "enemySlot" + (i + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Tests")]
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 1c518c47a9854ba19045abd1f211eb7d
|
guid: cd1500230107441cfa5e86657fa0b9e3
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -16,11 +16,6 @@ public partial class GameEntry : MonoBehaviour
|
|||||||
|
|
||||||
public static TurnBattleComponent TurnBattle { get; private set; }
|
public static TurnBattleComponent TurnBattle { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 单局战斗协调器(普通类,非场景组件)。
|
|
||||||
/// </summary>
|
|
||||||
public static RunBattleCoordinator RunBattle { get; private set; }
|
|
||||||
|
|
||||||
private static void InitCustomComponents()
|
private static void InitCustomComponents()
|
||||||
{
|
{
|
||||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||||
@@ -28,6 +23,5 @@ public partial class GameEntry : MonoBehaviour
|
|||||||
Save = UnityGameFramework.Runtime.GameEntry.GetComponent<SaveComponent>();
|
Save = UnityGameFramework.Runtime.GameEntry.GetComponent<SaveComponent>();
|
||||||
Random = UnityGameFramework.Runtime.GameEntry.GetComponent<RandomComponent>();
|
Random = UnityGameFramework.Runtime.GameEntry.GetComponent<RandomComponent>();
|
||||||
TurnBattle = UnityGameFramework.Runtime.GameEntry.GetComponent<TurnBattleComponent>();
|
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
|
fileFormatVersion: 2
|
||||||
guid: 84f740882a72430ca27c4d77004257d8
|
guid: 9bd0c7df032194911a2aee0942338d48
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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>
|
/// <summary>
|
||||||
/// 每名参战玩家的战后结果。
|
/// 每名参战玩家的战后结果。
|
||||||
/// 保留原始战斗值:阵亡者 HP 为 0;恢复 1/1 的规则由 RunBattleCoordinator 统一应用。
|
/// 保留原始战斗值:阵亡者 HP 为 0;恢复 1/1 的规则由 TurnBattleComponent 统一应用。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BattlePlayerResult
|
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,643 @@
|
|||||||
|
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 GetRemainingCandidates())
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
/// 选择本轮下一个未行动的活跃单位;候选集为空时开启下一轮。
|
||||||
|
/// 调度规则(文档 6.1):
|
||||||
|
/// 1. 先制第一轮仍有玩家未行动时只在玩家中选择;
|
||||||
|
/// 2. 当前速度高者优先;
|
||||||
|
/// 3. 同速时玩家优先于敌人;
|
||||||
|
/// 4. 同阵营同速时 PartyOrder 小者优先;
|
||||||
|
/// 5. 已行动单位不因速度变化再次行动(M2 无速度变化效果);
|
||||||
|
/// 6. 候选集为空时开启下一轮,先制限制只适用于第一轮。
|
||||||
|
/// </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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按调度规则选本轮优先级最高的候选;没有候选时返回 null。
|
||||||
|
/// </summary>
|
||||||
|
private BattleUnit FindNextActor()
|
||||||
|
{
|
||||||
|
BattleUnit best = null;
|
||||||
|
foreach (BattleUnit unit in Units)
|
||||||
|
{
|
||||||
|
if (!IsActive(unit) || ActedUnitIds.Contains(unit.UnitId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPreemptiveRound && unit.Faction == BattleFaction.Enemy && HasUnactedPlayer())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best == null || CompareActors(unit, best) < 0)
|
||||||
|
{
|
||||||
|
best = unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// 敌人随机决策并执行:从 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
|
fileFormatVersion: 2
|
||||||
guid: ec4ca0d279034f4a939fd35aedab383e
|
guid: 11f1778a3f4e34da691bcac2656ed3a6
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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
|
fileFormatVersion: 2
|
||||||
guid: bef66af163534a79b4b14ba27ed80665
|
guid: 5d551f82833074a5fa669f05ba7249eb
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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:
|
||||||
+27
-62
@@ -4,9 +4,9 @@ using SepCore.Definition;
|
|||||||
namespace SepCore.Battle
|
namespace SepCore.Battle
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单位剩余状态快照。
|
/// 单个战斗状态的只读视图。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BattleStatusSnapshot
|
public sealed class BattleStatusView
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 状态类型。
|
/// 状态类型。
|
||||||
@@ -18,7 +18,7 @@ namespace SepCore.Battle
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly int RemainingRounds;
|
public readonly int RemainingRounds;
|
||||||
|
|
||||||
public BattleStatusSnapshot(BattleStatusType statusType, int remainingRounds)
|
public BattleStatusView(BattleStatusType statusType, int remainingRounds)
|
||||||
{
|
{
|
||||||
StatusType = statusType;
|
StatusType = statusType;
|
||||||
RemainingRounds = remainingRounds;
|
RemainingRounds = remainingRounds;
|
||||||
@@ -27,8 +27,9 @@ namespace SepCore.Battle
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单个战斗单位的只读视图。
|
/// 单个战斗单位的只读视图。
|
||||||
|
/// 内部调度与效果结算不得读取本视图;视图只在外部边界按需生成。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BattleUnitSnapshot
|
public sealed class BattleUnitView
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 本场战斗单位的唯一运行时标识;重复配置的敌人拥有不同 ID。
|
/// 本场战斗单位的唯一运行时标识;重复配置的敌人拥有不同 ID。
|
||||||
@@ -46,7 +47,7 @@ namespace SepCore.Battle
|
|||||||
public readonly int ConfigId;
|
public readonly int ConfigId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同阵营同速度的最终并列规则顺序。
|
/// 同阵营同速度的最终并列规则顺序,也是显示顺序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly int PartyOrder;
|
public readonly int PartyOrder;
|
||||||
|
|
||||||
@@ -70,16 +71,6 @@ namespace SepCore.Battle
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly int MaxMp;
|
public readonly int MaxMp;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 攻击。
|
|
||||||
/// </summary>
|
|
||||||
public readonly int Atk;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 魔力。
|
|
||||||
/// </summary>
|
|
||||||
public readonly int Mat;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前速度。
|
/// 当前速度。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -96,19 +87,13 @@ namespace SepCore.Battle
|
|||||||
public readonly bool IsEscaped;
|
public readonly bool IsEscaped;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 剩余状态列表。
|
/// 当前剩余状态列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly IReadOnlyList<BattleStatusSnapshot> Statuses;
|
public readonly IReadOnlyList<BattleStatusView> Statuses;
|
||||||
|
|
||||||
/// <summary>
|
public BattleUnitView(int battleUnitId, BattleFaction faction, int configId, int partyOrder,
|
||||||
/// 可用行动配置 ID 列表。
|
int currentHp, int maxHp, int currentMp, int maxMp, int speed,
|
||||||
/// </summary>
|
bool isDefeated, bool isEscaped, IReadOnlyList<BattleStatusView> statuses)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
BattleUnitId = battleUnitId;
|
BattleUnitId = battleUnitId;
|
||||||
Faction = faction;
|
Faction = faction;
|
||||||
@@ -118,29 +103,15 @@ namespace SepCore.Battle
|
|||||||
MaxHp = maxHp;
|
MaxHp = maxHp;
|
||||||
CurrentMp = currentMp;
|
CurrentMp = currentMp;
|
||||||
MaxMp = maxMp;
|
MaxMp = maxMp;
|
||||||
Atk = atk;
|
|
||||||
Mat = mat;
|
|
||||||
Speed = speed;
|
Speed = speed;
|
||||||
IsDefeated = isDefeated;
|
IsDefeated = isDefeated;
|
||||||
IsEscaped = isEscaped;
|
IsEscaped = isEscaped;
|
||||||
Statuses = statuses != null ? Copy(statuses) : new BattleStatusSnapshot[0];
|
Statuses = statuses != null ? Copy(statuses) : new BattleStatusView[0];
|
||||||
AvailableActionIds = availableActionIds != null ? Copy(availableActionIds) : new int[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BattleStatusSnapshot[] Copy(IReadOnlyList<BattleStatusSnapshot> source)
|
private static BattleStatusView[] Copy(IReadOnlyList<BattleStatusView> source)
|
||||||
{
|
{
|
||||||
BattleStatusSnapshot[] copy = new BattleStatusSnapshot[source.Count];
|
BattleStatusView[] copy = new BattleStatusView[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];
|
|
||||||
for (int i = 0; i < source.Count; i++)
|
for (int i = 0; i < source.Count; i++)
|
||||||
{
|
{
|
||||||
copy[i] = source[i];
|
copy[i] = source[i];
|
||||||
@@ -151,21 +122,16 @@ namespace SepCore.Battle
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 只读战斗快照,是 UI 和敌人策略唯一允许读取的战斗状态。
|
/// 当前战斗的只读视图,是 UI 唯一允许读取的战斗状态。
|
||||||
/// 每次推进都创建逻辑上的只读快照,不把可修改的内部列表或战斗单位引用暴露给调用方。
|
/// 每次推进后在外部边界生成,不把内部可变对象暴露给 UI 或探索层。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BattleSnapshot
|
public sealed class BattleViewState
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前轮次,从 1 开始。
|
/// 当前轮次,从 1 开始。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly int RoundNumber;
|
public readonly int RoundNumber;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 当前是否为先制战斗第一轮。
|
|
||||||
/// </summary>
|
|
||||||
public readonly bool IsPreemptiveRound;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前行动者;战斗完成时为 0。
|
/// 当前行动者;战斗完成时为 0。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -174,7 +140,7 @@ namespace SepCore.Battle
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 全部单位的只读视图,保持稳定显示顺序。
|
/// 全部单位的只读视图,保持稳定显示顺序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly IReadOnlyList<BattleUnitSnapshot> Units;
|
public readonly IReadOnlyList<BattleUnitView> Units;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 本轮从当前行动者开始的剩余运行时单位 ID;速度变化后重建。
|
/// 本轮从当前行动者开始的剩余运行时单位 ID;速度变化后重建。
|
||||||
@@ -182,25 +148,24 @@ namespace SepCore.Battle
|
|||||||
public readonly IReadOnlyList<int> RemainingTurnOrder;
|
public readonly IReadOnlyList<int> RemainingTurnOrder;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 流程状态。
|
/// 当前玩家可用的行动配置 ID;当前行动者不是玩家或战斗完成时为空。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly BattleFlowState FlowState;
|
public readonly IReadOnlyList<int> AvailableActionIds;
|
||||||
|
|
||||||
public BattleSnapshot(int roundNumber, bool isPreemptiveRound, int currentActorUnitId,
|
public BattleViewState(int roundNumber, int currentActorUnitId,
|
||||||
IReadOnlyList<BattleUnitSnapshot> units, IReadOnlyList<int> remainingTurnOrder,
|
IReadOnlyList<BattleUnitView> units, IReadOnlyList<int> remainingTurnOrder,
|
||||||
BattleFlowState flowState)
|
IReadOnlyList<int> availableActionIds)
|
||||||
{
|
{
|
||||||
RoundNumber = roundNumber;
|
RoundNumber = roundNumber;
|
||||||
IsPreemptiveRound = isPreemptiveRound;
|
|
||||||
CurrentActorUnitId = currentActorUnitId;
|
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];
|
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++)
|
for (int i = 0; i < source.Count; i++)
|
||||||
{
|
{
|
||||||
copy[i] = source[i];
|
copy[i] = source[i];
|
||||||
@@ -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 System.Collections.Generic;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.Definition;
|
||||||
|
using SepCore.UI;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
namespace SepCore.CustomComponent
|
namespace SepCore.CustomComponent
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单局战斗组件。
|
/// 单局战斗组件,也是战斗模块的唯一外部入口。
|
||||||
/// 负责本局临时角色状态、计时、探索暂停标志和战斗占用的最小状态。
|
/// 负责本局临时角色状态、计时、探索暂停标志、战斗占用和当前唯一的 BattleRuntime。
|
||||||
|
/// 外部通过 TryStartBattle 启动战斗、CloseBattle 关闭调试战斗;
|
||||||
|
/// 战斗规则(调度、指令校验、效果、敌人决策)在内部共享同一个 BattleRuntime,
|
||||||
|
/// 不为模块内部的函数调用建立请求/响应 DTO。
|
||||||
/// 本局种子与共享随机源由独立的 RandomComponent 持有,战斗只沿用,不创建私有随机源。
|
/// 本局种子与共享随机源由独立的 RandomComponent 持有,战斗只沿用,不创建私有随机源。
|
||||||
/// 战斗协调(构建请求、打开 UI、回写结果)由独立的 RunBattleCoordinator 负责。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TurnBattleComponent : GameFrameworkComponent
|
public class TurnBattleComponent : GameFrameworkComponent
|
||||||
{
|
{
|
||||||
@@ -17,6 +23,16 @@ namespace SepCore.CustomComponent
|
|||||||
private bool _timerPaused;
|
private bool _timerPaused;
|
||||||
private bool _explorationPaused;
|
private bool _explorationPaused;
|
||||||
private bool _battleActive;
|
private bool _battleActive;
|
||||||
|
private BattleRuntime _runtime;
|
||||||
|
private IBattleConfigProvider _config;
|
||||||
|
private Action<BattleResult> _onCompleted;
|
||||||
|
private Action<BattleStep> _stepListener;
|
||||||
|
private Coroutine _autoAdvanceRoutine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 敌人自动行动前的间歇时间(秒),用于让 UI 展示行动过程。
|
||||||
|
/// </summary>
|
||||||
|
private const float AutoAdvanceDelaySeconds = 0.6f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取本局临时角色状态列表(只读)。
|
/// 获取本局临时角色状态列表(只读)。
|
||||||
@@ -45,16 +61,17 @@ namespace SepCore.CustomComponent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsBattleActive => _battleActive;
|
public bool IsBattleActive => _battleActive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前唯一的战斗运行时;仅战斗期间非 null。
|
||||||
|
/// </summary>
|
||||||
|
internal BattleRuntime Runtime => _runtime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 开始一局新的单局:重置全部临时状态。
|
/// 开始一局新的单局:重置全部临时状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void BeginRun()
|
public void BeginRun()
|
||||||
{
|
{
|
||||||
_players.Clear();
|
ResetRunState();
|
||||||
_elapsedMs = 0;
|
|
||||||
_timerPaused = false;
|
|
||||||
_explorationPaused = false;
|
|
||||||
_battleActive = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,11 +79,7 @@ namespace SepCore.CustomComponent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void EndRun()
|
public void EndRun()
|
||||||
{
|
{
|
||||||
_players.Clear();
|
ResetRunState();
|
||||||
_elapsedMs = 0;
|
|
||||||
_timerPaused = false;
|
|
||||||
_explorationPaused = false;
|
|
||||||
_battleActive = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,26 +96,145 @@ namespace SepCore.CustomComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预留战斗占用,拒绝同一帧或未结束战斗的重复进入。
|
/// 尝试开始一场战斗,是战斗模块的唯一外部入口。
|
||||||
|
/// 校验通过后预留战斗占用、暂停探索更新与单局计时并打开 BattleForm;
|
||||||
|
/// 校验失败不消耗随机数、不修改单局状态。
|
||||||
|
/// 战斗完成时通过 onCompleted 一次性回调返回 BattleResult;调试入口可不提供回调。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>预留成功返回 true;已存在战斗占用时返回 false。</returns>
|
/// <param name="encounter">探索层创建的遭遇输入。</param>
|
||||||
public bool TryReserveBattle()
|
/// <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;
|
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;
|
_battleActive = true;
|
||||||
|
_explorationPaused = true;
|
||||||
|
_timerPaused = true;
|
||||||
|
|
||||||
|
GameEntry.UI.OpenUIForm(UIFormType.BattleForm);
|
||||||
|
Log.Info("Battle started with encounter '{0}'.", encounter.EncounterId);
|
||||||
|
|
||||||
|
// M2:敌人速度更高时开局行动者为敌人,同样按间歇节奏自动推进
|
||||||
|
ScheduleAutoAdvance();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 释放战斗占用。
|
/// 提交当前玩家指令并同步推进单次行动,返回本次推进的行动记录、最新视图和可能的最终结果。
|
||||||
|
/// 非法指令不消耗行动机会;玩家行动后若轮到敌人,由组件按间歇节奏自动推进敌人回合,
|
||||||
|
/// 每一步通过已注册的推进监听推送给 UI。
|
||||||
|
/// 战斗完成后向 onCompleted 一次性回调返回 BattleResult。
|
||||||
/// </summary>
|
/// </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;
|
_battleActive = false;
|
||||||
|
Log.Info("Battle closed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -132,5 +264,97 @@ namespace SepCore.CustomComponent
|
|||||||
|
|
||||||
_elapsedMs += Time.deltaTime * 1000f;
|
_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()
|
||||||
|
{
|
||||||
|
// 战斗 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)
|
||||||
|
{
|
||||||
|
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,11 +11,18 @@ namespace SepCore.Debugger
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 战斗壳层调试入口(仅供 Editor/Development Build,正式构建不注册)。
|
/// 战斗壳层调试入口(仅供 Editor/Development Build,正式构建不注册)。
|
||||||
/// 模拟探索层:缺少单局状态时用配表角色与固定种子初始化临时状态,
|
/// 模拟探索层:缺少单局状态时用配表角色与固定种子初始化临时状态,
|
||||||
/// 然后通过 RunBattleCoordinator 以固定调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
|
/// 然后通过 TurnBattleComponent 以调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
|
||||||
|
/// 支持 1v1 / 2v2 / 4v4 与先制开关。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BattleDebuggerWindow : IDebuggerWindow
|
public class BattleDebuggerWindow : IDebuggerWindow
|
||||||
{
|
{
|
||||||
private const int DebugRunSeed = 240829;
|
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)
|
public void Initialize(params object[] args)
|
||||||
{
|
{
|
||||||
@@ -40,7 +47,36 @@ namespace SepCore.Debugger
|
|||||||
public void OnDraw()
|
public void OnDraw()
|
||||||
{
|
{
|
||||||
GUILayout.Label("<b>Battle Shell</b>");
|
GUILayout.Label("<b>Battle Shell</b>");
|
||||||
GUILayout.BeginHorizontal("box");
|
|
||||||
|
GUILayout.BeginVertical("box");
|
||||||
|
{
|
||||||
|
GUILayout.Label("队伍规模(玩家数/敌人队伍):");
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
{
|
||||||
|
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)))
|
if (GUILayout.Button("Open", GUILayout.Height(30)))
|
||||||
{
|
{
|
||||||
@@ -49,10 +85,12 @@ namespace SepCore.Debugger
|
|||||||
|
|
||||||
if (GUILayout.Button("Close", GUILayout.Height(30)))
|
if (GUILayout.Button("Close", GUILayout.Height(30)))
|
||||||
{
|
{
|
||||||
GameEntry.RunBattle.EndDebugBattle();
|
GameEntry.TurnBattle.CloseBattle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
DrawRunState();
|
DrawRunState();
|
||||||
}
|
}
|
||||||
@@ -77,32 +115,29 @@ namespace SepCore.Debugger
|
|||||||
GUILayout.EndVertical();
|
GUILayout.EndVertical();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OpenShell()
|
private void OpenShell()
|
||||||
{
|
|
||||||
InitDebugRunStateIfNeeded();
|
|
||||||
|
|
||||||
// 固定调试遭遇:EncounterId=1,敌人队伍预设 1,普通战斗
|
|
||||||
bool started = GameEntry.RunBattle.TryStartBattle(new BattleEncounter(1, 1, false));
|
|
||||||
if (!started)
|
|
||||||
{
|
|
||||||
Log.Warning("Battle debug shell can not start, maybe a battle is already active.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void InitDebugRunStateIfNeeded()
|
|
||||||
{
|
{
|
||||||
if (GameEntry.Random.Random == null)
|
if (GameEntry.Random.Random == null)
|
||||||
{
|
{
|
||||||
GameEntry.Random.BeginRun(DebugRunSeed);
|
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>();
|
List<RunPlayerState> players = new List<RunPlayerState>();
|
||||||
GlobalConfig global = GameEntry.Luban.Global != null ? GameEntry.Luban.Global.Data : null;
|
GlobalConfig global = GameEntry.Luban.Global != null ? GameEntry.Luban.Global.Data : null;
|
||||||
@@ -112,9 +147,20 @@ namespace SepCore.Debugger
|
|||||||
return players;
|
return players;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
int order = 1;
|
||||||
foreach (int characterId in global.NewGameCharacterIds)
|
foreach (int characterId in global.NewGameCharacterIds)
|
||||||
{
|
{
|
||||||
|
if (order > count)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(characterId);
|
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(characterId);
|
||||||
if (config == null)
|
if (config == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 68a0e1b7c828a496d858968e77f3826b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2d55ddba8b14246bcb3c84719d8ef2b1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ff9bdccc0a8fa4a70a750d29c757252a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.CustomComponent;
|
||||||
|
using SepCore.Definition;
|
||||||
|
|
||||||
|
namespace SepCore.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BattleRuntimeAttackLoopTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void PlayerAttack_DealsAtkDamageAndHandsTurnToEnemy()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.AreEqual(1, step.Events.Count);
|
||||||
|
BattleEvent battleEvent = step.Events[0];
|
||||||
|
Assert.AreEqual(1, battleEvent.ActorUnitId);
|
||||||
|
Assert.AreEqual(BattleActionType.Attack, battleEvent.CommandType);
|
||||||
|
Assert.AreEqual(1, battleEvent.ActionConfigId);
|
||||||
|
Assert.AreEqual(2, battleEvent.TargetUnitId);
|
||||||
|
Assert.AreEqual(50, battleEvent.BeforeHp);
|
||||||
|
Assert.AreEqual(36, battleEvent.AfterHp);
|
||||||
|
Assert.AreEqual(36, runtime.GetUnit(2).CurrentHp);
|
||||||
|
Assert.AreEqual(120, runtime.GetUnit(1).CurrentHp);
|
||||||
|
Assert.AreEqual(2, runtime.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(1, runtime.RoundNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnemyCounterAttack_DamagesPlayerAndStartsNextRound()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
|
||||||
|
BattleStep step = runtime.AdvanceEnemyTurn();
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.AreEqual(1, step.Events.Count);
|
||||||
|
BattleEvent battleEvent = step.Events[0];
|
||||||
|
Assert.AreEqual(2, battleEvent.ActorUnitId);
|
||||||
|
Assert.AreEqual(1, battleEvent.TargetUnitId);
|
||||||
|
Assert.AreEqual(120, battleEvent.BeforeHp);
|
||||||
|
Assert.AreEqual(112, battleEvent.AfterHp);
|
||||||
|
Assert.AreEqual(112, runtime.GetUnit(1).CurrentHp);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(2, runtime.RoundNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AttackLoop_EndsWithVictory()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep final = PlayToCompletion(runtime);
|
||||||
|
|
||||||
|
Assert.NotNull(final.Result);
|
||||||
|
Assert.AreEqual(BattleOutcome.Victory, final.Result.Outcome);
|
||||||
|
Assert.AreEqual(1, final.Result.EncounterId);
|
||||||
|
Assert.AreEqual(1, final.Result.Players.Count);
|
||||||
|
Assert.AreEqual(1001, final.Result.Players[0].CharacterId);
|
||||||
|
Assert.False(final.Result.Players[0].WasDefeated);
|
||||||
|
Assert.False(final.Result.Players[0].Escaped);
|
||||||
|
Assert.Greater(final.Result.Players[0].CurrentHp, 0);
|
||||||
|
Assert.True(runtime.IsCompleted);
|
||||||
|
Assert.AreEqual(0, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AttackLoop_EndsWithTotalDefeat_WhenPlayerDies()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player(currentHp: 5, maxHp: 5));
|
||||||
|
|
||||||
|
BattleStep final = PlayToCompletion(runtime);
|
||||||
|
|
||||||
|
Assert.NotNull(final.Result);
|
||||||
|
Assert.AreEqual(BattleOutcome.TotalDefeat, final.Result.Outcome);
|
||||||
|
Assert.AreEqual(0, final.Result.Players[0].CurrentHp);
|
||||||
|
Assert.True(final.Result.Players[0].WasDefeated);
|
||||||
|
Assert.True(runtime.IsCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayerAttack_WithHealEffect_ClampsHpToMax()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
config.AddAction(TestConfigs.AttackAction(2, 200, BattleStatType.None, 0));
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player(), config);
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(TestBattle.Attack(1, 2, 2));
|
||||||
|
|
||||||
|
Assert.AreEqual(1, step.Events.Count);
|
||||||
|
Assert.AreEqual(50, step.Events[0].AfterHp);
|
||||||
|
Assert.AreEqual(50, runtime.GetUnit(2).CurrentHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InvalidCommand_WrongActor_IsRejectedWithoutConsumption()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(TestBattle.Attack(999, 2));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(120, runtime.GetUnit(1).CurrentHp);
|
||||||
|
Assert.AreEqual(50, runtime.GetUnit(2).CurrentHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InvalidCommand_SkillCommand_IsRejected()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(
|
||||||
|
new BattleCommand(1, BattleActionType.Skill, 101, new List<int> { 2 }));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InvalidCommand_ActionConfigTypeMismatch_IsRejected()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player(), config);
|
||||||
|
|
||||||
|
// 行动 1 是攻击,却用 Skill 类型提交
|
||||||
|
BattleStep step = runtime.SubmitCommand(
|
||||||
|
new BattleCommand(1, BattleActionType.Skill, 1, new List<int> { 2 }));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InvalidCommand_AllyTarget_IsRejected()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(TestBattle.Attack(1, 1));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InvalidCommand_NoTarget_IsRejected()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(
|
||||||
|
new BattleCommand(1, BattleActionType.Attack, 1, new List<int>()));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CompletedBattle_RejectsFurtherCommands()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
PlayToCompletion(runtime);
|
||||||
|
Assert.True(runtime.IsCompleted);
|
||||||
|
|
||||||
|
BattleStep step = runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(0, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BattleStep PlayToCompletion(BattleRuntime runtime)
|
||||||
|
{
|
||||||
|
BattleStep step = null;
|
||||||
|
int guard = 0;
|
||||||
|
while (runtime.Result == null && guard++ < 200)
|
||||||
|
{
|
||||||
|
BattleUnit actor = runtime.CurrentActor;
|
||||||
|
if (actor != null && actor.Faction == BattleFaction.Player)
|
||||||
|
{
|
||||||
|
int targetUnitId = 0;
|
||||||
|
foreach (BattleUnit unit in runtime.Units)
|
||||||
|
{
|
||||||
|
if (unit.Faction == BattleFaction.Enemy && BattleRuntime.IsActive(unit))
|
||||||
|
{
|
||||||
|
targetUnitId = unit.UnitId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step = runtime.SubmitCommand(TestBattle.Attack(actor.UnitId, targetUnitId));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
step = runtime.AdvanceEnemyTurn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5e6abbbfbf1d9431ea5cfe74917ee817
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.CustomComponent;
|
||||||
|
|
||||||
|
namespace SepCore.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BattleRuntimeCreateTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Create_WithStandard1v1Inputs_BuildsExpectedUnits()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
Assert.NotNull(runtime);
|
||||||
|
Assert.False(runtime.IsCompleted);
|
||||||
|
Assert.AreEqual(1, runtime.RoundNumber);
|
||||||
|
Assert.AreEqual(2, runtime.Units.Length);
|
||||||
|
|
||||||
|
BattleUnit player = runtime.Units[0];
|
||||||
|
Assert.AreEqual(1, player.UnitId);
|
||||||
|
Assert.AreEqual(BattleFaction.Player, player.Faction);
|
||||||
|
Assert.AreEqual(1001, player.ConfigId);
|
||||||
|
Assert.AreEqual(1, player.PartyOrder);
|
||||||
|
Assert.AreEqual(120, player.CurrentHp);
|
||||||
|
Assert.AreEqual(120, player.MaxHp);
|
||||||
|
Assert.AreEqual(40, player.CurrentMp);
|
||||||
|
Assert.AreEqual(14, player.Atk);
|
||||||
|
Assert.AreEqual(6, player.Mat);
|
||||||
|
Assert.AreEqual(12, player.Speed);
|
||||||
|
CollectionAssert.AreEqual(new List<int> { 1, 101 }, player.ActionIds);
|
||||||
|
|
||||||
|
BattleUnit enemy = runtime.Units[1];
|
||||||
|
Assert.AreEqual(2, enemy.UnitId);
|
||||||
|
Assert.AreEqual(BattleFaction.Enemy, enemy.Faction);
|
||||||
|
Assert.AreEqual(3001, enemy.ConfigId);
|
||||||
|
Assert.AreEqual(50, enemy.CurrentHp);
|
||||||
|
Assert.AreEqual(8, enemy.Atk);
|
||||||
|
CollectionAssert.AreEqual(new List<int> { 201 }, enemy.ActionIds);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_WithNullInputs_ReturnsNull()
|
||||||
|
{
|
||||||
|
RunPlayerState player = TestBattle.Player();
|
||||||
|
BattleEncounter encounter = TestBattle.Encounter();
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
IRunRandomSource random = new TestRandomSource();
|
||||||
|
|
||||||
|
Assert.Null(BattleRuntime.Create(null, new List<RunPlayerState> { player }, config, random));
|
||||||
|
Assert.Null(BattleRuntime.Create(encounter, null, config, random));
|
||||||
|
Assert.Null(BattleRuntime.Create(encounter, new List<RunPlayerState>(), config, random));
|
||||||
|
Assert.Null(BattleRuntime.Create(encounter, new List<RunPlayerState> { player }, null, random));
|
||||||
|
Assert.Null(BattleRuntime.Create(encounter, new List<RunPlayerState> { player }, config, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_WithMissingEnemyParty_ReturnsNull()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
|
||||||
|
Assert.Null(BattleRuntime.Create(new BattleEncounter(1, 999, false),
|
||||||
|
new List<RunPlayerState> { TestBattle.Player() }, config, new TestRandomSource()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_WithMissingEnemyConfig_ReturnsNull()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
config.AddParty(TestConfigs.Party(5001, 9999));
|
||||||
|
|
||||||
|
Assert.Null(BattleRuntime.Create(new BattleEncounter(1, 5001, false),
|
||||||
|
new List<RunPlayerState> { TestBattle.Player() }, config, new TestRandomSource()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6f72e4b9e477543aea96022278d94914
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.CustomComponent;
|
||||||
|
using SepCore.Definition;
|
||||||
|
|
||||||
|
namespace SepCore.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BattleRuntimeEnemyTurnTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void AdvanceEnemyTurn_WhenPlayerIsCurrent_ReturnsNoOp()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleStep step = runtime.AdvanceEnemyTurn();
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(120, runtime.GetUnit(1).CurrentHp);
|
||||||
|
Assert.AreEqual(50, runtime.GetUnit(2).CurrentHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnemyWithoutUsableActions_SkipsItsTurn()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
// 敌人唯一行动改为 MP 不足(100 > 敌人 MP 0),无可用行动
|
||||||
|
config.AddAction(TestConfigs.AttackAction(201, 0, BattleStatType.ATK, -1000, 100));
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player(), config);
|
||||||
|
|
||||||
|
runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
BattleStep step = runtime.AdvanceEnemyTurn();
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(2, runtime.RoundNumber);
|
||||||
|
Assert.AreEqual(120, runtime.GetUnit(1).CurrentHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnemyWithNoActionIds_SkipsItsTurn()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
config.AddEnemy(TestConfigs.Enemy(3001, 50, 8));
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player(), config);
|
||||||
|
|
||||||
|
runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
BattleStep step = runtime.AdvanceEnemyTurn();
|
||||||
|
|
||||||
|
Assert.Null(step.Result);
|
||||||
|
Assert.IsEmpty(step.Events);
|
||||||
|
Assert.AreEqual(1, runtime.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(2, runtime.RoundNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnemyWithMultipleActions_ChoosesByRandomSequence()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
config.AddAction(TestConfigs.AttackAction(201, 0, BattleStatType.ATK, -1000));
|
||||||
|
config.AddAction(TestConfigs.AttackAction(202, -30, BattleStatType.None, 0));
|
||||||
|
config.AddEnemy(TestConfigs.Enemy(3001, 50, 8, 201, 202));
|
||||||
|
config.AddParty(TestConfigs.Party(4001, 3001));
|
||||||
|
|
||||||
|
// 随机序列 [0] -> 选 201,伤害 = ATK = 8
|
||||||
|
BattleRuntime runtimeA = TestBattle.Create1v1(TestBattle.Player(), config, new TestRandomSource(0));
|
||||||
|
runtimeA.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
BattleStep stepA = runtimeA.AdvanceEnemyTurn();
|
||||||
|
Assert.AreEqual(112, stepA.Events[0].AfterHp);
|
||||||
|
|
||||||
|
// 随机序列 [1] -> 选 202,固定伤害 30
|
||||||
|
BattleRuntime runtimeB = TestBattle.Create1v1(TestBattle.Player(), config, new TestRandomSource(1));
|
||||||
|
runtimeB.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
BattleStep stepB = runtimeB.AdvanceEnemyTurn();
|
||||||
|
Assert.AreEqual(90, stepB.Events[0].AfterHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SameInputsAndRandomSequence_ProduceSameRecords()
|
||||||
|
{
|
||||||
|
TestConfigProvider config = TestConfigProvider.Standard1v1();
|
||||||
|
config.AddAction(TestConfigs.AttackAction(201, 0, BattleStatType.ATK, -1000));
|
||||||
|
config.AddAction(TestConfigs.AttackAction(202, -30, BattleStatType.None, 0));
|
||||||
|
config.AddEnemy(TestConfigs.Enemy(3001, 50, 8, 201, 202));
|
||||||
|
config.AddParty(TestConfigs.Party(4001, 3001));
|
||||||
|
|
||||||
|
BattleRuntime runtimeA = TestBattle.Create1v1(TestBattle.Player(), config, new TestRandomSource(0, 0, 0, 0));
|
||||||
|
BattleRuntime runtimeB = TestBattle.Create1v1(TestBattle.Player(), config, new TestRandomSource(0, 0, 0, 0));
|
||||||
|
|
||||||
|
List<string> recordsA = CollectRecords(runtimeA);
|
||||||
|
List<string> recordsB = CollectRecords(runtimeB);
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(recordsA, recordsB);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
int targetUnitId = 0;
|
||||||
|
foreach (BattleUnit unit in runtime.Units)
|
||||||
|
{
|
||||||
|
if (unit.Faction == BattleFaction.Enemy && BattleRuntime.IsActive(unit))
|
||||||
|
{
|
||||||
|
targetUnitId = unit.UnitId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step = runtime.SubmitCommand(TestBattle.Attack(actor.UnitId, targetUnitId));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8d0eb453c4b42453abb2e86fad636fe9
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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:
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.CustomComponent;
|
||||||
|
|
||||||
|
namespace SepCore.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BattleRuntimeViewAndLifecycleTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void BuildViewState_ReflectsUnitsAndTurnOrder()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
|
||||||
|
BattleViewState view = runtime.BuildViewState();
|
||||||
|
|
||||||
|
Assert.AreEqual(1, view.RoundNumber);
|
||||||
|
Assert.AreEqual(1, view.CurrentActorUnitId);
|
||||||
|
Assert.AreEqual(2, view.Units.Count);
|
||||||
|
CollectionAssert.AreEqual(new List<int> { 1, 2 }, view.RemainingTurnOrder);
|
||||||
|
CollectionAssert.AreEqual(new List<int> { 1, 101 }, view.AvailableActionIds);
|
||||||
|
|
||||||
|
BattleUnitView playerView = view.Units[0];
|
||||||
|
Assert.AreEqual(BattleFaction.Player, playerView.Faction);
|
||||||
|
Assert.AreEqual(1001, playerView.ConfigId);
|
||||||
|
Assert.AreEqual(1, playerView.PartyOrder);
|
||||||
|
Assert.AreEqual(120, playerView.CurrentHp);
|
||||||
|
Assert.AreEqual(40, playerView.CurrentMp);
|
||||||
|
Assert.AreEqual(12, playerView.Speed);
|
||||||
|
Assert.False(playerView.IsDefeated);
|
||||||
|
Assert.False(playerView.IsEscaped);
|
||||||
|
Assert.IsEmpty(playerView.Statuses);
|
||||||
|
|
||||||
|
BattleUnitView enemyView = view.Units[1];
|
||||||
|
Assert.AreEqual(BattleFaction.Enemy, enemyView.Faction);
|
||||||
|
Assert.AreEqual(3001, enemyView.ConfigId);
|
||||||
|
Assert.AreEqual(50, enemyView.CurrentHp);
|
||||||
|
Assert.False(enemyView.IsDefeated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildViewState_AfterPlayerAction_HighlightsEnemyAndKeepsAllUnits()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
runtime.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
|
||||||
|
BattleViewState view = runtime.BuildViewState();
|
||||||
|
|
||||||
|
Assert.AreEqual(2, view.CurrentActorUnitId);
|
||||||
|
// 同轮内列表仍包含全部单位(已行动玩家不被隐藏)
|
||||||
|
CollectionAssert.AreEqual(new List<int> { 2 }, view.RemainingTurnOrder);
|
||||||
|
Assert.AreEqual(2, view.Units.Count);
|
||||||
|
Assert.AreEqual(36, view.Units[1].CurrentHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildViewState_AfterCompletion_HasNoActor()
|
||||||
|
{
|
||||||
|
BattleRuntime runtime = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
int guard = 0;
|
||||||
|
while (runtime.Result == null && guard++ < 200)
|
||||||
|
{
|
||||||
|
BattleUnit actor = runtime.CurrentActor;
|
||||||
|
if (actor != null && actor.Faction == BattleFaction.Player)
|
||||||
|
{
|
||||||
|
int targetUnitId = 0;
|
||||||
|
foreach (BattleUnit unit in runtime.Units)
|
||||||
|
{
|
||||||
|
if (unit.Faction == BattleFaction.Enemy && BattleRuntime.IsActive(unit))
|
||||||
|
{
|
||||||
|
targetUnitId = unit.UnitId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.SubmitCommand(TestBattle.Attack(actor.UnitId, targetUnitId));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runtime.AdvanceEnemyTurn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BattleViewState view = runtime.BuildViewState();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, view.CurrentActorUnitId);
|
||||||
|
Assert.IsEmpty(view.RemainingTurnOrder);
|
||||||
|
Assert.IsEmpty(view.AvailableActionIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SequentialBattles_DoNotInheritState()
|
||||||
|
{
|
||||||
|
// 第一场打到玩家残血
|
||||||
|
BattleRuntime first = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
first.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
first.AdvanceEnemyTurn();
|
||||||
|
Assert.AreEqual(112, first.GetUnit(1).CurrentHp);
|
||||||
|
|
||||||
|
// 第二场用同一份输入重新创建:应从满值开局
|
||||||
|
BattleRuntime second = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
Assert.AreEqual(120, second.GetUnit(1).CurrentHp);
|
||||||
|
Assert.AreEqual(50, second.GetUnit(2).CurrentHp);
|
||||||
|
Assert.AreEqual(1, second.RoundNumber);
|
||||||
|
Assert.AreEqual(1, second.CurrentActorUnitId);
|
||||||
|
Assert.False(second.IsCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SequentialBattles_UseIndependentActedSets()
|
||||||
|
{
|
||||||
|
BattleRuntime first = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
first.SubmitCommand(TestBattle.Attack(1, 2));
|
||||||
|
Assert.IsTrue(first.ActedUnitIds.Contains(1));
|
||||||
|
|
||||||
|
BattleRuntime second = TestBattle.Create1v1(TestBattle.Player());
|
||||||
|
Assert.IsEmpty(second.ActedUnitIds);
|
||||||
|
Assert.AreEqual(1, second.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c78ea02415ec84677ae1925546f846c5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.CustomComponent;
|
||||||
|
using SepCore.Definition;
|
||||||
|
|
||||||
|
namespace SepCore.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用反射构造 Luban 生成的只读配置行,供测试的假配置提供者使用。
|
||||||
|
/// </summary>
|
||||||
|
internal static class TestConfigFactory
|
||||||
|
{
|
||||||
|
public static T Create<T>(params object[] fieldValues)
|
||||||
|
{
|
||||||
|
T instance = (T)FormatterServices.GetUninitializedObject(typeof(T));
|
||||||
|
for (int i = 0; i < fieldValues.Length; i += 2)
|
||||||
|
{
|
||||||
|
FieldInfo field = typeof(T).GetField((string)fieldValues[i]);
|
||||||
|
field.SetValue(instance, fieldValues[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 与真实配表数值对齐的测试配置构建器。
|
||||||
|
/// </summary>
|
||||||
|
internal static class TestConfigs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 单体敌方攻击行动。
|
||||||
|
/// </summary>
|
||||||
|
public static BattleActionConfig AttackAction(int id, int flat, BattleStatType sourceStat,
|
||||||
|
int sourceScalePermille, int mpCost = 0)
|
||||||
|
{
|
||||||
|
return TestConfigFactory.Create<BattleActionConfig>(
|
||||||
|
"Id", id, "Name", "攻击" + id, "ActionType", BattleActionType.Attack,
|
||||||
|
"TargetType", BattleTargetType.SingleEnemy, "MpCost", mpCost,
|
||||||
|
"Effects", new List<BattleEffect>
|
||||||
|
{
|
||||||
|
TestConfigFactory.Create<BattleEffect>(
|
||||||
|
"TargetStat", BattleStatType.HP, "FlatValue", flat,
|
||||||
|
"SourceStat", sourceStat, "SourceScalePermille", sourceScalePermille,
|
||||||
|
"Status", BattleStatusType.None, "DurationRounds", 0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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", speed, "ThreatLevelId", 1, "AiType", EnemyAiType.Random,
|
||||||
|
"ActionIds", new List<int>(actionIds), "DropTableId", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EnemyPartyConfig Party(int id, params int[] enemyIds)
|
||||||
|
{
|
||||||
|
return TestConfigFactory.Create<EnemyPartyConfig>(
|
||||||
|
"Id", id, "Name", "队伍" + id, "EnemyIds", new List<int>(enemyIds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内存配置提供者:EditMode 使用,不依赖 Luban 与 GameEntry。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TestConfigProvider : IBattleConfigProvider
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, EnemyPartyConfig> _parties = new Dictionary<int, EnemyPartyConfig>();
|
||||||
|
private readonly Dictionary<int, EnemyConfig> _enemies = new Dictionary<int, EnemyConfig>();
|
||||||
|
private readonly Dictionary<int, BattleActionConfig> _actions = new Dictionary<int, BattleActionConfig>();
|
||||||
|
private GlobalConfig _global;
|
||||||
|
|
||||||
|
public void AddParty(EnemyPartyConfig party)
|
||||||
|
{
|
||||||
|
_parties[party.Id] = party;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEnemy(EnemyConfig enemy)
|
||||||
|
{
|
||||||
|
_enemies[enemy.Id] = enemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAction(BattleActionConfig action)
|
||||||
|
{
|
||||||
|
_actions[action.Id] = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGlobal(GlobalConfig global)
|
||||||
|
{
|
||||||
|
_global = global;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnemyPartyConfig GetEnemyParty(int id)
|
||||||
|
{
|
||||||
|
EnemyPartyConfig value;
|
||||||
|
return _parties.TryGetValue(id, out value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnemyConfig GetEnemy(int id)
|
||||||
|
{
|
||||||
|
EnemyConfig value;
|
||||||
|
return _enemies.TryGetValue(id, out value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BattleActionConfig GetAction(int id)
|
||||||
|
{
|
||||||
|
BattleActionConfig value;
|
||||||
|
return _actions.TryGetValue(id, out value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GlobalConfig GetGlobal()
|
||||||
|
{
|
||||||
|
return _global;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准 1v1:行动 1(玩家攻击,伤害 = ATK)、行动 201(敌人攻击,伤害 = ATK)、
|
||||||
|
/// 敌人 3001(50 HP,ATK 8,行动 201)、队伍 4001(单个 3001)。
|
||||||
|
/// </summary>
|
||||||
|
public static TestConfigProvider Standard1v1()
|
||||||
|
{
|
||||||
|
TestConfigProvider provider = new TestConfigProvider();
|
||||||
|
provider.AddAction(TestConfigs.AttackAction(1, 0, BattleStatType.ATK, -1000));
|
||||||
|
provider.AddAction(TestConfigs.AttackAction(201, 0, BattleStatType.ATK, -1000));
|
||||||
|
provider.AddEnemy(TestConfigs.Enemy(3001, 50, 8, 201));
|
||||||
|
provider.AddParty(TestConfigs.Party(4001, 3001));
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可注入序列的测试随机源;用尽后返回 minInclusive。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TestRandomSource : IRunRandomSource
|
||||||
|
{
|
||||||
|
private readonly Queue<int> _values = new Queue<int>();
|
||||||
|
|
||||||
|
public TestRandomSource(params int[] values)
|
||||||
|
{
|
||||||
|
foreach (int value in values)
|
||||||
|
{
|
||||||
|
_values.Enqueue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int NextInt(int minInclusive, int maxExclusive)
|
||||||
|
{
|
||||||
|
return _values.Count > 0 ? _values.Dequeue() : minInclusive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RollPermille(int successPermille)
|
||||||
|
{
|
||||||
|
return NextInt(0, 1000) < successPermille;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准测试输入与指令构造。
|
||||||
|
/// </summary>
|
||||||
|
internal static class TestBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 标准玩家:120 HP / 40 MP,ATK 14,攻击行动 1,技能行动 101。
|
||||||
|
/// </summary>
|
||||||
|
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 = characterId,
|
||||||
|
PartyOrder = partyOrder,
|
||||||
|
CurrentHp = currentHp,
|
||||||
|
CurrentMp = 40,
|
||||||
|
MaxHp = maxHp,
|
||||||
|
MaxMp = 40,
|
||||||
|
Atk = atk,
|
||||||
|
Mat = 6,
|
||||||
|
Speed = speed,
|
||||||
|
AttackActionId = 1,
|
||||||
|
SkillActionId = 101
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BattleEncounter Encounter(int encounterId = 1, int enemyPartyConfigId = 4001)
|
||||||
|
{
|
||||||
|
return new BattleEncounter(encounterId, enemyPartyConfigId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BattleCommand Attack(int actorUnitId, int targetUnitId, int actionId = 1)
|
||||||
|
{
|
||||||
|
return new BattleCommand(actorUnitId, BattleActionType.Attack, actionId,
|
||||||
|
new List<int> { targetUnitId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建标准 1v1 运行时。
|
||||||
|
/// </summary>
|
||||||
|
public static BattleRuntime Create1v1(RunPlayerState player, IBattleConfigProvider config = null,
|
||||||
|
IRunRandomSource random = null)
|
||||||
|
{
|
||||||
|
return BattleRuntime.Create(Encounter(), new List<RunPlayerState> { player },
|
||||||
|
config ?? TestConfigProvider.Standard1v1(), random ?? new TestRandomSource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 12a6461a326b54f55bdfa3630c77e175
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Tests",
|
||||||
|
"optionalUnityReferences": [
|
||||||
|
"TestAssemblies"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
"GUID:11ec41dbc3557e6499b2845ba531a9b0",
|
||||||
|
"GUID:a04b272484018094ab427253f20c416c",
|
||||||
|
"GUID:363c5eb08ff8e6a439b85e37b8c20d96"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6ca799cc4b05645b1842927808f15538
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using SepCore.AsyncTask;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.Definition;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
namespace SepCore.UI
|
namespace SepCore.UI
|
||||||
{
|
{
|
||||||
@@ -15,26 +20,80 @@ namespace SepCore.UI
|
|||||||
[SerializeField] private TextMeshProUGUI hpText;
|
[SerializeField] private TextMeshProUGUI hpText;
|
||||||
[SerializeField] private TextMeshProUGUI mpText;
|
[SerializeField] private TextMeshProUGUI mpText;
|
||||||
|
|
||||||
public GameObject ActiveMarker => activeMarker;
|
private int _iconVersion;
|
||||||
public Image Icon => icon;
|
private int _currentUnitId;
|
||||||
public Image HpFill => hpFill;
|
|
||||||
public Image MpFill => mpFill;
|
|
||||||
public TextMeshProUGUI CharacterName => characterName;
|
|
||||||
public TextMeshProUGUI HpText => hpText;
|
|
||||||
public TextMeshProUGUI MpText => mpText;
|
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
/// <summary>
|
||||||
public void ConfigureEditor(GameObject marker, Image iconImage, Image hpFillImage, Image mpFillImage,
|
/// 用战斗单位视图填充我方卡片:名称、HP/MP 数值与血条、当前行动者标记和配置图标。
|
||||||
TextMeshProUGUI nameLabel, TextMeshProUGUI hpLabel, TextMeshProUGUI mpLabel)
|
/// 同一单位复用时不重复加载图标。
|
||||||
|
/// </summary>
|
||||||
|
public void SetUnit(BattleUnitView unit, bool isCurrentActor)
|
||||||
{
|
{
|
||||||
activeMarker = marker;
|
if (unit == null)
|
||||||
icon = iconImage;
|
{
|
||||||
hpFill = hpFillImage;
|
return;
|
||||||
mpFill = mpFillImage;
|
}
|
||||||
characterName = nameLabel;
|
|
||||||
hpText = hpLabel;
|
if (characterName != null)
|
||||||
mpText = mpLabel;
|
{
|
||||||
|
characterName.text = BattleUnitViewHelper.GetDisplayName(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hpText != null)
|
||||||
|
{
|
||||||
|
hpText.text = unit.CurrentHp + " / " + unit.MaxHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpText != null)
|
||||||
|
{
|
||||||
|
mpText.text = unit.CurrentMp + " / " + unit.MaxMp;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetBar(hpFill, unit.CurrentHp, unit.MaxHp);
|
||||||
|
SetBar(mpFill, unit.CurrentMp, unit.MaxMp);
|
||||||
|
|
||||||
|
if (activeMarker != null)
|
||||||
|
{
|
||||||
|
activeMarker.SetActive(isCurrentActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentUnitId != unit.BattleUnitId)
|
||||||
|
{
|
||||||
|
_currentUnitId = unit.BattleUnitId;
|
||||||
|
_iconVersion++;
|
||||||
|
ShowIconAsync(BattleUnitViewHelper.GetPlayerIconConfig(unit.ConfigId), _iconVersion).Forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步加载角色图标;iconVersion 用于防止复用格子时旧加载结果覆盖新内容。
|
||||||
|
/// </summary>
|
||||||
|
private async UniTaskVoid ShowIconAsync(SpriteConfig iconConfig, int iconVersion)
|
||||||
|
{
|
||||||
|
if (iconConfig == null || icon == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite sprite = await SpriteLoader.LoadSpriteAsync(iconConfig);
|
||||||
|
if (sprite == null || _iconVersion != iconVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.sprite = sprite;
|
||||||
|
icon.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBar(Image fill, int current, int max)
|
||||||
|
{
|
||||||
|
if (fill == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ratio = max > 0 ? Mathf.Clamp01((float)current / max) : 0f;
|
||||||
|
fill.rectTransform.anchorMax = new Vector2(ratio, 1f);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using SepCore.AsyncTask;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.Definition;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
namespace SepCore.UI
|
namespace SepCore.UI
|
||||||
{
|
{
|
||||||
@@ -14,24 +19,79 @@ namespace SepCore.UI
|
|||||||
[SerializeField] private TextMeshProUGUI enemyName;
|
[SerializeField] private TextMeshProUGUI enemyName;
|
||||||
[SerializeField] private TextMeshProUGUI hpText;
|
[SerializeField] private TextMeshProUGUI hpText;
|
||||||
|
|
||||||
public Button TargetButton => targetButton;
|
private int _iconVersion;
|
||||||
public GameObject SelectedMarker => selectedMarker;
|
private int _currentUnitId;
|
||||||
public Image Icon => icon;
|
|
||||||
public Image HpFill => hpFill;
|
|
||||||
public TextMeshProUGUI EnemyName => enemyName;
|
|
||||||
public TextMeshProUGUI HpText => hpText;
|
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
/// <summary>
|
||||||
public void ConfigureEditor(Button button, GameObject marker, Image iconImage, Image hpFillImage,
|
/// 用战斗单位视图填充敌人槽:名称、HP 数值与血条、配置图标和当前行动者标记。
|
||||||
TextMeshProUGUI nameLabel, TextMeshProUGUI hpLabel)
|
/// 阵亡或已逃跑目标不可选;同一单位复用时不重复加载图标。
|
||||||
|
/// </summary>
|
||||||
|
public void SetEnemy(BattleUnitView unit, bool isCurrentActor)
|
||||||
{
|
{
|
||||||
targetButton = button;
|
if (unit == null)
|
||||||
selectedMarker = marker;
|
{
|
||||||
icon = iconImage;
|
return;
|
||||||
hpFill = hpFillImage;
|
}
|
||||||
enemyName = nameLabel;
|
|
||||||
hpText = hpLabel;
|
if (enemyName != null)
|
||||||
|
{
|
||||||
|
enemyName.text = BattleUnitViewHelper.GetDisplayName(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hpText != null)
|
||||||
|
{
|
||||||
|
hpText.text = unit.CurrentHp + " / " + unit.MaxHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetBar(hpFill, unit.CurrentHp, unit.MaxHp);
|
||||||
|
|
||||||
|
if (selectedMarker != null)
|
||||||
|
{
|
||||||
|
selectedMarker.SetActive(isCurrentActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetButton != null)
|
||||||
|
{
|
||||||
|
targetButton.interactable = !unit.IsDefeated && !unit.IsEscaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentUnitId != unit.BattleUnitId)
|
||||||
|
{
|
||||||
|
_currentUnitId = unit.BattleUnitId;
|
||||||
|
_iconVersion++;
|
||||||
|
ShowIconAsync(BattleUnitViewHelper.GetEnemyIconConfig(unit.ConfigId), _iconVersion).Forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步加载敌人图标;iconVersion 用于防止复用格子时旧加载结果覆盖新内容。
|
||||||
|
/// </summary>
|
||||||
|
private async UniTaskVoid ShowIconAsync(SpriteConfig iconConfig, int iconVersion)
|
||||||
|
{
|
||||||
|
if (iconConfig == null || icon == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite sprite = await SpriteLoader.LoadSpriteAsync(iconConfig);
|
||||||
|
if (sprite == null || _iconVersion != iconVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.sprite = sprite;
|
||||||
|
icon.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBar(Image fill, int current, int max)
|
||||||
|
{
|
||||||
|
if (fill == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ratio = max > 0 ? Mathf.Clamp01((float)current / max) : 0f;
|
||||||
|
fill.rectTransform.anchorMax = new Vector2(ratio, 1f);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,65 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.Definition;
|
||||||
|
using UnityEngine;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
namespace SepCore.UI
|
namespace SepCore.UI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 战斗界面逻辑(手写 partial,与自动生成的 BattleForm.cs 合并)。
|
/// 战斗界面逻辑(手写 partial,与自动生成的 BattleForm.cs 合并)。
|
||||||
/// 首版为半透明壳层:只负责展示快照、收集指令与占位反馈;
|
/// UI 只读取 BattleViewState / BattleStep / BattleResult,不持有 BattleRuntime 或 BattleUnit;
|
||||||
/// 不修改战斗数据、不读取存档、不决定掉落,也不承担暂停地图计时等流程职责。
|
/// 玩家指令经 GameEntry.TurnBattle.SubmitCommand 提交,推进结果统一回填界面。
|
||||||
/// 暂停地图更新由单局流程(RunBattleCoordinator / 探索层)负责。
|
/// 我方卡片按玩家单位数显示/隐藏;敌人槽和回合顺序槽按当前战局从模板动态实例化。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class BattleForm : UGuiForm
|
public partial class BattleForm : UGuiForm
|
||||||
{
|
{
|
||||||
|
private BattleResult _result;
|
||||||
|
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)
|
protected override void OnInit(object userData)
|
||||||
{
|
{
|
||||||
base.OnInit(userData);
|
base.OnInit(userData);
|
||||||
|
|
||||||
View.attackButton.onClick.AddListener(OnAttackButtonClick);
|
// M1 只开放攻击;道具、技能、逃跑在后续里程碑接入
|
||||||
View.skillButton.onClick.AddListener(OnSkillButtonClick);
|
|
||||||
View.itemButton.onClick.AddListener(OnItemButtonClick);
|
|
||||||
View.escapeButton.onClick.AddListener(OnEscapeButtonClick);
|
|
||||||
|
|
||||||
// 道具行动保留但首版禁用,不产生 BattleCommand
|
|
||||||
View.itemButton.interactable = false;
|
View.itemButton.interactable = false;
|
||||||
|
View.skillButton.interactable = false;
|
||||||
|
View.escapeButton.interactable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnOpen(object userData)
|
protected override void OnOpen(object userData)
|
||||||
{
|
{
|
||||||
base.OnOpen(userData);
|
base.OnOpen(userData);
|
||||||
|
|
||||||
View.currentActorText.text = string.Empty;
|
// UIForm 实例复用:每次打开都重新注册按钮监听(OnClose 会移除)
|
||||||
Log.Info("BattleForm opened as empty shell.");
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
_result = null;
|
||||||
|
_displayedRound = 0;
|
||||||
|
_turnSlots.Clear();
|
||||||
|
_turnSlotUnitIds.Clear();
|
||||||
|
_enemySlots.Clear();
|
||||||
|
Refresh(GameEntry.TurnBattle != null ? GameEntry.TurnBattle.GetViewState() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnClose(bool isShutdown, object userData)
|
protected override void OnClose(bool isShutdown, object userData)
|
||||||
{
|
{
|
||||||
|
if (GameEntry.TurnBattle != null)
|
||||||
|
{
|
||||||
|
GameEntry.TurnBattle.SetStepListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
View.attackButton.onClick.RemoveListener(OnAttackButtonClick);
|
View.attackButton.onClick.RemoveListener(OnAttackButtonClick);
|
||||||
View.skillButton.onClick.RemoveListener(OnSkillButtonClick);
|
View.skillButton.onClick.RemoveListener(OnSkillButtonClick);
|
||||||
View.itemButton.onClick.RemoveListener(OnItemButtonClick);
|
View.itemButton.onClick.RemoveListener(OnItemButtonClick);
|
||||||
@@ -43,22 +70,312 @@ namespace SepCore.UI
|
|||||||
|
|
||||||
private void OnAttackButtonClick()
|
private void OnAttackButtonClick()
|
||||||
{
|
{
|
||||||
Log.Info("BattleForm placeholder: attack clicked.");
|
if (GameEntry.TurnBattle == null || !GameEntry.TurnBattle.IsBattleActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BattleViewState view = GameEntry.TurnBattle.GetViewState();
|
||||||
|
if (view == null || view.CurrentActorUnitId == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BattleUnitView actor = FindUnit(view, view.CurrentActorUnitId);
|
||||||
|
if (actor == null || actor.Faction != BattleFaction.Player)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int attackActionId = FindAttackActionId(view);
|
||||||
|
int targetUnitId = FindFirstActiveEnemy(view);
|
||||||
|
if (attackActionId == 0 || targetUnitId == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BattleStep step = GameEntry.TurnBattle.SubmitCommand(new BattleCommand(
|
||||||
|
actor.BattleUnitId, BattleActionType.Attack, attackActionId,
|
||||||
|
new List<int> { targetUnitId }));
|
||||||
|
ApplyStep(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyStep(BattleStep step)
|
||||||
|
{
|
||||||
|
if (step == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.Result != null)
|
||||||
|
{
|
||||||
|
_result = step.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Refresh(step.View);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Refresh(BattleViewState view)
|
||||||
|
{
|
||||||
|
if (view == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshPlayerCards(view);
|
||||||
|
RefreshEnemySlots(view);
|
||||||
|
RefreshTurnSlots(view);
|
||||||
|
RefreshActionPanel(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshPlayerCards(BattleViewState view)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
foreach (BattleUnitView unit in view.Units)
|
||||||
|
{
|
||||||
|
if (unit.Faction != BattleFaction.Player)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BattleActorCardItem card = GetPlayerCard(index);
|
||||||
|
if (card != null)
|
||||||
|
{
|
||||||
|
card.gameObject.SetActive(true);
|
||||||
|
card.SetUnit(unit, unit.BattleUnitId == view.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = index; i < 4; i++)
|
||||||
|
{
|
||||||
|
BattleActorCardItem card = GetPlayerCard(i);
|
||||||
|
if (card != null)
|
||||||
|
{
|
||||||
|
card.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshEnemySlots(BattleViewState view)
|
||||||
|
{
|
||||||
|
BattleEnemySlotItem template = View.battleEnemySlotTemplate;
|
||||||
|
if (template == null || View.enemySlotsRoot == null)
|
||||||
|
{
|
||||||
|
Log.Warning("BattleForm enemy slots are not configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BattleUnitView> enemies = new List<BattleUnitView>();
|
||||||
|
foreach (BattleUnitView unit in view.Units)
|
||||||
|
{
|
||||||
|
if (unit.Faction == BattleFaction.Enemy)
|
||||||
|
{
|
||||||
|
enemies.Add(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 敌人数量在一场战斗内固定:数量变化(新开战斗/规模不同)时才重建,之后原地复用
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshTurnSlots(BattleViewState view)
|
||||||
|
{
|
||||||
|
BattleTurnSlotItem template = View.battleTurnSlotTemplate;
|
||||||
|
if (template == null || View.turnSlotsRoot == null)
|
||||||
|
{
|
||||||
|
Log.Warning("BattleForm turn slots are not configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.RoundNumber != _displayedRound ||
|
||||||
|
(view.CurrentActorUnitId != 0 && !_turnSlotUnitIds.Contains(view.CurrentActorUnitId)))
|
||||||
|
{
|
||||||
|
RebuildTurnSlots(view, template);
|
||||||
|
_displayedRound = view.RoundNumber;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同轮内只移动当前行动者高亮,不隐藏已行动单位
|
||||||
|
for (int i = 0; i < _turnSlots.Count; i++)
|
||||||
|
{
|
||||||
|
_turnSlots[i].SetCurrentActor(_turnSlotUnitIds[i] == view.CurrentActorUnitId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新一轮开始时刷新本轮顺序:从当前行动者开始的剩余单位按调度优先级排列,
|
||||||
|
/// 当前行动者高亮;本轮内已行动单位保持可见。
|
||||||
|
/// 数量与上一轮相同时复用槽对象,只重建单位映射与内容;
|
||||||
|
/// 先制第一轮只有玩家候选,敌人进入行动阶段时(当前行动者不在列表)自动重建为剩余敌人。
|
||||||
|
/// </summary>
|
||||||
|
private void RebuildTurnSlots(BattleViewState view, BattleTurnSlotItem template)
|
||||||
|
{
|
||||||
|
List<BattleUnitView> units = new List<BattleUnitView>();
|
||||||
|
foreach (int unitId in view.RemainingTurnOrder)
|
||||||
|
{
|
||||||
|
BattleUnitView unit = FindUnit(view, unitId);
|
||||||
|
if (unit != null)
|
||||||
|
{
|
||||||
|
units.Add(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshActionPanel(BattleViewState view)
|
||||||
|
{
|
||||||
|
if (_result != null)
|
||||||
|
{
|
||||||
|
View.currentActorText.text = GetOutcomeText(_result.Outcome);
|
||||||
|
View.attackButton.interactable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BattleUnitView actor = FindUnit(view, view.CurrentActorUnitId);
|
||||||
|
bool playerTurn = actor != null && actor.Faction == BattleFaction.Player;
|
||||||
|
View.currentActorText.text = string.Format("第 {0} 轮 {1} 行动",
|
||||||
|
view.RoundNumber, actor != null ? BattleUnitViewHelper.GetDisplayName(actor) : string.Empty);
|
||||||
|
View.attackButton.interactable = playerTurn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetOutcomeText(BattleOutcome outcome)
|
||||||
|
{
|
||||||
|
switch (outcome)
|
||||||
|
{
|
||||||
|
case BattleOutcome.Victory:
|
||||||
|
return "胜利!";
|
||||||
|
case BattleOutcome.AllEscaped:
|
||||||
|
return "全员逃跑";
|
||||||
|
case BattleOutcome.PartialEscapeDefeat:
|
||||||
|
return "部分逃跑,战斗失败";
|
||||||
|
case BattleOutcome.TotalDefeat:
|
||||||
|
return "全员阵亡,单局失败";
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindAttackActionId(BattleViewState view)
|
||||||
|
{
|
||||||
|
foreach (int actionId in view.AvailableActionIds)
|
||||||
|
{
|
||||||
|
BattleActionConfig action = GameEntry.Luban.Get<BattleActionConfig>(actionId);
|
||||||
|
if (action != null && action.ActionType == BattleActionType.Attack)
|
||||||
|
{
|
||||||
|
return actionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindFirstActiveEnemy(BattleViewState view)
|
||||||
|
{
|
||||||
|
foreach (BattleUnitView unit in view.Units)
|
||||||
|
{
|
||||||
|
if (unit.Faction == BattleFaction.Enemy && !unit.IsDefeated && !unit.IsEscaped)
|
||||||
|
{
|
||||||
|
return unit.BattleUnitId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BattleUnitView FindUnit(BattleViewState view, int unitId)
|
||||||
|
{
|
||||||
|
foreach (BattleUnitView unit in view.Units)
|
||||||
|
{
|
||||||
|
if (unit.BattleUnitId == unitId)
|
||||||
|
{
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BattleActorCardItem GetPlayerCard(int index)
|
||||||
|
{
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return View.playerCard1;
|
||||||
|
case 1:
|
||||||
|
return View.playerCard2;
|
||||||
|
case 2:
|
||||||
|
return View.playerCard3;
|
||||||
|
case 3:
|
||||||
|
return View.playerCard4;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearSlots(RectTransform root, Transform template)
|
||||||
|
{
|
||||||
|
for (int i = root.childCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Transform child = root.GetChild(i);
|
||||||
|
if (child == template)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Destroy(child.gameObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSkillButtonClick()
|
private void OnSkillButtonClick()
|
||||||
{
|
{
|
||||||
Log.Info("BattleForm placeholder: skill clicked.");
|
// M3 接入
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnItemButtonClick()
|
private void OnItemButtonClick()
|
||||||
{
|
{
|
||||||
Log.Info("BattleForm placeholder: item clicked.");
|
// 道具首版禁用,不产生指令
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEscapeButtonClick()
|
private void OnEscapeButtonClick()
|
||||||
{
|
{
|
||||||
Log.Info("BattleForm placeholder: escape clicked.");
|
// M5 接入
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using SepCore.AsyncTask;
|
||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.Definition;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
@@ -11,17 +15,68 @@ namespace SepCore.UI
|
|||||||
[SerializeField] private Image icon;
|
[SerializeField] private Image icon;
|
||||||
[SerializeField] private TextMeshProUGUI label;
|
[SerializeField] private TextMeshProUGUI label;
|
||||||
|
|
||||||
public GameObject ActiveMarker => activeMarker;
|
private int _iconVersion;
|
||||||
public Image Icon => icon;
|
private int _currentUnitId;
|
||||||
public TextMeshProUGUI Label => label;
|
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
/// <summary>
|
||||||
public void ConfigureEditor(GameObject marker, Image iconImage, TextMeshProUGUI slotLabel)
|
/// 填充一个回合顺序槽:单位显示名、配置图标与当前行动者标记。
|
||||||
|
/// 槽被复用到同一单位时不重复加载图标。
|
||||||
|
/// </summary>
|
||||||
|
public void SetTurnSlot(BattleUnitView unit, bool isCurrentActor)
|
||||||
{
|
{
|
||||||
activeMarker = marker;
|
if (unit == null)
|
||||||
icon = iconImage;
|
{
|
||||||
label = slotLabel;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label != null)
|
||||||
|
{
|
||||||
|
label.text = BattleUnitViewHelper.GetDisplayName(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCurrentActor(isCurrentActor);
|
||||||
|
|
||||||
|
if (_currentUnitId != unit.BattleUnitId)
|
||||||
|
{
|
||||||
|
_currentUnitId = unit.BattleUnitId;
|
||||||
|
_iconVersion++;
|
||||||
|
LoadIconAsync(unit, _iconVersion).Forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 只更新当前行动者标记,不改变显示名。
|
||||||
|
/// 同轮内单位行动后由 Logic 逐槽移动高亮。
|
||||||
|
/// </summary>
|
||||||
|
public void SetCurrentActor(bool isCurrentActor)
|
||||||
|
{
|
||||||
|
if (activeMarker != null)
|
||||||
|
{
|
||||||
|
activeMarker.SetActive(isCurrentActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步加载单位图标(玩家/敌人分别走各自配置);iconVersion 防止旧加载覆盖新内容。
|
||||||
|
/// </summary>
|
||||||
|
private async UniTaskVoid LoadIconAsync(BattleUnitView unit, int iconVersion)
|
||||||
|
{
|
||||||
|
SpriteConfig iconConfig = unit.Faction == BattleFaction.Player
|
||||||
|
? BattleUnitViewHelper.GetPlayerIconConfig(unit.ConfigId)
|
||||||
|
: BattleUnitViewHelper.GetEnemyIconConfig(unit.ConfigId);
|
||||||
|
if (iconConfig == null || icon == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite sprite = await SpriteLoader.LoadSpriteAsync(iconConfig);
|
||||||
|
if (sprite == null || _iconVersion != iconVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.sprite = sprite;
|
||||||
|
icon.gameObject.SetActive(true);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using SepCore.Battle;
|
||||||
|
using SepCore.Definition;
|
||||||
|
|
||||||
|
namespace SepCore.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 战斗单位视图的显示辅助:名称与图标解析。
|
||||||
|
/// 玩家图标来自 CharacterConfig.Icon_Ref,敌人图标来自 EnemyConfig.Icon_Ref。
|
||||||
|
/// </summary>
|
||||||
|
public static class BattleUnitViewHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取单位显示名;配置缺失时回退为配置 ID。
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDisplayName(BattleUnitView unit)
|
||||||
|
{
|
||||||
|
if (unit == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit.Faction == BattleFaction.Player)
|
||||||
|
{
|
||||||
|
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(unit.ConfigId);
|
||||||
|
return config != null ? config.Name : unit.ConfigId.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EnemyConfig config = GameEntry.Luban.Get<EnemyConfig>(unit.ConfigId);
|
||||||
|
return config != null ? config.Name : unit.ConfigId.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取玩家单位图标配置;配置缺失时返回 null。
|
||||||
|
/// </summary>
|
||||||
|
public static SpriteConfig GetPlayerIconConfig(int characterId)
|
||||||
|
{
|
||||||
|
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(characterId);
|
||||||
|
return config != null ? config.Icon_Ref : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取敌人单位图标配置;配置缺失时返回 null。
|
||||||
|
/// </summary>
|
||||||
|
public static SpriteConfig GetEnemyIconConfig(int enemyId)
|
||||||
|
{
|
||||||
|
EnemyConfig config = GameEntry.Luban.Get<EnemyConfig>(enemyId);
|
||||||
|
return config != null ? config.Icon_Ref : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: baad0253f7a15458f818a57c66db4939
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -12,7 +12,7 @@ namespace SepCore.UI
|
|||||||
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard2;
|
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard2;
|
||||||
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard3;
|
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard3;
|
||||||
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard4;
|
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard4;
|
||||||
[SerializeField] public global::UnityEngine.UI.HorizontalLayoutGroup enemySlotsRoot;
|
[SerializeField] public global::UnityEngine.RectTransform enemySlotsRoot;
|
||||||
[SerializeField] public global::SepCore.UI.BattleEnemySlotItem battleEnemySlotTemplate;
|
[SerializeField] public global::SepCore.UI.BattleEnemySlotItem battleEnemySlotTemplate;
|
||||||
[SerializeField] public global::TMPro.TextMeshProUGUI currentActorText;
|
[SerializeField] public global::TMPro.TextMeshProUGUI currentActorText;
|
||||||
[SerializeField] public global::UnityEngine.UI.Button attackButton;
|
[SerializeField] public global::UnityEngine.UI.Button attackButton;
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ MonoBehaviour:
|
|||||||
playerCard2: {fileID: 17428003479946422}
|
playerCard2: {fileID: 17428003479946422}
|
||||||
playerCard3: {fileID: 7735077048895435792}
|
playerCard3: {fileID: 7735077048895435792}
|
||||||
playerCard4: {fileID: 20465036694219600}
|
playerCard4: {fileID: 20465036694219600}
|
||||||
enemySlotsRoot: {fileID: 2896567169176617828}
|
enemySlotsRoot: {fileID: 2225454379744901781}
|
||||||
battleEnemySlotTemplate: {fileID: 9135779921912607044}
|
battleEnemySlotTemplate: {fileID: 9135779921912607044}
|
||||||
currentActorText: {fileID: 3734726615128117434}
|
currentActorText: {fileID: 3734726615128117434}
|
||||||
attackButton: {fileID: 4646156403976603773}
|
attackButton: {fileID: 4646156403976603773}
|
||||||
@@ -1900,11 +1900,11 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
componentReferences:
|
componentReferences:
|
||||||
- component: {fileID: 2225454379744901781}
|
- component: {fileID: 2225454379744901781}
|
||||||
generateReference: 0
|
|
||||||
variableName: enemyRootRectTransform
|
|
||||||
- component: {fileID: 2896567169176617828}
|
|
||||||
generateReference: 1
|
generateReference: 1
|
||||||
variableName: enemySlotsRoot
|
variableName: enemySlotsRoot
|
||||||
|
- component: {fileID: 2896567169176617828}
|
||||||
|
generateReference: 0
|
||||||
|
variableName: enemySlotsRoot
|
||||||
--- !u!1 &2613939670594043691
|
--- !u!1 &2613939670594043691
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user