draft options working; spells won't cast though

This commit is contained in:
Ian Woods 2026-07-14 20:56:50 -07:00
parent 2429efbf6a
commit 7cf15dcaa6
37 changed files with 1340 additions and 0 deletions

View file

@ -8,6 +8,7 @@ using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using TD.Core;
using TD.Gameplay;
using TD.Gameplay.BuilderSpells;
using TD.Gameplay.Draft;
using TD.Towers;
using TD.UI.Minimap;
@ -115,6 +116,27 @@ namespace TD.UI
private bool draftSubscribed;
private PlayerDraft subscribedDraft;
// Spell hotbar (bottom-ui Section 6). Frame is display:none while the local player
// has no granted spells; rebuilt on grant (append-only, so this is rare). Cooldown
// state changes continuously and has no change event, so it's polled every Update.
private VisualElement spellHotbarFrame;
private VisualElement spellHotbar;
private bool spellLoadoutSubscribed;
private PlayerSpellLoadout subscribedSpellLoadout;
private readonly List<SpellSlotUi> spellSlotUis = new List<SpellSlotUi>();
private readonly struct SpellSlotUi
{
public readonly VisualElement Cell;
public readonly Label CooldownLabel;
public SpellSlotUi(VisualElement cell, Label cooldownLabel)
{
Cell = cell;
CooldownLabel = cooldownLabel;
}
}
// Chat panel (bottom-left, above portrait) — programmatic. The container
// holds both the scrollable feed and the input. Highlight + scroll
// interactivity are toggled on the container when typing.
@ -294,6 +316,23 @@ namespace TD.UI
RebuildDraftCards();
}
// Hook the local player's spell loadout so the hotbar rebuilds when a new spell is
// granted. Retried each Update until the local PlayerSpellLoadout exists.
private void TrySubscribeSpellLoadout()
{
var loadout = PlayerSpellLoadout.Local;
if (loadout == null) return;
loadout.OnLoadoutChanged += HandleSpellLoadoutChanged;
subscribedSpellLoadout = loadout;
spellLoadoutSubscribed = true;
RebuildSpellHotbar();
}
private void HandleSpellLoadoutChanged()
{
RebuildSpellHotbar();
}
// ----- Draft overlay ----------------------------------------------
// Non-modal draft UI: a card row floats at top-center while a draft is active, and
@ -434,6 +473,103 @@ namespace TD.UI
draftPanel.style.display = (hasDraft || canShowBuy) ? DisplayStyle.Flex : DisplayStyle.None;
}
// ----- Spell hotbar -------------------------------------------------
// Rebuilds the hotbar cells from the local player's current loadout. Called on
// OnLoadoutChanged (a new spell granted) — spells are append-only, so this is rare,
// not a per-frame concern.
private void RebuildSpellHotbar()
{
if (spellHotbar == null) return;
spellHotbar.Clear();
spellSlotUis.Clear();
var loadout = PlayerSpellLoadout.Local;
int slotCount = loadout?.SlotCount ?? 0;
if (spellHotbarFrame != null)
spellHotbarFrame.style.display = slotCount > 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (loadout == null) return;
var layout = SpellHotkeys.Layout;
for (int i = 0; i < slotCount; i++)
{
var kind = loadout.GetKind(i);
if (kind == null) continue;
var definition = BuilderSpellPool.Instance?.Get(kind.Value);
var cell = new VisualElement();
cell.AddToClassList("spell-slot");
var icon = new VisualElement();
icon.AddToClassList("spell-slot-icon");
icon.pickingMode = PickingMode.Ignore;
if (definition?.Icon != null)
icon.style.backgroundImage = new StyleBackground(definition.Icon);
cell.Add(icon);
if (i < layout.Length)
{
var hkLabel = new Label(SpellKeyToDisplay(layout[i]));
hkLabel.AddToClassList("spell-slot-hotkey");
hkLabel.pickingMode = PickingMode.Ignore;
cell.Add(hkLabel);
}
var cooldownLabel = new Label("");
cooldownLabel.AddToClassList("spell-slot-cooldown-label");
cooldownLabel.pickingMode = PickingMode.Ignore;
cell.Add(cooldownLabel);
cell.RegisterCallback<MouseEnterEvent>(_ => ShowSpellTooltip(definition));
cell.RegisterCallback<MouseLeaveEvent>(_ => ClearTooltip());
spellHotbar.Add(cell);
spellSlotUis.Add(new SpellSlotUi(cell, cooldownLabel));
}
}
// Per-frame: dims each slot while on cooldown and shows the remaining whole seconds.
// Cooldown has no change event (it advances continuously with server time), so this
// has to be polled — cheap at hotbar scale (at most MaxSpellSlots cells).
private void UpdateSpellCooldowns()
{
if (spellSlotUis.Count == 0) return;
var loadout = PlayerSpellLoadout.Local;
if (loadout == null) return;
for (int i = 0; i < spellSlotUis.Count; i++)
{
var ui = spellSlotUis[i];
bool onCooldown = loadout.IsSlotOnCooldown(i);
ui.Cell.EnableInClassList("on-cooldown", onCooldown);
ui.CooldownLabel.text = onCooldown
? $"{Mathf.CeilToInt(loadout.GetCooldownRemaining(i))}"
: "";
}
}
// Renders a spell hotkey as its display glyph. SpellHotkeys.Layout uses Key.DigitN,
// whose ToString() is "DigitN" — strip the prefix so the badge shows "1", not "Digit1".
private static string SpellKeyToDisplay(Key key)
{
string s = key.ToString();
return s.StartsWith("Digit") ? s.Substring("Digit".Length) : s;
}
// Lightweight tooltip for spell slots — reuses the tooltip box (title + desc + cooldown).
private void ShowSpellTooltip(BuilderSpellDefinition def)
{
if (ttTitle == null || def == null) return;
ttTitle.text = def.DisplayName;
ttDesc.text = def.Description ?? "";
ttStats.text = $"Cooldown: {def.Cooldown:0.#}s";
ttCost.text = "";
}
private void InitializeUI()
{
var doc = GetComponent<UIDocument>();
@ -481,6 +617,8 @@ namespace TD.UI
ttStats = Require<Label>(root, "tt-stats");
ttCost = Require<Label>(root, "tt-cost");
rejectionLabel = Require<Label>(root, "rejection-label");
spellHotbarFrame = Require<VisualElement>(root, "spell-hotbar-frame");
spellHotbar = Require<VisualElement>(root, "spell-hotbar");
// Map area and its transparent ancestors must not consume pointer
// events so clicks reach the 3D scene underneath. The bottom-ui is now
@ -581,6 +719,13 @@ namespace TD.UI
}
draftSubscribed = false;
subscribedDraft = null;
if (spellLoadoutSubscribed && subscribedSpellLoadout != null)
{
subscribedSpellLoadout.OnLoadoutChanged -= HandleSpellLoadoutChanged;
}
spellLoadoutSubscribed = false;
subscribedSpellLoadout = null;
}
private void TrySubscribeSelection()
@ -607,11 +752,15 @@ namespace TD.UI
if (!draftSubscribed)
TrySubscribeDraft();
if (!spellLoadoutSubscribed)
TrySubscribeSpellLoadout();
RefreshGoldDisplay();
RefreshMatchStateDisplays();
UpdateBuildProgressIfShown();
UpdateEnemyInfoIfShown();
UpdateDraftVisibility();
UpdateSpellCooldowns();
HandleChatInput();
// Skip gameplay hotkeys while the chat input is focused — letters