diff --git a/src/game/engine/scenes/tests/improvement_proof.gd b/src/game/engine/scenes/tests/improvement_proof.gd new file mode 100644 index 00000000..01602013 --- /dev/null +++ b/src/game/engine/scenes/tests/improvement_proof.gd @@ -0,0 +1,433 @@ +extends Node2D +## Phase 10 Improvement Proof Scene. +## Proves: Engineer can build Farm on grassland, improvement completes after +## build_turns, tile yields update with +1 food from Farm improvement. +## Self-capturing: renders two panels, 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 CityScript: GDScript = preload("res://engine/src/entities/city.gd") +const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd") +const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd") +const ImprovementScript: GDScript = preload("res://engine/src/entities/improvement.gd") +const ImprovementManagerScript: GDScript = preload( + "res://engine/src/modules/management/improvement_manager.gd" +) + +const CELL_W: int = 11 +const CELL_H: int = 8 +const MARGIN: Vector2i = Vector2i(10, 40) +const OUTPUT_DIR: String = "user://screenshots" + +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), +} + +var _game_map: RefCounted = null +var _player: RefCounted = null +var _engineer: RefCounted = null +var _imp_manager: RefCounted = null +var _engineer_pos: Vector2i = Vector2i.ZERO +var _farm_tile_biome: String = "" + +var _yields_before: Dictionary = {} +var _yields_after: Dictionary = {} +var _buildable_list: Array[Dictionary] = [] +var _build_turns: int = 0 +var _improvement_applied: bool = false + +var _captured: bool = false +var _screenshot_name: String = "phase10_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_game() + _run_improvement_cycle() + queue_redraw() + + for _i: int in range(12): + await get_tree().process_frame + _capture_and_quit() + + +func _generate_map() -> void: + print("=== Phase 10 Improvement 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" % [_game_map.width, _game_map.height]) + + +func _setup_game() -> void: + _player = PlayerScript.new() + _player.index = 0 + _player.player_name = "Dwarf King" + _player.race_id = "dwarf" + _player.gold = 50 + + # Find a grassland tile for the Engineer (Farm requires grassland) + var center: Vector2i = HexUtilsScript.offset_to_axial( + Vector2i(_game_map.width / 2, _game_map.height / 2) + ) + _engineer_pos = _find_terrain_tile(center, "grassland", 20) + + # Set tile ownership to player so improvement is valid + var tile: Resource = _game_map.get_tile(_engineer_pos) as Resource + if tile != null: + tile.owner = 0 + _farm_tile_biome = tile.biome_id + + # Create Engineer unit + _engineer = UnitScript.new() + _engineer.id = "eng_0_1" + _engineer.type_id = "dwarf_engineer" + _engineer.owner = 0 + _engineer.name = "Engineer" + _engineer.position = _engineer_pos + _engineer.can_build_improvements = true + _engineer.movement_remaining = 2 + _engineer.max_hp = 10 + _engineer.hp = 10 + _engineer.unit_type = "civilian" + _player.units = [_engineer] + + # Create ImprovementManager + _imp_manager = ImprovementManagerScript.new() + + # Record yields before improvement + _yields_before = _get_tile_yields(_engineer_pos) + + # Get buildable list for display + _buildable_list = _imp_manager.get_buildable_improvements( + _engineer, _game_map, _player + ) + + print("Engineer at %s on %s" % [str(_engineer_pos), _farm_tile_biome]) + print("Buildable improvements: %d" % _buildable_list.size()) + for entry: Dictionary in _buildable_list: + print(" - %s (%d turns)" % [entry.get("name", ""), entry.get("build_turns", 0)]) + + +func _run_improvement_cycle() -> void: + ## Start building a Farm and simulate turns until completion. + var started: bool = _imp_manager.start_improvement(_engineer, "farm", _player) + if not started: + push_error("Phase10Proof: Failed to start Farm improvement") + return + + _build_turns = ImprovementScript.get_build_time("farm") + print("Farm started — %d turns to complete" % _build_turns) + + # Simulate turns by decrementing turns_remaining on pending improvements + for turn: int in range(_build_turns): + for i: int in range(_player.pending_improvements.size()): + var pending: Dictionary = _player.pending_improvements[i] as Dictionary + pending["turns_remaining"] = pending.get("turns_remaining", 1) - 1 + if pending["turns_remaining"] <= 0: + var tile_pos: Vector2i = Vector2i( + pending.get("x", 0), pending.get("y", 0) + ) + var imp_type: String = pending.get("type", "") + EventBus.improvement_completed.emit(tile_pos, imp_type) + _improvement_applied = true + print("Turn %d: Farm completed at %s" % [turn + 1, str(tile_pos)]) + + # Remove completed + var remaining: Array = [] + for i: int in range(_player.pending_improvements.size()): + var pending: Dictionary = _player.pending_improvements[i] as Dictionary + if pending.get("turns_remaining", 0) > 0: + remaining.append(pending) + _player.pending_improvements = remaining + + # Record yields after improvement + _yields_after = _get_tile_yields(_engineer_pos) + + var food_before: int = _yields_before.get("food", 0) + var food_after: int = _yields_after.get("food", 0) + print("Tile yields: food %d → %d (delta +%d)" % [ + food_before, food_after, food_after - food_before + ]) + + +func _get_tile_yields(pos: Vector2i) -> Dictionary: + var tile: Resource = _game_map.get_tile(pos) as Resource + if tile == null: + return {} + return tile.get_yields() + + +func _find_terrain_tile(near: Vector2i, biome: String, max_radius: int) -> Vector2i: + ## Find the nearest tile with the specified biome. + var center_tile: Resource = _game_map.tiles.get(near) + if center_tile != null and center_tile.biome_id == biome: + 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 tile.biome_id == biome: + return pos + # Fallback: any land tile + 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_w: float = _game_map.width * CELL_W + var p1_x: float = MARGIN.x + var p2_x: float = p1_x + map_w + 20 + + # Header + draw_string(font, Vector2(MARGIN.x, 18), + "Phase 10 Proof — Tile Improvements: Engineer Build + Farm Completion", + HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color.WHITE) + + # Panel labels + draw_string(font, Vector2(p1_x, MARGIN.y - 6), + "Panel 1: Engineer on Map + Build Menu", + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.8, 0.6, 0.2)) + draw_string(font, Vector2(p2_x, MARGIN.y - 6), + "Panel 2: Farm Completed — Yields Updated", + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.4, 0.9, 0.4)) + + _draw_map_panel(p1_x) + _draw_results_panel(p2_x, font) + + # Footer + var footer_y: float = MARGIN.y + (_game_map.height + 4) * CELL_H + 20 + var status: String = "Farm completed" if _improvement_applied else "Farm in progress" + var food_delta: int = _yields_after.get("food", 0) - _yields_before.get("food", 0) + draw_string(font, Vector2(MARGIN.x, footer_y), + "%s — tile food yield %s%d" % [ + status, + "+" if food_delta >= 0 else "", + food_delta, + ], + HORIZONTAL_ALIGNMENT_LEFT, -1, 12, Color(0.3, 0.9, 0.3)) + + +func _draw_map_panel(px: float) -> void: + var font: Font = ThemeDB.fallback_font + + # Draw terrain grid + for tile_ref: Resource in _game_map.tiles.values(): + var pos: Vector2i = tile_ref.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_ref.biome_id, Color(0.5, 0.0, 0.5)) + draw_rect(Rect2(x, y, CELL_W - 1, CELL_H - 1), base) + + # Engineer marker: cyan square + var eng_off: Vector2i = HexUtilsScript.axial_to_offset(_engineer_pos) + var ex: float = px + eng_off.x * CELL_W + var ey: float = MARGIN.y + eng_off.y * CELL_H + if eng_off.x & 1: + ey += CELL_H * 0.5 + draw_rect(Rect2(ex + 1, ey + 1, CELL_W - 2, CELL_H - 2), Color(0.2, 0.9, 0.9)) + + # Label + draw_string(font, Vector2(ex - 4, ey + CELL_H + 8), + "Engineer", HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color(0.2, 0.9, 0.9)) + + # Farm highlight after completion + if _improvement_applied: + draw_rect(Rect2(ex - 1, ey - 1, CELL_W + 1, CELL_H + 1), Color(0.3, 0.9, 0.3, 0.5), false, 2.0) + + # Build menu popup simulation (right of map) + var menu_x: float = px + _game_map.width * CELL_W + 8 + var menu_y: float = MARGIN.y + 20 + draw_string(font, Vector2(menu_x, menu_y), + "Build Improvement:", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.9, 0.8, 0.3)) + menu_y += 14 + + # Draw buildable list + for entry: Dictionary in _buildable_list: + var imp_name: String = entry.get("name", "") + var turns: int = entry.get("build_turns", 0) + var is_farm: bool = entry.get("id", "") == "farm" + var row_color: Color = Color(0.3, 1.0, 0.3) if is_farm else Color(0.7, 0.7, 0.7) + var prefix: String = "► " if is_farm else " " + draw_string(font, Vector2(menu_x, menu_y), + "%s%s (%d turns)" % [prefix, imp_name, turns], + HORIZONTAL_ALIGNMENT_LEFT, -1, 9, row_color) + menu_y += 12 + + # Legend + var legend_y: float = MARGIN.y + (_game_map.height + 2) * CELL_H + 2 + draw_rect(Rect2(px, legend_y, 8, 8), Color(0.2, 0.9, 0.9)) + draw_string(font, Vector2(px + 11, legend_y + 8), + "engineer", HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color.WHITE) + draw_rect(Rect2(px + 70, legend_y, 8, 8), Color(0.3, 0.9, 0.3, 0.5)) + draw_string(font, Vector2(px + 81, legend_y + 8), + "farm built", HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color.WHITE) + + +func _draw_results_panel(px: float, font: Font) -> void: + var y: float = MARGIN.y + + draw_string(font, Vector2(px, y), + "Tile: %s at %s" % [_farm_tile_biome, str(_engineer_pos)], + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.8, 0.7, 0.5)) + y += 16 + + draw_string(font, Vector2(px, y), + "Improvement: Farm (%d turns to build)" % _build_turns, + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.9, 0.8, 0.3)) + y += 16 + + draw_string(font, Vector2(px, y), + "Status: %s" % ("Completed" if _improvement_applied else "In Progress"), + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, + Color(0.3, 1.0, 0.3) if _improvement_applied else Color(1.0, 0.7, 0.2)) + y += 20 + + # Divider + draw_rect(Rect2(px, y, 400, 1), Color(0.35, 0.30, 0.22)) + y += 12 + + # Yields comparison + draw_string(font, Vector2(px, y), + "YIELDS COMPARISON", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.75, 0.70, 0.55)) + y += 16 + + var yield_keys: Array = [ + ["Food", "food", Color(0.3, 0.9, 0.3)], + ["Production", "production", Color(0.9, 0.6, 0.2)], + ["Trade", "trade", Color(0.95, 0.85, 0.15)], + ["Culture", "culture", Color(0.75, 0.35, 0.95)], + ] + + draw_string(font, Vector2(px, y), + " Before After Delta", + HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.6, 0.6, 0.6)) + y += 14 + + for row: Array in yield_keys: + var label: String = row[0] as String + var key: String = row[1] as String + var color: Color = row[2] as Color + var before: int = _yields_before.get(key, 0) + var after: int = _yields_after.get(key, 0) + var delta: int = after - before + var delta_str: String = "+%d" % delta if delta > 0 else str(delta) + var delta_color: Color = Color(0.3, 1.0, 0.3) if delta > 0 else color + draw_string(font, Vector2(px, y), + "%-12s %4d %4d %s" % [label, before, after, delta_str], + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, color) + if delta > 0: + draw_string(font, Vector2(px + 310, y), + delta_str, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, delta_color) + y += 14 + + y += 12 + + # Improvement data summary + draw_string(font, Vector2(px, y), + "Improvement data from JSON:", + HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.6, 0.6, 0.6)) + y += 14 + var farm_yields: Dictionary = ImprovementScript.get_yield_bonus("farm") + draw_string(font, Vector2(px, y), + "Farm yields: food +%d, prod +%d, gold +%d" % [ + farm_yields.get("food", 0), + farm_yields.get("production", 0), + farm_yields.get("gold", 0), + ], + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.7, 0.9, 0.7)) + y += 14 + + var farm_terrain: String = "grassland, plains, enchanted_forest" + draw_string(font, Vector2(px, y), + "Valid terrain: %s" % farm_terrain, + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.7, 0.9, 0.7)) + y += 20 + + # Tile state verification + var tile: Resource = _game_map.get_tile(_engineer_pos) as Resource + var tile_imp: String = tile.improvement if tile != null else "(null)" + draw_string(font, Vector2(px, y), + "Tile.improvement = \"%s\"" % tile_imp, + HORIZONTAL_ALIGNMENT_LEFT, -1, 10, + Color(0.3, 1.0, 0.3) if tile_imp == "farm" else Color(1.0, 0.4, 0.4)) + + +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("Phase10Proof: 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("Phase10Proof: Save failed: %s" % error_string(err)) + + get_tree().quit() diff --git a/src/game/engine/scenes/tests/improvement_proof.gd.uid b/src/game/engine/scenes/tests/improvement_proof.gd.uid new file mode 100644 index 00000000..eb67b2b2 --- /dev/null +++ b/src/game/engine/scenes/tests/improvement_proof.gd.uid @@ -0,0 +1 @@ +uid://bp0lcyalm1ah1 diff --git a/src/game/engine/scenes/tests/improvement_proof.tscn b/src/game/engine/scenes/tests/improvement_proof.tscn new file mode 100644 index 00000000..c798353e --- /dev/null +++ b/src/game/engine/scenes/tests/improvement_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://imp10proof1"] + +[ext_resource type="Script" path="res://engine/scenes/tests/improvement_proof.gd" id="1_script"] + +[node name="ImprovementProof" type="Node2D"] +script = ExtResource("1_script")