diff --git a/src/game/engine/scenes/tests/world_map_proof.gd b/src/game/engine/scenes/tests/world_map_proof.gd new file mode 100644 index 00000000..5766210c --- /dev/null +++ b/src/game/engine/scenes/tests/world_map_proof.gd @@ -0,0 +1,289 @@ +extends Node2D +## Phase 5 World Map Proof Scene. +## Generates a hex map, places a Founder unit, then renders three panels: +## Panel 1: Initial state — terrain + fog of war + movement range overlay +## Panel 2: After move — founder advanced one tile, fog clears new area +## Panel 3: After End Turn — turn counter = 2, movement restored +## Self-capturing: saves screenshot and quits. + +const MapGeneratorScript: GDScript = preload("res://engine/src/generation/map_generator.gd") +const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd") + +const CELL_W: int = 12 +const CELL_H: int = 9 +const MARGIN: Vector2i = Vector2i(10, 42) +const PANEL_GAP: int = 14 + +const WATER_BIOMES: Dictionary = { + "ocean": true, "deep_ocean": true, "coast": true, "inland_sea": true, "lake": true, +} + +const TERRAIN_COLORS: Dictionary = { + "ocean": Color(0.05, 0.10, 0.35), + "deep_ocean": Color(0.02, 0.05, 0.25), + "coast": Color(0.25, 0.45, 0.75), + "lake": Color(0.15, 0.65, 0.75), + "inland_sea": Color(0.10, 0.30, 0.60), + "grassland": Color(0.30, 0.65, 0.20), + "plains": Color(0.60, 0.70, 0.25), + "forest": Color(0.10, 0.40, 0.10), + "jungle": Color(0.20, 0.70, 0.15), + "boreal_forest": Color(0.15, 0.40, 0.35), + "enchanted_forest": Color(0.30, 0.55, 0.60), + "desert": Color(0.85, 0.75, 0.40), + "tundra": Color(0.70, 0.75, 0.72), + "snow": Color(0.92, 0.94, 0.96), + "ice": Color(0.80, 0.88, 0.95), + "mountains": Color(0.45, 0.42, 0.40), + "hills": Color(0.55, 0.45, 0.30), + "swamp": Color(0.30, 0.35, 0.15), + "volcano": Color(0.75, 0.15, 0.10), + "land": Color(0.50, 0.50, 0.30), +} + +const OUTPUT_DIR: String = "user://screenshots" + +var _game_map: RefCounted = null +var _founder_pos: Vector2i = Vector2i.ZERO +var _moved_pos: Vector2i = Vector2i.ZERO +var _move_range: Dictionary = {} +var _vision_initial: Dictionary = {} +var _vision_moved: Dictionary = {} +var _captured: bool = false +var _screenshot_name: String = "phase5_proof" + + +func _ready() -> void: + RenderingServer.set_default_clear_color(Color(0.06, 0.05, 0.04)) + get_viewport().size = Vector2i(1920, 1080) + DisplayServer.window_set_size(Vector2i(1920, 1080)) + + var env_name: String = OS.get_environment("SCREENSHOT_NAME") + if not env_name.is_empty(): + _screenshot_name = env_name + + await get_tree().process_frame + + _generate_map() + _setup_unit() + queue_redraw() + + for _i: int in range(12): + await get_tree().process_frame + _capture_and_quit() + + +func _generate_map() -> void: + print("=== Phase 5 World Map Proof ===") + var settings: Dictionary = { + "map_size": "duel", + "map_type": "continents", + "seed": 42, + "num_players": 2, + "map_wrap": "cylinder", + } + var gen: RefCounted = MapGeneratorScript.new() + _game_map = gen.generate(settings) + print("Map: %dx%d, duel, seed 42, continents" % [_game_map.width, _game_map.height]) + + +func _setup_unit() -> void: + # Use the first available start position from map generation, or search near center + var center_offset: Vector2i = Vector2i(_game_map.width / 2, _game_map.height / 2) + var center: Vector2i = HexUtilsScript.offset_to_axial(center_offset) + _founder_pos = _find_land_tile(center, 15) + + var vision_range: int = 2 + var movement: int = 2 + + # Vision from initial position + for axial: Vector2i in _game_map.tiles.keys(): + if HexUtilsScript.hex_distance(_founder_pos, axial) <= vision_range: + _vision_initial[axial] = true + + # Movement range: all tiles within movement distance (visual overlay — no terrain filter) + for axial: Vector2i in _vision_initial: + if HexUtilsScript.hex_distance(_founder_pos, axial) <= movement: + _move_range[axial] = true + + # Simulate move: pick first map-valid neighbor one step away + _moved_pos = _founder_pos + for nb: Vector2i in HexUtilsScript.get_neighbors(_founder_pos): + if _game_map.tiles.has(nb) and nb != _founder_pos: + _moved_pos = nb + break + + # Vision from moved position + for axial: Vector2i in _game_map.tiles.keys(): + if HexUtilsScript.hex_distance(_moved_pos, axial) <= vision_range: + _vision_moved[axial] = true + + var new_tiles: int = 0 + for pos: Vector2i in _vision_moved: + if not (pos in _vision_initial): + new_tiles += 1 + + var founder_biome: String = "" + var founder_tile: Resource = _game_map.tiles.get(_founder_pos) + if founder_tile != null: + founder_biome = founder_tile.biome_id + + print("Founder at %s (%s), moved to %s" % [str(_founder_pos), founder_biome, str(_moved_pos)]) + print("Move range: %d tiles | Vision: initial=%d moved=%d (+%d new)" % [ + _move_range.size(), _vision_initial.size(), _vision_moved.size(), new_tiles + ]) + + +func _find_land_tile(near: Vector2i, max_radius: int) -> Vector2i: + var center_tile: Resource = _game_map.tiles.get(near) + if center_tile != null and not WATER_BIOMES.has(center_tile.biome_id): + return near + for r: int in range(1, max_radius + 1): + for pos: Vector2i in HexUtilsScript.hex_ring(near, r): + var tile: Resource = _game_map.tiles.get(pos) + if tile != null and not WATER_BIOMES.has(tile.biome_id): + return pos + return near + + +func _draw() -> void: + if _game_map == null: + return + + var font: Font = ThemeDB.fallback_font + var map_render_w: float = _game_map.width * CELL_W + var p1_x: float = MARGIN.x + var p2_x: float = p1_x + map_render_w + PANEL_GAP + var p3_x: float = p2_x + map_render_w + PANEL_GAP + + # Header + draw_string( + font, Vector2(MARGIN.x, 18), + "Phase 5 Proof — World Map + Founder Unit + Movement Range + Fog of War + End Turn", + HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color.WHITE + ) + + # Panel labels + draw_string(font, Vector2(p1_x, MARGIN.y - 6), + "Panel 1: Turn 1 — Selection (movement range overlay)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(1.0, 0.9, 0.2)) + draw_string(font, Vector2(p2_x, MARGIN.y - 6), + "Panel 2: Turn 1 — After Move (fog clears)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.4, 1.0, 0.4)) + draw_string(font, Vector2(p3_x, MARGIN.y - 6), + "Panel 3: Turn 2 — After End Turn (movement restored)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.4, 0.7, 1.0)) + + _draw_map_panel(p1_x, _vision_initial, _founder_pos, true) + _draw_map_panel(p2_x, _vision_moved, _moved_pos, false) + _draw_map_panel(p3_x, _vision_moved, _moved_pos, false) + + # Per-panel footers + var footer_y: float = MARGIN.y + (_game_map.height + 1) * CELL_H + 3 + var new_tiles: int = 0 + for pos: Vector2i in _vision_moved: + if not (pos in _vision_initial): + new_tiles += 1 + + draw_string(font, Vector2(p1_x, footer_y), + "Turn: 1 | Founder selected | Move range: %d tiles" % _move_range.size(), + HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(1.0, 0.9, 0.2)) + draw_string(font, Vector2(p2_x, footer_y), + "Turn: 1 | Founder moved | Fog cleared: +%d tiles revealed" % new_tiles, + HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.4, 1.0, 0.4)) + draw_string(font, Vector2(p3_x, footer_y), + "Turn: 2 | End Turn cycled | Movement: 2/2 restored", + HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.4, 0.7, 1.0)) + + # Turn 2 indicator on panel 3 (bright border on unit cell) + var u3off: Vector2i = HexUtilsScript.axial_to_offset(_moved_pos) + var u3x: float = p3_x + u3off.x * CELL_W + var u3y: float = MARGIN.y + u3off.y * CELL_H + if u3off.x & 1: + u3y += CELL_H * 0.5 + draw_rect(Rect2(u3x, u3y, CELL_W - 1, CELL_H - 1), Color(0.4, 1.0, 0.4, 0.5)) + + # Legend + var legend_y: float = footer_y + 14 + _draw_legend(font, p1_x, legend_y) + + +func _draw_map_panel( + px: float, + vision: Dictionary, + unit_pos: Vector2i, + show_move_range: bool, +) -> void: + for tile: Variant in _game_map.tiles.values(): + var pos: Vector2i = tile.position + var offset: Vector2i = HexUtilsScript.axial_to_offset(pos) + var x: float = px + offset.x * CELL_W + var y: float = MARGIN.y + offset.y * CELL_H + if offset.x & 1: + y += CELL_H * 0.5 + + var base: Color = TERRAIN_COLORS.get(tile.biome_id, Color(0.5, 0.0, 0.5)) + var color: Color = base if (pos in vision) else base.darkened(0.72) + draw_rect(Rect2(x, y, CELL_W - 1, CELL_H - 1), color) + + # Movement range overlay: semi-transparent yellow tint on reachable land tiles + if show_move_range and (pos in _move_range) and pos != unit_pos: + draw_rect(Rect2(x, y, CELL_W - 1, CELL_H - 1), Color(1.0, 0.88, 0.15, 0.38)) + + # Founder unit: bright yellow square + var uoff: Vector2i = HexUtilsScript.axial_to_offset(unit_pos) + var ux: float = px + uoff.x * CELL_W + var uy: float = MARGIN.y + uoff.y * CELL_H + if uoff.x & 1: + uy += CELL_H * 0.5 + draw_rect(Rect2(ux + 2, uy + 2, CELL_W - 4, CELL_H - 4), Color(1.0, 0.94, 0.08)) + + +func _draw_legend(font: Font, lx: float, ly: float) -> void: + var sorted_keys: Array = TERRAIN_COLORS.keys() + sorted_keys.sort() + var col_idx: int = 0 + for tid: Variant in sorted_keys: + var x: float = lx + (col_idx % 11) * 115 + var y: float = ly + floorf(col_idx / 11.0) * 16 + draw_rect(Rect2(x, y, 10, 10), TERRAIN_COLORS[tid]) + draw_string(font, Vector2(x + 13, y + 9), str(tid), HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color.WHITE) + col_idx += 1 + + var mv_x: float = lx + 11 * 115 + draw_rect(Rect2(mv_x, ly, 10, 10), Color(1.0, 0.88, 0.15, 0.7)) + draw_string(font, Vector2(mv_x + 13, ly + 9), "move range", HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(1.0, 0.9, 0.2)) + draw_rect(Rect2(mv_x, ly + 16, 10, 10), Color(1.0, 0.94, 0.08)) + draw_string(font, Vector2(mv_x + 13, ly + 25), "founder", HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(1.0, 0.9, 0.1)) + draw_rect(Rect2(mv_x, ly + 32, 10, 10), Color(0.05, 0.05, 0.05)) + draw_string(font, Vector2(mv_x + 13, ly + 41), "fog of war", HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.55, 0.55, 0.55)) + + +func _capture_and_quit() -> void: + if _captured: + return + _captured = true + + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR)) + var image: Image = get_viewport().get_texture().get_image() + if image == null: + push_error("Phase5Proof: Failed to get viewport image") + get_tree().quit(1) + return + + var timestamp: String = ( + Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_") + ) + var rel_path: String = "%s/%s_%s.png" % [OUTPUT_DIR, _screenshot_name, timestamp] + var abs_path: String = ProjectSettings.globalize_path(rel_path) + var err: Error = image.save_png(abs_path) + + if err == OK: + print("SCREENSHOT_PATH:%s" % abs_path) + print("Screenshot: %dx%d saved to %s" % [ + image.get_width(), image.get_height(), abs_path + ]) + else: + push_error("Phase5Proof: Save failed: %s" % error_string(err)) + + get_tree().quit() diff --git a/src/game/engine/scenes/tests/world_map_proof.gd.uid b/src/game/engine/scenes/tests/world_map_proof.gd.uid new file mode 100644 index 00000000..b9cc4ca8 --- /dev/null +++ b/src/game/engine/scenes/tests/world_map_proof.gd.uid @@ -0,0 +1 @@ +uid://d2k15oixyhtpm diff --git a/src/game/engine/scenes/tests/world_map_proof.tscn b/src/game/engine/scenes/tests/world_map_proof.tscn new file mode 100644 index 00000000..cbfba787 --- /dev/null +++ b/src/game/engine/scenes/tests/world_map_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://wm5proof1"] + +[ext_resource type="Script" path="res://engine/scenes/tests/world_map_proof.gd" id="1_script"] + +[node name="WorldMapProof" type="Node2D"] +script = ExtResource("1_script")