#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(); [SerializeField] private Font? uiFont; [SerializeField] private TMP_FontAsset? tutorialFont; [SerializeField] private RectTransform? tutorialAnchorRoot; private readonly Dictionary _screens = new(); private readonly Dictionary _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("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("grid-input-surface"), worldCamera); _gridInput.GridClicked += OnGridClicked; _root.RegisterCallback(_ => { 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("title-screen"); _screens[ScreenKind.Lobby] = Require("lobby-screen"); _screens[ScreenKind.Factory] = Require("factory-screen"); _screens[ScreenKind.Reward] = Require("reward-screen"); _screens[ScreenKind.End] = Require("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("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(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("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