#nullable enable using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using ReplacedPerson.Core; using ShrinkTutorial; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace ReplacedPerson.Runtime { public sealed class ReplacedPersonDemoController : MonoBehaviour { private enum DemoScreen { Title, Chapter, Deck, Lan, Battle, Reward, Settings } private readonly Dictionary _screens = new(); private readonly List _selectedNormalIndices = new(); private readonly List _selectedEndIndices = new(); private ReplacedPersonGameService? _service; private int _rerollSlot = -1; private string _lastError = string.Empty; private string _transport = "TCP"; private bool _networkBattle; private ReplacedPersonAudio? _audio; private DemoScreen? _activeScreen; private string _lastResolutionKey = string.Empty; private Coroutine? _resolutionSequence; private bool _resolutionAnimating; private bool _deferServiceRefresh; private ReplacedTurnSubmission? _presentationLocalSubmission; private ReplacedTurnSubmission? _presentationEnemySubmission; private int _presentationRound; private int _presentationLeadPlayer = -1; private int _lastPresentedRound; private Transform UiRoot => transform.Find("Canvas/SafeAreaRoot")!; private void Awake() { EnsureEventSystem(); _audio = GetComponent() ?? gameObject.AddComponent(); var tutorialSettings = Resources.Load("ReplacedPersonTutorialSettings"); if (tutorialSettings != null) ShrinkTutorialManager.EnsureInstance(tutorialSettings); _service = ReplacedPersonGameService.Current ?? new ReplacedPersonGameService(); CacheScreens(); Bind("Screens/TitleScreen/Actions/ContinueButton", OpenChapter); Bind("Screens/TitleScreen/Actions/DeckButton", () => Show(DemoScreen.Deck)); Bind("Screens/TitleScreen/Actions/LanButton", () => Show(DemoScreen.Lan)); Bind("Screens/TitleScreen/Actions/SettingsButton", () => Show(DemoScreen.Settings)); BindBackButtons(); Bind("Screens/ChapterScreen/Footer/StartBattleButton", StartChapterBattle); Bind("Screens/BattleScreen/PlayerArea/Actions/SubmitButton", SubmitTurn); Bind("Screens/BattleScreen/PlayerArea/Actions/PassButton", PassTurn); Bind("Screens/BattleScreen/PlayerArea/Actions/Reroll1Button", () => SetReroll(0)); Bind("Screens/BattleScreen/PlayerArea/Actions/Reroll2Button", () => SetReroll(1)); Bind("Screens/BattleScreen/PlayerArea/Actions/Reroll3Button", () => SetReroll(2)); Bind("Screens/RewardScreen/Footer/ContinueButton", () => Show(_networkBattle ? DemoScreen.Lan : DemoScreen.Chapter)); Bind("Screens/RewardScreen/Content/Ornaments/GoldEyeButton", () => ToggleOrnament("ornament.gold-eye")); Bind("Screens/RewardScreen/Content/Ornaments/GoldChainButton", () => ToggleOrnament("ornament.gold-chain")); Bind("Screens/RewardScreen/Content/Ornaments/WornSuitButton", () => ToggleOrnament("ornament.worn-suit")); Bind("Screens/DeckScreen/Footer/SaveButton", () => _service.SaveAsync().Forget(LogException)); Bind("Screens/DeckScreen/Footer/ResetButton", ResetDeck); Bind("Screens/SettingsScreen/Content/TcpButton", () => SetTransport("TCP")); Bind("Screens/SettingsScreen/Content/KcpButton", () => SetTransport("KCP")); Bind("Screens/LanScreen/Content/HostButton", HostLan); Bind("Screens/LanScreen/Content/JoinButton", JoinLan); Bind("Screens/LanScreen/Content/StartMatchButton", StartLanBattle); _service.Changed += RefreshCurrent; Show(DemoScreen.Title); ApplyLaunchArguments(); } private void OnDestroy() { if (_service != null) _service.Changed -= RefreshCurrent; if (ReplacedLanRuntime.Instance != null) ReplacedLanRuntime.Instance.Changed -= RefreshCurrent; } private void CacheScreens() { var root = UiRoot.Find("Screens"); if (root == null) throw new InvalidOperationException("Missing UI path Canvas/SafeAreaRoot/Screens."); foreach (DemoScreen value in Enum.GetValues(typeof(DemoScreen))) { var child = root.Find(value + "Screen"); if (child == null) throw new InvalidOperationException("Missing screen: " + value); _screens.Add(value, child.gameObject); } } private void BindBackButtons() { foreach (var screen in new[] { "ChapterScreen", "DeckScreen", "LanScreen", "SettingsScreen" }) Bind($"Screens/{screen}/Header/BackButton", () => Show(DemoScreen.Title)); Bind("Screens/BattleScreen/Header/BackButton", ExitBattle); } private void ExitBattle() => Show(_networkBattle ? DemoScreen.Lan : DemoScreen.Chapter); private void Show(DemoScreen screen) { var changed = _activeScreen != screen; foreach (var pair in _screens) pair.Value.SetActive(pair.Key == screen); _activeScreen = screen; switch (screen) { case DemoScreen.Chapter: RefreshChapter(); break; case DemoScreen.Deck: RefreshDeck(); break; case DemoScreen.Battle: RefreshBattle(); break; case DemoScreen.Reward: RefreshReward(); break; case DemoScreen.Settings: RefreshSettings(); break; case DemoScreen.Lan: RefreshLan(); break; } if (!changed) return; var motion = _screens[screen].GetComponent() ?? _screens[screen].AddComponent(); motion.Play(); if (screen == DemoScreen.Reward) { var state = ActiveState(); var localWinner = ActivePlayerIndex() == 0 ? ReplacedWinner.PlayerOne : ReplacedWinner.PlayerTwo; if (state?.Winner == localWinner) _audio?.PlayVictory(); else _audio?.PlayDefeat(); } } private void OpenChapter() => Show(DemoScreen.Chapter); private void StartChapterBattle() { _networkBattle = false; var enemy = _service!.SaveData.ChapterProgress > 0 ? "greed.full" : "greed.tutorial"; _selectedNormalIndices.Clear(); _selectedEndIndices.Clear(); _rerollSlot = -1; _lastError = string.Empty; ResetResolutionPresentation(); _service.StartAi(enemy); Show(DemoScreen.Battle); ResetCardDetail(); _audio?.PlayCardDraw(); var tutorial = ShrinkTutorialManager.EnsureInstance(); tutorial.StartTutorial("replaced-person.first-match"); ApplyTutorialPresentation(tutorial); } private void SubmitTurn() { var state = ActiveState(); if (state == null) return; var localPlayer = ActivePlayerIndex(); if (localPlayer < 0) return; var self = state.Players[localPlayer]; var submittedRound = state.Round; var submittedLeadPlayer = state.LeadPlayer; var enemySubmission = state.Submissions[1 - localPlayer]?.Clone(); var replayStart = !_networkBattle ? _service?.Match?.Replay.Count ?? 0 : 0; var endCount = state.IsLastStand ? 2 : 1; var submission = new ReplacedTurnSubmission { NormalCardIds = SelectedCardIds(self.NormalHand, _selectedNormalIndices, 4), EndCardIds = SelectedCardIds(self.EndHand, _selectedEndIndices, endCount), RerollSlot = _rerollSlot }; _deferServiceRefresh = true; ReplacedCommandResult result; try { result = _networkBattle ? ReplacedLanRuntime.Instance!.Submit(submission) : _service!.SubmitPlayer(submission); } finally { _deferServiceRefresh = false; } _lastError = result.Accepted ? string.Empty : result.Message; if (result.Accepted) { CaptureCommittedPresentation(submission, enemySubmission, submittedRound, submittedLeadPlayer, replayStart); _audio?.PlayLock(); ShrinkTutorialManager.Instance?.CompleteStep("rp.matchup-submitted"); _selectedNormalIndices.Clear(); _selectedEndIndices.Clear(); _rerollSlot = -1; } else _audio?.PlayError(); RefreshBattle(); } private void PassTurn() { var state = ActiveState(); var localPlayer = ActivePlayerIndex(); if (state == null || localPlayer < 0) return; var submission = new ReplacedTurnSubmission { Pass = true, DiscardEndOnPass = false }; var submittedRound = state.Round; var submittedLeadPlayer = state.LeadPlayer; var enemySubmission = state.Submissions[1 - localPlayer]?.Clone(); var replayStart = !_networkBattle ? _service?.Match?.Replay.Count ?? 0 : 0; _deferServiceRefresh = true; ReplacedCommandResult result; try { result = _networkBattle ? ReplacedLanRuntime.Instance!.Submit(submission) : _service!.SubmitPlayer(submission); } finally { _deferServiceRefresh = false; } _lastError = result.Accepted ? string.Empty : result.Message; if (result.Accepted) { CaptureCommittedPresentation(submission, enemySubmission, submittedRound, submittedLeadPlayer, replayStart); _audio?.PlayLock(); } else _audio?.PlayError(); RefreshBattle(); } private void SetReroll(int slot) { _rerollSlot = _rerollSlot == slot ? -1 : slot; ShrinkTutorialManager.Instance?.CompleteStep("rp.reroll-marked"); RefreshBattle(); } private void RefreshCurrent() { if (_deferServiceRefresh) return; var active = _screens.FirstOrDefault(pair => pair.Value.activeSelf).Key; Show(active); } private void RefreshChapter() { var progress = _service!.SaveData.ChapterProgress; Text("Screens/ChapterScreen/Content/ChapterLabel").text = "第一章 · 贪婪"; Text("Screens/ChapterScreen/Content/StoryText").text = progress switch { 0 => "办公室的灯没有熄灭。葛朗台把亏损归罪于每一个人,也把家人的沉默换算成数字。黑色的影子从账本背后站起:‘我就是你。’", 1 => "梦醒之后,他仍把亲人的死亡视作一笔支出。贪婪再次逼近,金链、褶皱西装和势利的眼睛逐渐从宿主身上剥离。", _ => "贪婪被击败,装饰与卡牌已经进入收藏。第一章试玩内容完成。" }; var buttonText = Text("Screens/ChapterScreen/Footer/StartBattleButton/Label"); buttonText.text = progress == 0 ? "进入第一战" : progress == 1 ? "进入第二战" : "重战完整形态"; } private void RefreshBattle() { var state = ActiveState(); if (state == null) return; var catalog = ActiveContent(); var localPlayer = ActivePlayerIndex(); if (localPlayer < 0) return; var resolutionKey = BuildResolutionKey(state); var hasNewResolution = state.LastResolution.Count > 0 && !string.Equals(_lastResolutionKey, resolutionKey, StringComparison.Ordinal); var needsRoundIntro = !hasNewResolution && !_resolutionAnimating && state.Phase != ReplacedMatchPhase.Completed && state.Round != _lastPresentedRound; if (hasNewResolution || needsRoundIntro) { _resolutionAnimating = true; if (hasNewResolution) _lastResolutionKey = resolutionKey; } if (state.Phase == ReplacedMatchPhase.Completed && !hasNewResolution && !_resolutionAnimating) { Show(DemoScreen.Reward); return; } var self = state.Players[localPlayer]; var enemy = state.Players[1 - localPlayer]; RemoveInvalidIndices(_selectedNormalIndices, self.NormalHand.Count); RemoveInvalidIndices(_selectedEndIndices, self.EndHand.Count); Text("Screens/BattleScreen/Header/RoundText").text = $"回合 {state.Round} 先手骰 {state.InitiativeRoll}"; Text("Screens/BattleScreen/EnemyArea/EnemyStatus").text = $"{enemy.DisplayName} HP {Math.Max(0, enemy.Health)}/{enemy.MaxHealth} COST {enemy.Cost}/10"; Text("Screens/BattleScreen/PlayerArea/PlayerStatus").text = $"被替代之人 HP {Math.Max(0, self.Health)}/{self.MaxHealth} COST {self.Cost}/10"; Text("Screens/BattleScreen/EnemyArea/EnemyQueue").text = BuildSubmission(catalog, state.Submissions[1 - localPlayer], "等待敌方锁定"); for (var i = 0; i < 3; i++) { var slotObject = UiRoot.Find($"Screens/BattleScreen/EffectArea/Slot{i + 1}")!.gameObject; var motion = slotObject.GetComponent() ?? slotObject.AddComponent(); if (!motion.IsPlaying) Text($"Screens/BattleScreen/EffectArea/Slot{i + 1}/Text").text = BuildSlot(state, i, localPlayer); } Text("Screens/BattleScreen/LogPanel/LogText").text = string.Join("\n", state.Log.TakeLast(7)); Text("Screens/BattleScreen/PlayerArea/SelectionText").text = (_selectedNormalIndices.Count == 0 ? "普通牌执行:未选择" : "普通牌执行:" + FormatSelectedCards(self.NormalHand, _selectedNormalIndices, id => catalog.Cards[id].Name, " → ")) + "\n" + (_selectedEndIndices.Count == 0 ? "结束牌:未选择" : "结束牌:" + FormatSelectedCards(self.EndHand, _selectedEndIndices, id => catalog.EndCards[id].Name, " / ")) + (_rerollSlot >= 0 ? $" 重掷槽 {_rerollSlot + 1}" : string.Empty) + (string.IsNullOrWhiteSpace(_lastError) ? string.Empty : "\n" + _lastError + ""); PopulateCardButtons("Screens/BattleScreen/PlayerArea/NormalHand", self.NormalHand, false); PopulateCardButtons("Screens/BattleScreen/PlayerArea/EndHand", self.EndHand, true); var playerTurn = (state.Phase == ReplacedMatchPhase.AwaitingLead ? state.LeadPlayer : 1 - state.LeadPlayer) == localPlayer && (!_networkBattle || ReplacedLanRuntime.Instance?.AwaitingCommand != true) && !_resolutionAnimating; Button("Screens/BattleScreen/PlayerArea/Actions/SubmitButton").interactable = playerTurn; Button("Screens/BattleScreen/PlayerArea/Actions/PassButton").interactable = playerTurn; SetHandInteraction("Screens/BattleScreen/PlayerArea/NormalHand", playerTurn); SetHandInteraction("Screens/BattleScreen/PlayerArea/EndHand", playerTurn); for (var i = 0; i < 3; i++) { var button = Button($"Screens/BattleScreen/PlayerArea/Actions/Reroll{i + 1}Button"); button.interactable = playerTurn; button.image.color = _rerollSlot == i ? new Color(0.75f, 0.19f, 0.16f, 1f) : new Color(0.20f, 0.22f, 0.23f, 1f); } if (hasNewResolution) { if (_resolutionSequence != null) StopCoroutine(_resolutionSequence); _resolutionSequence = StartCoroutine(PlayResolutionSequence(state.LastResolution.ToArray(), localPlayer)); } else if (needsRoundIntro) { if (_resolutionSequence != null) StopCoroutine(_resolutionSequence); _resolutionSequence = StartCoroutine(PlayRoundIntroSequence(state, localPlayer)); } } private void PopulateCardButtons(string path, IReadOnlyList cards, bool endings) { var root = UiRoot.Find(path)!; var template = root.Find("CardButtonTemplate")!.gameObject; for (var i = root.childCount - 1; i >= 0; i--) { var child = root.GetChild(i); if (child.gameObject != template) Destroy(child.gameObject); } for (var i = 0; i < cards.Count; i++) { var id = cards[i]; var handIndex = i; var item = Instantiate(template, root); item.name = (endings ? "End_" : "Normal_") + i; item.SetActive(true); var selectedIndices = endings ? _selectedEndIndices : _selectedNormalIndices; var order = selectedIndices.IndexOf(handIndex); var selected = order >= 0; var orderPrefix = selected ? $"[{order + 1}] " : string.Empty; var label = item.transform.Find("Label")!.GetComponent(); if (endings) { var definition = ActiveContent().EndCards[id]; label.text = $"{orderPrefix}{definition.Name}\n结束牌\n{definition.Cost}C"; } else { var definition = ActiveContent().Cards[id]; label.text = $"{orderPrefix}{definition.Name}\n{TypeName(definition.EffectType)} {definition.BaseValue}+{definition.Factor}D6\n{definition.Cost}C"; } var button = item.GetComponent