M0 回合制战斗壳层:接口与单局状态、调试入口、暂停联动

This commit is contained in:
huangjun
2026-09-01 10:20:02 +08:00
parent fce331da84
commit 8413c2688b
82 changed files with 12032 additions and 8 deletions
@@ -36,6 +36,10 @@ namespace SepCore.Definition
/// 关于
/// </summary>
AboutForm = 102,
/// <summary>
/// 战斗界面
/// </summary>
BattleForm = 103,
}
}
@@ -0,0 +1,592 @@
using System;
using System.Linq;
using SepCore.UI;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace SepCore.Editor
{
public static class BattleUIPrefabBuilder
{
private const string PrefabPath = "Assets/GameMain/UI/UIForms/BattleForm.prefab";
private const string CharacterAtlasPath = "Assets/GameMain/Textures/Characters.png";
private const string EnemyAtlasPath = "Assets/GameMain/Textures/Monsters.png";
private const string ItemAtlasPath = "Assets/GameMain/Textures/Items.png";
private const string PreviewCameraName = "__BattlePreviewCamera";
private const string PreviewRootName = "__BattlePreview";
private const string PreviousScenePathKey = "BattleUI.PreviousScenePath";
private static readonly Color Overlay = new Color32(8, 7, 12, 184);
private static readonly Color Charcoal = new Color32(25, 24, 30, 255);
private static readonly Color CharcoalSoft = new Color32(42, 39, 47, 255);
private static readonly Color Ivory = new Color32(244, 237, 214, 255);
private static readonly Color Ink = new Color32(43, 37, 35, 255);
private static readonly Color White = new Color32(248, 248, 246, 255);
private static readonly Color MutedWhite = new Color32(192, 190, 184, 255);
private static readonly Color Green = new Color32(62, 164, 94, 255);
private static readonly Color Blue = new Color32(62, 119, 190, 255);
private static readonly Color Red = new Color32(191, 69, 65, 255);
private static readonly Color Gold = new Color32(218, 169, 64, 255);
private static readonly Color EnemyFrame = new Color32(91, 39, 54, 235);
private static TMP_FontAsset font;
private static Sprite panelBeige;
private static Sprite panelBrown;
private static Sprite panelBlue;
private static Sprite buttonBlue;
private static Sprite buttonBluePressed;
private static Sprite buttonBrown;
private static Sprite buttonBrownPressed;
private static Sprite buttonBeige;
private static Sprite buttonBeigePressed;
private static Sprite buttonGrey;
private static Sprite buttonGreyPressed;
private static Sprite escapeIcon;
private static Sprite[] playerIcons;
private static Sprite[] enemyIcons;
private static Sprite attackIcon;
private static Sprite skillIcon;
private static Sprite itemIcon;
[MenuItem("Utility/UI/Build Battle UI")]
public static void Build()
{
LoadAssets();
Scene previousScene = SceneManager.GetActiveScene();
Scene temporaryScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
GameObject root = null;
try
{
SceneManager.SetActiveScene(temporaryScene);
root = BuildRoot();
UISerializationRoot serializationRoot = root.GetComponent<UISerializationRoot>();
serializationRoot.RefreshItems();
UIAssetsTools.ApplyGeneratedComponentsToRoot(serializationRoot);
bool success;
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath, out success);
if (!success)
{
throw new InvalidOperationException("Failed to save BattleForm prefab.");
}
}
finally
{
if (root != null)
{
UnityEngine.Object.DestroyImmediate(root);
}
if (previousScene.IsValid())
{
SceneManager.SetActiveScene(previousScene);
}
EditorSceneManager.CloseScene(temporaryScene, true);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Selection.activeObject = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
Debug.Log("Battle UI prefab rebuilt: " + PrefabPath);
}
[MenuItem("Utility/UI/Preview Battle UI")]
public static void OpenPreview()
{
ClosePreviewInternal();
Scene previousScene = SceneManager.GetActiveScene();
SessionState.SetString(PreviousScenePathKey, previousScene.path);
Scene previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
SceneManager.SetActiveScene(previewScene);
GameObject cameraObject = new GameObject(PreviewCameraName, typeof(Camera));
Camera previewCamera = cameraObject.GetComponent<Camera>();
previewCamera.clearFlags = CameraClearFlags.SolidColor;
previewCamera.backgroundColor = new Color(0.22f, 0.25f, 0.23f, 1f);
previewCamera.orthographic = true;
previewCamera.orthographicSize = 5f;
cameraObject.transform.position = new Vector3(0f, 0f, -10f);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
if (prefab == null)
{
throw new InvalidOperationException("Build BattleForm before opening its preview.");
}
GameObject previewRoot = (GameObject)PrefabUtility.InstantiatePrefab(prefab, previewScene);
previewRoot.name = PreviewRootName;
Canvas canvas = previewRoot.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = previewCamera;
canvas.planeDistance = 1f;
previewRoot.GetComponent<RectTransform>().localScale = Vector3.one;
Selection.activeGameObject = previewRoot;
Debug.Log("Battle UI preview opened in an unsaved scene.");
}
[MenuItem("Utility/UI/Close Battle UI Preview")]
public static void ClosePreview()
{
ClosePreviewInternal();
}
private static void ClosePreviewInternal()
{
Camera[] cameras = Resources.FindObjectsOfTypeAll<Camera>();
Camera previewCamera = cameras.FirstOrDefault(candidate => candidate != null &&
candidate.gameObject.name == PreviewCameraName && candidate.gameObject.scene.IsValid());
if (previewCamera == null)
{
return;
}
Scene previewScene = previewCamera.gameObject.scene;
string previousPath = SessionState.GetString(PreviousScenePathKey, string.Empty);
for (int i = 0; i < SceneManager.sceneCount; i++)
{
Scene loadedScene = SceneManager.GetSceneAt(i);
if (loadedScene.IsValid() && loadedScene.path == previousPath)
{
SceneManager.SetActiveScene(loadedScene);
break;
}
}
EditorSceneManager.CloseScene(previewScene, true);
SessionState.EraseString(PreviousScenePathKey);
}
private static GameObject BuildRoot()
{
GameObject root = new GameObject("BattleForm", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler),
typeof(GraphicRaycaster), typeof(CanvasGroup), typeof(UISerializationRoot));
root.layer = LayerMask.NameToLayer("UI");
Canvas canvas = root.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.pixelPerfect = true;
canvas.overrideSorting = true;
canvas.sortingOrder = 500;
CanvasScaler scaler = root.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
RectTransform rootRect = root.GetComponent<RectTransform>();
rootRect.anchorMin = Vector2.zero;
rootRect.anchorMax = Vector2.one;
rootRect.offsetMin = Vector2.zero;
rootRect.offsetMax = Vector2.zero;
AddImage(Stretch("BattleOverlay", rootRect, 0f, 0f, 0f, 0f), Overlay, null);
RectTransform safeFrame = Fixed("SafeFrame", rootRect, new Vector2(0.5f, 0.5f),
new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(1920f, 1080f));
BuildTurnOrder(safeFrame);
BuildPlayers(safeFrame);
BuildEnemies(safeFrame);
BuildActionPanel(safeFrame);
return root;
}
private static void BuildTurnOrder(RectTransform parent)
{
RectTransform panel = AddPanel(
Fixed("TurnOrderPanel", parent, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -24f), new Vector2(1016f, 112f)), panelBrown, Color.white);
AddImage(TopStretch("ActiveAccent", panel, 0f, 0f, 0f, 6f), Gold, null);
TextMeshProUGUI round = AddText(
Fixed("RoundText", panel, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f),
new Vector2(28f, 0f), new Vector2(156f, 74f)), "ROUND 01", 25, Ivory,
TextAlignmentOptions.MidlineLeft, FontStyles.Bold);
RegisterReference(round, "roundText");
Sprite[] icons =
{
playerIcons[3], enemyIcons[2], playerIcons[0], enemyIcons[0],
playerIcons[2], enemyIcons[1], playerIcons[1], enemyIcons[3]
};
string[] labels = { "P4", "E3", "P1", "E1", "P3", "E2", "P2", "E4" };
for (int i = 0; i < 8; i++)
{
RectTransform slotRect = Fixed("TurnSlot" + (i + 1), panel, new Vector2(0f, 0.5f),
new Vector2(0f, 0.5f), new Vector2(194f + i * 98f, 0f), new Vector2(82f, 82f));
Image background = AddImage(slotRect, i == 0 ? Ivory : CharcoalSoft, null);
background.raycastTarget = false;
RectTransform marker = Stretch("ActiveMarker", slotRect, -3f, -3f, -3f, -3f);
Image markerImage = AddImage(marker, Color.clear, null);
Outline markerOutline = marker.gameObject.AddComponent<Outline>();
markerOutline.effectColor = Gold;
markerOutline.effectDistance = new Vector2(3f, -3f);
markerOutline.useGraphicAlpha = false;
marker.gameObject.SetActive(i == 0);
Image icon = AddSprite(Fixed("Icon", slotRect, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -5f), new Vector2(54f, 54f)), icons[i], Color.white, true);
TextMeshProUGUI label = AddText(
Fixed("Label", slotRect, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 5f), new Vector2(68f, 22f)), labels[i], 15,
i == 0 ? Ink : MutedWhite, TextAlignmentOptions.Center, FontStyles.Bold);
BattleTurnSlotItem slot = slotRect.gameObject.AddComponent<BattleTurnSlotItem>();
slot.ConfigureEditor(marker.gameObject, icon, label);
RegisterReference(slot, "turnSlot" + (i + 1));
}
}
private static void BuildPlayers(RectTransform parent)
{
int[] hp = { 92, 150, 58, 100 };
int[] maxHp = { 120, 150, 90, 100 };
int[] mp = { 24, 10, 72, 50 };
int[] maxMp = { 40, 30, 80, 50 };
CreatePlayerCard(parent, 0, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(32f, -32f), false,
hp[0], maxHp[0], mp[0], maxMp[0], false);
CreatePlayerCard(parent, 1, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-32f, -32f), true,
hp[1], maxHp[1], mp[1], maxMp[1], false);
CreatePlayerCard(parent, 2, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(32f, 28f), false,
hp[2], maxHp[2], mp[2], maxMp[2], false);
CreatePlayerCard(parent, 3, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-32f, 28f), true,
hp[3], maxHp[3], mp[3], maxMp[3], true);
}
private static void CreatePlayerCard(RectTransform parent, int index, Vector2 anchor, Vector2 pivot,
Vector2 position, bool mirror, int hp, int maxHp, int mp, int maxMp, bool active)
{
RectTransform cardRect = AddPanel(Fixed("PlayerCard" + (index + 1), parent, anchor, pivot, position,
new Vector2(400f, 176f)), panelBeige, Color.white);
RectTransform marker = TopStretch("ActiveMarker", cardRect, 0f, 0f, 0f, 7f);
AddImage(marker, Gold, null);
marker.gameObject.SetActive(active);
float iconX = mirror ? 278f : 18f;
float contentX = mirror ? 18f : 140f;
RectTransform iconFrame = Fixed("IconFrame", cardRect, new Vector2(0f, 1f), new Vector2(0f, 1f),
new Vector2(iconX, -18f), new Vector2(104f, 140f));
AddImage(iconFrame, Charcoal, null);
Image icon = AddSprite(Fixed("Icon", iconFrame, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -8f), new Vector2(88f, 96f)), playerIcons[index], Color.white, true);
AddText(Fixed("PartyIndex", iconFrame, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 7f), new Vector2(84f, 24f)), "P" + (index + 1), 14, MutedWhite,
TextAlignmentOptions.Center, FontStyles.Bold);
TextMeshProUGUI name = AddText(
Fixed("CharacterName", cardRect, new Vector2(0f, 1f), new Vector2(0f, 1f),
new Vector2(contentX, -14f), new Vector2(242f, 38f)), "角色 " + (index + 1), 24, Ink,
mirror ? TextAlignmentOptions.TopRight : TextAlignmentOptions.TopLeft, FontStyles.Bold);
Image hpFill = CreateBar(cardRect, "HP", contentX, 64f, 242f, Green, hp, maxHp, out TextMeshProUGUI hpText);
Image mpFill = CreateBar(cardRect, "MP", contentX, 112f, 242f, Blue, mp, maxMp, out TextMeshProUGUI mpText);
BattleActorCardItem card = cardRect.gameObject.AddComponent<BattleActorCardItem>();
card.ConfigureEditor(marker.gameObject, icon, hpFill, mpFill, name, hpText, mpText);
RegisterReference(card, "playerCard" + (index + 1));
}
private static void BuildEnemies(RectTransform parent)
{
RectTransform enemyRoot = Fixed("EnemyRoot", parent, new Vector2(0.5f, 0.5f),
new Vector2(0.5f, 0.5f), new Vector2(0f, 32f), new Vector2(820f, 290f));
int[] hp = { 36, 80, 44, 61 };
int[] maxHp = { 50, 80, 60, 80 };
for (int i = 0; i < 4; i++)
{
RectTransform slotRect = Fixed("EnemySlot" + (i + 1), enemyRoot, new Vector2(0.5f, 0.5f),
new Vector2(0.5f, 0.5f), new Vector2(-300f + i * 200f, 0f), new Vector2(170f, 248f));
Image targetGraphic = AddImage(slotRect, new Color(EnemyFrame.r, EnemyFrame.g, EnemyFrame.b, 0.68f), null);
targetGraphic.raycastTarget = true;
Button targetButton = slotRect.gameObject.AddComponent<Button>();
targetButton.targetGraphic = targetGraphic;
targetButton.transition = Selectable.Transition.ColorTint;
RectTransform selected = Stretch("SelectedMarker", slotRect, -4f, -4f, -4f, -4f);
AddImage(selected, Color.clear, null);
Outline selectedOutline = selected.gameObject.AddComponent<Outline>();
selectedOutline.effectColor = Gold;
selectedOutline.effectDistance = new Vector2(4f, -4f);
selectedOutline.useGraphicAlpha = false;
selected.gameObject.SetActive(i == 1);
Image icon = AddSprite(Fixed("Icon", slotRect, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -12f), new Vector2(142f, 146f)), enemyIcons[i], Color.white, true);
TextMeshProUGUI name = AddText(
Fixed("EnemyName", slotRect, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 55f), new Vector2(154f, 30f)), "敌人 " + (i + 1), 18, White,
TextAlignmentOptions.Center, FontStyles.Bold);
Image hpFill = CreateEnemyBar(slotRect, hp[i], maxHp[i], out TextMeshProUGUI hpText);
BattleEnemySlotItem slot = slotRect.gameObject.AddComponent<BattleEnemySlotItem>();
slot.ConfigureEditor(targetButton, selected.gameObject, icon, hpFill, name, hpText);
RegisterReference(slot, "enemySlot" + (i + 1));
}
}
private static void BuildActionPanel(RectTransform parent)
{
RectTransform panel = AddPanel(
Fixed("ActionPanel", parent, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 28f), new Vector2(920f, 176f)), panelBlue, Color.white);
AddImage(TopStretch("CurrentAccent", panel, 0f, 0f, 0f, 7f), Gold, null);
TextMeshProUGUI currentActor = AddText(
Fixed("CurrentActorText", panel, new Vector2(0f, 1f), new Vector2(0f, 1f),
new Vector2(28f, -15f), new Vector2(500f, 38f)), "角色 4", 24, White,
TextAlignmentOptions.MidlineLeft, FontStyles.Bold);
RegisterReference(currentActor, "currentActorText");
AddText(Fixed("TurnState", panel, new Vector2(1f, 1f), new Vector2(1f, 1f),
new Vector2(-28f, -17f), new Vector2(260f, 34f)), "YOUR MOVE", 15, Gold,
TextAlignmentOptions.MidlineRight, FontStyles.Bold);
Button attack = CreateActionButton(panel, "AttackButton", "攻击", attackIcon, 25f, ButtonStyle.Brown);
Button skill = CreateActionButton(panel, "SkillButton", "技能", skillIcon, 243f, ButtonStyle.Blue);
Button item = CreateActionButton(panel, "ItemButton", "道具", itemIcon, 461f, ButtonStyle.Grey);
Button escape = CreateActionButton(panel, "EscapeButton", "逃跑", escapeIcon, 679f, ButtonStyle.Beige);
item.interactable = false;
RegisterReference(attack, "attackButton");
RegisterReference(skill, "skillButton");
RegisterReference(item, "itemButton");
RegisterReference(escape, "escapeButton");
}
private static Button CreateActionButton(RectTransform parent, string name, string label, Sprite icon,
float x, ButtonStyle style)
{
RectTransform rect = Fixed(name, parent, new Vector2(0f, 1f), new Vector2(0f, 1f),
new Vector2(x, -66f), new Vector2(216f, 82f));
Sprite normal;
Sprite pressed;
Color textColor;
switch (style)
{
case ButtonStyle.Blue:
normal = buttonBlue;
pressed = buttonBluePressed;
textColor = White;
break;
case ButtonStyle.Brown:
normal = buttonBrown;
pressed = buttonBrownPressed;
textColor = White;
break;
case ButtonStyle.Beige:
normal = buttonBeige;
pressed = buttonBeigePressed;
textColor = Ink;
break;
default:
normal = buttonGrey;
pressed = buttonGreyPressed;
textColor = Ink;
break;
}
Image background = AddImage(rect, Color.white, normal);
background.type = Image.Type.Sliced;
background.raycastTarget = true;
Button button = rect.gameObject.AddComponent<Button>();
button.targetGraphic = background;
button.transition = Selectable.Transition.SpriteSwap;
SpriteState state = button.spriteState;
state.highlightedSprite = normal;
state.selectedSprite = normal;
state.pressedSprite = pressed;
state.disabledSprite = pressed;
button.spriteState = state;
Color iconColor = style == ButtonStyle.Beige ? Ink : Color.white;
AddSprite(Fixed("Icon", rect, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f),
new Vector2(24f, 0f), new Vector2(46f, 46f)), icon, iconColor, true);
AddText(Fixed("Label", rect, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f),
new Vector2(80f, 0f), new Vector2(112f, 50f)), label, 25, textColor,
TextAlignmentOptions.Center, FontStyles.Bold);
return button;
}
private static Image CreateBar(RectTransform parent, string label, float x, float top, float width,
Color fillColor, int current, int maximum, out TextMeshProUGUI valueText)
{
RectTransform bar = Fixed(label + "Bar", parent, new Vector2(0f, 1f), new Vector2(0f, 1f),
new Vector2(x, -top), new Vector2(width, 32f));
AddImage(bar, Charcoal, null);
RectTransform fillRect = Stretch("Fill", bar, 3f, 3f, 3f, 3f);
fillRect.anchorMax = new Vector2(Mathf.Clamp01((float)current / maximum), 1f);
fillRect.offsetMax = new Vector2(0f, -3f);
Image fill = AddImage(fillRect, fillColor, null);
valueText = AddText(Stretch("Value", bar, 10f, 10f, 0f, 0f),
label + " " + current + " / " + maximum, 15, White, TextAlignmentOptions.Center, FontStyles.Bold);
return fill;
}
private static Image CreateEnemyBar(RectTransform parent, int current, int maximum,
out TextMeshProUGUI valueText)
{
RectTransform bar = Fixed("HPBar", parent, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 17f), new Vector2(150f, 26f));
AddImage(bar, Charcoal, null);
RectTransform fillRect = Stretch("Fill", bar, 3f, 3f, 3f, 3f);
fillRect.anchorMax = new Vector2(Mathf.Clamp01((float)current / maximum), 1f);
fillRect.offsetMax = new Vector2(0f, -3f);
Image fill = AddImage(fillRect, Red, null);
valueText = AddText(Stretch("Value", bar, 6f, 6f, 0f, 0f), current + " / " + maximum, 13, White,
TextAlignmentOptions.Center, FontStyles.Bold);
return fill;
}
private static void LoadAssets()
{
font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>("Assets/GameMain/Fonts/MainTMPFont.asset");
panelBeige = LoadSprite("Assets/GameMain/Textures/UI/panel_beige.png");
panelBrown = LoadSprite("Assets/GameMain/Textures/UI/panel_brown.png");
panelBlue = LoadSprite("Assets/GameMain/Textures/UI/panel_blue.png");
buttonBlue = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_blue.png");
buttonBluePressed = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_blue_pressed.png");
buttonBrown = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_brown.png");
buttonBrownPressed = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_brown_pressed.png");
buttonBeige = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_beige.png");
buttonBeigePressed = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_beige_pressed.png");
buttonGrey = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_grey.png");
buttonGreyPressed = LoadSprite("Assets/GameMain/Textures/UI/buttonLong_grey_pressed.png");
escapeIcon = LoadSprite("Assets/GameMain/Textures/UI/arrowSilver_left.png");
playerIcons = Enumerable.Range(0, 4).Select(i => LoadSprite(CharacterAtlasPath, "rogues_" + i)).ToArray();
int[] enemyIndexes = { 0, 1, 2, 3 };
enemyIcons = enemyIndexes.Select(i => LoadSprite(EnemyAtlasPath, "monsters_" + i)).ToArray();
attackIcon = LoadSprite(ItemAtlasPath, "items_0");
skillIcon = LoadSprite(ItemAtlasPath, "items_110");
itemIcon = LoadSprite(ItemAtlasPath, "items_220");
if (font == null || playerIcons.Any(sprite => sprite == null) || enemyIcons.Any(sprite => sprite == null))
{
throw new InvalidOperationException("Battle UI assets are incomplete.");
}
}
private static Sprite LoadSprite(string path)
{
return AssetDatabase.LoadAssetAtPath<Sprite>(path);
}
private static Sprite LoadSprite(string path, string name)
{
return AssetDatabase.LoadAllAssetsAtPath(path).OfType<Sprite>().FirstOrDefault(sprite => sprite.name == name);
}
private static RectTransform AddPanel(RectTransform rect, Sprite sprite, Color color)
{
Image image = AddImage(rect, color, sprite);
image.type = Image.Type.Sliced;
return rect;
}
private static Image AddImage(RectTransform rect, Color color, Sprite sprite)
{
Image image = rect.gameObject.AddComponent<Image>();
image.color = color;
image.sprite = sprite;
image.raycastTarget = false;
return image;
}
private static Image AddSprite(RectTransform rect, Sprite sprite, Color color, bool preserveAspect)
{
Image image = AddImage(rect, color, sprite);
image.preserveAspect = preserveAspect;
return image;
}
private static TextMeshProUGUI AddText(RectTransform rect, string value, float fontSize, Color color,
TextAlignmentOptions alignment, FontStyles style)
{
TextMeshProUGUI text = rect.gameObject.AddComponent<TextMeshProUGUI>();
text.font = font;
text.text = value;
text.fontSize = fontSize;
text.fontStyle = style;
text.color = color;
text.alignment = alignment;
text.enableWordWrapping = false;
text.overflowMode = TextOverflowModes.Ellipsis;
text.raycastTarget = false;
text.characterSpacing = 0f;
return text;
}
private static RectTransform NewRect(string name, Transform parent)
{
GameObject gameObject = new GameObject(name, typeof(RectTransform));
gameObject.layer = LayerMask.NameToLayer("UI");
RectTransform rect = gameObject.GetComponent<RectTransform>();
rect.SetParent(parent, false);
return rect;
}
private static RectTransform Fixed(string name, Transform parent, Vector2 anchor, Vector2 pivot,
Vector2 position, Vector2 size)
{
RectTransform rect = NewRect(name, parent);
rect.anchorMin = anchor;
rect.anchorMax = anchor;
rect.pivot = pivot;
rect.anchoredPosition = position;
rect.sizeDelta = size;
return rect;
}
private static RectTransform Stretch(string name, Transform parent, float left, float right, float bottom,
float top)
{
RectTransform rect = NewRect(name, parent);
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = new Vector2(left, bottom);
rect.offsetMax = new Vector2(-right, -top);
return rect;
}
private static RectTransform TopStretch(string name, Transform parent, float left, float right, float top,
float height)
{
RectTransform rect = NewRect(name, parent);
rect.anchorMin = new Vector2(0f, 1f);
rect.anchorMax = Vector2.one;
rect.pivot = new Vector2(0.5f, 1f);
rect.offsetMin = new Vector2(left, -top - height);
rect.offsetMax = new Vector2(-right, -top);
return rect;
}
private static void RegisterReference(Component component, string variableName)
{
UISerializationItem item = component.GetComponent<UISerializationItem>();
if (item == null)
{
item = component.gameObject.AddComponent<UISerializationItem>();
}
item.RefreshComponents();
item.SetReference(component, true, variableName);
}
private enum ButtonStyle
{
Blue,
Brown,
Beige,
Grey
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 019291a41e12a4f2088fe5542024e54a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -6,7 +6,8 @@
"GUID:a2d8a19598eca814496b089021d08d60",
"GUID:a04b272484018094ab427253f20c416c",
"GUID:11ec41dbc3557e6499b2845ba531a9b0",
"GUID:73a59a40d41e9ce48b3e0515aeec4217"
"GUID:73a59a40d41e9ce48b3e0515aeec4217",
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],
@@ -17,4 +18,4 @@
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
}
@@ -8,6 +8,7 @@
using GameFramework.Localization;
using System;
using SepCore.Sound;
using SepCore.Debugger;
using SepCore.Definition;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
@@ -20,6 +21,11 @@ namespace SepCore.Procedure
{
base.OnEnter(procedureOwner);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// 战斗壳层调试入口:仅编辑器与开发构建注册,正式构建不可见
GameEntry.Debugger.RegisterDebuggerWindow("Battle/Shell", new BattleDebuggerWindow());
#endif
// 构建信息:发布版本时,把一些数据以 Json 的格式写入 Assets/GameMain/Configs/BuildInfo.txt,供游戏逻辑读取
GameEntry.BuiltinData.InitBuildInfo();
@@ -12,10 +12,22 @@ public partial class GameEntry : MonoBehaviour
public static SaveComponent Save { get; private set; }
public static RandomComponent Random { get; private set; }
public static TurnBattleComponent TurnBattle { get; private set; }
/// <summary>
/// 单局战斗协调器(普通类,非场景组件)。
/// </summary>
public static RunBattleCoordinator RunBattle { get; private set; }
private static void InitCustomComponents()
{
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
Luban = UnityGameFramework.Runtime.GameEntry.GetComponent<LubanComponent>();
Save = UnityGameFramework.Runtime.GameEntry.GetComponent<SaveComponent>();
Random = UnityGameFramework.Runtime.GameEntry.GetComponent<RandomComponent>();
TurnBattle = UnityGameFramework.Runtime.GameEntry.GetComponent<TurnBattleComponent>();
RunBattle = new RunBattleCoordinator();
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0b415c54b5bb04d62bf67dfdddc382c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,178 @@
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,11 @@
fileFormatVersion: 2
guid: 84f740882a72430ca27c4d77004257d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 玩家指令。
/// 非法指令(错误行动者、错误阵营、错误目标数量、阵亡或逃跑目标)不得消耗 MP、行动机会或随机数。
/// </summary>
public sealed class BattleCommand
{
/// <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<int> TargetUnitIds;
public BattleCommand(int actorUnitId, BattleActionType commandType, int actionConfigId,
IReadOnlyList<int> targetUnitIds)
{
ActorUnitId = actorUnitId;
CommandType = commandType;
ActionConfigId = actionConfigId;
TargetUnitIds = targetUnitIds != null ? Copy(targetUnitIds) : new int[0];
}
private static int[] Copy(IReadOnlyList<int> source)
{
int[] copy = new int[source.Count];
for (int i = 0; i < source.Count; i++)
{
copy[i] = source[i];
}
return copy;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce89addfe88643eda73478128e385d5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
namespace SepCore.Battle
{
/// <summary>
/// 遭遇输入,由探索层在碰撞发生时创建。
/// 地图坐标、碰撞器、GameObject 和警惕组件不进入战斗请求,由探索层继续持有。
/// </summary>
public sealed class BattleEncounter
{
/// <summary>
/// 触发碰撞的地图敌人实例的单局唯一标识;不能使用敌人配置 ID 代替。
/// </summary>
public readonly int EncounterId;
/// <summary>
/// 该地图敌人代表的敌人队伍预设 ID。
/// </summary>
public readonly int EnemyPartyConfigId;
/// <summary>
/// 碰撞时敌人警惕值未满为 true,否则为 false。
/// </summary>
public readonly bool IsPreemptive;
public BattleEncounter(int encounterId, int enemyPartyConfigId, bool isPreemptive)
{
EncounterId = encounterId;
EnemyPartyConfigId = enemyPartyConfigId;
IsPreemptive = isPreemptive;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42d8ce9387864e68a141ace8c990f06f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
namespace SepCore.Battle
{
/// <summary>
/// 战斗单位阵营。
/// </summary>
public enum BattleFaction
{
/// <summary>
/// 玩家阵营。
/// </summary>
Player,
/// <summary>
/// 敌人阵营。
/// </summary>
Enemy,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d809efd802bf499c8237b00546a35c15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
namespace SepCore.Battle
{
/// <summary>
/// 战斗流程状态。
/// </summary>
public enum BattleFlowState
{
/// <summary>
/// 等待当前玩家行动者提交指令。
/// </summary>
AwaitingPlayerCommand,
/// <summary>
/// 战斗完成,结果已产出。
/// </summary>
Completed,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bef66af163534a79b4b14ba27ed80665
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
namespace SepCore.Battle
{
/// <summary>
/// 战斗结束结果,四种结果互斥。
/// </summary>
public enum BattleOutcome
{
/// <summary>
/// 所有敌人阵亡。
/// </summary>
Victory,
/// <summary>
/// 所有玩家成功逃跑,且没有玩家阵亡。
/// </summary>
AllEscaped,
/// <summary>
/// 至少一名玩家逃跑,其余仍在战斗中的玩家全部阵亡。
/// </summary>
PartialEscapeDefeat,
/// <summary>
/// 所有玩家阵亡,没有任何玩家成功逃跑。
/// </summary>
TotalDefeat,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56279ec78df04139b731da9dd07e7f14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c518c47a9854ba19045abd1f211eb7d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,84 @@
using System.Collections.Generic;
namespace SepCore.Battle
{
/// <summary>
/// 每名参战玩家的战后结果。
/// 保留原始战斗值:阵亡者 HP 为 0;恢复 1/1 的规则由 RunBattleCoordinator 统一应用。
/// </summary>
public sealed class BattlePlayerResult
{
/// <summary>
/// 角色配置标识。
/// </summary>
public readonly int CharacterId;
/// <summary>
/// 战斗结束时的当前 HP。
/// </summary>
public readonly int CurrentHp;
/// <summary>
/// 战斗结束时的当前 MP。
/// </summary>
public readonly int CurrentMp;
/// <summary>
/// 是否在本场战斗中阵亡。
/// </summary>
public readonly bool WasDefeated;
/// <summary>
/// 是否在本场战斗中成功逃跑。
/// </summary>
public readonly bool Escaped;
public BattlePlayerResult(int characterId, int currentHp, int currentMp, bool wasDefeated, bool escaped)
{
CharacterId = characterId;
CurrentHp = currentHp;
CurrentMp = currentMp;
WasDefeated = wasDefeated;
Escaped = escaped;
}
}
/// <summary>
/// 战斗结果。
/// </summary>
public sealed class BattleResult
{
/// <summary>
/// 关联原地图敌人实例。
/// </summary>
public readonly int EncounterId;
/// <summary>
/// 四种战斗结果之一。
/// </summary>
public readonly BattleOutcome Outcome;
/// <summary>
/// 每名参战玩家的原始战后结果。
/// </summary>
public readonly IReadOnlyList<BattlePlayerResult> Players;
public BattleResult(int encounterId, BattleOutcome outcome, IReadOnlyList<BattlePlayerResult> players)
{
EncounterId = encounterId;
Outcome = outcome;
Players = players != null ? Copy(players) : new BattlePlayerResult[0];
}
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,11 @@
fileFormatVersion: 2
guid: da5169df3c26408ba2b665960f5632fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,82 @@
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,11 @@
fileFormatVersion: 2
guid: ec4ca0d279034f4a939fd35aedab383e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,223 @@
using System.Collections.Generic;
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 单位剩余状态快照。
/// </summary>
public sealed class BattleStatusSnapshot
{
/// <summary>
/// 状态类型。
/// </summary>
public readonly BattleStatusType StatusType;
/// <summary>
/// 剩余持续次数(行动机会);重复施加取较大值,移除后为 0。
/// </summary>
public readonly int RemainingRounds;
public BattleStatusSnapshot(BattleStatusType statusType, int remainingRounds)
{
StatusType = statusType;
RemainingRounds = remainingRounds;
}
}
/// <summary>
/// 单个战斗单位的只读视图。
/// </summary>
public sealed class BattleUnitSnapshot
{
/// <summary>
/// 本场战斗单位的唯一运行时标识;重复配置的敌人拥有不同 ID。
/// </summary>
public readonly int BattleUnitId;
/// <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 CurrentHp;
/// <summary>
/// HP 上限。
/// </summary>
public readonly int MaxHp;
/// <summary>
/// 当前 MP。
/// </summary>
public readonly int CurrentMp;
/// <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>
/// 是否阵亡。
/// </summary>
public readonly bool IsDefeated;
/// <summary>
/// 是否已逃跑。
/// </summary>
public readonly bool IsEscaped;
/// <summary>
/// 剩余状态列表。
/// </summary>
public readonly IReadOnlyList<BattleStatusSnapshot> Statuses;
/// <summary>
/// 可用行动配置 ID 列表。
/// </summary>
public readonly IReadOnlyList<int> AvailableActionIds;
public BattleUnitSnapshot(int battleUnitId, BattleFaction faction, int configId, int partyOrder,
int currentHp, int maxHp, int currentMp, int maxMp, int atk, int mat, int speed,
bool isDefeated, bool isEscaped, IReadOnlyList<BattleStatusSnapshot> statuses,
IReadOnlyList<int> availableActionIds)
{
BattleUnitId = battleUnitId;
Faction = faction;
ConfigId = configId;
PartyOrder = partyOrder;
CurrentHp = currentHp;
MaxHp = maxHp;
CurrentMp = currentMp;
MaxMp = maxMp;
Atk = atk;
Mat = mat;
Speed = speed;
IsDefeated = isDefeated;
IsEscaped = isEscaped;
Statuses = statuses != null ? Copy(statuses) : new BattleStatusSnapshot[0];
AvailableActionIds = availableActionIds != null ? Copy(availableActionIds) : new int[0];
}
private static BattleStatusSnapshot[] Copy(IReadOnlyList<BattleStatusSnapshot> source)
{
BattleStatusSnapshot[] copy = new BattleStatusSnapshot[source.Count];
for (int i = 0; i < source.Count; i++)
{
copy[i] = source[i];
}
return copy;
}
private static int[] Copy(IReadOnlyList<int> source)
{
int[] copy = new int[source.Count];
for (int i = 0; i < source.Count; i++)
{
copy[i] = source[i];
}
return copy;
}
}
/// <summary>
/// 只读战斗快照,是 UI 和敌人策略唯一允许读取的战斗状态。
/// 每次推进都创建逻辑上的只读快照,不把可修改的内部列表或战斗单位引用暴露给调用方。
/// </summary>
public sealed class BattleSnapshot
{
/// <summary>
/// 当前轮次,从 1 开始。
/// </summary>
public readonly int RoundNumber;
/// <summary>
/// 当前是否为先制战斗第一轮。
/// </summary>
public readonly bool IsPreemptiveRound;
/// <summary>
/// 当前行动者;战斗完成时为 0。
/// </summary>
public readonly int CurrentActorUnitId;
/// <summary>
/// 全部单位的只读视图,保持稳定显示顺序。
/// </summary>
public readonly IReadOnlyList<BattleUnitSnapshot> Units;
/// <summary>
/// 本轮从当前行动者开始的剩余运行时单位 ID;速度变化后重建。
/// </summary>
public readonly IReadOnlyList<int> RemainingTurnOrder;
/// <summary>
/// 流程状态。
/// </summary>
public readonly BattleFlowState FlowState;
public BattleSnapshot(int roundNumber, bool isPreemptiveRound, int currentActorUnitId,
IReadOnlyList<BattleUnitSnapshot> units, IReadOnlyList<int> remainingTurnOrder,
BattleFlowState flowState)
{
RoundNumber = roundNumber;
IsPreemptiveRound = isPreemptiveRound;
CurrentActorUnitId = currentActorUnitId;
Units = units != null ? Copy(units) : new BattleUnitSnapshot[0];
RemainingTurnOrder = remainingTurnOrder != null ? Copy(remainingTurnOrder) : new int[0];
FlowState = flowState;
}
private static BattleUnitSnapshot[] Copy(IReadOnlyList<BattleUnitSnapshot> source)
{
BattleUnitSnapshot[] copy = new BattleUnitSnapshot[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++)
{
copy[i] = source[i];
}
return copy;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5bfa0a211d34ba6874d7d0b276192a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 040ffec92b064e3fbd9d9a8db9b39291
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 战斗配置访问接口,隔离 BattleSession 与 GameEntry.Luban。
/// 运行时适配器读取现有 Luban 表,测试使用内存配置。
/// 配置缺失属于战斗启动失败,不允许用零值单位继续战斗。
/// </summary>
public interface IBattleConfigProvider
{
/// <summary>
/// 按 ID 获取敌人队伍预设。
/// </summary>
EnemyPartyConfig GetEnemyParty(int id);
/// <summary>
/// 按 ID 获取敌人配置。
/// </summary>
EnemyConfig GetEnemy(int id);
/// <summary>
/// 按 ID 获取行动配置。
/// </summary>
BattleActionConfig GetAction(int id);
/// <summary>
/// 获取全局配置。
/// </summary>
GlobalConfig GetGlobal();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf1d3721d95a45a0a88d81baae7ec0f7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
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);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e803cf69950a4fb9b0b8d63603f99a4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
namespace SepCore.Battle
{
/// <summary>
/// 探索层恢复接口。
/// 碰撞方主动把 BattleEncounter 交给 RunBattleCoordinator;协调器不扫描场景寻找敌人。
/// ApplyBattleReturn 必须按 EncounterId 精确操作触发敌人,不能按 EnemyConfigId 批量处理同类敌人。
/// </summary>
public interface IExplorationBattleBridge
{
/// <summary>
/// 执行探索恢复计划。
/// </summary>
/// <param name="plan">由 RunBattleCoordinator 生成的恢复计划。</param>
void ApplyBattleReturn(BattleReturnPlan plan);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cf022ae8498b418784037549ce415e3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bdc9ce5dfa33f4f409f98053881b767a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
namespace SepCore.CustomComponent
{
/// <summary>
/// 本局共享随机数接口。
/// 同一单局的地图生成、敌人 AI、逃跑和敌人掉落依次消费同一个实例;
/// 战斗不能根据 EncounterId、回合数或当前时间重新播种。
/// </summary>
public interface IRunRandomSource
{
/// <summary>
/// 返回 [minInclusive, maxExclusive) 范围内的随机整数。
/// </summary>
int NextInt(int minInclusive, int maxExclusive);
/// <summary>
/// 以千分比概率判定是否成功;RollPermille(0) 必失败,RollPermille(1000) 必成功。
/// </summary>
bool RollPermille(int successPermille);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1e17f39494674cf08295b96df17642e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
using UnityGameFramework.Runtime;
namespace SepCore.CustomComponent
{
/// <summary>
/// 单局共享随机组件。
/// 种子由局外(战备/部署界面玩家输入)带入,进入单局时初始化;
/// 地图生成、物资点、敌人 AI、逃跑判定和敌人掉落依次消费同一个随机源,
/// 战斗只沿用该随机源,不创建战斗私有随机源。
/// 对应 Docs/GameDesign/03_RunExploration.md 的随机生成器约定。
/// </summary>
public class RandomComponent : GameFrameworkComponent
{
private int _seed;
private IRunRandomSource _random;
/// <summary>
/// 获取本局随机种子;未开始单局时为 0。
/// </summary>
public int Seed => _seed;
/// <summary>
/// 获取本局共享随机源;未开始单局时为 null。
/// </summary>
public IRunRandomSource Random => _random;
/// <summary>
/// 开始一局新的单局:使用局外带入的种子初始化共享随机源。
/// </summary>
/// <param name="seed">本局随机种子。</param>
public void BeginRun(int seed)
{
_seed = seed;
_random = new RunRandomSource(seed);
}
/// <summary>
/// 结束当前单局,清空种子与随机源。
/// </summary>
public void EndRun()
{
_seed = 0;
_random = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 373a779ff43a4289b8e8cb23459d7bb3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using System;
namespace SepCore.CustomComponent
{
/// <summary>
/// 以 System.Random 实现的本局共享随机源。
/// 由 TurnBattleComponent 以单局 seed 创建,供地图生成、敌人 AI、逃跑与掉落共用。
/// EditMode 测试使用可注入的序列随机源,不依赖本类。
/// </summary>
public sealed class RunRandomSource : IRunRandomSource
{
private readonly Random _random;
/// <summary>
/// 使用指定种子创建随机源。
/// </summary>
public RunRandomSource(int seed)
{
_random = new Random(seed);
}
public int NextInt(int minInclusive, int maxExclusive)
{
return _random.Next(minInclusive, maxExclusive);
}
public bool RollPermille(int successPermille)
{
return NextInt(0, 1000) < successPermille;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4dc1aacc472441329e01cf158f17981e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2569cd06130964d66b109493dabdfc77
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,112 @@
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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0146e261d98d40109183900a1d033fc9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,64 @@
namespace SepCore.CustomComponent
{
/// <summary>
/// 单局临时角色状态。
/// 数值由单局快照构建器在开战前从角色与装备结算得到;战斗期间只读,战斗结束回写当前 HP/MP。
/// </summary>
public sealed class RunPlayerState
{
/// <summary>
/// 角色配置标识。
/// </summary>
public int CharacterId;
/// <summary>
/// 当前战备中的顺序,1 开始。
/// </summary>
public int PartyOrder;
/// <summary>
/// 当前 HP。
/// </summary>
public int CurrentHp;
/// <summary>
/// 当前 MP。
/// </summary>
public int CurrentMp;
/// <summary>
/// 已结算装备加成的最终 HP 上限。
/// </summary>
public int MaxHp;
/// <summary>
/// 已结算装备加成的最终 MP 上限。
/// </summary>
public int MaxMp;
/// <summary>
/// 已结算装备加成的最终攻击。
/// </summary>
public int Atk;
/// <summary>
/// 已结算装备加成的最终魔力。
/// </summary>
public int Mat;
/// <summary>
/// 已结算装备加成的最终速度。
/// </summary>
public int Speed;
/// <summary>
/// 普通攻击行动配置 ID。
/// </summary>
public int AttackActionId;
/// <summary>
/// 该角色首版唯一技能行动配置 ID。
/// </summary>
public int SkillActionId;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3caaef559cc641769b9ede0b4d033532
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace SepCore.CustomComponent
{
/// <summary>
/// 单局战斗组件。
/// 负责本局临时角色状态、计时、探索暂停标志和战斗占用的最小状态。
/// 本局种子与共享随机源由独立的 RandomComponent 持有,战斗只沿用,不创建私有随机源。
/// 战斗协调(构建请求、打开 UI、回写结果)由独立的 RunBattleCoordinator 负责。
/// </summary>
public class TurnBattleComponent : GameFrameworkComponent
{
private readonly List<RunPlayerState> _players = new List<RunPlayerState>();
private float _elapsedMs;
private bool _timerPaused;
private bool _explorationPaused;
private bool _battleActive;
/// <summary>
/// 获取本局临时角色状态列表(只读)。
/// </summary>
public IReadOnlyList<RunPlayerState> Players => _players;
/// <summary>
/// 获取本局已流逝时间(毫秒)。
/// </summary>
public long RunElapsedMs => (long)_elapsedMs;
/// <summary>
/// 获取单局计时是否已暂停。
/// </summary>
public bool IsTimerPaused => _timerPaused;
/// <summary>
/// 获取探索更新是否已暂停。
/// 战斗期间为 true,地图更新统一以此作为门禁。
/// </summary>
public bool IsExplorationPaused => _explorationPaused;
/// <summary>
/// 获取当前是否存在战斗占用。
/// 战斗重复进入由此标志拒绝。
/// </summary>
public bool IsBattleActive => _battleActive;
/// <summary>
/// 开始一局新的单局:重置全部临时状态。
/// </summary>
public void BeginRun()
{
_players.Clear();
_elapsedMs = 0;
_timerPaused = false;
_explorationPaused = false;
_battleActive = false;
}
/// <summary>
/// 结束当前单局,清空全部临时状态。
/// </summary>
public void EndRun()
{
_players.Clear();
_elapsedMs = 0;
_timerPaused = false;
_explorationPaused = false;
_battleActive = false;
}
/// <summary>
/// 用指定状态替换本局临时角色列表,保持战备顺序。
/// </summary>
/// <param name="players">新的临时角色状态,可为空。</param>
public void ReplacePlayers(IEnumerable<RunPlayerState> players)
{
_players.Clear();
if (players != null)
{
_players.AddRange(players);
}
}
/// <summary>
/// 预留战斗占用,拒绝同一帧或未结束战斗的重复进入。
/// </summary>
/// <returns>预留成功返回 true;已存在战斗占用时返回 false。</returns>
public bool TryReserveBattle()
{
if (_battleActive)
{
return false;
}
_battleActive = true;
return true;
}
/// <summary>
/// 释放战斗占用。
/// </summary>
public void ReleaseBattle()
{
_battleActive = false;
}
/// <summary>
/// 设置探索更新暂停状态。
/// </summary>
/// <param name="paused">是否暂停探索更新。</param>
public void SetExplorationPaused(bool paused)
{
_explorationPaused = paused;
}
/// <summary>
/// 设置单局计时暂停状态。
/// </summary>
/// <param name="paused">是否暂停单局计时。</param>
public void SetTimerPaused(bool paused)
{
_timerPaused = paused;
}
private void Update()
{
if (_timerPaused)
{
return;
}
_elapsedMs += Time.deltaTime * 1000f;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ceac378d38794bafb31a203753565121
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,144 @@
using GameFramework.Debugger;
using SepCore.Battle;
using SepCore.CustomComponent;
using SepCore.Definition;
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace SepCore.Debugger
{
/// <summary>
/// 战斗壳层调试入口(仅供 Editor/Development Build,正式构建不注册)。
/// 模拟探索层:缺少单局状态时用配表角色与固定种子初始化临时状态,
/// 然后通过 RunBattleCoordinator 以固定调试遭遇开始/结束战斗,不依赖尚未实现的地图敌人。
/// </summary>
public class BattleDebuggerWindow : IDebuggerWindow
{
private const int DebugRunSeed = 240829;
public void Initialize(params object[] args)
{
}
public void Shutdown()
{
}
public void OnEnter()
{
}
public void OnLeave()
{
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
}
public void OnDraw()
{
GUILayout.Label("<b>Battle Shell</b>");
GUILayout.BeginHorizontal("box");
{
if (GUILayout.Button("Open", GUILayout.Height(30)))
{
OpenShell();
}
if (GUILayout.Button("Close", GUILayout.Height(30)))
{
GameEntry.RunBattle.EndDebugBattle();
}
}
GUILayout.EndHorizontal();
DrawRunState();
}
private static void DrawRunState()
{
GUILayout.Label("<b>Run State</b>");
GUILayout.BeginVertical("box");
{
TurnBattleComponent runBattle = GameEntry.TurnBattle;
if (runBattle == null)
{
GUILayout.Label("TurnBattleComponent missing.");
return;
}
GUILayout.Label("RunElapsedMs: " + runBattle.RunElapsedMs.ToString());
GUILayout.Label("IsTimerPaused: " + runBattle.IsTimerPaused);
GUILayout.Label("IsExplorationPaused: " + runBattle.IsExplorationPaused);
GUILayout.Label("IsBattleActive: " + runBattle.IsBattleActive);
}
GUILayout.EndVertical();
}
private static 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)
{
GameEntry.Random.BeginRun(DebugRunSeed);
}
if (GameEntry.TurnBattle.Players.Count == 0)
{
GameEntry.TurnBattle.ReplacePlayers(BuildDebugPlayers());
}
}
private static List<RunPlayerState> BuildDebugPlayers()
{
List<RunPlayerState> players = new List<RunPlayerState>();
GlobalConfig global = GameEntry.Luban.Global != null ? GameEntry.Luban.Global.Data : null;
if (global == null)
{
Log.Warning("Can not build debug players without global config.");
return players;
}
int order = 1;
foreach (int characterId in global.NewGameCharacterIds)
{
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(characterId);
if (config == null)
{
Log.Warning("Debug character '{0}' missing in config, skipped.", characterId.ToString());
continue;
}
players.Add(new RunPlayerState
{
CharacterId = characterId,
PartyOrder = order++,
CurrentHp = config.MaxHp,
CurrentMp = config.MaxMp,
MaxHp = config.MaxHp,
MaxMp = config.MaxMp,
Atk = config.Atk,
Mat = config.Mat,
Speed = config.Speed,
AttackActionId = config.AttackActionId,
SkillActionId = config.SkillActionId,
});
}
return players;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0024ae1248654a28b520630f8c4a149b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d85f3e1b668434911af528b43e3ad41f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace SepCore.UI
{
[DisallowMultipleComponent]
public sealed class BattleActorCardItem : MonoBehaviour
{
[SerializeField] private GameObject activeMarker;
[SerializeField] private Image icon;
[SerializeField] private Image hpFill;
[SerializeField] private Image mpFill;
[SerializeField] private TextMeshProUGUI characterName;
[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;
#if UNITY_EDITOR
public void ConfigureEditor(GameObject marker, Image iconImage, Image hpFillImage, Image mpFillImage,
TextMeshProUGUI nameLabel, TextMeshProUGUI hpLabel, TextMeshProUGUI mpLabel)
{
activeMarker = marker;
icon = iconImage;
hpFill = hpFillImage;
mpFill = mpFillImage;
characterName = nameLabel;
hpText = hpLabel;
mpText = mpLabel;
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 084cbeaade555447b83112b1c528e263
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace SepCore.UI
{
[DisallowMultipleComponent]
public sealed class BattleEnemySlotItem : MonoBehaviour
{
[SerializeField] private Button targetButton;
[SerializeField] private GameObject selectedMarker;
[SerializeField] private Image icon;
[SerializeField] private Image hpFill;
[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;
#if UNITY_EDITOR
public void ConfigureEditor(Button button, GameObject marker, Image iconImage, Image hpFillImage,
TextMeshProUGUI nameLabel, TextMeshProUGUI hpLabel)
{
targetButton = button;
selectedMarker = marker;
icon = iconImage;
hpFill = hpFillImage;
enemyName = nameLabel;
hpText = hpLabel;
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8efb693fb8da3406ea05e81cb56a6c20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,64 @@
using UnityGameFramework.Runtime;
namespace SepCore.UI
{
/// <summary>
/// 战斗界面逻辑(手写 partial,与自动生成的 BattleForm.cs 合并)。
/// 首版为半透明壳层:只负责展示快照、收集指令与占位反馈;
/// 不修改战斗数据、不读取存档、不决定掉落,也不承担暂停地图计时等流程职责。
/// 暂停地图更新由单局流程(RunBattleCoordinator / 探索层)负责。
/// </summary>
public partial class BattleForm : UGuiForm
{
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);
// 道具行动保留但首版禁用,不产生 BattleCommand
View.itemButton.interactable = false;
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
View.currentActorText.text = string.Empty;
Log.Info("BattleForm opened as empty shell.");
}
protected override void OnClose(bool isShutdown, object userData)
{
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()
{
Log.Info("BattleForm placeholder: attack clicked.");
}
private void OnSkillButtonClick()
{
Log.Info("BattleForm placeholder: skill clicked.");
}
private void OnItemButtonClick()
{
Log.Info("BattleForm placeholder: item clicked.");
}
private void OnEscapeButtonClick()
{
Log.Info("BattleForm placeholder: escape clicked.");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 090bea621c85436a8cbe0ad03aa94ebb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
// <auto-generated />
using UnityEngine;
namespace SepCore.UI
{
[DisallowMultipleComponent]
public partial class BattleForm : UGuiForm
{
[SerializeField] private BattleView _view = null;
public BattleView View => _view;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9442df9c5d6124f96a96de27d76fc7e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace SepCore.UI
{
[DisallowMultipleComponent]
public sealed class BattleTurnSlotItem : MonoBehaviour
{
[SerializeField] private GameObject activeMarker;
[SerializeField] private Image icon;
[SerializeField] private TextMeshProUGUI label;
public GameObject ActiveMarker => activeMarker;
public Image Icon => icon;
public TextMeshProUGUI Label => label;
#if UNITY_EDITOR
public void ConfigureEditor(GameObject marker, Image iconImage, TextMeshProUGUI slotLabel)
{
activeMarker = marker;
icon = iconImage;
label = slotLabel;
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8703508b1b1894f67aeb402ef5b11113
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
// <auto-generated />
using UnityEngine;
namespace SepCore.UI
{
[DisallowMultipleComponent]
public partial class BattleView : MonoBehaviour
{
[SerializeField] public global::UnityEngine.RectTransform turnSlotsRoot;
[SerializeField] public global::SepCore.UI.BattleTurnSlotItem battleTurnSlotTemplate;
[SerializeField] public global::SepCore.UI.BattleActorCardItem playerCard1;
[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::SepCore.UI.BattleEnemySlotItem battleEnemySlotTemplate;
[SerializeField] public global::TMPro.TextMeshProUGUI currentActorText;
[SerializeField] public global::UnityEngine.UI.Button attackButton;
[SerializeField] public global::UnityEngine.UI.Button skillButton;
[SerializeField] public global::UnityEngine.UI.Button itemButton;
[SerializeField] public global::UnityEngine.UI.Button escapeButton;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da8c2ddc0d704496db8c9d57244ecca9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +1,6 @@
using System.Collections.Generic;
using SepCore.Base;
using SepCore.Definition;
using UnityEngine;
using UnityEngine.UI;
using UnityGameFramework.Runtime;
namespace SepCore.UI