M1 回合制战斗架构收敛与 1v1 攻击闭环:BattleRuntime 内核、组件唯一入口、UI 数据驱动与 EditMode 测试
- 删除中间契约(BattleStartRequest/BattlePlayerInput/BattleReturnPlan/桥接与策略接口/FlowState),收敛为 BattleViewState/BattleStep/一层 BattleEvent - 新增内部 BattleRuntime/BattleUnit/BattleStatus 共享上下文;TurnBattleComponent 成为唯一外部入口,吸收启动/暂停/回写职责 - 内核支持 1v1 攻击闭环:效果公式、HP/MP 钳制、阵亡、Victory/TotalDefeat、敌人随机决策;敌人行动间歇 0.6s 由组件协程编排并经 StepListener 推送 - UI:玩家卡片/敌人槽/回合槽按战局重建,图标配置驱动(含新增 EnemyConfig.Icon),每轮刷新一次 TurnSlots 且轮内只移动高亮 - 新增 Tests 程序集 + InternalsVisibleTo,25 个 BattleRuntime 纯逻辑用例全部通过
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using SepCore.AsyncTask;
|
||||
using SepCore.Battle;
|
||||
using SepCore.Definition;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace SepCore.UI
|
||||
{
|
||||
@@ -15,26 +20,74 @@ namespace SepCore.UI
|
||||
[SerializeField] private TextMeshProUGUI hpText;
|
||||
[SerializeField] private TextMeshProUGUI mpText;
|
||||
|
||||
public GameObject ActiveMarker => activeMarker;
|
||||
public Image Icon => icon;
|
||||
public Image HpFill => hpFill;
|
||||
public Image MpFill => mpFill;
|
||||
public TextMeshProUGUI CharacterName => characterName;
|
||||
public TextMeshProUGUI HpText => hpText;
|
||||
public TextMeshProUGUI MpText => mpText;
|
||||
private int _iconVersion;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void ConfigureEditor(GameObject marker, Image iconImage, Image hpFillImage, Image mpFillImage,
|
||||
TextMeshProUGUI nameLabel, TextMeshProUGUI hpLabel, TextMeshProUGUI mpLabel)
|
||||
/// <summary>
|
||||
/// 用战斗单位视图填充我方卡片:名称、HP/MP 数值与血条、当前行动者标记和配置图标。
|
||||
/// </summary>
|
||||
public void SetUnit(BattleUnitView unit, bool isCurrentActor)
|
||||
{
|
||||
activeMarker = marker;
|
||||
icon = iconImage;
|
||||
hpFill = hpFillImage;
|
||||
mpFill = mpFillImage;
|
||||
characterName = nameLabel;
|
||||
hpText = hpLabel;
|
||||
mpText = mpLabel;
|
||||
if (unit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (characterName != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
_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 UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace SepCore.UI
|
||||
{
|
||||
@@ -14,24 +19,74 @@ namespace SepCore.UI
|
||||
[SerializeField] private TextMeshProUGUI enemyName;
|
||||
[SerializeField] private TextMeshProUGUI hpText;
|
||||
|
||||
public Button TargetButton => targetButton;
|
||||
public GameObject SelectedMarker => selectedMarker;
|
||||
public Image Icon => icon;
|
||||
public Image HpFill => hpFill;
|
||||
public TextMeshProUGUI EnemyName => enemyName;
|
||||
public TextMeshProUGUI HpText => hpText;
|
||||
private int _iconVersion;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void ConfigureEditor(Button button, GameObject marker, Image iconImage, Image hpFillImage,
|
||||
TextMeshProUGUI nameLabel, TextMeshProUGUI hpLabel)
|
||||
/// <summary>
|
||||
/// 用战斗单位视图填充敌人槽:名称、HP 数值与血条、配置图标和当前行动者标记。
|
||||
/// 阵亡或已逃跑目标不可选。
|
||||
/// </summary>
|
||||
public void SetEnemy(BattleUnitView unit, bool isCurrentActor)
|
||||
{
|
||||
targetButton = button;
|
||||
selectedMarker = marker;
|
||||
icon = iconImage;
|
||||
hpFill = hpFillImage;
|
||||
enemyName = nameLabel;
|
||||
hpText = hpLabel;
|
||||
if (unit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_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,15 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.Battle;
|
||||
using SepCore.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace SepCore.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 战斗界面逻辑(手写 partial,与自动生成的 BattleForm.cs 合并)。
|
||||
/// 首版为半透明壳层:只负责展示快照、收集指令与占位反馈;
|
||||
/// 不修改战斗数据、不读取存档、不决定掉落,也不承担暂停地图计时等流程职责。
|
||||
/// 暂停地图更新由单局流程(RunBattleCoordinator / 探索层)负责。
|
||||
/// 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);
|
||||
@@ -19,20 +28,35 @@ namespace SepCore.UI
|
||||
View.itemButton.onClick.AddListener(OnItemButtonClick);
|
||||
View.escapeButton.onClick.AddListener(OnEscapeButtonClick);
|
||||
|
||||
// 道具行动保留但首版禁用,不产生 BattleCommand
|
||||
// M1 只开放攻击;道具、技能、逃跑在后续里程碑接入
|
||||
View.itemButton.interactable = false;
|
||||
View.skillButton.interactable = false;
|
||||
View.escapeButton.interactable = false;
|
||||
}
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
|
||||
View.currentActorText.text = string.Empty;
|
||||
Log.Info("BattleForm opened as empty shell.");
|
||||
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);
|
||||
@@ -43,22 +67,285 @@ namespace SepCore.UI
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
Log.Info("BattleForm placeholder: skill clicked.");
|
||||
// M3 接入
|
||||
}
|
||||
|
||||
private void OnItemButtonClick()
|
||||
{
|
||||
Log.Info("BattleForm placeholder: item clicked.");
|
||||
// 道具首版禁用,不产生指令
|
||||
}
|
||||
|
||||
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 UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -11,17 +15,62 @@ namespace SepCore.UI
|
||||
[SerializeField] private Image icon;
|
||||
[SerializeField] private TextMeshProUGUI label;
|
||||
|
||||
public GameObject ActiveMarker => activeMarker;
|
||||
public Image Icon => icon;
|
||||
public TextMeshProUGUI Label => label;
|
||||
private int _iconVersion;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void ConfigureEditor(GameObject marker, Image iconImage, TextMeshProUGUI slotLabel)
|
||||
/// <summary>
|
||||
/// 填充一个回合顺序槽:单位显示名、配置图标与当前行动者标记。
|
||||
/// </summary>
|
||||
public void SetTurnSlot(BattleUnitView unit, bool isCurrentActor)
|
||||
{
|
||||
activeMarker = marker;
|
||||
icon = iconImage;
|
||||
label = slotLabel;
|
||||
if (unit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (label != null)
|
||||
{
|
||||
label.text = BattleUnitViewHelper.GetDisplayName(unit);
|
||||
}
|
||||
|
||||
SetCurrentActor(isCurrentActor);
|
||||
|
||||
_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 playerCard3;
|
||||
[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::TMPro.TextMeshProUGUI currentActorText;
|
||||
[SerializeField] public global::UnityEngine.UI.Button attackButton;
|
||||
|
||||
Reference in New Issue
Block a user