Paint towers with some the colors of the wind

This commit is contained in:
Ben Calegari 2026-06-02 23:59:44 -07:00
parent fec4433691
commit 04ead32846
15 changed files with 584 additions and 32 deletions

View file

@ -27,6 +27,9 @@ namespace TD.UI
[Tooltip("The local client's TowerPlacementController.")]
[SerializeField] private TowerPlacementController placementController;
[Tooltip("The local client's TowerPaintController (drives the Paint tab + paint cursor).")]
[SerializeField] private TowerPaintController paintController;
[Tooltip("The TowerPlacementManager NetworkObject in the scene.")]
[SerializeField] private TowerPlacementManager placementManager;
@ -68,6 +71,9 @@ namespace TD.UI
private VisualElement statLines;
private VisualElement commandGrid;
private VisualElement actionFrame; // hidden via display:none when no actions are available
private VisualElement commandTabs; // Build/Paint tab row — shown only for a Builder selection
private Button tabBuild;
private Button tabPaint;
private VisualElement buildProgressContainer; // info-panel sub-view, shown for BuildSiteVisual selections
private VisualElement buildProgressFill; // width driven each frame from progress
private Label buildProgressPercent;
@ -110,6 +116,12 @@ namespace TD.UI
// ----- State ------------------------------------------------------
// Which command-grid tab is active for a Builder selection. Build = tower buttons
// (default), Paint = color swatches. Reset to Build whenever a non-builder is
// selected so reselecting a builder always starts on Build.
private enum CommandTab { Build, Paint }
private CommandTab activeTab = CommandTab.Build;
private Coroutine rejectionFadeCoroutine;
private bool placementManagerReady; // true once TowerPlacementManager.Instance is non-null
private bool uiInitialized;
@ -249,6 +261,12 @@ namespace TD.UI
statLines = Require<VisualElement>(root, "stat-lines");
commandGrid = Require<VisualElement>(root, "command-grid");
actionFrame = Require<VisualElement>(root, "action-frame");
commandTabs = Require<VisualElement>(root, "command-tabs");
tabBuild = Require<Button>(root, "tab-build");
tabPaint = Require<Button>(root, "tab-paint");
if (tabBuild != null) tabBuild.clicked += () => SwitchTab(CommandTab.Build);
if (tabPaint != null) tabPaint.clicked += () => SwitchTab(CommandTab.Paint);
buildProgressContainer = Require<VisualElement>(root, "build-progress");
buildProgressFill = Require<VisualElement>(root, "build-progress-fill");
buildProgressPercent = Require<Label>(root, "build-progress-percent");
@ -681,6 +699,19 @@ namespace TD.UI
if (actionFrame != null)
actionFrame.style.display = hasActions ? DisplayStyle.Flex : DisplayStyle.None;
// Build/Paint tabs only make sense for the builder's build menu. For any
// other selection (or none), reset to the Build tab and exit paint mode so
// the cursor never gets stuck as a brush after deselecting the builder.
bool isBuilder = selection is Builder;
if (!isBuilder)
{
activeTab = CommandTab.Build;
paintController?.CancelPaint();
}
if (commandTabs != null)
commandTabs.style.display = isBuilder ? DisplayStyle.Flex : DisplayStyle.None;
RefreshTabActiveState();
commandGrid.Clear();
if (!hasActions) return; // grid stays empty; frame is hidden anyway
@ -689,12 +720,24 @@ namespace TD.UI
if (selection is Builder)
{
int i = 0;
foreach (var (def, typeId) in placementManager.GetAvailableDefinitions())
if (activeTab == CommandTab.Paint)
{
if (i >= GRID_MAX) break;
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
i++;
// Paint swatches: Red/Green/Blue then a Reset (None) brush, mapped to
// the Q/W/E/R hotkey slots via the shared CreateActionButton wiring.
cells[0] = CreatePaintButton(PaintColor.Red, HotkeyLayout[0]);
cells[1] = CreatePaintButton(PaintColor.Green, HotkeyLayout[1]);
cells[2] = CreatePaintButton(PaintColor.Blue, HotkeyLayout[2]);
cells[3] = CreatePaintButton(PaintColor.None, HotkeyLayout[3]);
}
else
{
int i = 0;
foreach (var (def, typeId) in placementManager.GetAvailableDefinitions())
{
if (i >= GRID_MAX) break;
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
i++;
}
}
}
else if (selection is TowerInstance tower)
@ -745,6 +788,63 @@ namespace TD.UI
return btn;
}
// ----- Build/Paint tabs -------------------------------------------
// Switch the active command-grid tab and rebuild for the current selection.
// Leaving the Paint tab also exits paint mode so the cursor reverts.
private void SwitchTab(CommandTab tab)
{
if (tab == CommandTab.Build)
paintController?.CancelPaint();
activeTab = tab;
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
}
private void RefreshTabActiveState()
{
tabBuild?.EnableInClassList("active", activeTab == CommandTab.Build);
tabPaint?.EnableInClassList("active", activeTab == CommandTab.Paint);
}
// Paint swatch: clicking enters paint mode with that color (None = Reset brush).
// Built on CreateActionButton so the Q/W/E/R hotkey wiring comes for free; the
// icon placeholder is tinted to the swatch color.
private VisualElement CreatePaintButton(PaintColor color, Key hotkey)
{
var btn = CreateActionButton(
costText: "",
hotkey: hotkey,
onClick: () =>
{
if (paintController != null)
paintController.BeginPaint(color);
else
Debug.LogWarning("[HUDController] No TowerPaintController assigned.");
});
var icon = btn.Q<VisualElement>(null, "cmd-icon-placeholder");
if (icon != null)
icon.style.backgroundColor = PaintColors.Get(color);
string label = color == PaintColor.None ? "Reset" : color.ToString();
btn.RegisterCallback<MouseEnterEvent>(_ => ShowPaintTooltip(label, color));
btn.RegisterCallback<MouseLeaveEvent>(_ => ClearTooltip());
return btn;
}
// Lightweight tooltip for paint swatches — reuses the tooltip box (title + desc).
private void ShowPaintTooltip(string label, PaintColor color)
{
if (ttTitle == null) return;
ttTitle.text = label;
ttDesc.text = color == PaintColor.None
? "Clear paint — revert tower to its owner color."
: "Paint your towers this color.";
ttStats.text = "";
ttCost.text = "";
}
private static VisualElement CreateEmptySlot()
{
var slot = new VisualElement();