Files
Workspace/Assets/Demos/Demo2/Runtime/Demo2DemoController.cs
T
2026-08-18 02:03:34 +08:00

500 lines
23 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Demo2.Domain;
using Demo2.ECS;
using ShrinkTutorial;
using TMPro;
using Unity.Entities;
using UnityEngine;
using UnityEngine.UIElements;
namespace Demo2.Runtime
{
public sealed class Demo2DemoController : MonoBehaviour
{
private enum ScreenKind { Title, Lobby, Factory, Reward, End }
[SerializeField] private Camera? worldCamera;
[SerializeField] private Demo2MachineProxyPool? proxyPool;
[SerializeField] private Demo2ItemBatchRenderer? itemBatchRenderer;
[SerializeField] private UIDocument? uiDocument;
[SerializeField] private StyleSheet? uiStyleSheet;
[SerializeField] private Sprite? titleBackground;
[SerializeField] private Sprite[] machineIcons = Array.Empty<Sprite>();
[SerializeField] private Font? uiFont;
[SerializeField] private TMP_FontAsset? tutorialFont;
[SerializeField] private RectTransform? tutorialAnchorRoot;
private readonly Dictionary<ScreenKind, VisualElement> _screens = new();
private readonly Dictionary<string, RectTransform> _tutorialAnchors = new(StringComparer.Ordinal);
private Demo2GameService? _service;
private Demo2LanRuntime? _lan;
private Demo2GridInput? _gridInput;
private VisualElement? _root;
private ScreenKind _screen;
private Demo2MachineKind _selectedKind = Demo2MachineKind.Conveyor;
private byte _rotation;
private bool _removeMode;
private Demo2TransportMode _transportMode = Demo2TransportMode.Tcp;
private bool _dirty = true;
private bool _uiReady;
private float _visualTimer;
private long _networkBuildSequence;
private int _selectedRecipeId;
private static readonly (string anchorId, string elementName)[] TutorialTargets =
{
("demo2.miner", "miner-button"),
("demo2.conveyor", "conveyor-button"),
("demo2.grid", "grid-input-surface"),
("demo2.smelter", "smelter-button"),
("demo2.recipe", "recipe-button"),
("demo2.turret", "turret-button"),
("demo2.ready", "ready-button"),
("demo2.reward", "reward-choose-1")
};
private void Awake()
{
if (Application.isBatchMode || Array.IndexOf(Environment.GetCommandLineArgs(), "-demo2-headless") >= 0)
{
enabled = false;
return;
}
_service = Demo2GameService.Current ?? new Demo2GameService();
_lan = new Demo2LanRuntime(_service);
if (tutorialFont != null && !TMP_Settings.fallbackFontAssets.Contains(tutorialFont)) TMP_Settings.fallbackFontAssets.Add(tutorialFont);
var tutorialSettings = Resources.Load<ShrinkTutorialSettings>("Demo2TutorialSettings");
if (tutorialSettings != null) ShrinkTutorialManager.EnsureInstance(tutorialSettings);
_service.Changed += MarkDirty;
_lan.Changed += MarkDirty;
}
private void Start()
{
if (uiDocument == null) throw new InvalidOperationException("Demo2 UIDocument is not assigned.");
_root = uiDocument.rootVisualElement;
if (uiStyleSheet != null && !_root.styleSheets.Contains(uiStyleSheet)) _root.styleSheets.Add(uiStyleSheet);
if (uiFont != null) _root.style.unityFont = uiFont;
CacheScreens();
CacheTutorialAnchors();
ApplyArt();
BindTitle();
BindLobby();
BindFactory();
BindReward();
BindEnd();
if (worldCamera == null) throw new InvalidOperationException("Demo2 WorldCamera is not assigned.");
_gridInput = new Demo2GridInput(Require<VisualElement>("grid-input-surface"), worldCamera);
_gridInput.GridClicked += OnGridClicked;
_root.RegisterCallback<GeometryChangedEvent>(_ =>
{
ApplySafeArea();
SyncTutorialAnchors();
});
_uiReady = true;
Show(ScreenKind.Title);
_root.schedule.Execute(ApplySafeArea).ExecuteLater(1);
}
private void Update()
{
if (!_uiReady) return;
_lan?.Pump();
if (_screen == ScreenKind.Factory && ShrinkTutorialManager.Instance?.IsRunning != true) _service?.AdvanceFrame(Time.unscaledDeltaTime);
RoutePhaseScreen();
_visualTimer -= Time.unscaledDeltaTime;
if (_visualTimer <= 0)
{
_visualTimer = 0.1f;
proxyPool?.Refresh(_service?.EcsWorld);
itemBatchRenderer?.Bind(_service?.EcsWorld);
_dirty = true;
}
if (_dirty) Refresh();
}
private void OnDestroy()
{
if (_service != null) _service.Changed -= MarkDirty;
if (_lan != null) _lan.Changed -= MarkDirty;
if (_gridInput != null)
{
_gridInput.GridClicked -= OnGridClicked;
_gridInput.Dispose();
}
_lan?.Dispose();
}
private void CacheScreens()
{
_screens[ScreenKind.Title] = Require<VisualElement>("title-screen");
_screens[ScreenKind.Lobby] = Require<VisualElement>("lobby-screen");
_screens[ScreenKind.Factory] = Require<VisualElement>("factory-screen");
_screens[ScreenKind.Reward] = Require<VisualElement>("reward-screen");
_screens[ScreenKind.End] = Require<VisualElement>("end-screen");
}
private void CacheTutorialAnchors()
{
if (tutorialAnchorRoot == null) return;
foreach (var target in TutorialTargets)
{
var child = tutorialAnchorRoot.Find(target.anchorId);
if (child is RectTransform rect) _tutorialAnchors[target.anchorId] = rect;
}
}
private void ApplyArt()
{
if (titleBackground != null) Require<VisualElement>("title-background").style.backgroundImage = new StyleBackground(titleBackground);
var names = new[] { "miner", "conveyor", "splitter", "smelter", "assembler", "loader", "turret", "generator" };
for (var i = 0; i < names.Length && i < machineIcons.Length; i++)
if (machineIcons[i] != null) Require<VisualElement>(names[i] + "-icon").style.backgroundImage = new StyleBackground(machineIcons[i]);
}
private void BindTitle()
{
Bind("solo-button", StartSolo);
Bind("host-button", HostLan);
Bind("join-button", JoinLan);
Bind("transport-button", ToggleTransport);
}
private void BindLobby()
{
Bind("lobby-back-button", LeaveLobby);
Bind("lobby-ready-button", () => _lan?.SetReady(true));
Bind("lobby-start-button", StartHostedMatch);
}
private void BindFactory()
{
var pairs = new[]
{
(Demo2MachineKind.Miner, "miner-button"),
(Demo2MachineKind.Conveyor, "conveyor-button"),
(Demo2MachineKind.Splitter, "splitter-button"),
(Demo2MachineKind.Smelter, "smelter-button"),
(Demo2MachineKind.Assembler, "assembler-button"),
(Demo2MachineKind.Loader, "loader-button"),
(Demo2MachineKind.Turret, "turret-button"),
(Demo2MachineKind.Generator, "generator-button")
};
foreach (var pair in pairs)
{
var local = pair.Item1;
Bind(pair.Item2, () => SelectMachine(local));
}
Bind("rotate-button", () => { _rotation = (byte)((_rotation + 1) & 3); _dirty = true; });
Bind("remove-button", () => { _removeMode = !_removeMode; _dirty = true; });
Bind("ready-button", ReadyForProduction);
Bind("speed-1-button", () => _service?.SetSpeed(1));
Bind("speed-4-button", () => _service?.SetSpeed(4));
Bind("speed-10-button", () => _service?.SetSpeed(10));
Bind("save-button", () => _service?.SaveAsync().Forget(LogException));
Bind("load-button", () => _service?.LoadAsync().Forget(LogException));
Bind("exit-button", ExitToTitle);
Bind("recipe-button", ConfirmRecipe);
Bind("rule-up-1", () => _service?.ReorderRule(0, 0));
Bind("rule-up-2", () => _service?.ReorderRule(1, 0));
Bind("rule-up-3", () => _service?.ReorderRule(2, 1));
Bind("rule-up-4", () => _service?.ReorderRule(3, 2));
Bind("rule-up-5", () => _service?.ReorderRule(4, 3));
}
private void BindReward()
{
Bind("reward-choose-1", () => ChooseReward(0));
Bind("reward-choose-2", () => ChooseReward(1));
Bind("reward-choose-3", () => ChooseReward(2));
}
private void BindEnd()
{
Bind("restart-button", StartSolo);
Bind("title-button", ExitToTitle);
}
private void StartSolo()
{
_lan?.Stop();
_service?.StartNewGame();
Show(ScreenKind.Factory);
ShrinkTutorialManager.Instance?.StartTutorial("demo2.first-cycle");
}
private void HostLan()
{
_service?.StartNewGame();
_lan?.Host(27320, _transportMode, "host");
Show(ScreenKind.Lobby);
}
private void JoinLan()
{
var value = Require<TextField>("direct-ip-input").value;
var host = string.IsNullOrWhiteSpace(value) ? "127.0.0.1" : value.Trim();
_service?.StartNewGame(20260817);
_lan?.Join(host, 27320, _transportMode, "client");
Show(ScreenKind.Lobby);
}
private void StartHostedMatch()
{
if (_lan?.IsHost == true) Show(ScreenKind.Factory);
}
private void LeaveLobby()
{
_lan?.Stop();
Show(ScreenKind.Title);
}
private void ExitToTitle()
{
_lan?.Stop();
Show(ScreenKind.Title);
}
private void ToggleTransport()
{
_transportMode = _transportMode == Demo2TransportMode.Tcp ? Demo2TransportMode.Kcp : Demo2TransportMode.Tcp;
_dirty = true;
}
private void SelectMachine(Demo2MachineKind kind)
{
_selectedKind = kind;
_selectedRecipeId = _service != null && _service.Content.Machines.TryGetValue(kind, out var machine) ? machine.RecipeId : 0;
_removeMode = false;
_dirty = true;
if (kind == Demo2MachineKind.Miner) ShrinkTutorialManager.Instance?.CompleteStep("demo2.miner-selected");
if (kind == Demo2MachineKind.Conveyor) ShrinkTutorialManager.Instance?.CompleteStep("demo2.conveyor-selected");
if (kind == Demo2MachineKind.Smelter) ShrinkTutorialManager.Instance?.CompleteStep("demo2.smelter-selected");
if (kind == Demo2MachineKind.Turret) ShrinkTutorialManager.Instance?.CompleteStep("demo2.turret-selected");
}
private void ConfirmRecipe()
{
if (_service != null && _selectedRecipeId == 0 && _service.Content.Recipes.Count > 0)
_selectedRecipeId = _service.Content.Recipes.Keys.OrderBy(value => value).First();
ShrinkTutorialManager.Instance?.CompleteStep("demo2.recipe-selected");
_dirty = true;
}
private void ReadyForProduction()
{
if (_service == null) return;
if (_lan?.IsConnected == true) _lan.SetReady(true);
else _service.SetReady("local", true);
ShrinkTutorialManager.Instance?.CompleteStep("demo2.production-started");
}
private void OnGridClicked(int x, int y, int button)
{
if (_service == null || _service.Phase != Demo2Phase.Build) return;
var remove = _removeMode || button == 1;
var command = new Demo2BuildCommand
{
Sequence = ++_networkBuildSequence,
IdempotencyToken = (_lan?.LocalPlayerId ?? "local") + "-" + _networkBuildSequence,
PlayerId = _lan?.LocalPlayerId ?? "local",
Kind = _selectedKind,
X = x,
Y = y,
Rotation = _rotation,
RecipeId = _selectedRecipeId,
Remove = remove
};
if (_lan?.IsConnected == true) _lan.SendBuild(command);
else
{
var result = _service.SubmitBuild(command);
if (result.Accepted && !remove)
{
if (_selectedKind == Demo2MachineKind.Miner) ShrinkTutorialManager.Instance?.CompleteStep("demo2.miner-placed");
if (_selectedKind == Demo2MachineKind.Conveyor) ShrinkTutorialManager.Instance?.CompleteStep("demo2.conveyor-placed");
if (_selectedKind == Demo2MachineKind.Turret) ShrinkTutorialManager.Instance?.CompleteStep("demo2.turret-placed");
}
}
}
private void ChooseReward(int index)
{
if (_service == null || index < 0 || index >= _service.RewardChoices.Count) return;
var ruleId = _service.RewardChoices[index];
if (_lan?.IsConnected == true) _lan.ChooseReward(ruleId, -1);
else if (_service.ChooseReward(ruleId)) ShrinkTutorialManager.Instance?.CompleteStep("demo2.rule-chosen");
}
private void RoutePhaseScreen()
{
if (_service == null || _screen is ScreenKind.Title or ScreenKind.Lobby) return;
if (_service.Phase == Demo2Phase.Reward && _screen != ScreenKind.Reward) Show(ScreenKind.Reward);
else if (_service.Phase is Demo2Phase.Victory or Demo2Phase.Defeat && _screen != ScreenKind.End) Show(ScreenKind.End);
else if (_service.Phase is Demo2Phase.Build or Demo2Phase.Production or Demo2Phase.Defense && _screen != ScreenKind.Factory) Show(ScreenKind.Factory);
}
private void Show(ScreenKind screen)
{
_screen = screen;
foreach (var pair in _screens) pair.Value.style.display = pair.Key == screen ? DisplayStyle.Flex : DisplayStyle.None;
if (worldCamera != null) worldCamera.gameObject.SetActive(screen == ScreenKind.Factory);
if (proxyPool != null) proxyPool.gameObject.SetActive(screen == ScreenKind.Factory);
if (itemBatchRenderer != null) itemBatchRenderer.gameObject.SetActive(screen == ScreenKind.Factory);
_dirty = true;
_root?.schedule.Execute(SyncTutorialAnchors).ExecuteLater(1);
}
private void Refresh()
{
_dirty = false;
SetText("transport-button", "传输:" + _transportMode.ToString().ToUpperInvariant());
if (_service == null) return;
SetText("lobby-status", _lan?.Status ?? "offline");
SetText("lobby-players", BuildPlayerList());
Require<Button>("lobby-start-button").SetEnabled(_lan?.IsHost == true);
if (_service.EcsWorld != null)
{
var manager = _service.EcsWorld.EntityManager;
var economy = manager.GetComponentData<Demo2EconomyState>(_service.EcsWorld.Singleton);
var core = manager.GetComponentData<Demo2CoreHealth>(_service.EcsWorld.Singleton);
SetText("core-text", "核心 " + new string('◆', Math.Max(0, core.Value)));
SetText("money-text", "资金 " + SaturatingMath.Format(economy.Money));
SetText("phase-text", $"周期 {_service.Cycle}/5 · {PhaseName(_service.Phase)}");
SetText("timer-text", TimeSpan.FromSeconds(_service.PhaseRemainingSeconds).ToString(@"mm\:ss"));
SetText("target-text", $"目标火力 {SaturatingMath.Format(_service.Content.Waves[Math.Max(0, _service.Cycle - 1)].TotalHealth)}\n载荷 × 倍率\n{SaturatingMath.Format(economy.Payload)} × {economy.MultiplierMilli / 1000f:0.00}");
}
SetText("selection-text", (_removeMode ? "拆除模式" : "建造 " + MachineName(_selectedKind)) + $" · 朝向 {_rotation * 90}°");
SetText("error-text", _service.LastError);
SetText("network-text", _lan?.Status ?? "单人本地");
var recipeName = _service.Content.Recipes.TryGetValue(_selectedRecipeId, out var recipe) ? recipe.Name : "无";
SetText("recipe-button", "配方:" + recipeName);
RefreshMachineSelection();
RefreshRules();
RefreshRewards();
SetText("end-title", _service.Phase == Demo2Phase.Victory ? "工厂守住了核心" : "核心防线失守");
SetText("end-summary", $"最终周期 {_service.Cycle}/5\n火力 {SaturatingMath.Format(_service.LastFirepower)} / 敌军 {SaturatingMath.Format(_service.LastEnemyHealth)}");
}
private void RefreshMachineSelection()
{
var names = new[] { "miner", "conveyor", "splitter", "smelter", "assembler", "loader", "turret", "generator" };
var kinds = new[] { Demo2MachineKind.Miner, Demo2MachineKind.Conveyor, Demo2MachineKind.Splitter, Demo2MachineKind.Smelter, Demo2MachineKind.Assembler, Demo2MachineKind.Loader, Demo2MachineKind.Turret, Demo2MachineKind.Generator };
for (var i = 0; i < names.Length; i++) Require<Button>(names[i] + "-button").EnableInClassList("selected-machine", !_removeMode && kinds[i] == _selectedKind);
Require<Button>("remove-button").EnableInClassList("selected-remove", _removeMode);
}
private void RefreshRules()
{
for (var i = 0; i < 5; i++)
{
if (i < _service!.RuleIds.Count && _service.Content.Rules.TryGetValue(_service.RuleIds[i], out var rule))
{
SetText("rule-name-" + (i + 1), $"{i + 1}. {rule.Name}");
SetText("rule-description-" + (i + 1), rule.Description);
}
else
{
SetText("rule-name-" + (i + 1), $"{i + 1}. 空槽位");
SetText("rule-description-" + (i + 1), "防守成功后选择规则");
}
}
}
private void RefreshRewards()
{
for (var i = 0; i < 3; i++)
{
var button = Require<Button>("reward-choose-" + (i + 1));
Demo2RuleDefinition? rule = null;
var available = i < _service!.RewardChoices.Count && _service.Content.Rules.TryGetValue(_service.RewardChoices[i], out rule);
button.SetEnabled(available);
if (!available || rule == null) continue;
SetText("reward-category-" + (i + 1), CategoryName(rule.Category));
SetText("reward-name-" + (i + 1), rule.Name);
SetText("reward-description-" + (i + 1), rule.Description);
}
}
private string BuildPlayerList()
{
if (_lan == null || _lan.Players.Count == 0) return "等待房间同步…";
return string.Join("\n", _lan.Players.Select(player => (player.Connected ? "● " : "○ ") + player.DisplayName + (player.Ready ? " 已准备" : " 未准备")));
}
private void ApplySafeArea()
{
if (_root == null) return;
var safe = Require<VisualElement>("safe-area");
var area = Screen.safeArea;
var width = Mathf.Max(1, Screen.width);
var height = Mathf.Max(1, Screen.height);
var panelWidth = _root.resolvedStyle.width;
var panelHeight = _root.resolvedStyle.height;
if (float.IsNaN(panelWidth) || float.IsNaN(panelHeight) || panelWidth <= 0 || panelHeight <= 0) return;
safe.style.left = area.xMin / width * panelWidth;
safe.style.right = (width - area.xMax) / width * panelWidth;
safe.style.top = (height - area.yMax) / height * panelHeight;
safe.style.bottom = area.yMin / height * panelHeight;
}
private void SyncTutorialAnchors()
{
if (_root == null || tutorialAnchorRoot == null) return;
var panelWidth = Mathf.Max(1, _root.resolvedStyle.width);
var panelHeight = Mathf.Max(1, _root.resolvedStyle.height);
tutorialAnchorRoot.sizeDelta = new Vector2(Screen.width, Screen.height);
foreach (var target in TutorialTargets)
{
if (!_tutorialAnchors.TryGetValue(target.anchorId, out var anchor)) continue;
var element = _root.Q<VisualElement>(target.elementName);
var displayed = element != null && IsDisplayed(element);
anchor.gameObject.SetActive(displayed);
if (!displayed || element == null) continue;
var bounds = element.worldBound;
var x = bounds.xMin / panelWidth * Screen.width;
var y = Screen.height - bounds.yMax / panelHeight * Screen.height;
var width = bounds.width / panelWidth * Screen.width;
var height = bounds.height / panelHeight * Screen.height;
anchor.anchorMin = Vector2.zero;
anchor.anchorMax = Vector2.zero;
anchor.pivot = Vector2.zero;
anchor.anchoredPosition = new Vector2(x, y);
anchor.sizeDelta = new Vector2(width, height);
}
}
private static bool IsDisplayed(VisualElement element)
{
for (var current = element; current != null; current = current.parent)
if (current.resolvedStyle.display == DisplayStyle.None) return false;
return true;
}
private void Bind(string name, Action action) => Require<Button>(name).clicked += action;
private T Require<T>(string name) where T : VisualElement
=> _root?.Q<T>(name) ?? throw new InvalidOperationException("Missing Demo2 UI Toolkit element: " + name);
private void SetText(string name, string value)
{
var text = Require<TextElement>(name);
text.text = value;
}
private void MarkDirty() => _dirty = true;
private static string PhaseName(Demo2Phase phase) => phase switch { Demo2Phase.Build => "建造", Demo2Phase.Production => "自动生产", Demo2Phase.Defense => "炮塔结算", Demo2Phase.Reward => "规则选牌", _ => phase.ToString() };
private static string MachineName(Demo2MachineKind kind) => kind switch { Demo2MachineKind.Miner => "采集器", Demo2MachineKind.Conveyor => "传送带", Demo2MachineKind.Splitter => "分流器", Demo2MachineKind.Smelter => "熔炼机", Demo2MachineKind.Assembler => "组装机", Demo2MachineKind.Loader => "装填器", Demo2MachineKind.Turret => "炮塔", Demo2MachineKind.Generator => "能量站", _ => kind.ToString() };
private static string CategoryName(Demo2RuleCategory category) => category switch { Demo2RuleCategory.Production => "生产链修正", Demo2RuleCategory.Logistics => "物流条件", Demo2RuleCategory.MachineState => "机器状态", Demo2RuleCategory.CycleSettlement => "周期结算", Demo2RuleCategory.RiskExchange => "风险交换", _ => category.ToString() };
private static void LogException(Exception exception) => Debug.LogException(exception);
}
}