Initial commit

This commit is contained in:
rudder
2026-08-12 14:03:44 +03:00
commit 9a7b4a57dc
304 changed files with 82311 additions and 0 deletions
@@ -0,0 +1,81 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class BackpackModalPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset backpackItemRowTemplate;
[SerializeField] private CozyCollectorSpriteLibrary spriteLibrary;
private VisualElement _root;
private ScrollView _itemsScroll;
private VisualElement _emptyNote;
private Button _closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.BackpackChanged += Rebuild;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.BackpackChanged -= Rebuild;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_root = root.Q<VisualElement>("backpack-modal");
_itemsScroll = _root.Q<ScrollView>("backpack-items-scroll");
_emptyNote = _root.Q<VisualElement>("backpack-empty");
_closeButton = _root.Q<Button>("backpack-close-button");
_closeButton.clicked -= HandleCloseClicked;
_closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
Rebuild();
}
private void ApplyVisibility()
{
_root.style.display = controller.ActiveModal == CozyModal.Backpack ? DisplayStyle.Flex : DisplayStyle.None;
}
private void Rebuild()
{
_itemsScroll.contentContainer.Clear();
var items = controller.BackpackItems;
_emptyNote.style.display = items == null || items.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (items == null)
{
return;
}
foreach (var item in items)
{
var rowRoot = backpackItemRowTemplate.CloneTree();
_itemsScroll.contentContainer.Add(rowRoot);
var row = new BackpackItemRowView(rowRoot);
row.Set(
spriteLibrary.ForSlug(item.Slug),
string.IsNullOrEmpty(item.Name) ? item.Slug : item.Name,
item.Slug,
item.Amount);
}
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0c73aae57f5c749b88f5bf2c22538af5
@@ -0,0 +1,70 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class BootOverlayPanel : MonoBehaviour
{
private const float SpinnerDegreesPerSecond = 180f;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private VisualElement _overlay;
private Label _statusLabel;
private VisualElement _spinner;
private IVisualElementScheduledItem _spinSchedule;
private float _angle;
private float _lastTick;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.PhaseChanged += ApplyVisibility;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.PhaseChanged -= ApplyVisibility;
_spinSchedule?.Pause();
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_overlay = root.Q<VisualElement>("boot-overlay");
_statusLabel = root.Q<Label>("boot-status");
_spinner = root.Q<VisualElement>("boot-spinner");
_spinSchedule = _spinner.schedule.Execute(Spin).Every(16);
ApplyVisibility();
}
private void Update()
{
_statusLabel.text = controller.StatusMessage;
}
private void Spin()
{
var now = Time.unscaledTime;
_angle = (_angle + SpinnerDegreesPerSecond * (now - _lastTick)) % 360f;
_lastTick = now;
_spinner.style.rotate = new Rotate(Angle.Degrees(_angle));
}
private void ApplyVisibility()
{
var visible = controller.Phase == CozyPhase.Booting || controller.Phase == CozyPhase.Connecting;
_overlay.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
if (visible)
{
_lastTick = Time.unscaledTime;
_spinSchedule.Resume();
}
else
{
_spinSchedule.Pause();
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f54172384bff5402686bb558e2abb5b7
@@ -0,0 +1,89 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class EventInspectorPanel : MonoBehaviour
{
private static readonly Dictionary<string, Color> CapabilityColors = new Dictionary<string, Color>
{
{ "auth", new Color(0.486f, 0.769f, 1f) },
{ "config", new Color(0.945f, 0.776f, 0.490f) },
{ "scenario", new Color(0.769f, 0.635f, 1f) },
{ "store", new Color(1f, 0.698f, 0.490f) },
{ "leaderboard", new Color(0.482f, 0.878f, 0.690f) },
{ "storage", new Color(0.624f, 0.690f, 0.769f) },
{ "inventory", new Color(0.898f, 0.604f, 0.796f) },
{ "wallet", new Color(1f, 0.831f, 0.475f) },
{ "system", new Color(0.667f, 0.706f, 0.635f) },
};
private static readonly Dictionary<string, string> CapabilityLabels = new Dictionary<string, string>
{
{ "auth", "Auth" },
{ "config", "Config" },
{ "scenario", "Scenario" },
{ "store", "Store" },
{ "leaderboard", "Board" },
{ "storage", "Storage" },
{ "inventory", "Items" },
{ "wallet", "Wallet" },
{ "system", "System" },
};
private static readonly Color FallbackColor = new Color(0.667f, 0.706f, 0.635f);
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset logRowTemplate;
private ScrollView _scroll;
private VisualElement _emptyNote;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.LogAdded += Rebuild;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.LogAdded -= Rebuild;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_scroll = root.Q<ScrollView>("event-log-scroll");
_emptyNote = root.Q("event-log-empty");
Rebuild();
}
private void Rebuild()
{
var container = _scroll.contentContainer;
container.Clear();
var entries = controller.LogEntries;
_emptyNote.style.display = entries == null || entries.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (entries == null)
{
return;
}
foreach (var entry in entries)
{
var rowRoot = logRowTemplate.CloneTree();
container.Add(rowRoot);
string key = entry.Capability == null ? string.Empty : entry.Capability.ToLowerInvariant();
string label = CapabilityLabels.TryGetValue(key, out var known) ? known : entry.Capability;
Color color = CapabilityColors.TryGetValue(key, out var knownColor) ? knownColor : FallbackColor;
string detail = string.IsNullOrEmpty(entry.Detail) ? string.Empty : $" — {entry.Detail}";
new LogRowView(rowRoot).Set(entry.Timestamp.ToString("HH:mm:ss"), label, color, entry.Message, detail);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a134e625b6e114c4b80b9f55a14e48b3
@@ -0,0 +1,119 @@
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class HudPanel : MonoBehaviour
{
private const string TimerLowClass = "timer-low";
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private GameWorld world;
private Label _scoreLabel;
private Label _bestLabel;
private Label _timeLeftLabel;
private VisualElement _timerFill;
private Label _walletLabel;
private Button _shopButton;
private Button _backpackButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
world.StateChanged += Refresh;
world.RoundFinished += HandleRoundFinished;
controller.WalletsChanged += RefreshWallet;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
world.StateChanged -= Refresh;
world.RoundFinished -= HandleRoundFinished;
controller.WalletsChanged -= RefreshWallet;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_scoreLabel = root.Q<Label>("hud-score");
_bestLabel = root.Q<Label>("hud-best");
_timeLeftLabel = root.Q<Label>("hud-time-left");
_timerFill = root.Q("hud-timer-fill");
_walletLabel = root.Q<Label>("hud-wallet");
_shopButton = root.Q<Button>("hud-shop-button");
_backpackButton = root.Q<Button>("hud-backpack-button");
_shopButton.clicked -= HandleShopClicked;
_shopButton.clicked += HandleShopClicked;
_backpackButton.clicked -= HandleBackpackClicked;
_backpackButton.clicked += HandleBackpackClicked;
Refresh();
RefreshWallet();
}
private void Update()
{
RefreshTimer();
}
private void Refresh()
{
_scoreLabel.text = world.Score.ToString();
_bestLabel.text = controller.Save != null ? controller.Save.BestScore.ToString() : "0";
RefreshTimer();
}
private void RefreshTimer()
{
float roundSeconds = controller.Tuning != null ? Mathf.Max(1f, controller.Tuning.RoundSeconds) : 1f;
float fraction = Mathf.Clamp01(world.TimeLeftSeconds / roundSeconds);
_timeLeftLabel.text = $"{Mathf.CeilToInt(Mathf.Max(0f, world.TimeLeftSeconds))}s";
_timerFill.style.width = Length.Percent(fraction * 100f);
_timerFill.EnableInClassList(TimerLowClass, fraction <= 0.2f);
}
private void RefreshWallet()
{
_walletLabel.text = WalletLabel();
}
private string WalletLabel()
{
var wallets = controller.Wallets;
if (wallets == null || wallets.Count == 0)
{
return "—";
}
var builder = new StringBuilder();
foreach (var wallet in wallets)
{
if (builder.Length > 0)
{
builder.Append(" · ");
}
builder.Append($"{wallet.Balance:0.##} {wallet.Currency}".TrimEnd());
}
return builder.ToString();
}
private void HandleRoundFinished(int score, int collected)
{
Refresh();
}
private void HandleShopClicked()
{
controller.OpenModal(CozyModal.Shop);
}
private void HandleBackpackClicked()
{
controller.OpenModal(CozyModal.Backpack);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2e5e5cb2ad54545a6847ad8c90d1f7d8
@@ -0,0 +1,102 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class OfferModalPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private VisualElement root;
private Label titleLabel;
private Label nameLabel;
private Label priceLabel;
private Button buyButton;
private Button declineButton;
private Button closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.OfferChanged += Refresh;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.OfferChanged -= Refresh;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
this.root = root.Q<VisualElement>("offer-modal");
titleLabel = this.root.Q<Label>("offer-title");
nameLabel = this.root.Q<Label>("offer-name");
priceLabel = this.root.Q<Label>("offer-price");
buyButton = this.root.Q<Button>("offer-buy-button");
declineButton = this.root.Q<Button>("offer-decline-button");
closeButton = this.root.Q<Button>("offer-close-button");
buyButton.clicked -= HandleBuyClicked;
buyButton.clicked += HandleBuyClicked;
declineButton.clicked -= HandleDeclineClicked;
declineButton.clicked += HandleDeclineClicked;
closeButton.clicked -= HandleCloseClicked;
closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
Refresh();
}
private void ApplyVisibility()
{
root.style.display = controller.ActiveModal == CozyModal.Offer ? DisplayStyle.Flex : DisplayStyle.None;
}
private void Refresh()
{
var offerInfo = controller.CurrentOffer;
if (offerInfo == null)
{
titleLabel.text = "Offer";
nameLabel.text = "—";
priceLabel.text = string.Empty;
buyButton.SetEnabled(false);
return;
}
titleLabel.text = string.IsNullOrEmpty(offerInfo.StoreName) ? offerInfo.StoreSlug : offerInfo.StoreName;
var offer = offerInfo.Offers != null && offerInfo.Offers.Count > 0 ? offerInfo.Offers[0] : null;
if (offer == null)
{
nameLabel.text = "—";
priceLabel.text = string.Empty;
buyButton.SetEnabled(false);
return;
}
nameLabel.text = string.IsNullOrEmpty(offer.Name) ? offer.OfferId : offer.Name;
priceLabel.text = ShopModalPanel.PriceLabel(offer.Price, offer.Currency);
buyButton.SetEnabled(true);
}
private void HandleBuyClicked()
{
controller.BuyScenarioOffer();
}
private void HandleDeclineClicked()
{
controller.DeclineScenarioOffer();
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b996708622cc54d328cac546b893e111
@@ -0,0 +1,92 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class RemoteConfigPanel : MonoBehaviour
{
private const string FlashClass = "flash";
private const long FlashMilliseconds = 600;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private Label _playerSpeedValue;
private Label _spawnIntervalValue;
private Label _collectibleScoreValue;
private Label _roundSecondsValue;
private VisualElement _playerSpeedRow;
private VisualElement _spawnIntervalRow;
private VisualElement _collectibleScoreRow;
private VisualElement _roundSecondsRow;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.TuningChanged += HandleTuningChanged;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.TuningChanged -= HandleTuningChanged;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_playerSpeedValue = root.Q<Label>("rc-value-player-speed");
_spawnIntervalValue = root.Q<Label>("rc-value-spawn-interval");
_collectibleScoreValue = root.Q<Label>("rc-value-collectible-score");
_roundSecondsValue = root.Q<Label>("rc-value-round-seconds");
_playerSpeedRow = root.Q("rc-row-player-speed");
_spawnIntervalRow = root.Q("rc-row-spawn-interval");
_collectibleScoreRow = root.Q("rc-row-collectible-score");
_roundSecondsRow = root.Q("rc-row-round-seconds");
Refresh();
}
private void Refresh()
{
var tuning = controller.Tuning;
if (tuning == null)
{
return;
}
_playerSpeedValue.text = $"{Mathf.RoundToInt(tuning.PlayerSpeed)}";
_spawnIntervalValue.text = $"{Mathf.RoundToInt(tuning.SpawnIntervalMs)} ms";
_collectibleScoreValue.text = $"+{Mathf.RoundToInt(tuning.CollectibleScore)}";
_roundSecondsValue.text = $"{Mathf.RoundToInt(tuning.RoundSeconds)} s";
}
private void HandleTuningChanged(string key)
{
Refresh();
string normalized = key == null ? string.Empty : key.ToLowerInvariant().Replace("_", string.Empty).Replace("-", string.Empty).Replace(" ", string.Empty);
if (normalized.Contains("playerspeed"))
{
Flash(_playerSpeedRow);
}
else if (normalized.Contains("spawninterval"))
{
Flash(_spawnIntervalRow);
}
else if (normalized.Contains("collectiblescore"))
{
Flash(_collectibleScoreRow);
}
else if (normalized.Contains("roundseconds"))
{
Flash(_roundSecondsRow);
}
}
private static void Flash(VisualElement row)
{
row.AddToClassList(FlashClass);
row.schedule.Execute(() => row.RemoveFromClassList(FlashClass)).StartingIn(FlashMilliseconds);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1d57e6a031317461cb98e41f6940c8d3
@@ -0,0 +1,117 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class RoundResultModalPanel : MonoBehaviour
{
private const int MaxLeaderboardRows = 10;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset leaderboardRowTemplate;
private VisualElement root;
private Label scoreLabel;
private Label bestLabel;
private Label collectedLabel;
private ScrollView leaderboardScroll;
private VisualElement emptyNote;
private Button playAgainButton;
private Button claimButton;
private Button endButton;
private Button closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.RoundResultChanged += Refresh;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.RoundResultChanged -= Refresh;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
this.root = root.Q<VisualElement>("round-result-modal");
scoreLabel = this.root.Q<Label>("rr-score");
bestLabel = this.root.Q<Label>("rr-best");
collectedLabel = this.root.Q<Label>("rr-collected");
leaderboardScroll = this.root.Q<ScrollView>("rr-leaderboard-scroll");
emptyNote = this.root.Q<VisualElement>("rr-empty");
playAgainButton = this.root.Q<Button>("rr-play-again-button");
claimButton = this.root.Q<Button>("rr-claim-button");
endButton = this.root.Q<Button>("rr-end-button");
closeButton = this.root.Q<Button>("rr-close-button");
playAgainButton.clicked -= HandlePlayAgainClicked;
playAgainButton.clicked += HandlePlayAgainClicked;
claimButton.clicked -= HandleClaimClicked;
claimButton.clicked += HandleClaimClicked;
endButton.clicked -= HandleEndClicked;
endButton.clicked += HandleEndClicked;
closeButton.clicked -= HandleCloseClicked;
closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
Refresh();
}
private void ApplyVisibility()
{
root.style.display = controller.ActiveModal == CozyModal.RoundResult ? DisplayStyle.Flex : DisplayStyle.None;
}
private void Refresh()
{
var result = controller.RoundResult;
scoreLabel.text = result != null ? result.Score.ToString() : "0";
bestLabel.text = result != null ? result.Best.ToString() : "0";
collectedLabel.text = result != null ? result.Collected.ToString() : "0";
leaderboardScroll.Clear();
var entries = result != null ? result.TopEntries : null;
emptyNote.style.display = entries == null || entries.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (entries == null)
{
return;
}
int count = Mathf.Min(entries.Count, MaxLeaderboardRows);
for (int i = 0; i < count; i++)
{
var entry = entries[i];
var rowRoot = leaderboardRowTemplate.CloneTree();
var row = new LeaderboardRowView(rowRoot);
row.Set(entry.Rank, string.IsNullOrEmpty(entry.PlayerName) ? "player" : entry.PlayerName, entry.Score);
leaderboardScroll.Add(rowRoot);
}
}
private void HandlePlayAgainClicked()
{
controller.PlayAgain();
}
private void HandleClaimClicked()
{
controller.ClaimScenarioLeaderboardReward();
}
private void HandleEndClicked()
{
controller.EndScenarioLeaderboard();
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f3916e69e6ce4c78b909c659160ad95
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cde562fa676ee420795c27bd66549cab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class BackpackItemRowView
{
private readonly VisualElement _icon;
private readonly Label _nameLabel;
private readonly Label _metaLabel;
private readonly Label _amountLabel;
public BackpackItemRowView(VisualElement root)
{
_icon = root.Q<VisualElement>("item-icon");
_nameLabel = root.Q<Label>("item-name");
_metaLabel = root.Q<Label>("item-meta");
_amountLabel = root.Q<Label>("item-amount");
}
public void Set(Sprite icon, string itemName, string slug, int amount)
{
_icon.style.backgroundImage = new StyleBackground(icon);
_nameLabel.text = itemName;
_metaLabel.text = slug;
_amountLabel.text = $"×{amount}";
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d7a25568da97f48f3b07abf321ad2f82
@@ -0,0 +1,25 @@
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class LeaderboardRowView
{
private readonly Label rankLabel;
private readonly Label playerLabel;
private readonly Label scoreLabel;
public LeaderboardRowView(VisualElement root)
{
rankLabel = root.Q<Label>("lb-rank");
playerLabel = root.Q<Label>("lb-player");
scoreLabel = root.Q<Label>("lb-score");
}
public void Set(int rank, string playerName, int score)
{
rankLabel.text = rank > 0 ? $"#{rank}" : "—";
playerLabel.text = playerName;
scoreLabel.text = score.ToString();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 29788c49080d642ff919c1d25b5dd979
@@ -0,0 +1,31 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class LogRowView
{
private readonly Label _timeLabel;
private readonly Label _tagLabel;
private readonly Label _messageLabel;
private readonly Label _detailLabel;
public LogRowView(VisualElement root)
{
_timeLabel = root.Q<Label>("log-time");
_tagLabel = root.Q<Label>("log-tag");
_messageLabel = root.Q<Label>("log-message");
_detailLabel = root.Q<Label>("log-detail");
}
public void Set(string time, string tag, Color tagColor, string message, string detail)
{
_timeLabel.text = time;
_tagLabel.text = tag;
_tagLabel.style.color = tagColor;
_tagLabel.style.backgroundColor = new Color(tagColor.r, tagColor.g, tagColor.b, 0.18f);
_messageLabel.text = message;
_detailLabel.text = detail;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ffa213acafee84d818a3cf631f0bb01b
@@ -0,0 +1,26 @@
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ShopOfferRowView
{
private readonly Label _nameLabel;
private readonly Label _priceLabel;
private readonly Button _buyButton;
public Button BuyButton => _buyButton;
public ShopOfferRowView(VisualElement root)
{
_nameLabel = root.Q<Label>("offer-name");
_priceLabel = root.Q<Label>("offer-price");
_buyButton = root.Q<Button>("offer-buy-button");
}
public void Set(string offerName, string price)
{
_nameLabel.text = offerName;
_priceLabel.text = price;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 94bf1e4d6fb50418487ed96aa4d65d4b
@@ -0,0 +1,32 @@
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ToastView
{
private const string ToneInfoClass = "tone-info";
private const string ToneSuccessClass = "tone-success";
private const string ToneErrorClass = "tone-error";
private readonly Label _titleLabel;
private readonly Label _messageLabel;
public ToastView(VisualElement root)
{
Root = root;
_titleLabel = root.Q<Label>("toast-title");
_messageLabel = root.Q<Label>("toast-message");
}
public VisualElement Root { get; }
public void Set(string title, string message, CozyToastTone tone)
{
_titleLabel.text = title;
_messageLabel.text = message;
Root.EnableInClassList(ToneInfoClass, tone == CozyToastTone.Info);
Root.EnableInClassList(ToneSuccessClass, tone == CozyToastTone.Success);
Root.EnableInClassList(ToneErrorClass, tone == CozyToastTone.Error);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d468efab7b7645f78186c03b82f5d32
@@ -0,0 +1,87 @@
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class SessionCardPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private Label _nicknameLabel;
private Label _playerIdLabel;
private Label _deviceIdLabel;
private Label _regionLabel;
private Label _projectKeyLabel;
private Label _walletsLabel;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.PhaseChanged += Refresh;
controller.WalletsChanged += RefreshWallets;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.PhaseChanged -= Refresh;
controller.WalletsChanged -= RefreshWallets;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_nicknameLabel = root.Q<Label>("session-nickname");
_playerIdLabel = root.Q<Label>("session-player-id");
_deviceIdLabel = root.Q<Label>("session-device-id");
_regionLabel = root.Q<Label>("session-region");
_projectKeyLabel = root.Q<Label>("session-project-key");
_walletsLabel = root.Q<Label>("session-wallets");
Refresh();
}
private void Refresh()
{
var identity = controller.Identity;
if (identity == null)
{
_nicknameLabel.text = "—";
_playerIdLabel.text = "—";
_deviceIdLabel.text = "—";
_regionLabel.text = "—";
_projectKeyLabel.text = "—";
}
else
{
_nicknameLabel.text = string.IsNullOrEmpty(identity.Nickname)
? $"Player {identity.PlayerId.Substring(0, Mathf.Min(6, identity.PlayerId.Length))}"
: identity.Nickname;
_playerIdLabel.text = identity.PlayerId;
_deviceIdLabel.text = identity.DeviceId;
_regionLabel.text = string.IsNullOrEmpty(identity.Region) ? "global" : identity.Region;
_projectKeyLabel.text = identity.ProjectKey;
}
RefreshWallets();
}
private void RefreshWallets()
{
var wallets = controller.Wallets;
if (wallets == null || wallets.Count == 0)
{
_walletsLabel.text = "No wallets";
return;
}
var builder = new StringBuilder();
foreach (var wallet in wallets)
{
builder.AppendLine($"{wallet.Balance:0.##} {wallet.Currency}".TrimEnd());
}
_walletsLabel.text = builder.ToString().TrimEnd();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 900b9e31987914b56a3bba037e2a830c
@@ -0,0 +1,43 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class SetupOverlayPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private VisualElement _overlay;
private Label _errorLabel;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.PhaseChanged += ApplyVisibility;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.PhaseChanged -= ApplyVisibility;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_overlay = root.Q<VisualElement>("setup-overlay");
_errorLabel = root.Q<Label>("setup-error");
ApplyVisibility();
}
private void Update()
{
_errorLabel.text = controller.SetupError;
}
private void ApplyVisibility()
{
_overlay.style.display = controller.Phase == CozyPhase.Setup ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6de014257693b4ced830566d18e49444
@@ -0,0 +1,115 @@
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ShopModalPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset shopOfferRowTemplate;
private VisualElement _root;
private Label _walletLabel;
private ScrollView _offersScroll;
private VisualElement _emptyNote;
private Button _closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.ShopChanged += Rebuild;
controller.WalletsChanged += RefreshWallet;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.ShopChanged -= Rebuild;
controller.WalletsChanged -= RefreshWallet;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_root = root.Q<VisualElement>("shop-modal");
_walletLabel = _root.Q<Label>("shop-wallet");
_offersScroll = _root.Q<ScrollView>("shop-offers-scroll");
_emptyNote = _root.Q<VisualElement>("shop-empty");
_closeButton = _root.Q<Button>("shop-close-button");
_closeButton.clicked -= HandleCloseClicked;
_closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
RefreshWallet();
Rebuild();
}
private void ApplyVisibility()
{
_root.style.display = controller.ActiveModal == CozyModal.Shop ? DisplayStyle.Flex : DisplayStyle.None;
}
private void RefreshWallet()
{
var wallets = controller.Wallets;
if (wallets == null || wallets.Count == 0)
{
_walletLabel.text = "Wallet: —";
return;
}
var builder = new StringBuilder("Wallet: ");
foreach (var wallet in wallets)
{
if (builder.Length > "Wallet: ".Length)
{
builder.Append(" · ");
}
builder.Append($"{wallet.Balance:0.##} {wallet.Currency}".TrimEnd());
}
_walletLabel.text = builder.ToString();
}
private void Rebuild()
{
_offersScroll.contentContainer.Clear();
var offers = controller.ShopOffers;
_emptyNote.style.display = offers == null || offers.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (offers == null)
{
return;
}
foreach (var offer in offers)
{
var rowRoot = shopOfferRowTemplate.CloneTree();
_offersScroll.contentContainer.Add(rowRoot);
var row = new ShopOfferRowView(rowRoot);
row.Set(string.IsNullOrEmpty(offer.Name) ? offer.OfferId : offer.Name, PriceLabel(offer.Price, offer.Currency));
string offerId = offer.OfferId;
row.BuyButton.clicked += () => controller.BuyShopOffer(offerId);
}
}
internal static string PriceLabel(double price, string currency)
{
if (price <= 0)
{
return "Free";
}
return $"{price:0.##} {currency}".TrimEnd();
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 36789e549043d400a92f3a84849b4038
@@ -0,0 +1,78 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ToastsPanel : MonoBehaviour
{
private const float ToastLifetimeSeconds = 4.2f;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset toastTemplate;
private readonly List<ToastView> _toasts = new List<ToastView>();
private readonly List<float> _dismissAt = new List<float>();
private VisualElement _container;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ToastRequested += HandleToastRequested;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ToastRequested -= HandleToastRequested;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_container = root.Q("toasts");
}
private void Update()
{
for (int i = _toasts.Count - 1; i >= 0; i--)
{
if (Time.unscaledTime >= _dismissAt[i])
{
Dismiss(i);
}
}
}
private void HandleToastRequested(string title, string message, CozyToastTone tone)
{
var root = toastTemplate.CloneTree();
_container.Add(root);
var toast = new ToastView(root);
toast.Set(title, message, tone);
toast.Root.RegisterCallback<ClickEvent>(_ => Dismiss(toast));
_toasts.Add(toast);
_dismissAt.Add(Time.unscaledTime + ToastLifetimeSeconds);
}
private void Dismiss(ToastView toast)
{
int index = _toasts.IndexOf(toast);
if (index >= 0)
{
Dismiss(index);
}
}
private void Dismiss(int index)
{
var toast = _toasts[index];
_toasts.RemoveAt(index);
_dismissAt.RemoveAt(index);
toast.Root.RemoveFromHierarchy();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 57e0ed949960344bd80072dd54a52c4f
@@ -0,0 +1,134 @@
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public static class UiDebugProbe
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Install()
{
new GameObject("UiDebugProbe").AddComponent<Probe>();
}
private sealed class Probe : MonoBehaviour
{
private PanelRenderer _panelRenderer;
private VisualElement _root;
private IEnumerator Start()
{
yield return null;
yield return null;
_panelRenderer = FindFirstObjectByType<PanelRenderer>();
if (_panelRenderer == null)
{
Debug.Log("[UiProbe] PanelRenderer: NOT FOUND");
}
else
{
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
if (_root == null)
{
Dump("after-scene-load");
}
while (true)
{
yield return new WaitForSecondsRealtime(10f);
Dump("periodic");
}
}
private void OnDestroy()
{
if (_panelRenderer != null)
{
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
}
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_root = root;
Dump("ui-reload");
}
private void Dump(string tag)
{
var sb = new StringBuilder();
sb.AppendLine($"[UiProbe] --- {tag} ---");
sb.AppendLine($"screen={Screen.width}x{Screen.height}");
if (_panelRenderer == null)
{
sb.AppendLine("PanelRenderer: NOT FOUND");
Debug.Log(sb.ToString());
return;
}
var ps = _panelRenderer.panelSettings;
sb.AppendLine(ps == null
? "panelSettings: NULL"
: $"panelSettings={ps.name} scaleMode={ps.scaleMode} ref={ps.referenceResolution.x}x{ps.referenceResolution.y} match={ps.match} scale={ps.scale}");
if (_root == null)
{
sb.AppendLine("root: NULL (reload callback not fired yet)");
Debug.Log(sb.ToString());
return;
}
sb.AppendLine($"root worldBound={_root.worldBound} children={_root.childCount}");
var uiRoot = _root.childCount > 0 ? _root[0] : null;
sb.AppendLine($"uiRoot={uiRoot?.name} children={uiRoot?.childCount}");
if (uiRoot != null)
{
foreach (var child in uiRoot.Children())
{
Describe(sb, child);
}
}
var renderers = FindObjectsByType<PanelRenderer>(FindObjectsSortMode.None);
sb.AppendLine($"panelRenderers={renderers.Length} panel={(_root.panel == null ? "null" : "ok")}");
Describe(sb, _root.Q("hud-score"));
Describe(sb, _root.Q("hud-timer-fill"));
Describe(sb, _root.Q("session-card"));
Describe(sb, _root.Q("remote-config"));
Describe(sb, _root.Q("event-log-scroll"));
var controller = FindFirstObjectByType<CozyCollectorController>();
sb.AppendLine(controller == null
? "controller: NOT FOUND"
: $"controller phase={controller.Phase} activeModal={controller.ActiveModal} status='{controller.StatusMessage}'");
var cam = Camera.main;
sb.AppendLine(cam == null
? "camera: NOT FOUND"
: $"camera rect={cam.rect} targetTexture={(cam.targetTexture == null ? "null" : cam.targetTexture.name)} enabled={cam.enabled} clearFlags={cam.clearFlags} time={Time.time:0.#} timeScale={Time.timeScale}");
Debug.Log(sb.ToString());
}
private static void Describe(StringBuilder sb, VisualElement element)
{
if (element == null)
{
sb.AppendLine(" <element not found>");
return;
}
var r = element.resolvedStyle;
sb.AppendLine($" {element.name}: display={r.display} visibility={r.visibility} opacity={r.opacity} " +
$"size={r.width:0.#}x{r.height:0.#} worldBound={element.worldBound} classes='{string.Join(",", element.GetClasses())}'");
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b70a6370f0b8b48aabebe030ae087adf