From 53cb92041ce790d0908ed41718cf7b6c8b6495df Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 16:09:56 +0100 Subject: [PATCH] UI pass: medieval-warm Theme + real Build drawer thumbnails Theme (scenes/ui/medieval_theme.gd): - Procedural builder for an app-wide Theme: parchment buttons on dark-wood border, tan panels, ink text, gold focus ring. - Applied on the root Window in Main._ready, with a tree-walk that re-seeds every CanvasLayer's topmost Control (CanvasLayer interrupts the root-Window theme cascade in Godot 4). - Late-mounting popups + modals get themed via a child_entered_tree hook on each CanvasLayer. Build drawer thumbnails (scenes/ui/build_drawer_thumb.gd): - New BuildDrawerThumb Control that dispatches on tool_id and draws a recognisable silhouette of the entity each tool builds. 17 tools covered: chop/mine/dig_grave/no_roof (Designate), stone+wood walls, wood+stone floors, door, crate, bed, torch, 5 workbenches (Carpenter/Smelter/Millstone/Hearth/Cremation Pyre), stockpile, graveyard. - Replaces the flat ColorRect placeholder. _add_tool_btn signature changed from (label, color, callback) to (label, tool_id, callback). --- scenes/main/main.gd | 42 ++++ scenes/ui/build_drawer.gd | 66 ++++--- scenes/ui/build_drawer_thumb.gd | 338 ++++++++++++++++++++++++++++++++ scenes/ui/medieval_theme.gd | 198 +++++++++++++++++++ 4 files changed, 614 insertions(+), 30 deletions(-) create mode 100644 scenes/ui/build_drawer_thumb.gd create mode 100644 scenes/ui/medieval_theme.gd diff --git a/scenes/main/main.gd b/scenes/main/main.gd index c5b559f..b3e6686 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -20,6 +20,11 @@ const RESUME_TOAST_SCRIPT: Script = preload("res://scenes/ui/resume_toa # Phase 17 — PawnDetailPanel (layer 18) and SettingsMenu (layer 26). const PAWN_DETAIL_PANEL_SCRIPT: Script = preload("res://scenes/ui/pawn_detail_panel.gd") const WORKBENCH_PANEL_SCRIPT: Script = preload("res://scenes/ui/workbench_panel.gd") +const MEDIEVAL_THEME_SCRIPT: Script = preload("res://scenes/ui/medieval_theme.gd") + +# Built once in _ready and re-applied to any CanvasLayer-rooted Control because +# CanvasLayer doesn't propagate the root-Window theme cascade. +var _app_theme: Theme = null const SETTINGS_MENU_SCRIPT: Script = preload("res://scenes/ui/settings_menu.gd") # Phase 17 (Agent B) — BuildDrawer bottom-sheet (layer 16). const BUILD_DRAWER_SCRIPT: Script = preload("res://scenes/ui/build_drawer.gd") @@ -40,6 +45,13 @@ func _ready() -> void: assert(SaveSystem != null, "SaveSystem autoload missing") assert(Autosave != null, "Autosave autoload missing") + # Medieval-warm Theme — assigned on the root Window first. Cascade alone + # doesn't reach Controls inside CanvasLayers (CanvasLayer has no theme + # property), so we also walk the tree post-mount and apply to every Control + # encountered. (2026-05-16 polish pass.) + _app_theme = MEDIEVAL_THEME_SCRIPT.build() + get_tree().root.theme = _app_theme + # Phase 15 — Storyteller UI layers. Runtime-instantiated so no .tscn edit is # needed. CanvasLayer ensures correct draw order above World/TopBar regardless # of parent-node position. @@ -156,3 +168,33 @@ func _ready() -> void: top_bar._add_work_log_btns() Audit.log("main", "Phase 17 (Agent C) — WorkPriorityMatrix + AlertsLog mounted.") + + # Apply the medieval theme to every Control under each CanvasLayer. + # CanvasLayers interrupt the root-Window theme cascade so we have to seed + # each one explicitly. Defer one frame so panels that build their UI in + # _ready (PawnDetailPanel, WorkbenchPanel, BuildDrawer) finish first. + call_deferred("_apply_theme_to_canvas_layers") + + +## Walks the scene tree and assigns _app_theme to every Control directly under +## a CanvasLayer (the topmost Control in each layer's branch). From there the +## standard Control-to-Control cascade carries the theme to all descendants. +## Also catches popups and modals that mount later via child_entered_tree. +func _apply_theme_to_canvas_layers() -> void: + for c in get_children(): + if c is CanvasLayer: + _seed_layer_theme(c) + # Watch for late additions (popup menus, modals). + if not c.child_entered_tree.is_connected(_on_layer_child_added): + c.child_entered_tree.connect(_on_layer_child_added) + + +func _seed_layer_theme(layer: CanvasLayer) -> void: + for c in layer.get_children(): + if c is Control and c.theme == null: + c.theme = _app_theme + + +func _on_layer_child_added(node: Node) -> void: + if node is Control and node.theme == null: + node.theme = _app_theme diff --git a/scenes/ui/build_drawer.gd b/scenes/ui/build_drawer.gd index bacfa28..07b9ae0 100644 --- a/scenes/ui/build_drawer.gd +++ b/scenes/ui/build_drawer.gd @@ -180,10 +180,10 @@ func _build_designate_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.chop"), Color(0.3, 0.7, 0.2), func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) - _add_tool_btn(flow, Strings.t(&"tool.mine"), Color(0.6, 0.6, 0.6), func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) - _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),Color(0.4, 0.3, 0.2), func() -> void: _activate(&"dig_grave",&"", Strings.t(&"tool.dig_grave"))) - _add_tool_btn(flow, Strings.t(&"tool.no_roof"), Color(0.7, 0.7, 0.9), func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) + _add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) + _add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) + _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),&"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave"))) + _add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) return box @@ -197,44 +197,44 @@ func _build_build_tab() -> Control: box.add_child(flow) # Wall — show material chooser on first tap. - _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), Color(0.55, 0.55, 0.55), + _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), &"build_wall_stone", func() -> void: _activate_wall(&"stone")) - _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), Color(0.65, 0.45, 0.25), + _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), &"build_wall_wood", func() -> void: _activate_wall(&"wood")) # Floor. - _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), Color(0.60, 0.40, 0.20), + _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), &"build_floor_wood", func() -> void: _activate_floor(&"wood")) - _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), Color(0.60, 0.60, 0.55), + _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), &"build_floor_stone", func() -> void: _activate_floor(&"stone")) # Door + Crate. - _add_tool_btn(flow, Strings.t(&"tool.build_door"), Color(0.55, 0.35, 0.15), + _add_tool_btn(flow, Strings.t(&"tool.build_door"), &"build_door", func() -> void: _activate(&"build_door", &"", Strings.t(&"tool.build_door"))) - _add_tool_btn(flow, Strings.t(&"tool.build_crate"), Color(0.65, 0.45, 0.10), + _add_tool_btn(flow, Strings.t(&"tool.build_crate"), &"build_crate", func() -> void: _activate(&"build_crate", &"", Strings.t(&"tool.build_crate"))) # Bed + Torch. - _add_tool_btn(flow, Strings.t(&"tool.build_bed"), Color(0.40, 0.40, 0.80), + _add_tool_btn(flow, Strings.t(&"tool.build_bed"), &"build_bed", func() -> void: _activate(&"build_bed", &"", Strings.t(&"tool.build_bed"))) - _add_tool_btn(flow, Strings.t(&"tool.build_torch"), Color(0.90, 0.70, 0.20), + _add_tool_btn(flow, Strings.t(&"tool.build_torch"), &"build_torch", func() -> void: _activate(&"build_torch", &"", Strings.t(&"tool.build_torch"))) # Workbenches. _add_tool_btn(flow, Strings.t(&"tool.workbench_carpenter"), - Color(0.50, 0.35, 0.15), + &"build_workbench_carpenter", func() -> void: _activate(&"build_workbench_carpenter", &"", Strings.t(&"tool.workbench_carpenter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_smelter"), - Color(0.60, 0.55, 0.45), + &"build_workbench_smelter", func() -> void: _activate(&"build_workbench_smelter", &"", Strings.t(&"tool.workbench_smelter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_millstone"), - Color(0.55, 0.55, 0.55), + &"build_workbench_millstone", func() -> void: _activate(&"build_workbench_millstone", &"", Strings.t(&"tool.workbench_millstone"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_hearth"), - Color(0.80, 0.35, 0.15), + &"build_workbench_hearth", func() -> void: _activate(&"build_workbench_hearth", &"", Strings.t(&"tool.workbench_hearth"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_cremation_pyre"), - Color(0.30, 0.25, 0.20), + &"build_workbench_cremation_pyre", func() -> void: _activate(&"build_workbench_cremation_pyre", &"", Strings.t(&"tool.workbench_cremation_pyre"))) return box @@ -248,9 +248,9 @@ func _build_stockpile_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), Color(0.30, 0.60, 0.30), + _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), &"paint_stockpile", func() -> void: _activate(&"paint_stockpile", &"", Strings.t(&"tool.stockpile_general"))) - _add_tool_btn(flow, Strings.t(&"tool.graveyard"), Color(0.25, 0.20, 0.15), + _add_tool_btn(flow, Strings.t(&"tool.graveyard"), &"graveyard", func() -> void: _activate(&"graveyard", &"", Strings.t(&"tool.graveyard"))) return box @@ -291,10 +291,13 @@ func _make_flow_grid() -> GridContainer: return g -## Add a single tool button to `container`. Each button is a VBoxContainer of -## [ColorRect icon area + Label] wrapped in a Button so the whole cell is one -## touch target. -func _add_tool_btn(container: Control, label_text: String, icon_color: Color, callback: Callable) -> void: +const _THUMB_SCRIPT: Script = preload("res://scenes/ui/build_drawer_thumb.gd") + + +## Add a single tool button to `container`. The button is a VBoxContainer of +## [thumb preview + Label] wrapped in a Button so the whole cell is one touch +## target. `tool_id` drives the procedural preview shape (BuildDrawerThumb). +func _add_tool_btn(container: Control, label_text: String, tool_id: StringName, callback: Callable) -> void: var btn := Button.new() btn.custom_minimum_size = Vector2(BTN_SIZE, BTN_SIZE + LABEL_HEIGHT) btn.focus_mode = Control.FOCUS_NONE @@ -303,13 +306,16 @@ func _add_tool_btn(container: Control, label_text: String, icon_color: Color, ca vb.mouse_filter = Control.MOUSE_FILTER_IGNORE vb.add_theme_constant_override("separation", 2) - # Icon area — procedural colored rect (real sprites land with Phase 17 art pass). - var icon := ColorRect.new() - icon.color = icon_color - icon.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) - icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - vb.add_child(icon) + # Procedural preview of the entity this tool builds. + var thumb := Control.new() + thumb.set_script(_THUMB_SCRIPT) + # Use .set() — the static type is Control (set_script doesn't refine it), + # but the runtime instance has the tool_id property from the script. + thumb.set("tool_id", tool_id) + thumb.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) + thumb.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + thumb.mouse_filter = Control.MOUSE_FILTER_IGNORE + vb.add_child(thumb) # Label. var lbl := Label.new() diff --git a/scenes/ui/build_drawer_thumb.gd b/scenes/ui/build_drawer_thumb.gd new file mode 100644 index 0000000..22ec4d5 --- /dev/null +++ b/scenes/ui/build_drawer_thumb.gd @@ -0,0 +1,338 @@ +class_name BuildDrawerThumb extends Control +## Procedural preview thumbnail for one BuildDrawer tool button. +## +## Each tool_id dispatches to a small _draw call that renders a recognisable +## silhouette of the entity that tool builds. All shapes live inside a 40×40 +## box centred in this control's size — the parent button supplies the panel +## frame; this widget only paints the icon. +## +## Cheaper than instantiating live entity scenes into SubViewports; pure draw +## calls, no allocations, recomputes only when the button repaints. + +## Tool identifier — drives the shape dispatch in _draw. +var tool_id: StringName = &"" + + +func _draw() -> void: + var c := size / 2.0 + Vector2(0, -2) # slight upward bias for label clearance + # Soft shadow under every thumb (subtle depth cue against the parchment). + draw_circle(c + Vector2(0, 12), 14.0, Color(0, 0, 0, 0.08)) + var outline := Color(0.10, 0.07, 0.05, 0.85) + match tool_id: + &"build_wall_stone": + # Grey brick wall with staggered courses. + var brick := Color(0.62, 0.60, 0.58) + var brick_hi := Color(0.78, 0.76, 0.72) + var mortar := Color(0.30, 0.28, 0.26) + var r := Rect2(c.x - 16, c.y - 16, 32, 32) + draw_rect(r, brick) + # Three brick courses, staggered seams. + for i in 3: + var y: float = r.position.y + i * 11 + 4 + draw_line(Vector2(r.position.x, y), Vector2(r.end.x, y), mortar, 1.0) + # Vertical seams (staggered per row). + for i in 3: + var y: float = r.position.y + i * 11 + 4 + var stagger: float = 0.0 if i % 2 == 0 else 8.0 + var x_offs: Array[float] = [-8.0, 0.0, 8.0] + for x_off in x_offs: + var x: float = c.x + x_off + stagger + if x >= r.position.x and x <= r.end.x: + draw_line(Vector2(x, y - 11), Vector2(x, y), mortar, 1.0) + # Top edge highlight. + draw_rect(Rect2(r.position.x, r.position.y, r.size.x, 3), brick_hi) + draw_rect(r, outline, false, 1.0) + &"build_wall_wood": + # Brown wood-plank wall, 3 vertical planks. + var wood := Color(0.65, 0.45, 0.25) + var wood_dark := Color(0.42, 0.27, 0.12) + var r := Rect2(c.x - 16, c.y - 16, 32, 32) + draw_rect(r, wood) + draw_line(Vector2(c.x - 5.5, r.position.y), Vector2(c.x - 5.5, r.end.y), wood_dark, 1.0) + draw_line(Vector2(c.x + 5.5, r.position.y), Vector2(c.x + 5.5, r.end.y), wood_dark, 1.0) + # Knot details (small dots). + draw_circle(Vector2(c.x - 11, c.y - 6), 1.2, wood_dark) + draw_circle(Vector2(c.x + 1, c.y + 4), 1.2, wood_dark) + draw_rect(r, outline, false, 1.0) + &"build_floor_wood": + # Tan floorboard plan-view — 4 planks horizontal with grain. + var board := Color(0.78, 0.58, 0.32) + var board_dark := Color(0.50, 0.32, 0.16) + var r := Rect2(c.x - 16, c.y - 12, 32, 24) + draw_rect(r, board) + for i in 1.0: + pass # placeholder loop + draw_line(Vector2(r.position.x, c.y - 6), Vector2(r.end.x, c.y - 6), board_dark, 1.0) + draw_line(Vector2(r.position.x, c.y), Vector2(r.end.x, c.y), board_dark, 1.0) + draw_line(Vector2(r.position.x, c.y + 6), Vector2(r.end.x, c.y + 6), board_dark, 1.0) + draw_rect(r, outline, false, 1.0) + &"build_floor_stone": + # Grey paving stones — 2×2 grid with cross-mortar. + var stone := Color(0.65, 0.63, 0.60) + var mortar := Color(0.35, 0.33, 0.30) + var r := Rect2(c.x - 16, c.y - 12, 32, 24) + draw_rect(r, stone) + draw_line(Vector2(c.x, r.position.y), Vector2(c.x, r.end.y), mortar, 1.0) + draw_line(Vector2(r.position.x, c.y), Vector2(r.end.x, c.y), mortar, 1.0) + draw_rect(r, outline, false, 1.0) + &"build_door": + # Wooden door — rounded-top arch silhouette with a handle dot. + var door := Color(0.55, 0.35, 0.15) + var door_hi := Color(0.75, 0.50, 0.25) + var handle := Color(0.95, 0.80, 0.20) + # Body + var pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 10, c.y + 16), Vector2(c.x - 10, c.y - 6), + Vector2(c.x - 8, c.y - 12), Vector2(c.x, c.y - 16), + Vector2(c.x + 8, c.y - 12), Vector2(c.x + 10, c.y - 6), + Vector2(c.x + 10, c.y + 16), + ]) + draw_colored_polygon(pts, door) + # Inner plank seam. + draw_line(Vector2(c.x, c.y - 14), Vector2(c.x, c.y + 14), door_hi, 1.0) + # Handle. + draw_circle(Vector2(c.x + 5, c.y + 4), 1.5, handle) + draw_polyline(pts + PackedVector2Array([pts[0]]), outline, 1.0) + &"build_crate": + # Wooden crate — brown box with X cross-bracing on the front. + var wood := Color(0.65, 0.42, 0.18) + var wood_dark := Color(0.42, 0.27, 0.10) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, wood) + # Top edge bevel. + draw_rect(Rect2(r.position.x, r.position.y, r.size.x, 3), Color(0.85, 0.62, 0.30)) + # X cross-bracing. + draw_line(r.position, r.end, wood_dark, 2.0) + draw_line(Vector2(r.position.x, r.end.y), Vector2(r.end.x, r.position.y), wood_dark, 2.0) + draw_rect(r, outline, false, 1.0) + &"build_bed": + # Top-down bed view — wood frame, pillow top, blanket body. + var frame := Color(0.55, 0.35, 0.15) + var blanket := Color(0.55, 0.42, 0.78) + var pillow := Color(0.95, 0.93, 0.85) + var r := Rect2(c.x - 12, c.y - 14, 24, 28) + # Frame. + draw_rect(r, frame) + # Blanket inside (slightly inset). + draw_rect(Rect2(r.position.x + 2, r.position.y + 8, r.size.x - 4, r.size.y - 10), blanket) + # Pillow at top. + draw_rect(Rect2(r.position.x + 2, r.position.y + 2, r.size.x - 4, 5), pillow) + # Outline. + draw_rect(r, outline, false, 1.0) + &"build_torch": + # Wall torch — vertical brown shaft with orange flame above. + var shaft := Color(0.45, 0.28, 0.12) + var bracket := Color(0.35, 0.35, 0.38) + var flame_outer := Color(0.95, 0.40, 0.05) + var flame_inner := Color(1.00, 0.85, 0.30) + # Wall bracket + draw_rect(Rect2(c.x - 8, c.y + 6, 16, 4), bracket) + # Torch shaft. + draw_rect(Rect2(c.x - 2, c.y - 6, 4, 14), shaft) + # Flame teardrop. + draw_circle(Vector2(c.x, c.y - 10), 5.0, flame_outer) + draw_circle(Vector2(c.x, c.y - 12), 2.5, flame_inner) + # Smoke wisp. + draw_line(Vector2(c.x, c.y - 16), Vector2(c.x + 2, c.y - 20), Color(0.70, 0.70, 0.70, 0.6), 1.0) + &"build_workbench_carpenter": + # Wood bench with saw on top, two legs visible. + var plank_top := Color(0.78, 0.55, 0.30) + var plank_front := Color(0.55, 0.38, 0.22) + var leg := Color(0.32, 0.22, 0.10) + var saw_blade := Color(0.82, 0.82, 0.85) + var saw_handle := Color(0.55, 0.30, 0.15) + # Legs. + draw_rect(Rect2(c.x - 14, c.y, 3, 16), leg) + draw_rect(Rect2(c.x + 11, c.y, 3, 16), leg) + # Bench body. + draw_rect(Rect2(c.x - 16, c.y - 4, 32, 8), plank_front) + draw_rect(Rect2(c.x - 16, c.y - 8, 32, 4), plank_top) + # Saw blade. + draw_rect(Rect2(c.x - 4, c.y - 12, 12, 2), saw_blade) + draw_rect(Rect2(c.x + 8, c.y - 13, 4, 4), saw_handle) + draw_rect(Rect2(c.x - 16, c.y - 12, 32, 16), outline, false, 1.0) + &"build_workbench_smelter": + # Stone furnace with ember opening + smoke from chimney. + var stone := Color(0.55, 0.55, 0.55) + var stone_front := Color(0.42, 0.42, 0.43) + var ember := Color(0.98, 0.55, 0.10) + var ember_core := Color(1.00, 0.85, 0.30) + var chimney := Color(0.32, 0.30, 0.30) + var smoke := Color(0.75, 0.73, 0.70, 0.7) + # Stone body. + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 22), stone_front) + draw_rect(Rect2(c.x - 14, c.y - 11, 28, 4), stone) + # Ember mouth. + draw_rect(Rect2(c.x - 7, c.y - 2, 14, 8), Color(0.18, 0.10, 0.06)) + draw_rect(Rect2(c.x - 5, c.y, 10, 4), ember) + draw_rect(Rect2(c.x - 3, c.y + 1, 6, 2), ember_core) + # Chimney + smoke. + draw_rect(Rect2(c.x + 4, c.y - 16, 5, 6), chimney) + draw_line(Vector2(c.x + 6, c.y - 18), Vector2(c.x + 5, c.y - 23), smoke, 1.5) + draw_rect(Rect2(c.x - 14, c.y - 11, 28, 25), outline, false, 1.0) + &"build_workbench_millstone": + # Wood frame + round grindstone wheel. + var frame_front := Color(0.42, 0.26, 0.12) + var frame_top := Color(0.55, 0.36, 0.18) + var wheel := Color(0.55, 0.53, 0.50) + var wheel_rim := Color(0.20, 0.18, 0.16) + var wheel_dark := Color(0.34, 0.32, 0.30) + # Frame. + draw_rect(Rect2(c.x - 14, c.y + 2, 28, 12), frame_front) + draw_rect(Rect2(c.x - 14, c.y - 1, 28, 5), frame_top) + # Wheel. + draw_circle(c + Vector2(0, -4), 11.0, wheel_rim) + draw_circle(c + Vector2(0, -4), 9.5, wheel) + # Front-face shadow (lower half). + draw_rect(Rect2(c.x - 9, c.y - 4, 18, 9), wheel_dark) + # Center pin. + draw_circle(c + Vector2(0, -4), 2.0, Color(0.18, 0.16, 0.14)) + draw_rect(Rect2(c.x - 14, c.y - 15, 28, 29), outline, false, 1.0) + &"build_workbench_hearth": + # Tall stone fireplace with wood mantle + flame. + var stone := Color(0.60, 0.58, 0.55) + var stone_dark := Color(0.42, 0.40, 0.38) + var mantle := Color(0.50, 0.34, 0.20) + var opening := Color(0.10, 0.05, 0.02) + var flame_outer := Color(0.95, 0.40, 0.05) + var flame_inner := Color(1.00, 0.85, 0.30) + var log_wood := Color(0.55, 0.32, 0.15) + # Stone surround. + draw_rect(Rect2(c.x - 14, c.y - 16, 28, 32), stone) + draw_line(Vector2(c.x - 14, c.y - 8), Vector2(c.x + 14, c.y - 8), stone_dark, 1.0) + # Mantle band. + draw_rect(Rect2(c.x - 14, c.y - 6, 28, 3), mantle) + # Opening. + draw_rect(Rect2(c.x - 9, c.y - 2, 18, 18), opening) + # Log. + draw_rect(Rect2(c.x - 6, c.y + 10, 12, 3), log_wood) + # Flame. + draw_rect(Rect2(c.x - 4, c.y + 4, 8, 6), flame_outer) + draw_rect(Rect2(c.x - 3, c.y + 1, 6, 4), flame_outer) + draw_rect(Rect2(c.x - 2, c.y + 4, 4, 4), flame_inner) + draw_rect(Rect2(c.x - 14, c.y - 16, 28, 32), outline, false, 1.0) + &"build_workbench_cremation_pyre": + # Charred wood pile with ember glow + ash smoke. + var base_top := Color(0.30, 0.22, 0.12) + var base_front := Color(0.22, 0.15, 0.08) + var ember := Color(0.95, 0.45, 0.10) + var ash_grey := Color(0.70, 0.68, 0.65, 0.7) + draw_rect(Rect2(c.x - 14, c.y - 4, 28, 16), base_front) + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 4), base_top) + draw_rect(Rect2(c.x - 9, c.y + 2, 18, 4), ember) + # Smoke wisps. + draw_rect(Rect2(c.x - 5, c.y - 14, 2, 6), ash_grey) + draw_rect(Rect2(c.x + 1, c.y - 16, 2, 8), ash_grey) + draw_rect(Rect2(c.x + 5, c.y - 12, 2, 4), ash_grey) + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 20), outline, false, 1.0) + &"paint_stockpile": + # Green tile with dashed boundary — a designated stockpile zone. + var fill := Color(0.35, 0.65, 0.30, 0.45) + var border := Color(0.20, 0.45, 0.15) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, fill) + # Dashed border: 4 dashes per side. + for i in 4: + draw_line(Vector2(r.position.x + i * 7, r.position.y), + Vector2(r.position.x + i * 7 + 4, r.position.y), border, 2.0) + draw_line(Vector2(r.position.x + i * 7, r.end.y), + Vector2(r.position.x + i * 7 + 4, r.end.y), border, 2.0) + for i in 3: + draw_line(Vector2(r.position.x, r.position.y + i * 8), + Vector2(r.position.x, r.position.y + i * 8 + 4), border, 2.0) + draw_line(Vector2(r.end.x, r.position.y + i * 8), + Vector2(r.end.x, r.position.y + i * 8 + 4), border, 2.0) + &"graveyard": + # Dark earth tile + grave cross marker. + var earth := Color(0.35, 0.28, 0.20) + var earth_hi := Color(0.50, 0.40, 0.28) + var cross := Color(0.78, 0.78, 0.76) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, earth) + draw_line(Vector2(r.position.x, r.position.y + 3), Vector2(r.end.x, r.position.y + 3), earth_hi, 1.0) + # Cross (gravestone marker). + draw_rect(Rect2(c.x - 1.5, c.y - 8, 3, 18), cross) + draw_rect(Rect2(c.x - 6, c.y - 4, 12, 3), cross) + draw_rect(r, outline, false, 1.0) + &"chop": + # Axe head + handle silhouette over a green target tile. + var grass := Color(0.35, 0.65, 0.30, 0.35) + var handle := Color(0.55, 0.35, 0.15) + var blade := Color(0.78, 0.80, 0.85) + var blade_dark := Color(0.45, 0.48, 0.52) + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), grass) + # Handle (diagonal). + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 6, c.y - 8), handle, 3.0) + # Axe head. + var ax_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x + 2, c.y - 12), Vector2(c.x + 12, c.y - 6), + Vector2(c.x + 8, c.y), Vector2(c.x - 2, c.y - 4), + ]) + draw_colored_polygon(ax_pts, blade) + draw_polyline(ax_pts + PackedVector2Array([ax_pts[0]]), blade_dark, 1.0) + &"mine": + # Pickaxe over a grey stone tile. + var stone := Color(0.62, 0.60, 0.58, 0.4) + var handle := Color(0.55, 0.35, 0.15) + var head := Color(0.48, 0.48, 0.52) + var head_dark := Color(0.28, 0.28, 0.32) + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), stone) + # Handle. + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 6, c.y - 8), handle, 3.0) + # Two-pointed pickaxe head. + var pk_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 6, c.y - 12), Vector2(c.x + 12, c.y - 6), + Vector2(c.x + 4, c.y - 2), Vector2(c.x, c.y - 6), + Vector2(c.x - 4, c.y - 4), + ]) + draw_colored_polygon(pk_pts, head) + draw_polyline(pk_pts + PackedVector2Array([pk_pts[0]]), head_dark, 1.0) + &"dig_grave": + # Shovel + earth mound. + var earth := Color(0.35, 0.25, 0.15) + var earth_hi := Color(0.55, 0.40, 0.25) + var handle := Color(0.55, 0.35, 0.15) + var blade := Color(0.55, 0.55, 0.60) + # Earth mound at bottom. + var mound: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 14, c.y + 12), Vector2(c.x + 14, c.y + 12), + Vector2(c.x + 8, c.y + 4), Vector2(c.x - 8, c.y + 4), + ]) + draw_colored_polygon(mound, earth) + draw_line(Vector2(c.x - 6, c.y + 6), Vector2(c.x + 6, c.y + 6), earth_hi, 1.0) + # Shovel handle. + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 4, c.y - 10), handle, 3.0) + # Shovel blade. + var sh_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x + 3, c.y - 12), Vector2(c.x + 10, c.y - 10), + Vector2(c.x + 10, c.y - 4), Vector2(c.x + 4, c.y - 4), + ]) + draw_colored_polygon(sh_pts, blade) + &"no_roof": + # Open square with up-arrow (cancel-roof designation). + var sky := Color(0.55, 0.75, 0.95, 0.4) + var border := Color(0.20, 0.40, 0.65) + var arrow := Color(0.95, 0.95, 0.95) + # Square outline (dashed corners) representing the cell. + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), sky) + # Corner brackets. + var corners: Array = [ + [Vector2(c.x - 14, c.y - 12), Vector2(1, 0), Vector2(0, 1)], + [Vector2(c.x + 14, c.y - 12), Vector2(-1, 0), Vector2(0, 1)], + [Vector2(c.x - 14, c.y + 12), Vector2(1, 0), Vector2(0, -1)], + [Vector2(c.x + 14, c.y + 12), Vector2(-1, 0), Vector2(0, -1)], + ] + for corner in corners: + var pos: Vector2 = corner[0] + var dx: Vector2 = corner[1] + var dy: Vector2 = corner[2] + draw_line(pos, pos + dx * 5.0, border, 2.0) + draw_line(pos, pos + dy * 5.0, border, 2.0) + # Up arrow centred. + draw_line(Vector2(c.x, c.y + 6), Vector2(c.x, c.y - 6), arrow, 2.0) + draw_line(Vector2(c.x, c.y - 6), Vector2(c.x - 4, c.y - 2), arrow, 2.0) + draw_line(Vector2(c.x, c.y - 6), Vector2(c.x + 4, c.y - 2), arrow, 2.0) + _: + # Unknown tool — small grey placeholder. + draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), Color(0.50, 0.50, 0.50)) + draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), outline, false, 1.0) diff --git a/scenes/ui/medieval_theme.gd b/scenes/ui/medieval_theme.gd new file mode 100644 index 0000000..afecbf7 --- /dev/null +++ b/scenes/ui/medieval_theme.gd @@ -0,0 +1,198 @@ +class_name MedievalTheme extends RefCounted +## Builds a Theme resource implementing the "Medieval warm" palette: +## Panel: tan (198, 168, 128) +## Border: dark wood (90, 55, 30) +## Button: parchment (230, 210, 170), hover lighter, pressed inset shadow +## Text: ink black with off-white on dark panels +## +## Applied on Main scene root so it cascades to every Control descendant. +## Per-panel overrides are still possible — only nodes that don't explicitly +## override a style pick up the global Theme. + +# ── palette ────────────────────────────────────────────────────────────────── +const C_PANEL := Color(0.776, 0.659, 0.502) # tan +const C_PANEL_DARK := Color(0.353, 0.216, 0.118) # dark wood +const C_BUTTON := Color(0.902, 0.824, 0.667) # parchment +const C_BUTTON_HOV := Color(0.961, 0.902, 0.769) # warm parchment +const C_BUTTON_PRESS:= Color(0.776, 0.694, 0.529) # pressed shadow +const C_BUTTON_DIS := Color(0.690, 0.620, 0.510) # disabled +const C_INK := Color(0.106, 0.078, 0.039) +const C_INK_DIM := Color(0.353, 0.275, 0.196) +const C_ACCENT := Color(0.580, 0.180, 0.110) # wax-seal red, for selected tabs +const C_ACCENT_GOLD := Color(0.831, 0.620, 0.149) + + +static func build() -> Theme: + var theme := Theme.new() + # Default font size — small UI, but legible on phone. + theme.default_font_size = 14 + + # ── Button (drives OptionButton + MenuButton too) ───────────────────────── + theme.set_stylebox("normal", "Button", _btn_box(C_BUTTON, false)) + theme.set_stylebox("hover", "Button", _btn_box(C_BUTTON_HOV, false)) + theme.set_stylebox("pressed", "Button", _btn_box(C_BUTTON_PRESS, true)) + theme.set_stylebox("disabled", "Button", _btn_box(C_BUTTON_DIS, false)) + theme.set_stylebox("focus", "Button", _focus_box()) + theme.set_color("font_color", "Button", C_INK) + theme.set_color("font_hover_color", "Button", C_INK) + theme.set_color("font_pressed_color", "Button", C_INK) + theme.set_color("font_disabled_color", "Button", C_INK_DIM) + theme.set_constant("h_separation", "Button", 6) + + # OptionButton inherits Button styling automatically via class fallback in + # Godot, but we set explicit copies in case overrides land. + theme.set_stylebox("normal", "OptionButton", _btn_box(C_BUTTON, false)) + theme.set_stylebox("hover", "OptionButton", _btn_box(C_BUTTON_HOV, false)) + theme.set_stylebox("pressed", "OptionButton", _btn_box(C_BUTTON_PRESS, true)) + theme.set_stylebox("focus", "OptionButton", _focus_box()) + theme.set_color("font_color", "OptionButton", C_INK) + + # ── CheckBox ────────────────────────────────────────────────────────────── + theme.set_color("font_color", "CheckBox", C_INK) + theme.set_color("font_hover_color", "CheckBox", C_INK) + theme.set_color("font_pressed_color", "CheckBox", C_INK) + + # ── PanelContainer ──────────────────────────────────────────────────────── + theme.set_stylebox("panel", "PanelContainer", _panel_box()) + + # ── Panel (raw) — same look as PanelContainer ───────────────────────────── + theme.set_stylebox("panel", "Panel", _panel_box()) + + # ── PopupMenu (recipe picker) ───────────────────────────────────────────── + theme.set_stylebox("panel", "PopupMenu", _panel_box()) + theme.set_stylebox("hover", "PopupMenu", _btn_box(C_BUTTON_HOV, false)) + theme.set_color("font_color", "PopupMenu", C_INK) + theme.set_color("font_hover_color", "PopupMenu", C_INK) + + # ── Label — defaults to ink-on-tan; per-label modulate still works ──────── + theme.set_color("font_color", "Label", C_INK) + + # ── SpinBox / LineEdit ──────────────────────────────────────────────────── + theme.set_stylebox("normal", "LineEdit", _btn_box(C_BUTTON, true)) + theme.set_stylebox("focus", "LineEdit", _focus_box()) + theme.set_color("font_color", "LineEdit", C_INK) + theme.set_color("caret_color", "LineEdit", C_INK) + + # ── Slider (audio sliders in SettingsMenu) ──────────────────────────────── + theme.set_stylebox("slider", "HSlider", _slider_track()) + theme.set_stylebox("grabber_area", "HSlider", _slider_fill()) + theme.set_stylebox("grabber_area_highlight", "HSlider", _slider_fill()) + + # ── ScrollContainer scrollbar ───────────────────────────────────────────── + theme.set_stylebox("scroll", "VScrollBar", _scrollbar_track()) + theme.set_stylebox("grabber", "VScrollBar", _btn_box(C_PANEL_DARK, false)) + theme.set_stylebox("grabber_pressed","VScrollBar", _btn_box(C_PANEL_DARK, true)) + + # ── HSeparator / VSeparator (used between bill rows in WorkbenchPanel) ──── + var sep := StyleBoxFlat.new() + sep.bg_color = C_PANEL_DARK + sep.content_margin_top = 1 + sep.content_margin_bottom = 1 + theme.set_stylebox("separator", "HSeparator", sep) + theme.set_stylebox("separator", "VSeparator", sep) + + return theme + + +# ── helpers ────────────────────────────────────────────────────────────────── + +static func _btn_box(fill: Color, pressed: bool) -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = fill + s.border_color = C_PANEL_DARK + s.border_width_left = 1 + s.border_width_right = 1 + s.border_width_top = 1 + s.border_width_bottom = 1 + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + if pressed: + # Inset shadow on top + left (pressed-in look). + s.shadow_color = Color(0, 0, 0, 0.35) + s.shadow_size = 0 + # Visually offset content slightly down/right when pressed. + s.content_margin_top = 4 + s.content_margin_left = 7 + s.content_margin_right = 5 + s.content_margin_bottom = 2 + else: + # Subtle drop shadow for depth. + s.shadow_color = Color(0, 0, 0, 0.20) + s.shadow_size = 2 + s.shadow_offset = Vector2(0, 1) + s.content_margin_top = 3 + s.content_margin_left = 6 + s.content_margin_right = 6 + s.content_margin_bottom = 3 + return s + + +static func _panel_box() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_PANEL + s.border_color = C_PANEL_DARK + s.border_width_left = 2 + s.border_width_right = 2 + s.border_width_top = 2 + s.border_width_bottom = 2 + s.corner_radius_top_left = 6 + s.corner_radius_top_right = 6 + s.corner_radius_bottom_left = 6 + s.corner_radius_bottom_right = 6 + s.shadow_color = Color(0, 0, 0, 0.35) + s.shadow_size = 4 + s.shadow_offset = Vector2(0, 2) + s.content_margin_top = 8 + s.content_margin_left = 10 + s.content_margin_right = 10 + s.content_margin_bottom = 8 + return s + + +static func _focus_box() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = Color(0, 0, 0, 0) # transparent + s.border_color = C_ACCENT_GOLD + s.border_width_left = 2 + s.border_width_right = 2 + s.border_width_top = 2 + s.border_width_bottom = 2 + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + return s + + +static func _slider_track() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_PANEL_DARK + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + s.content_margin_top = 4 + s.content_margin_bottom = 4 + return s + + +static func _slider_fill() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_ACCENT_GOLD + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + return s + + +static func _scrollbar_track() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = Color(C_PANEL_DARK.r, C_PANEL_DARK.g, C_PANEL_DARK.b, 0.3) + s.corner_radius_top_left = 3 + s.corner_radius_top_right = 3 + s.corner_radius_bottom_left = 3 + s.corner_radius_bottom_right = 3 + return s