diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6311fa0..1400973 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,12 @@ jobs: chmod +x .tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 ./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --version + - name: Install Godot system dependencies + shell: bash + run: | + set -euo pipefail + sudo apt-get update && sudo apt-get install -y libfontconfig1 + - name: Run headless smoke test shell: bash run: | diff --git a/scripts/main.gd b/scripts/main.gd index 7c060b8..8d5f5e8 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -16,6 +16,7 @@ const GoalProgression := preload("res://scripts/goal_progression.gd") const GoalReward := preload("res://scripts/goal_reward.gd") const RESOURCE_TRENDS := Constants.RESOURCE_TRENDS const ColonyStance := preload("res://scripts/colony_stance.gd") +const TileRender := preload("res://scripts/tile_render.gd") @onready var world_grid: GridContainer = %WorldGrid @@ -2245,39 +2246,29 @@ func worker_texture(name: String, frame: int, carrying: String = "") -> Texture2 worker_texture_cache[cache_key] = texture return texture +## Delegates to TileRender.tile_style, passing scene state via context. func tile_style(tile: Dictionary, pos: Vector2i) -> StyleBoxFlat: - var style := StyleBoxFlat.new() - style.corner_radius_top_left = 8 - style.corner_radius_top_right = 8 - style.corner_radius_bottom_right = 8 - style.corner_radius_bottom_left = 8 - style.border_width_left = 2 - style.border_width_top = 2 - style.border_width_right = 2 - style.border_width_bottom = 2 - style.content_margin_left = 4 - style.content_margin_top = 3 - style.content_margin_right = 4 - style.content_margin_bottom = 3 - var kind := "stockpile" if pos == stockpile_pos else String(tile.kind) - style.bg_color = TILE_BACKDROPS.get(kind, Color("#1b2128")) - style.border_color = tile_accent(tile, pos) - style.shadow_color = Color(0, 0, 0, 0.25) - style.shadow_size = 2 - return style + var accent := tile_accent(tile, pos) + var render_theme := { + "TILE_BACKDROPS": TILE_BACKDROPS, + "stockpile_pos": stockpile_pos, + } + return TileRender.tile_style(tile, pos, render_theme, accent) + +## Delegates to TileRender.tile_accent, passing scene state via context. func tile_accent(tile: Dictionary, pos: Vector2i) -> Color: - if not pending_build_kind.is_empty() and pos == hovered_tile_pos(): - return Color("#73d38c") if can_place_at(pos, pending_build_kind) else Color("#d36b6b") - if pos == stockpile_pos: - return Color("#d4b36f") - if RESOURCE_COLORS.has(String(tile.resource)): - return RESOURCE_COLORS[String(tile.resource)] - if STRUCTURE_COLORS.has(String(tile.kind)): - return STRUCTURE_COLORS[String(tile.kind)] - if String(tile.kind) == "foundation": - return Color("#c7a25e") - return Color(1, 1, 1, 0.35) + var ctx := { + "pending_build_kind": pending_build_kind, + "hover_pos": hovered_tile_pos(), + "stockpile_pos": stockpile_pos, + "can_place_fn": can_place_at, + } + var render_theme := { + "RESOURCE_COLORS": RESOURCE_COLORS, + "STRUCTURE_COLORS": STRUCTURE_COLORS, + } + return TileRender.tile_accent(tile, pos, ctx, render_theme) func task_name(worker: Dictionary) -> String: if int(worker.get("break_ticks", 0)) > 0: diff --git a/scripts/tile_render.gd b/scripts/tile_render.gd new file mode 100644 index 0000000..0d23be1 --- /dev/null +++ b/scripts/tile_render.gd @@ -0,0 +1,70 @@ +## Pure tile rendering helpers — no scene references, fully testable. +## Consumes constants from constants.gd and a context dict from the caller. + +class_name TileRender + + +## Returns a StyleBoxFlat for the given tile using the provided theme. +## `theme` must contain TILE_BACKDROPS (Dictionary of kind → Color). +static func tile_style(tile: Dictionary, pos: Vector2i, theme: Dictionary, accent_color: Color) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.corner_radius_top_left = 8 + style.corner_radius_top_right = 8 + style.corner_radius_bottom_right = 8 + style.corner_radius_bottom_left = 8 + style.border_width_left = 2 + style.border_width_top = 2 + style.border_width_right = 2 + style.border_width_bottom = 2 + style.content_margin_left = 4 + style.content_margin_top = 3 + style.content_margin_right = 4 + style.content_margin_bottom = 3 + + var kind := String(tile.kind) + if theme.has("stockpile_pos") and pos == theme["stockpile_pos"]: + kind = "stockpile" + + style.bg_color = theme.get("TILE_BACKDROPS", {}).get(kind, Color("#1b2128")) + style.border_color = accent_color + style.shadow_color = Color(0, 0, 0, 0.25) + style.shadow_size = 2 + return style + + +## Returns the accent (border) color for a tile based on context. +## `context` must contain: +## - pending_build_kind: String (empty when not placing) +## - hover_pos: Vector2i (-1, -1 when no hover) +## - stockpile_pos: Vector2i +## - can_place_fn: Callable(pos: Vector2i, kind: String) -> bool +## `theme` must contain RESOURCE_COLORS and STRUCTURE_COLORS (from constants.gd). +static func tile_accent(tile: Dictionary, pos: Vector2i, context: Dictionary, theme: Dictionary) -> Color: + var pending_build_kind: String = String(context.get("pending_build_kind", "")) + var hover_pos: Vector2i = Vector2i(context.get("hover_pos", Vector2i(-1, -1))) + var stockpile_pos: Vector2i = Vector2i(context.get("stockpile_pos", Vector2i(-1, -1))) + var can_place_fn: Callable = context.get("can_place_fn", func(_p: Vector2i, _k: String): return false) + + # Build placement highlight + if not pending_build_kind.is_empty() and pos == hover_pos: + return Color("#73d38c") if can_place_fn.call(pos, pending_build_kind) else Color("#d36b6b") + + # Stockpile accent + if pos == stockpile_pos: + return Color("#d4b36f") + + # Resource accent + var resource_colors: Dictionary = theme.get("RESOURCE_COLORS", {}) + if resource_colors.has(String(tile.resource)): + return resource_colors[String(tile.resource)] + + # Structure accent + var structure_colors: Dictionary = theme.get("STRUCTURE_COLORS", {}) + if structure_colors.has(String(tile.kind)): + return structure_colors[String(tile.kind)] + + # Foundation accent + if String(tile.kind) == "foundation": + return Color("#c7a25e") + + return Color(1, 1, 1, 0.35) diff --git a/smoke.log b/smoke.log new file mode 100644 index 0000000..9c1ff46 --- /dev/null +++ b/smoke.log @@ -0,0 +1,3 @@ +Fontconfig error: Cannot load default config file: No such file: (null) +Godot Engine v4.2.2.stable.official.15073afe3 - https://godotengine.org + diff --git a/smoke_ci.log b/smoke_ci.log new file mode 100644 index 0000000..656d657 --- /dev/null +++ b/smoke_ci.log @@ -0,0 +1,3 @@ +libfontconfig.so.1: cannot open shared object file: No such file or directory +Godot Engine v4.2.2.stable.official.15073afe3 - https://godotengine.org + diff --git a/tests/test_tile_render.gd b/tests/test_tile_render.gd new file mode 100644 index 0000000..a5653fd --- /dev/null +++ b/tests/test_tile_render.gd @@ -0,0 +1,316 @@ +## Regression tests for scripts/tile_render.gd. +## Tests pure rendering functions without scene tree instantiation. +## +## Run: godot --headless --quit +## Or: godot --headless --main-pack windowstead.pck --script tests/test_tile_render.gd + +extends SceneTree + +const TileRender := preload("res://scripts/tile_render.gd") +const C := preload("res://scripts/constants.gd") + + +func _initialize() -> void: + var pass_count := 0 + var fail_count := 0 + var test_count := 0 + + # --- tile_style tests --- + test_count += 1; pass_count += test("tile_style returns StyleBoxFlat", _test_tile_style_returns_stylebox) + test_count += 1; pass_count += test("tile_style sets border color to accent", _test_tile_style_border_color) + test_count += 1; pass_count += test("tile_style uses TILE_BACKDROPS for bg", _test_tile_style_bg_from_backdrops) + test_count += 1; pass_count += test("tile_style stockpile override", _test_tile_style_stockpile_override) + test_count += 1; pass_count += test("tile_style unknown kind default backdrop", _test_tile_style_unknown_kind) + test_count += 1; pass_count += test("tile_style corner radius is 8", _test_tile_style_corner_radius) + test_count += 1; pass_count += test("tile_style shadow color and size", _test_tile_style_shadow) + + # --- tile_accent tests --- + test_count += 1; pass_count += test("tile_accent build placement valid (green)", _test_accent_build_valid) + test_count += 1; pass_count += test("tile_accent build placement invalid (red)", _test_accent_build_invalid) + test_count += 1; pass_count += test("tile_accent no build placement default", _test_accent_no_build) + test_count += 1; pass_count += test("tile_accent stockpile gold", _test_accent_stockpile) + test_count += 1; pass_count += test("tile_accent resource color", _test_accent_resource_color) + test_count += 1; pass_count += test("tile_accent structure color", _test_accent_structure_color) + test_count += 1; pass_count += test("tile_accent foundation accent", _test_accent_foundation) + test_count += 1; pass_count += test("tile_accent hover mismatch no highlight", _test_accent_hover_mismatch) + test_count += 1; pass_count += test("tile_accent empty context default", _test_accent_empty_context) + + # --- Integration with constants --- + test_count += 1; pass_count += test("tile_accent uses real RESOURCE_COLORS", _test_accent_real_resource_colors) + test_count += 1; pass_count += test("tile_accent uses real STRUCTURE_COLORS", _test_accent_real_structure_colors) + + fail_count = test_count - pass_count + print("\n=== TileRender Regression Tests ===") + print("Passed: %d" % pass_count) + print("Failed: %d" % fail_count) + + if fail_count > 0: + print("REGRESSION FAILURES DETECTED") + quit(1) + else: + print("All tile_render tests passed.") + quit(0) + + +func test(name: String, fn: Callable) -> int: + var ok := true + var error_msg := "" + var result: Variant = fn.call() + if result is Dictionary: + ok = result.get("ok", false) + error_msg = result.get("msg", "no detail") + elif result == false: + ok = false + error_msg = "returned false" + + if ok: + print(" ✓ %s" % name) + return 1 + else: + print(" ✗ %s: %s" % [name, error_msg]) + return 0 + + +# --- tile_style tests --- + +func _test_tile_style_returns_stylebox() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(0, 0) + var theme := {"TILE_BACKDROPS": {"ground": Color("#1b2128")}} + var accent := Color(1, 1, 1, 0.35) + var style := TileRender.tile_style(tile, pos, theme, accent) + return style is StyleBoxFlat + + +func _test_tile_style_border_color() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(0, 0) + var theme := {"TILE_BACKDROPS": {}} + var accent := Color(0.5, 0.3, 0.1, 1.0) + var style := TileRender.tile_style(tile, pos, theme, accent) + return style.border_color == accent + + +func _test_tile_style_bg_from_backdrops() -> bool: + var tile := {"kind": "tree", "resource": ""} + var pos := Vector2i(0, 0) + var theme := {"TILE_BACKDROPS": {"tree": Color("#2d4a2d")}} + var accent := Color(1, 1, 1, 0.35) + var style := TileRender.tile_style(tile, pos, theme, accent) + return style.bg_color == Color("#2d4a2d") + + +func _test_tile_style_stockpile_override() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(5, 3) + var theme := { + "TILE_BACKDROPS": {"ground": Color("#1b2128"), "stockpile": Color("#d4b36f")}, + "stockpile_pos": Vector2i(5, 3), + } + var accent := Color(1, 1, 1, 0.35) + var style := TileRender.tile_style(tile, pos, theme, accent) + return style.bg_color == Color("#d4b36f") + + +func _test_tile_style_unknown_kind() -> bool: + var tile := {"kind": "unknown", "resource": ""} + var pos := Vector2i(0, 0) + var theme := {"TILE_BACKDROPS": {}} + var accent := Color(1, 1, 1, 0.35) + var style := TileRender.tile_style(tile, pos, theme, accent) + return style.bg_color == Color("#1b2128") + + +func _test_tile_style_corner_radius() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(0, 0) + var theme := {"TILE_BACKDROPS": {}} + var accent := Color(1, 1, 1, 0.35) + var style := TileRender.tile_style(tile, pos, theme, accent) + return (style.corner_radius_top_left == 8 and + style.corner_radius_top_right == 8 and + style.corner_radius_bottom_right == 8 and + style.corner_radius_bottom_left == 8) + + +func _test_tile_style_shadow() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(0, 0) + var theme := {"TILE_BACKDROPS": {}} + var accent := Color(1, 1, 1, 0.35) + var style := TileRender.tile_style(tile, pos, theme, accent) + return style.shadow_color == Color(0, 0, 0, 0.25) and style.shadow_size == 2 + + +# --- tile_accent tests --- + +func _test_accent_build_valid() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(2, 2) + var can_place_fn := func(_p: Vector2i, _k: String): return true + var context := { + "pending_build_kind": "house", + "hover_pos": Vector2i(2, 2), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": can_place_fn, + } + var theme := {"RESOURCE_COLORS": {}, "STRUCTURE_COLORS": {}} + return TileRender.tile_accent(tile, pos, context, theme) == Color("#73d38c") + + +func _test_accent_build_invalid() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(2, 2) + var can_place_fn := func(_p: Vector2i, _k: String): return false + var context := { + "pending_build_kind": "house", + "hover_pos": Vector2i(2, 2), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": can_place_fn, + } + var theme := {"RESOURCE_COLORS": {}, "STRUCTURE_COLORS": {}} + return TileRender.tile_accent(tile, pos, context, theme) == Color("#d36b6b") + + +func _test_accent_no_build() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(2, 2) + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(2, 2), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := {"RESOURCE_COLORS": {}, "STRUCTURE_COLORS": {}} + return TileRender.tile_accent(tile, pos, context, theme) == Color(1, 1, 1, 0.35) + + +func _test_accent_stockpile() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(5, 3) + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(-1, -1), + "stockpile_pos": Vector2i(5, 3), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := {"RESOURCE_COLORS": {}, "STRUCTURE_COLORS": {}} + return TileRender.tile_accent(tile, pos, context, theme) == Color("#d4b36f") + + +func _test_accent_resource_color() -> bool: + var tile := {"kind": "ground", "resource": "wood"} + var pos := Vector2i(0, 0) + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(-1, -1), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := { + "RESOURCE_COLORS": {"wood": Color("#8b5e3c")}, + "STRUCTURE_COLORS": {}, + } + return TileRender.tile_accent(tile, pos, context, theme) == Color("#8b5e3c") + + +func _test_accent_structure_color() -> bool: + var tile := {"kind": "house", "resource": ""} + var pos := Vector2i(0, 0) + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(-1, -1), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := { + "RESOURCE_COLORS": {}, + "STRUCTURE_COLORS": {"house": Color("#6b8cce")}, + } + return TileRender.tile_accent(tile, pos, context, theme) == Color("#6b8cce") + + +func _test_accent_foundation() -> bool: + var tile := {"kind": "foundation", "resource": ""} + var pos := Vector2i(0, 0) + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(-1, -1), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := {"RESOURCE_COLORS": {}, "STRUCTURE_COLORS": {}} + return TileRender.tile_accent(tile, pos, context, theme) == Color("#c7a25e") + + +func _test_accent_hover_mismatch() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(3, 3) + var context := { + "pending_build_kind": "house", + "hover_pos": Vector2i(2, 2), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := {"RESOURCE_COLORS": {}, "STRUCTURE_COLORS": {}} + return TileRender.tile_accent(tile, pos, context, theme) == Color(1, 1, 1, 0.35) + + +func _test_accent_empty_context() -> bool: + var tile := {"kind": "ground", "resource": ""} + var pos := Vector2i(0, 0) + var context := {} + var theme := {} + return TileRender.tile_accent(tile, pos, context, theme) == Color(1, 1, 1, 0.35) + + +# --- Integration with real constants --- + +func _test_accent_real_resource_colors() -> bool: + # Verify tile_accent uses RESOURCE_COLORS from constants.gd + var wood_tile := {"kind": "ground", "resource": "wood"} + var stone_tile := {"kind": "ground", "resource": "stone"} + var food_tile := {"kind": "ground", "resource": "food"} + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(-1, -1), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := { + "RESOURCE_COLORS": C.RESOURCE_COLORS, + "STRUCTURE_COLORS": {}, + } + + var wood_color := TileRender.tile_accent(wood_tile, Vector2i(0, 0), context, theme) + var stone_color := TileRender.tile_accent(stone_tile, Vector2i(0, 0), context, theme) + var food_color := TileRender.tile_accent(food_tile, Vector2i(0, 0), context, theme) + + return (wood_color == C.RESOURCE_COLORS["wood"] and + stone_color == C.RESOURCE_COLORS["stone"] and + food_color == C.RESOURCE_COLORS["food"]) + + +func _test_accent_real_structure_colors() -> bool: + # Verify tile_accent uses STRUCTURE_COLORS from constants.gd + var hut_tile := {"kind": "hut", "resource": ""} + var workshop_tile := {"kind": "workshop", "resource": ""} + var garden_tile := {"kind": "garden", "resource": ""} + var context := { + "pending_build_kind": "", + "hover_pos": Vector2i(-1, -1), + "stockpile_pos": Vector2i(-1, -1), + "can_place_fn": func(_p: Vector2i, _k: String): return true, + } + var theme := { + "RESOURCE_COLORS": {}, + "STRUCTURE_COLORS": C.STRUCTURE_COLORS, + } + + var hut_color := TileRender.tile_accent(hut_tile, Vector2i(0, 0), context, theme) + var workshop_color := TileRender.tile_accent(workshop_tile, Vector2i(0, 0), context, theme) + var garden_color := TileRender.tile_accent(garden_tile, Vector2i(0, 0), context, theme) + + return (hut_color == C.STRUCTURE_COLORS["hut"] and + workshop_color == C.STRUCTURE_COLORS["workshop"] and + garden_color == C.STRUCTURE_COLORS["garden"])