Files
Seek-Battle-Evacuate/Assets/GameMain/Scripts/UI/Battle/BattleForm.Logic.cs
T
huangjun 7dcac0ac87 M1 回合制战斗架构收敛与 1v1 攻击闭环:BattleRuntime 内核、组件唯一入口、UI 数据驱动与 EditMode 测试
- 删除中间契约(BattleStartRequest/BattlePlayerInput/BattleReturnPlan/桥接与策略接口/FlowState),收敛为 BattleViewState/BattleStep/一层 BattleEvent
- 新增内部 BattleRuntime/BattleUnit/BattleStatus 共享上下文;TurnBattleComponent 成为唯一外部入口,吸收启动/暂停/回写职责
- 内核支持 1v1 攻击闭环:效果公式、HP/MP 钳制、阵亡、Victory/TotalDefeat、敌人随机决策;敌人行动间歇 0.6s 由组件协程编排并经 StepListener 推送
- UI:玩家卡片/敌人槽/回合槽按战局重建,图标配置驱动(含新增 EnemyConfig.Icon),每轮刷新一次 TurnSlots 且轮内只移动高亮
- 新增 Tests 程序集 + InternalsVisibleTo,25 个 BattleRuntime 纯逻辑用例全部通过
2026-09-02 08:42:04 +08:00

351 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using SepCore.Battle;
using SepCore.Definition;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace SepCore.UI
{
/// <summary>
/// 战斗界面逻辑(手写 partial,与自动生成的 BattleForm.cs 合并)。
/// UI 只读取 BattleViewState / BattleStep / BattleResult,不持有 BattleRuntime 或 BattleUnit;
/// 玩家指令经 GameEntry.TurnBattle.SubmitCommand 提交,推进结果统一回填界面。
/// 我方卡片按玩家单位数显示/隐藏;敌人槽和回合顺序槽按当前战局从模板动态实例化。
/// </summary>
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>();
protected override void OnInit(object userData)
{
base.OnInit(userData);
View.attackButton.onClick.AddListener(OnAttackButtonClick);
View.skillButton.onClick.AddListener(OnSkillButtonClick);
View.itemButton.onClick.AddListener(OnItemButtonClick);
View.escapeButton.onClick.AddListener(OnEscapeButtonClick);
// M1 只开放攻击;道具、技能、逃跑在后续里程碑接入
View.itemButton.interactable = false;
View.skillButton.interactable = false;
View.escapeButton.interactable = false;
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
if (GameEntry.TurnBattle != null)
{
GameEntry.TurnBattle.SetStepListener(ApplyStep);
}
_result = null;
_displayedRound = 0;
_turnSlots.Clear();
_turnSlotUnitIds.Clear();
Refresh(GameEntry.TurnBattle != null ? GameEntry.TurnBattle.GetViewState() : null);
}
protected override void OnClose(bool isShutdown, object userData)
{
if (GameEntry.TurnBattle != null)
{
GameEntry.TurnBattle.SetStepListener(null);
}
View.attackButton.onClick.RemoveListener(OnAttackButtonClick);
View.skillButton.onClick.RemoveListener(OnSkillButtonClick);
View.itemButton.onClick.RemoveListener(OnItemButtonClick);
View.escapeButton.onClick.RemoveListener(OnEscapeButtonClick);
base.OnClose(isShutdown, userData);
}
private void OnAttackButtonClick()
{
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;
}
template.gameObject.SetActive(false);
ClearSlots(View.enemySlotsRoot, template.transform);
foreach (BattleUnitView unit in view.Units)
{
if (unit.Faction != BattleFaction.Enemy)
{
continue;
}
BattleEnemySlotItem slot = Instantiate(template, View.enemySlotsRoot);
slot.gameObject.SetActive(true);
slot.SetEnemy(unit, unit.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)
{
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)
{
template.gameObject.SetActive(false);
ClearSlots(View.turnSlotsRoot, template.transform);
_turnSlots.Clear();
_turnSlotUnitIds.Clear();
foreach (BattleUnitView unit in view.Units)
{
if (unit.IsDefeated || unit.IsEscaped)
{
continue;
}
BattleTurnSlotItem slot = Instantiate(template, View.turnSlotsRoot);
slot.gameObject.SetActive(true);
slot.SetTurnSlot(unit, unit.BattleUnitId == view.CurrentActorUnitId);
_turnSlots.Add(slot);
_turnSlotUnitIds.Add(unit.BattleUnitId);
}
}
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()
{
// M3 接入
}
private void OnItemButtonClick()
{
// 道具首版禁用,不产生指令
}
private void OnEscapeButtonClick()
{
// M5 接入
}
}
}