948 lines
48 KiB
C#
948 lines
48 KiB
C#
#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<DemoScreen, GameObject> _screens = new();
|
||
private readonly List<int> _selectedNormalIndices = new();
|
||
private readonly List<int> _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<ReplacedPersonAudio>() ?? gameObject.AddComponent<ReplacedPersonAudio>();
|
||
var tutorialSettings = Resources.Load<ShrinkTutorialSettings>("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<ReplacedScreenMotion>() ?? _screens[screen].AddComponent<ReplacedScreenMotion>();
|
||
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<ReplacedSlotResolutionMotion>() ?? slotObject.AddComponent<ReplacedSlotResolutionMotion>();
|
||
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<color=#E46F5D>" + _lastError + "</color>");
|
||
|
||
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<string> 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<TMP_Text>();
|
||
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<Button>();
|
||
button.image.color = selected ? new Color(0.74f, 0.19f, 0.16f, 0.96f) : new Color(0.90f, 0.87f, 0.78f, 0.96f);
|
||
label.color = selected ? new Color(.96f, .94f, .88f, 1f) : new Color(.12f, .13f, .13f, 1f);
|
||
var motion = item.GetComponent<ReplacedCardMotion>() ?? item.AddComponent<ReplacedCardMotion>();
|
||
motion.Configure(() => ShowCardDetail(id, endings), i * .025f);
|
||
var dragHandler = item.GetComponent<ReplacedCardDragHandler>();
|
||
dragHandler.Configure(position => ReorderCard(handIndex, endings, position));
|
||
button.onClick.AddListener(() =>
|
||
{
|
||
_audio?.PlayCardSelect();
|
||
ShowCardDetail(id, endings);
|
||
ToggleCard(handIndex, endings);
|
||
});
|
||
}
|
||
}
|
||
|
||
private void ApplyTutorialPresentation(ShrinkTutorialManager tutorial)
|
||
{
|
||
var font = GetComponentInChildren<TMP_Text>(true)?.font;
|
||
foreach (var text in tutorial.GetComponentsInChildren<TMP_Text>(true))
|
||
{
|
||
if (font != null) text.font = font;
|
||
if (string.Equals(text.text, "Skip", StringComparison.OrdinalIgnoreCase)) text.text = "跳过";
|
||
}
|
||
}
|
||
|
||
private void ToggleCard(int handIndex, bool ending)
|
||
{
|
||
var state = ActiveState()!;
|
||
var localPlayer = ActivePlayerIndex();
|
||
var hand = ending ? state.Players[localPlayer].EndHand : state.Players[localPlayer].NormalHand;
|
||
if (handIndex < 0 || handIndex >= hand.Count) return;
|
||
if (ending)
|
||
{
|
||
var max = state.IsLastStand ? 2 : 1;
|
||
if (_selectedEndIndices.Contains(handIndex)) _selectedEndIndices.Remove(handIndex);
|
||
else
|
||
{
|
||
if (_selectedEndIndices.Count >= max) _selectedEndIndices.RemoveAt(0);
|
||
_selectedEndIndices.Add(handIndex);
|
||
}
|
||
ShrinkTutorialManager.Instance?.CompleteStep("rp.end-selected");
|
||
}
|
||
else
|
||
{
|
||
if (_selectedNormalIndices.Contains(handIndex)) _selectedNormalIndices.Remove(handIndex);
|
||
else if (_selectedNormalIndices.Count < 4) _selectedNormalIndices.Add(handIndex);
|
||
ShrinkTutorialManager.Instance?.CompleteStep("rp.card-selected");
|
||
}
|
||
RefreshBattle();
|
||
}
|
||
|
||
private void ReorderCard(int handIndex, bool ending, Vector2 screenPosition)
|
||
{
|
||
var state = ActiveState();
|
||
var localPlayer = ActivePlayerIndex();
|
||
if (state == null || localPlayer < 0) return;
|
||
var hand = ending ? state.Players[localPlayer].EndHand : state.Players[localPlayer].NormalHand;
|
||
if (handIndex < 0 || handIndex >= hand.Count) return;
|
||
|
||
var selectedIndices = ending ? _selectedEndIndices : _selectedNormalIndices;
|
||
var max = ending && state.IsLastStand ? 2 : ending ? 1 : 4;
|
||
var wasSelected = selectedIndices.Remove(handIndex);
|
||
if (!wasSelected && selectedIndices.Count >= max)
|
||
{
|
||
if (!ending)
|
||
{
|
||
_lastError = "普通牌最多选择 4 张";
|
||
_audio?.PlayError();
|
||
RefreshBattle();
|
||
return;
|
||
}
|
||
selectedIndices.RemoveAt(0);
|
||
}
|
||
|
||
var insertIndex = FindDropOrder(selectedIndices, ending, screenPosition);
|
||
selectedIndices.Insert(Mathf.Clamp(insertIndex, 0, selectedIndices.Count), handIndex);
|
||
_lastError = string.Empty;
|
||
_audio?.PlayCardSelect();
|
||
ShowCardDetail(hand[handIndex], ending);
|
||
ShrinkTutorialManager.Instance?.CompleteStep(ending ? "rp.end-selected" : "rp.card-selected");
|
||
RefreshBattle();
|
||
}
|
||
|
||
private int FindDropOrder(IReadOnlyList<int> selectedIndices, bool ending, Vector2 screenPosition)
|
||
{
|
||
if (selectedIndices.Count == 0) return 0;
|
||
var handRoot = UiRoot.Find(HandPath(ending));
|
||
if (handRoot == null) return selectedIndices.Count;
|
||
var canvas = handRoot.GetComponentInParent<Canvas>()?.rootCanvas;
|
||
var camera = canvas == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
|
||
var closestOrder = -1;
|
||
var closestDistance = float.MaxValue;
|
||
var closestCenterX = 0f;
|
||
for (var order = 0; order < selectedIndices.Count; order++)
|
||
{
|
||
var card = handRoot.Find((ending ? "End_" : "Normal_") + selectedIndices[order]) as RectTransform;
|
||
if (card == null) continue;
|
||
var center = RectTransformUtility.WorldToScreenPoint(camera, card.TransformPoint(card.rect.center));
|
||
var distance = Mathf.Abs(screenPosition.x - center.x);
|
||
if (distance >= closestDistance) continue;
|
||
closestOrder = order;
|
||
closestDistance = distance;
|
||
closestCenterX = center.x;
|
||
}
|
||
if (closestOrder < 0) return selectedIndices.Count;
|
||
return screenPosition.x < closestCenterX ? closestOrder : closestOrder + 1;
|
||
}
|
||
|
||
private static string HandPath(bool ending) => ending
|
||
? "Screens/BattleScreen/PlayerArea/EndHand"
|
||
: "Screens/BattleScreen/PlayerArea/NormalHand";
|
||
|
||
private void SetHandInteraction(string path, bool interactable)
|
||
{
|
||
var root = UiRoot.Find(path);
|
||
if (root == null) return;
|
||
foreach (var button in root.GetComponentsInChildren<Button>(false))
|
||
{
|
||
if (button.gameObject.name == "CardButtonTemplate") continue;
|
||
button.interactable = interactable;
|
||
var group = button.GetComponent<CanvasGroup>();
|
||
if (group != null) group.interactable = interactable;
|
||
}
|
||
}
|
||
|
||
private void CaptureCommittedPresentation(ReplacedTurnSubmission localSubmission,
|
||
ReplacedTurnSubmission? enemySubmission, int round, int leadPlayer, int replayStart)
|
||
{
|
||
_presentationLocalSubmission = localSubmission.Clone();
|
||
_presentationEnemySubmission = enemySubmission?.Clone();
|
||
_presentationRound = round;
|
||
_presentationLeadPlayer = leadPlayer;
|
||
if (_presentationEnemySubmission != null || _networkBattle || _service?.Match == null) return;
|
||
var enemyCommand = _service.Match.Replay.Skip(replayStart).FirstOrDefault(command => command.PlayerIndex == 1);
|
||
_presentationEnemySubmission = enemyCommand?.Submission.Clone();
|
||
}
|
||
|
||
private void RefreshDeck()
|
||
{
|
||
var deck = _service!.SaveData.ActiveDeck;
|
||
var catalog = _service.Content.Current;
|
||
var errors = _service.ValidateDeck();
|
||
Text("Screens/DeckScreen/Content/DeckStatus").text =
|
||
$"流派:{DeckArchetype(deck, catalog)} 普通牌 {deck.NormalCards.Count}/12 结束牌 {deck.EndCards.Count}/4" +
|
||
(errors.Count == 0 ? " <color=#8FCB9B>牌组合法</color>" : " <color=#E46F5D>" + string.Join(",", errors) + "</color>");
|
||
PopulateDeckList("Screens/DeckScreen/Content/CurrentDeck/Viewport/Content", deck.NormalCards, id =>
|
||
{
|
||
deck.NormalCards.Remove(id); RefreshDeck();
|
||
}, catalog);
|
||
PopulateDeckList("Screens/DeckScreen/Content/Collection/Viewport/Content",
|
||
_service.SaveData.CollectedCards.Where(catalog.Cards.ContainsKey).Distinct().ToList(), id =>
|
||
{
|
||
var owned = _service.SaveData.CollectedCards.Count(value => value == id);
|
||
var inDeck = deck.NormalCards.Count(value => value == id);
|
||
if (deck.NormalCards.Count < 12 && inDeck < Math.Min(2, owned)) deck.NormalCards.Add(id);
|
||
RefreshDeck();
|
||
}, catalog);
|
||
}
|
||
|
||
private void PopulateDeckList(string path, IReadOnlyList<string> ids, Action<string> click, ReplacedContentCatalog catalog)
|
||
{
|
||
var root = UiRoot.Find(path)!;
|
||
var template = root.Find("RowTemplate")!.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 < ids.Count; i++)
|
||
{
|
||
var id = ids[i];
|
||
var row = Instantiate(template, root);
|
||
row.SetActive(true);
|
||
row.name = "Card_" + i;
|
||
var card = catalog.Cards[id];
|
||
row.transform.Find("Label")!.GetComponent<TMP_Text>().text = $"{card.Name} {TypeName(card.EffectType)} {card.BaseValue}+{card.Factor}D6 {card.Cost}C";
|
||
row.GetComponent<Button>().onClick.AddListener(() =>
|
||
{
|
||
_audio?.PlayCardSelect();
|
||
click(id);
|
||
});
|
||
}
|
||
}
|
||
|
||
private void ResetDeck()
|
||
{
|
||
_service!.SaveData.ActiveDeck = ReplacedBuiltInContent.CreateStarterDeck();
|
||
RefreshDeck();
|
||
}
|
||
|
||
private void RefreshReward()
|
||
{
|
||
var service = _service!;
|
||
var state = ActiveState();
|
||
var localWinner = ActivePlayerIndex() == 0 ? ReplacedWinner.PlayerOne : ReplacedWinner.PlayerTwo;
|
||
var won = state?.Winner == localWinner;
|
||
Text("Screens/RewardScreen/Content/ResultText").text = won ? "胜利" : "意识被压回黑暗";
|
||
Text("Screens/RewardScreen/Content/RewardText").text = _networkBattle
|
||
? (won ? "LAN 对局获胜。联网对局不修改章节奖励。" : "LAN 对局结束。可以返回大厅重新准备。")
|
||
: won
|
||
? "已收集:\n" + string.Join("\n", service.SaveData.Ornaments.Concat(service.SaveData.CollectedCards.Where(id => id.StartsWith("greed.", StringComparison.Ordinal))))
|
||
: "宿主的负面情绪仍在。可以重新进入战斗。";
|
||
Text("Screens/RewardScreen/Content/AppearanceText").text = service.SaveData.EquippedOrnaments.Count == 0
|
||
? "当前装饰:无"
|
||
: "当前装饰:" + string.Join(" / ", service.SaveData.EquippedOrnaments.Select(OrnamentName));
|
||
RefreshOrnamentButton("GoldEyeButton", "ornament.gold-eye", "金色势利眼");
|
||
RefreshOrnamentButton("GoldChainButton", "ornament.gold-chain", "金链");
|
||
RefreshOrnamentButton("WornSuitButton", "ornament.worn-suit", "褶皱西装");
|
||
}
|
||
|
||
private void ToggleOrnament(string id)
|
||
{
|
||
_service!.EquipOrnament(id);
|
||
RefreshReward();
|
||
}
|
||
|
||
private void RefreshOrnamentButton(string objectName, string id, string label)
|
||
{
|
||
var button = Button("Screens/RewardScreen/Content/Ornaments/" + objectName);
|
||
var owned = _service!.SaveData.Ornaments.Contains(id);
|
||
var equipped = _service.SaveData.EquippedOrnaments.Contains(id);
|
||
button.interactable = owned;
|
||
button.image.color = equipped ? new Color(.67f, .13f, .11f, 1f) : owned ? new Color(.25f, .27f, .27f, 1f) : new Color(.11f, .12f, .12f, .65f);
|
||
button.transform.Find("Label")!.GetComponent<TMP_Text>().text = owned ? label : label + " · 未获得";
|
||
}
|
||
|
||
private static string OrnamentName(string id) => id switch
|
||
{
|
||
"ornament.gold-eye" => "金色势利眼",
|
||
"ornament.gold-chain" => "金链",
|
||
"ornament.worn-suit" => "褶皱西装",
|
||
_ => id
|
||
};
|
||
|
||
private void RefreshSettings()
|
||
{
|
||
Text("Screens/SettingsScreen/Content/TransportStatus").text = "LAN 传输:" + _transport;
|
||
Button("Screens/SettingsScreen/Content/TcpButton").image.color = _transport == "TCP" ? Color.white : Color.gray;
|
||
Button("Screens/SettingsScreen/Content/KcpButton").image.color = _transport == "KCP" ? Color.white : Color.gray;
|
||
}
|
||
|
||
private void SetTransport(string value)
|
||
{
|
||
_transport = value;
|
||
PlayerPrefs.SetString("ReplacedPerson.Transport", value);
|
||
RefreshSettings();
|
||
}
|
||
|
||
private void RefreshLan()
|
||
{
|
||
var runtime = ReplacedLanRuntime.Instance;
|
||
Text("Screens/LanScreen/Content/StatusText").text = runtime?.Status ?? "尚未连接";
|
||
Text("Screens/LanScreen/Content/DiscoveryText").text = runtime == null || runtime.DiscoveredRooms.Count == 0
|
||
? "局域网发现会自动广播可用房间"
|
||
: "已发现:\n" + string.Join("\n", runtime.DiscoveredRooms.Take(3));
|
||
Button("Screens/LanScreen/Content/StartMatchButton").interactable = runtime?.IsMatchReady == true;
|
||
}
|
||
|
||
private void HostLan()
|
||
{
|
||
var runtime = EnsureLan();
|
||
runtime.StartHost(_transport == "KCP");
|
||
RefreshLan();
|
||
}
|
||
|
||
private void JoinLan()
|
||
{
|
||
var input = UiRoot.Find("Screens/LanScreen/Content/AddressInput")!.GetComponent<TMP_InputField>();
|
||
var runtime = EnsureLan();
|
||
runtime.Join(string.IsNullOrWhiteSpace(input.text) ? "127.0.0.1" : input.text.Trim(), _transport == "KCP");
|
||
RefreshLan();
|
||
}
|
||
|
||
private ReplacedLanRuntime EnsureLan()
|
||
{
|
||
var runtime = ReplacedLanRuntime.Ensure();
|
||
runtime.Changed -= RefreshCurrent;
|
||
runtime.Changed += RefreshCurrent;
|
||
return runtime;
|
||
}
|
||
|
||
private void ApplyLaunchArguments()
|
||
{
|
||
var arguments = Environment.GetCommandLineArgs();
|
||
var host = arguments.FirstOrDefault(value => value.StartsWith("--rp-host=", StringComparison.OrdinalIgnoreCase));
|
||
var join = arguments.FirstOrDefault(value => value.StartsWith("--rp-join=", StringComparison.OrdinalIgnoreCase));
|
||
if (host == null && join == null) return;
|
||
var runtime = EnsureLan();
|
||
if (arguments.Any(value => string.Equals(value, "--rp-auto", StringComparison.OrdinalIgnoreCase))) runtime.EnableAutoPlay();
|
||
if (host != null)
|
||
{
|
||
_transport = host.Substring("--rp-host=".Length).Trim().ToUpperInvariant() == "KCP" ? "KCP" : "TCP";
|
||
runtime.StartHost(_transport == "KCP");
|
||
}
|
||
else
|
||
{
|
||
var values = join!.Substring("--rp-join=".Length).Split(',');
|
||
_transport = values[0].Trim().ToUpperInvariant() == "KCP" ? "KCP" : "TCP";
|
||
runtime.Join(values.Length > 1 ? values[1].Trim() : "127.0.0.1", _transport == "KCP");
|
||
}
|
||
Show(DemoScreen.Lan);
|
||
}
|
||
|
||
private void StartLanBattle()
|
||
{
|
||
var runtime = ReplacedLanRuntime.Instance;
|
||
if (runtime?.IsMatchReady != true) return;
|
||
_networkBattle = true;
|
||
_selectedNormalIndices.Clear();
|
||
_selectedEndIndices.Clear();
|
||
_rerollSlot = -1;
|
||
_lastError = string.Empty;
|
||
ResetResolutionPresentation();
|
||
Show(DemoScreen.Battle);
|
||
ResetCardDetail();
|
||
_audio?.PlayCardDraw();
|
||
}
|
||
|
||
private string BuildSubmission(ReplacedContentCatalog catalog, ReplacedTurnSubmission? submission, string fallback)
|
||
{
|
||
if (submission == null) return fallback;
|
||
if (submission.Pass) return "跳过";
|
||
return string.Join(" → ", submission.NormalCardIds.Select(id => catalog.Cards[id].Name)) +
|
||
" ▷ " + string.Join(" / ", submission.EndCardIds.Select(id => catalog.EndCards[id].Name));
|
||
}
|
||
|
||
private ReplacedMatchState? ActiveState() => _networkBattle
|
||
? ReplacedLanRuntime.Instance?.MatchState
|
||
: _service?.Match?.State;
|
||
|
||
private ReplacedContentCatalog ActiveContent() => _networkBattle
|
||
? ReplacedLanRuntime.Instance?.Content ?? ReplacedBuiltInContent.Create()
|
||
: _service?.Match?.Content ?? _service!.Content.Current;
|
||
|
||
private int ActivePlayerIndex() => _networkBattle ? ReplacedLanRuntime.Instance?.LocalPlayerIndex ?? -1 : 0;
|
||
|
||
private static string BuildSlot(ReplacedMatchState state, int slot, int localPlayer)
|
||
{
|
||
var value = state.LastResolution.FirstOrDefault(item => item.SlotIndex == slot);
|
||
if (value == null) return $"槽 {slot + 1}\n等待拼点";
|
||
var localIsOne = localPlayer == 0;
|
||
var localType = localIsOne ? value.PlayerOneType : value.PlayerTwoType;
|
||
var enemyType = localIsOne ? value.PlayerTwoType : value.PlayerOneType;
|
||
var localValue = localIsOne ? value.PlayerOneValue : value.PlayerTwoValue;
|
||
var enemyValue = localIsOne ? value.PlayerTwoValue : value.PlayerOneValue;
|
||
var localRoll = localIsOne ? value.PlayerOneRoll : value.PlayerTwoRoll;
|
||
var enemyRoll = localIsOne ? value.PlayerTwoRoll : value.PlayerOneRoll;
|
||
var localRerolled = localIsOne ? value.PlayerOneRerolled : value.PlayerTwoRerolled;
|
||
var enemyRerolled = localIsOne ? value.PlayerTwoRerolled : value.PlayerOneRerolled;
|
||
return $"槽 {slot + 1}\n骰面 {DiceRoll(localRoll, localRerolled)} : {DiceRoll(enemyRoll, enemyRerolled)}\n" +
|
||
$"总点 {TypeName(localType)} {localValue} : {enemyValue} {TypeName(enemyType)}\n{value.Summary}";
|
||
}
|
||
|
||
private static string DiceRoll(int value, bool rerolled) => value <= 0 ? "-" : rerolled ? value + "(重掷)" : value.ToString();
|
||
|
||
private static string TypeName(ReplacedEffectType type) => type switch
|
||
{
|
||
ReplacedEffectType.Attack => "攻",
|
||
ReplacedEffectType.Dodge => "闪",
|
||
ReplacedEffectType.Counter => "反",
|
||
_ => "?"
|
||
};
|
||
|
||
private void ShowCardDetail(string id, bool ending)
|
||
{
|
||
var title = UiRoot.Find("Screens/BattleScreen/LogPanel/CardDetailTitle")?.GetComponent<TMP_Text>();
|
||
var detail = UiRoot.Find("Screens/BattleScreen/LogPanel/CardDetailText")?.GetComponent<TMP_Text>();
|
||
if (title == null || detail == null) return;
|
||
var catalog = ActiveContent();
|
||
if (ending)
|
||
{
|
||
var card = catalog.EndCards[id];
|
||
title.text = $"{card.Name} · 结束牌 · {card.Cost} COST";
|
||
detail.text = card.Description + "\n槽位修正:" + string.Join(" / ", Enumerable.Range(0, 3)
|
||
.Select(index => $"槽{index + 1} +{card.SlotBase[index]}始数 +{card.SlotFactor[index]}因数"));
|
||
return;
|
||
}
|
||
|
||
var normal = catalog.Cards[id];
|
||
var minimum = normal.BaseValue + normal.Factor;
|
||
var maximum = normal.BaseValue + normal.Factor * 6;
|
||
title.text = $"{normal.Name} · {FullTypeName(normal.EffectType)} · {normal.Cost} COST";
|
||
detail.text = $"{normal.Description}\n拼点:{normal.BaseValue} + {normal.Factor} × D6,范围 {minimum}–{maximum}。{EffectRule(normal.EffectType)}";
|
||
}
|
||
|
||
private void ResetCardDetail()
|
||
{
|
||
var title = UiRoot.Find("Screens/BattleScreen/LogPanel/CardDetailTitle")?.GetComponent<TMP_Text>();
|
||
var detail = UiRoot.Find("Screens/BattleScreen/LogPanel/CardDetailText")?.GetComponent<TMP_Text>();
|
||
if (title != null) title.text = "卡牌效果";
|
||
if (detail != null) detail.text = "悬停、点击或拖动卡牌查看完整效果。";
|
||
}
|
||
|
||
private void ResetResolutionPresentation()
|
||
{
|
||
if (_resolutionSequence != null) StopCoroutine(_resolutionSequence);
|
||
_resolutionSequence = null;
|
||
_resolutionAnimating = false;
|
||
_lastResolutionKey = string.Empty;
|
||
_lastPresentedRound = 0;
|
||
_presentationLocalSubmission = null;
|
||
_presentationEnemySubmission = null;
|
||
_presentationRound = 0;
|
||
_presentationLeadPlayer = -1;
|
||
}
|
||
|
||
private IEnumerator PlayResolutionSequence(ReplacedSlotResolution[] resolutions, int localPlayer)
|
||
{
|
||
var catalog = ActiveContent();
|
||
var round = _presentationRound > 0 ? _presentationRound : Math.Max(1, (ActiveState()?.Round ?? 1) - 1);
|
||
var leadPlayer = _presentationLeadPlayer >= 0 ? _presentationLeadPlayer : localPlayer;
|
||
if (leadPlayer == localPlayer)
|
||
{
|
||
yield return PlayLockStage(true, round, BuildSubmission(catalog, _presentationLocalSubmission, "牌序已锁定"));
|
||
yield return PlayLockStage(false, round, BuildSubmission(catalog, _presentationEnemySubmission, "牌序已锁定"));
|
||
}
|
||
else
|
||
{
|
||
yield return PlayLockStage(false, round, BuildSubmission(catalog, _presentationEnemySubmission, "牌序已锁定"));
|
||
yield return PlayLockStage(true, round, BuildSubmission(catalog, _presentationLocalSubmission, "牌序已锁定"));
|
||
}
|
||
|
||
Text("Screens/BattleScreen/Header/RoundText").text = $"回合 {round} 效果槽投掷 D6";
|
||
_audio?.PlayDice();
|
||
yield return EnsureBattlePhaseMotion().PlayStage("效果槽投掷", "三个槽位同时投掷 D6", new Color(.68f, .48f, .14f, 1f), .28f);
|
||
foreach (var resolution in resolutions.OrderBy(value => value.SlotIndex))
|
||
{
|
||
var slot = UiRoot.Find($"Screens/BattleScreen/EffectArea/Slot{resolution.SlotIndex + 1}");
|
||
slot?.GetComponent<ReplacedSlotResolutionMotion>()?.Play(resolution, localPlayer);
|
||
}
|
||
|
||
yield return new WaitForSecondsRealtime(.96f);
|
||
Text("Screens/BattleScreen/Header/RoundText").text = $"回合 {round} 骰点定格";
|
||
yield return new WaitForSecondsRealtime(.73f);
|
||
Text("Screens/BattleScreen/Header/RoundText").text = $"回合 {round} 拼点结算";
|
||
var localDamaged = resolutions.Any(resolution => localPlayer == 0 ? resolution.DamageToPlayerOne > 0 : resolution.DamageToPlayerTwo > 0);
|
||
var enemyDamaged = resolutions.Any(resolution => localPlayer == 0 ? resolution.DamageToPlayerTwo > 0 : resolution.DamageToPlayerOne > 0);
|
||
if (localDamaged || enemyDamaged) _audio?.PlayHit();
|
||
if (localDamaged) StartCoroutine(PulseDamage(Text("Screens/BattleScreen/PlayerArea/PlayerStatus")));
|
||
if (enemyDamaged) StartCoroutine(PulseDamage(Text("Screens/BattleScreen/EnemyArea/EnemyStatus")));
|
||
yield return new WaitForSecondsRealtime(.78f);
|
||
|
||
_presentationLocalSubmission = null;
|
||
_presentationEnemySubmission = null;
|
||
_presentationRound = 0;
|
||
_presentationLeadPlayer = -1;
|
||
var state = ActiveState();
|
||
if (state?.Phase == ReplacedMatchPhase.Completed)
|
||
{
|
||
_resolutionAnimating = false;
|
||
_resolutionSequence = null;
|
||
Show(DemoScreen.Reward);
|
||
yield break;
|
||
}
|
||
if (state != null) yield return PlayRoundIntroContent(state, localPlayer);
|
||
_resolutionAnimating = false;
|
||
_resolutionSequence = null;
|
||
RefreshBattle();
|
||
}
|
||
|
||
private IEnumerator PlayRoundIntroSequence(ReplacedMatchState state, int localPlayer)
|
||
{
|
||
yield return PlayRoundIntroContent(state, localPlayer);
|
||
_resolutionAnimating = false;
|
||
_resolutionSequence = null;
|
||
RefreshBattle();
|
||
}
|
||
|
||
private IEnumerator PlayRoundIntroContent(ReplacedMatchState state, int localPlayer)
|
||
{
|
||
var round = state.Round;
|
||
var initiativeRoll = state.InitiativeRoll;
|
||
var leadPlayer = state.LeadPlayer;
|
||
var enemySubmission = state.Submissions[1 - localPlayer]?.Clone();
|
||
var catalog = ActiveContent();
|
||
var leadName = leadPlayer == localPlayer ? "我方先手" : state.Players[1 - localPlayer].DisplayName + "先手";
|
||
Text("Screens/BattleScreen/Header/RoundText").text = $"回合 {round} 决定先手";
|
||
_audio?.PlayDice();
|
||
yield return EnsureBattlePhaseMotion().PlayDie("先手骰", initiativeRoll, leadName, new Color(.72f, .55f, .22f, 1f));
|
||
_lastPresentedRound = round;
|
||
if (leadPlayer != localPlayer && enemySubmission != null)
|
||
{
|
||
yield return PlayLockStage(false, round, BuildSubmission(catalog, enemySubmission, "牌序已锁定"));
|
||
}
|
||
}
|
||
|
||
private IEnumerator PlayLockStage(bool local, int round, string detail)
|
||
{
|
||
Text("Screens/BattleScreen/Header/RoundText").text = local
|
||
? $"回合 {round} 我方锁定"
|
||
: $"回合 {round} 敌方锁定";
|
||
if (local)
|
||
Text("Screens/BattleScreen/PlayerArea/SelectionText").text = "我方已锁定\n" + detail;
|
||
else
|
||
Text("Screens/BattleScreen/EnemyArea/EnemyQueue").text = detail;
|
||
if (!local) _audio?.PlayLock();
|
||
var title = local ? "我方锁定" : "敌方锁定";
|
||
var accent = local ? new Color(.76f, .19f, .16f, 1f) : new Color(.28f, .35f, .34f, 1f);
|
||
yield return EnsureBattlePhaseMotion().PlayStage(title, detail, accent);
|
||
}
|
||
|
||
private ReplacedBattlePhaseMotion EnsureBattlePhaseMotion()
|
||
{
|
||
var battle = UiRoot.Find("Screens/BattleScreen")!;
|
||
return battle.GetComponent<ReplacedBattlePhaseMotion>() ?? battle.gameObject.AddComponent<ReplacedBattlePhaseMotion>();
|
||
}
|
||
|
||
private static IEnumerator PulseDamage(TMP_Text text)
|
||
{
|
||
var original = text.color;
|
||
text.color = new Color(1f, .27f, .22f, 1f);
|
||
text.transform.localScale = Vector3.one * 1.08f;
|
||
var elapsed = 0f;
|
||
const float duration = .28f;
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.unscaledDeltaTime;
|
||
var t = Mathf.Clamp01(elapsed / duration);
|
||
text.color = Color.Lerp(new Color(1f, .27f, .22f, 1f), original, t);
|
||
text.transform.localScale = Vector3.one * Mathf.Lerp(1.08f, 1f, t);
|
||
yield return null;
|
||
}
|
||
text.color = original;
|
||
text.transform.localScale = Vector3.one;
|
||
}
|
||
|
||
private static string BuildResolutionKey(ReplacedMatchState state)
|
||
{
|
||
if (state.LastResolution.Count == 0) return string.Empty;
|
||
return state.Round + "|" + string.Join("|", state.LastResolution.OrderBy(value => value.SlotIndex).Select(value =>
|
||
$"{value.SlotIndex}:{value.PlayerOneRoll}:{value.PlayerTwoRoll}:{value.PlayerOneValue}:{value.PlayerTwoValue}:{value.DamageToPlayerOne}:{value.DamageToPlayerTwo}:{value.Summary}"));
|
||
}
|
||
|
||
private static string FullTypeName(ReplacedEffectType type) => type switch
|
||
{
|
||
ReplacedEffectType.Attack => "攻击",
|
||
ReplacedEffectType.Dodge => "闪避",
|
||
ReplacedEffectType.Counter => "反击",
|
||
_ => "未知"
|
||
};
|
||
|
||
private static string EffectRule(ReplacedEffectType type) => type switch
|
||
{
|
||
ReplacedEffectType.Attack => "攻击胜出时造成点数差伤害,单槽最多 8 点。",
|
||
ReplacedEffectType.Dodge => "对攻击胜出时取消攻击,并让下回合额外抽 1 张普通牌。",
|
||
ReplacedEffectType.Counter => "对攻击胜出时取消攻击并反伤点数差,单槽最多 6 点。",
|
||
_ => string.Empty
|
||
};
|
||
|
||
private static string DeckArchetype(ReplacedDeck deck, ReplacedContentCatalog catalog)
|
||
{
|
||
var types = deck.NormalCards.Where(catalog.Cards.ContainsKey).Select(id => catalog.Cards[id].EffectType).ToArray();
|
||
var counters = types.Count(value => value == ReplacedEffectType.Counter);
|
||
var attacks = types.Count(value => value == ReplacedEffectType.Attack);
|
||
var dodges = types.Count(value => value == ReplacedEffectType.Dodge);
|
||
if (counters >= 6) return "反击蓄势";
|
||
if (attacks >= 6) return "连续强攻";
|
||
if (dodges >= 6) return "闪避游击";
|
||
return "混合对位";
|
||
}
|
||
|
||
private static List<string> SelectedCardIds(IReadOnlyList<string> hand, IEnumerable<int> selectedIndices, int maximum)
|
||
{
|
||
return selectedIndices.Where(index => index >= 0 && index < hand.Count)
|
||
.Distinct()
|
||
.Take(maximum)
|
||
.Select(index => hand[index])
|
||
.ToList();
|
||
}
|
||
|
||
private static string FormatSelectedCards(IReadOnlyList<string> hand, IEnumerable<int> selectedIndices,
|
||
Func<string, string> name, string separator)
|
||
{
|
||
return string.Join(separator, selectedIndices.Where(index => index >= 0 && index < hand.Count)
|
||
.Select((index, order) => $"{order + 1}. {name(hand[index])}"));
|
||
}
|
||
|
||
private static void RemoveInvalidIndices(List<int> indices, int handCount)
|
||
{
|
||
indices.RemoveAll(index => index < 0 || index >= handCount);
|
||
}
|
||
|
||
private void Bind(string path, UnityEngine.Events.UnityAction action) => Button(path).onClick.AddListener(() =>
|
||
{
|
||
_audio?.PlayUiClick();
|
||
action();
|
||
});
|
||
private Button Button(string path) => UiRoot.Find(path)?.GetComponent<Button>() ?? throw new InvalidOperationException("Missing Button: " + path);
|
||
private TMP_Text Text(string path) => UiRoot.Find(path)?.GetComponent<TMP_Text>() ?? throw new InvalidOperationException("Missing TMP text: " + path);
|
||
private void LogException(Exception exception) { _lastError = exception.Message; Debug.LogException(exception); RefreshCurrent(); }
|
||
|
||
private static void EnsureEventSystem()
|
||
{
|
||
if (EventSystem.current != null) return;
|
||
var value = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
|
||
DontDestroyOnLoad(value);
|
||
}
|
||
}
|
||
}
|