Files
stickman/docs/specs/phase_18_palette_icons.md

10 KiB
Raw Permalink Blame History

Phase 18 — Sandbox Stage palette image icons

Status: Implemented and user-verified. Scope: The six cards in the Sandbox Stage EDIT mode PaletteDock only. The modal Prop and Stickman asset selectors and other editor controls retain their own artwork and behavior.

Goal and approved UI

Replace the emoji at the top of each left palette card with the supplied image for that placement tool. Keep the text labels Ground, Ramp, Step, Prop, Stickman, and Area below the artwork. The cards remain recognizable, centered, and usable at the current size; their selection highlight, tooltip, keyboard focus, click, drag, mode visibility, and vertical scrolling retain their current behavior. This is a presentation change to the existing six tools.

Placement ID Card label Source PNG after naming Original supplied filename
ground Ground res://resources/icons/ground.png ground.png
ramp Ramp res://resources/icons/ramp.png ramp.png
step Step res://resources/icons/step.png steps.png
prop Prop res://resources/icons/prop.png props.png
stickman Stickman res://resources/icons/stickman.png stickmen.png
area Area res://resources/icons/area.png area.png

The existing AssetCard image area is 64 × 64 px. Each image keeps its aspect ratio inside that area and stays centered above its text label. The button remains the full pointer and focus target; its image and label do not consume input. No extra caption, selector badge, or hover text is added. The current label tooltip remains available.

All six original PNG source images are 1254 × 1254 px and must stay at that source resolution after the three file renames. Set Godot's import size limit to 256 px for these six textures so the runtime texture is smaller while the original source data is preserved. Do not resample or overwrite the PNGs. Regenerate import metadata for the renamed files through Godot's importer; remove stale import metadata for the old names as part of the rename.

Existing hierarchy and data flow

SandboxStage (scripts/editors/stage/sandbox_stage.gd)
  └─ StageChrome : CanvasLayer
       └─ PaletteDock : PanelContainer (EDIT only)
            └─ ScrollContainer
                 └─ PaletteCards : VBoxContainer
                      ├─ Ground AssetCard : Button
                      ├─ Ramp AssetCard : Button
                      ├─ Step AssetCard : Button
                      ├─ Prop AssetCard : Button
                      ├─ Stickman AssetCard : Button
                      └─ Area AssetCard : Button
                           └─ VBoxContainer
                                ├─ TextureRect (64 × 64 image)
                                ├─ Label (emoji fallback; hidden for these cards)
                                └─ Label (tool name)

StageSpawner.get_palette_entries() supplies the existing palette dictionaries. StageChrome._build_palette() resolves an image by the entry's placement_id and passes it to AssetCard.configure(). The card already shows its TextureRect and hides its emoji label when a texture is passed. Its activated(payload: Dictionary) and drag_started(payload: Dictionary) signals continue through StageChrome.palette_activated and StageChrome.palette_drag_started to the existing SandboxStage placement handlers.

flowchart LR
    Registry[StageSpawner palette entry] --> Chrome[StageChrome placement ID lookup]
    PNG[Preloaded palette PNG] --> Chrome
    Chrome -->|entry, Texture2D, theme| Card[AssetCard.configure]
    Card -->|activated Dictionary| Stage[SandboxStage placement handler]
    Card -->|drag_started Dictionary| Stage

Target classes, resources, and contracts

Target Contract for Phase 18
scripts/editors/stage/stage_chrome.gd — class_name StageChrome extends CanvasLayer Own one fixed Dictionary mapping the six placement IDs to preloaded Texture2D resources. In _build_palette(entries: Array[Dictionary]) -> void, resolve a Texture2D by placement_id and call the existing AssetCard.configure(payload: Dictionary, icon_texture: Texture2D, theme: Variant) -> void. Keep _palette_buttons: Dictionary, card signal connections, mode visibility, scroll hierarchy, and placement routing intact. The mapping is presentation data; do not derive game behavior from image paths.
scripts/shared/ui/asset_card.gd — class_name AssetCard extends Button Use its existing texture path: 64 × 64 default thumbnail_size, TextureRect.STRETCH_KEEP_ASPECT_CENTERED, image visibility when the texture is present, and hidden glyph label. No new API or signal is required. Keep its activated(payload: Dictionary) and drag_started(payload: Dictionary) payload copies and drag threshold.
scripts/stage/stage_spawner.gd — class_name StageSpawner extends RefCounted Existing get_palette_entries() -> Array[Dictionary] remains the source of placement IDs, labels, categories, and selector flags. Its current emoji icon: String value can remain as the generic AssetCard fallback; the six Stage palette cards receive image textures and do not render it.
resources/icons/*.png and Godot import metadata Rename the three plural source filenames to their singular placement IDs. Preserve all six 1254 × 1254 PNG source files. Set process/size_limit=256 for each imported texture, and let Godot regenerate source and imported cache paths for renamed files.
sandbox_theme.json and SandboxTheme Keep palette card dimensions, layout, selected style, and scroll behavior. The existing palette.icon_size concerns emoji glyph sizing and does not set the 64 × 64 image area; Phase 18 needs no new theme token.

Representative mapping and call shape:

const PALETTE_ICON_TEXTURES: Dictionary = {
    "ground": preload("res://resources/icons/ground.png"),
    "ramp": preload("res://resources/icons/ramp.png"),
    "step": preload("res://resources/icons/step.png"),
    "prop": preload("res://resources/icons/prop.png"),
    "stickman": preload("res://resources/icons/stickman.png"),
    "area": preload("res://resources/icons/area.png"),
}

var placement_id: String = String(entry.get("placement_id", ""))
var icon_texture: Texture2D = PALETTE_ICON_TEXTURES.get(placement_id) as Texture2D
card.configure(entry, icon_texture, _stage_theme)

The current payload shape and signal values remain unchanged, for example:

{
    "placement_id": "step",
    "kind": "terrain",
    "label": "Step",
    "icon": "🪜", # Unused fallback while an image texture is present.
    "category": "Terrain",
    "opens_selector": false,
}

Observable acceptance outcomes

User path Expected outcome
Launch res://scenes/stage/sandbox_stage.tscn and enter EDIT. Six palette cards show their corresponding images and the existing text labels. No emoji is visible on these six cards. Each image is centered, fully visible, and keeps its shape within the 64 × 64 area.
Click each card, including Prop and Stickman. The same persistent placement tool or asset selector opens as before; the selected card's visual state and tooltip still work.
Drag a card into the stage and release, then try releasing over UI or cancelling. The existing one-placement drag flow places exactly one item on a valid stage release and none on an invalid or cancelled release. No stray ghost remains.
Switch EDIT → DIRECT or PLAY → EDIT. The palette follows its existing EDIT-only visibility and the selected placement state still follows the Stage.
Make the window short enough for palette overflow and scroll the dock. All six cards remain reachable and their image/label pairs remain inside their button bounds.
Inspect the six source PNGs and their Godot imports after the rename. The source PNGs remain 1254 × 1254; each import has a 256 px size limit and the three plural source filenames are no longer used.

For implementation verification, run a Godot import/syntax check and the focused existing palette regression suite, then inspect the rendered Sandbox Stage at normal and short window heights. A successful import alone cannot establish that each image is visually legible or mapped to the correct card.

Architectural trade-offs

  • Local preload map: The six images are fixed UI presentation assets, so StageChrome owns their paths. This keeps the spawn registry independent of artwork paths and uses Godot's resource cache rather than loading on every card creation. A missing preloaded file is detected during import instead of silently shipping an emoji in the dock.
  • Existing AssetCard: Its texture, glyph fallback, label, and input contracts already meet the UI specification. Reusing them avoids another palette control and keeps the modal asset selector's separate thumbnail workflow intact.
  • 256 px import cap with full-size sources: The 64 px display still has source resolution for scaling, while import memory is reduced. The original PNGs remain available at 1254 × 1254 for later art or export needs. The cap belongs to import metadata, not the source image bytes.

Implementation and verification

Phase 18 is implemented in scripts/editors/stage/stage_chrome.gd. The six palette placement IDs map to their corresponding preloaded images; step.png, prop.png, and stickman.png replace the plural filenames. All six source PNGs remain 1254 × 1254, with Godot import size limits set to 256 px. The cards keep their 64 × 64 image areas, labels, and existing interaction paths.

Verification Result
Godot 4.7.1 headless import Passed (exit 0); Windows certificate-store and global editor-settings warnings were reported.
tests/test_phase7_palette.gd Passed.
Headless Sandbox Stage launch Stage loaded successfully.
git diff --check Passed.
Godot Editor visual check User verified the palette appearance and placement behavior.