diff --git a/BuildController.gd b/BuildController.gd index 02fb933..f316929 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -543,13 +543,10 @@ func _refresh_build_status() -> void: # Prices are shown under each option now, so this only needs to say # whether the fleet is somewhere an extractor could go. - var planet: Node2D = _planet_under_fleet() - if planet == null: - build_status.text = "" - elif not GameState.is_planet_conquered(planet.planet_id): - build_status.text = "Over %s - capture it before building an extractor" % planet.planet_name + if _planet_under_fleet() != null: + build_status.text = "Over %s - an extractor can be built here" % _planet_under_fleet().planet_name else: - build_status.text = "Over %s - an extractor can be built here" % planet.planet_name + build_status.text = "" func _refresh_build_option_states() -> void: @@ -557,8 +554,7 @@ func _refresh_build_option_states() -> void: var definition: Resource = entry[4] var enabled: bool = GameState.is_building_unlocked(definition.definition_id) if definition.placement_mode == "planet": - var planet: Node2D = _planet_under_fleet() - enabled = enabled and planet != null and GameState.is_planet_conquered(planet.planet_id) + enabled = enabled and _planet_under_fleet() != null entry[1].enabled = enabled entry[1].queue_redraw() @@ -597,9 +593,6 @@ func _show_building_info(type: String) -> void: if planet == null: lines.append("No planet in reach - fly over one.") can_build = false - elif not GameState.is_planet_conquered(planet.planet_id): - lines.append("%s is not captured yet - take all its capture points." % planet.planet_name) - can_build = false elif GameState.has_extractor_at(planet.planet_id): lines.append("%s already has an extractor." % planet.planet_name) can_build = false @@ -646,9 +639,6 @@ func _confirm_build() -> void: if planet == null or GameState.has_extractor_at(planet.planet_id): build_status.text = "No planet in reach any more" return - if not GameState.is_planet_conquered(planet.planet_id): - build_status.text = "%s is not captured yet" % planet.planet_name - return if not GameState.spend(definition.cost): build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks] return diff --git a/ChaserEnemyDefinition.tres b/ChaserEnemyDefinition.tres deleted file mode 100644 index 90f4385..0000000 --- a/ChaserEnemyDefinition.tres +++ /dev/null @@ -1,16 +0,0 @@ -[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=3 format=3] - -[ext_resource type="Script" path="res://EnemyDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://EnemyPursuitComponent.tscn" id="2"] - -[resource] -script = ExtResource("1") -enemy_id = "chaser" -display_name = "Chaser" -max_hp = 3 -speed = 140.0 -acceleration = 260.0 -rotation_speed = 6.0 -hull_points = PackedVector2Array(0, -16, 12, 0, 0, 16, -12, 0) -glow_color = Color(1, 0.15, 0.2, 1) -component_scenes = Array[PackedScene]([ExtResource("2")]) diff --git a/Enemy.gd b/Enemy.gd index 5d5e796..5f1ee45 100644 --- a/Enemy.gd +++ b/Enemy.gd @@ -2,53 +2,50 @@ extends Area2D signal destroyed -const ENEMY_DEFINITIONS = preload("res://EnemyDefinitions.gd") +@export var speed: float = 140.0 +@export var acceleration: float = 260.0 +@export var rotation_speed: float = 6.0 +@export var max_hp: int = 3 -@export var enemy_id: String = "chaser" - -var definition: Resource +var velocity: Vector2 = Vector2.ZERO var hp: int -# Mirrors Ship.gd's own max_speed/acceleration: copied from the definition -# once, then mutable, so a test/preview can pin an enemy in place (e.g. -# `enemy.speed = 0.0`) without needing to know about definitions at all. -var speed: float -var acceleration: float -var rotation_speed: float -var max_hp: int +@onready var fleet: Node2D = get_node("../Fleet") + func _ready() -> void: - add_to_group("enemies") - definition = ENEMY_DEFINITIONS.get_definition(enemy_id) - speed = definition.speed - acceleration = definition.acceleration - rotation_speed = definition.rotation_speed - max_hp = definition.max_hp hp = max_hp - for component_scene in definition.component_scenes: - var component: Node = component_scene.instantiate() - add_child(component) - if component.has_method("setup"): - component.setup() + add_to_group("enemies") queue_redraw() -func get_component(capability: String) -> Node: - for child in get_children(): - if child.get("capability_id") == capability: - return child - return null - - -func has_capability(capability: String) -> bool: - return get_component(capability) != null - - func _physics_process(delta: float) -> void: - for child in get_children(): - if child.has_method("tick"): - child.tick(delta) + var target_pos: Vector2 = _nearest_ship_position() + var to_target: Vector2 = target_pos - position + + if to_target.length() > 1.0: + var desired_velocity: Vector2 = to_target.normalized() * speed + velocity = velocity.move_toward(desired_velocity, acceleration * delta) + position += velocity * delta + + if velocity.length() > 5.0: + var target_angle: float = velocity.angle() + PI / 2.0 + rotation = lerp_angle(rotation, target_angle, rotation_speed * delta) + + +func _nearest_ship_position() -> Vector2: + if fleet.ships.is_empty(): + return position + + var nearest: Node2D = fleet.ships[0] + var best_dist: float = position.distance_to(nearest.position) + for ship in fleet.ships: + var d: float = position.distance_to(ship.position) + if d < best_dist: + best_dist = d + nearest = ship + return nearest.position func take_damage(amount: int) -> void: @@ -60,9 +57,8 @@ func take_damage(amount: int) -> void: func _draw() -> void: - var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([ - Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0), + var points: PackedVector2Array = PackedVector2Array([ + Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0) ]) - var glow: Color = definition.glow_color if definition != null else Color(1.0, 0.15, 0.2) # Constant glow - see Ship.gd. Nothing animates, so no per-frame redraw. - NeonDraw.draw_glowing_polygon(self, points, glow, 1.0) + NeonDraw.draw_glowing_polygon(self, points, Color(1.0, 0.15, 0.2), 1.0) diff --git a/EnemyDefinition.gd b/EnemyDefinition.gd deleted file mode 100644 index bb0927b..0000000 --- a/EnemyDefinition.gd +++ /dev/null @@ -1,19 +0,0 @@ -class_name EnemyDefinition -extends Resource - -# Data-driven enemy stats and appearance, mirroring VesselDefinition's role -# for player ships. Enemy.gd instantiates component_scenes exactly like -# Ship.gd does, so behaviour (pursuit, ranged fire, ...) lives in capability -# components rather than in a type switch here. - -@export var enemy_id: String = "chaser" -@export var display_name: String = "Chaser" -@export var max_hp: int = 3 -@export var speed: float = 140.0 -@export var acceleration: float = 260.0 -@export var rotation_speed: float = 6.0 -@export var hull_points: PackedVector2Array = PackedVector2Array([ - Vector2(0, -16), Vector2(12, 0), Vector2(0, 16), Vector2(-12, 0), -]) -@export var glow_color: Color = Color(1.0, 0.15, 0.2) -@export var component_scenes: Array[PackedScene] = [] diff --git a/EnemyDefinition.gd.uid b/EnemyDefinition.gd.uid deleted file mode 100644 index 884a36d..0000000 --- a/EnemyDefinition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bkgt6o1m12l0g diff --git a/EnemyDefinitions.gd b/EnemyDefinitions.gd deleted file mode 100644 index fa910b5..0000000 --- a/EnemyDefinitions.gd +++ /dev/null @@ -1,19 +0,0 @@ -class_name EnemyDefinitions -extends RefCounted - -const DEFAULT_ID: String = "chaser" -const ORDER: PackedStringArray = ["chaser"] -const DEFINITIONS: Dictionary = { - "chaser": preload("res://ChaserEnemyDefinition.tres"), -} - - -static func get_definition(enemy_id: String) -> Resource: - return DEFINITIONS.get(enemy_id, DEFINITIONS[DEFAULT_ID]) - - -static func all() -> Array: - var result: Array = [] - for enemy_id in ORDER: - result.append(DEFINITIONS[enemy_id]) - return result diff --git a/EnemyDefinitions.gd.uid b/EnemyDefinitions.gd.uid deleted file mode 100644 index 736702a..0000000 --- a/EnemyDefinitions.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cfc4w3atpf8yj diff --git a/EnemyProjectile.gd b/EnemyProjectile.gd deleted file mode 100644 index 88d012d..0000000 --- a/EnemyProjectile.gd +++ /dev/null @@ -1,45 +0,0 @@ -extends Area2D - -# Mirrors Projectile.gd, but travels the other direction: fired by an enemy -# component (see EnemyRangedWeaponComponent, #45) and damages player_ships. -# This is the half of ranged combat that never existed before #44 - enemies -# could previously only hurt the fleet by physical contact. - -@export var speed: float = 260.0 -@export var lifetime: float = 3.0 -@export var damage: int = 1 -@export var bolt_length: float = 20.0 -@export var color: Color = Color(1.0, 0.4, 0.35) - -var velocity: Vector2 = Vector2.ZERO -var age: float = 0.0 - - -func _ready() -> void: - add_to_group("enemy_projectiles") - area_entered.connect(_on_area_entered) - queue_redraw() - - -func launch(direction: Vector2) -> void: - velocity = direction.normalized() * speed - rotation = direction.angle() + PI / 2.0 - - -func _physics_process(delta: float) -> void: - position += velocity * delta - age += delta - if age >= lifetime: - queue_free() - - -func _on_area_entered(area: Area2D) -> void: - if area.is_in_group("player_ships"): - area.take_damage(damage) - queue_free() - - -func _draw() -> void: - var tip: Vector2 = Vector2(0, -bolt_length * 0.6) - var tail: Vector2 = Vector2(0, bolt_length * 0.4) - NeonDraw.draw_glowing_line(self, tail, tip, color, 1.0, 0.5) diff --git a/EnemyProjectile.gd.uid b/EnemyProjectile.gd.uid deleted file mode 100644 index e5c290a..0000000 --- a/EnemyProjectile.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b4hja3oj3rdrc diff --git a/EnemyProjectile.tscn b/EnemyProjectile.tscn deleted file mode 100644 index 798768c..0000000 --- a/EnemyProjectile.tscn +++ /dev/null @@ -1,12 +0,0 @@ -[gd_scene load_steps=3 format=3] - -[ext_resource type="Script" path="res://EnemyProjectile.gd" id="1"] - -[sub_resource type="CircleShape2D" id="1"] -radius = 4.0 - -[node name="EnemyProjectile" type="Area2D"] -script = ExtResource("1") - -[node name="CollisionShape2D" type="CollisionShape2D" parent="."] -shape = SubResource("1") diff --git a/EnemyPursuitComponent.gd b/EnemyPursuitComponent.gd deleted file mode 100644 index 661f432..0000000 --- a/EnemyPursuitComponent.gd +++ /dev/null @@ -1,38 +0,0 @@ -class_name EnemyPursuitComponent -extends Node - -# Chases the nearest player ship. Moved out of Enemy.gd verbatim so the -# behaviour is opt-in per definition rather than hardcoded into every enemy. - -var capability_id: String = "pursuit" -var velocity: Vector2 = Vector2.ZERO - - -func tick(delta: float) -> void: - var enemy: Node2D = get_parent() - var target_pos: Vector2 = _nearest_ship_position(enemy) - var to_target: Vector2 = target_pos - enemy.position - - if to_target.length() > 1.0: - var desired_velocity: Vector2 = to_target.normalized() * enemy.speed - velocity = velocity.move_toward(desired_velocity, enemy.acceleration * delta) - enemy.position += velocity * delta - - if velocity.length() > 5.0: - var target_angle: float = velocity.angle() + PI / 2.0 - enemy.rotation = lerp_angle(enemy.rotation, target_angle, enemy.rotation_speed * delta) - - -func _nearest_ship_position(enemy: Node2D) -> Vector2: - var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet") - if fleet == null or fleet.ships.is_empty(): - return enemy.position - - var nearest: Node2D = fleet.ships[0] - var best_dist: float = enemy.position.distance_to(nearest.position) - for ship in fleet.ships: - var d: float = enemy.position.distance_to(ship.position) - if d < best_dist: - best_dist = d - nearest = ship - return nearest.position diff --git a/EnemyPursuitComponent.gd.uid b/EnemyPursuitComponent.gd.uid deleted file mode 100644 index 30c1640..0000000 --- a/EnemyPursuitComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://n8wohkrnl42e diff --git a/EnemyPursuitComponent.tscn b/EnemyPursuitComponent.tscn deleted file mode 100644 index 8b68d67..0000000 --- a/EnemyPursuitComponent.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://EnemyPursuitComponent.gd" id="1"] - -[node name="EnemyPursuitComponent" type="Node"] -script = ExtResource("1") diff --git a/Fleet.gd b/Fleet.gd index ba32666..949c0ac 100644 --- a/Fleet.gd +++ b/Fleet.gd @@ -32,10 +32,6 @@ var steer_direction: Vector2 = Vector2.ZERO func _ready() -> void: - # Lets anything resolve "the fleet" without a hardcoded ../Fleet sibling - # path - enemies parented under different hosts (e.g. a planet surface's - # SurfaceFeatures) need this to find their target. - add_to_group("player_fleet") # Researching a vessel upgrade improves the ships already flying, not # just the next one built (#5). GameState.vessel_upgraded.connect(_on_vessel_upgraded) diff --git a/GameState.gd b/GameState.gd index 5487299..3854f56 100644 --- a/GameState.gd +++ b/GameState.gd @@ -344,33 +344,6 @@ func reset_progression() -> void: vessel_levels.clear() -# Called from the main menu's Play button (#47): the autoload survives -# change_scene_to_file(), so without this a second playthrough in the same -# process would start from wherever the last one left off instead of a -# fresh game. -func reset_new_game() -> void: - has_return_position = false - surviving_ship_count = BASE_FLEET_SIZE - surviving_vessel_types = PackedStringArray() - return_world_position = Vector2.ZERO - return_camera_zoom = 1.0 - planet_name = "" - planet_id = "" - planet_definition_id = "rocky" - explored_chunks.clear() - bank_inventory = INVENTORY.new({RESOURCE_ORE: 500}) - fleet_inventory = INVENTORY.new() - buildings.clear() - next_building_id = 1 - stored_vessels.clear() - vessel_deliveries.clear() - extractors.clear() - transporters.clear() - planet_capture.clear() - reset_progression() - reset_formation() - - # --- building stock and production queue (#16) ----------------------------- # Since #12, a building cannot just deduct from the bank at the moment of @@ -1047,66 +1020,6 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted: return inventory -# Planet conquest: a planet must have every one of its capture points held -# before an extractor can be built there (see BuildController's placement -# gates). Keyed by the same stable planet_id as `extractors`, entirely -# separate from it - a planet can be conquered long before anyone builds an -# extractor on it, or never get one at all. -var planet_capture: Dictionary = {} - -signal planet_conquered(planet_id: String) - - -func register_capture_points(planet_key: String, total: int) -> void: - # Idempotent: landing again on a planet must not wipe points already - # captured on a previous visit. - if planet_capture.has(planet_key): - return - var captured: Array = [] - captured.resize(total) - captured.fill(false) - planet_capture[planet_key] = {"captured": captured, "conquered": false} - - -func capture_state(planet_key: String) -> Dictionary: - return planet_capture.get(planet_key, {"captured": [], "conquered": false}) - - -func is_point_captured(planet_key: String, index: int) -> bool: - var captured: Array = capture_state(planet_key).get("captured", []) - if index < 0 or index >= captured.size(): - return false - return bool(captured[index]) - - -func mark_point_captured(planet_key: String, index: int) -> void: - if not planet_capture.has(planet_key): - return - var state: Dictionary = planet_capture[planet_key] - var captured: Array = state.captured - if index < 0 or index >= captured.size(): - return - captured[index] = true - if state.conquered: - return - if captured.all(func(c): return bool(c)): - state.conquered = true - planet_conquered.emit(planet_key) - - -func is_planet_conquered(planet_key: String) -> bool: - return bool(capture_state(planet_key).get("conquered", false)) - - -func captured_point_count(planet_key: String) -> int: - var captured: Array = capture_state(planet_key).get("captured", []) - var count: int = 0 - for c in captured: - if c: - count += 1 - return count - - func _process(delta: float) -> void: # Runs in the autoload, so mining and transport continue while the player # is on a planet's surface or nowhere near either. diff --git a/GasCargoCounter.gd.uid b/GasCargoCounter.gd.uid deleted file mode 100644 index a5a329c..0000000 --- a/GasCargoCounter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dn5wt1hyaui81 diff --git a/GasCounter.gd.uid b/GasCounter.gd.uid deleted file mode 100644 index 4f2dca2..0000000 --- a/GasCounter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://wykjjb3qc01h diff --git a/GasVentField.gd.uid b/GasVentField.gd.uid deleted file mode 100644 index 4e2907b..0000000 --- a/GasVentField.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://di1kvjt0dwvqc diff --git a/MainMenu.gd b/MainMenu.gd deleted file mode 100644 index e5afbcb..0000000 --- a/MainMenu.gd +++ /dev/null @@ -1,33 +0,0 @@ -extends Node2D - -@onready var play_button: IconButton = $UI/PlayButton -@onready var editor_button: IconButton = $UI/EditorButton -@onready var quit_button: IconButton = $UI/QuitButton -@onready var status_label: Label = $UI/StatusLabel - - -func _ready() -> void: - play_button.pressed.connect(_on_play_pressed) - editor_button.pressed.connect(_on_editor_pressed) - # #48 wires the real map editor scene up to this button; until then it - # stays visible but explains itself instead of silently doing nothing - # or pointing at a scene that doesn't exist yet. - editor_button.enabled = false - quit_button.pressed.connect(_on_quit_pressed) - quit_button.visible = not OS.has_feature("web") - - -func _on_play_pressed() -> void: - GameState.reset_new_game() - get_tree().change_scene_to_file("res://Main.tscn") - - -func _on_editor_pressed() -> void: - if editor_button.enabled: - get_tree().change_scene_to_file("res://PlanetSetupEditor.tscn") - return - status_label.text = "Map editor coming soon" - - -func _on_quit_pressed() -> void: - get_tree().quit() diff --git a/MainMenu.gd.uid b/MainMenu.gd.uid deleted file mode 100644 index 165914d..0000000 --- a/MainMenu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://p7yl0hc43yai diff --git a/MainMenu.tscn b/MainMenu.tscn deleted file mode 100644 index 7cf4450..0000000 --- a/MainMenu.tscn +++ /dev/null @@ -1,139 +0,0 @@ -[gd_scene load_steps=4 format=3] - -[ext_resource type="Script" path="res://MainMenu.gd" id="1"] -[ext_resource type="Script" path="res://IconButton.gd" id="2"] -[ext_resource type="Shader" path="res://starfield.gdshader" id="3"] - -[sub_resource type="ShaderMaterial" id="1"] -shader = ExtResource("3") -shader_parameter/camera_cell = Vector2(0, 0) -shader_parameter/camera_frac = Vector2(0, 0) -shader_parameter/camera_zoom = 1.0 -shader_parameter/viewport_size = Vector2(720, 1280) -shader_parameter/cell_size = 60.0 -shader_parameter/bg_color = Color(0.02, 0.02, 0.08, 1) - -[node name="MainMenu" type="Node2D"] -script = ExtResource("1") - -[node name="BackgroundLayer" type="CanvasLayer" parent="."] -layer = -10 - -[node name="Starfield" type="ColorRect" parent="BackgroundLayer"] -material = SubResource("1") -mouse_filter = 2 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="UI" type="CanvasLayer" parent="."] - -[node name="TitleLabel" type="Label" parent="UI"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -300 -offset_right = 300 -offset_top = -560 -offset_bottom = -460 -theme_override_font_sizes/font_size = 60 -horizontal_alignment = 1 -text = "SPACESHIP TEST" - -[node name="PlayButton" type="Control" parent="UI"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = -300 -offset_bottom = -172 -script = ExtResource("2") -icon = 2 -glow_color = Color(0.35, 0.95, 0.55, 1) - -[node name="PlayLabel" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -160 -offset_right = 160 -offset_top = -160 -offset_bottom = -94 -theme_override_font_sizes/font_size = 32 -horizontal_alignment = 1 -text = "Play" - -[node name="EditorButton" type="Control" parent="UI"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = -20 -offset_bottom = 108 -script = ExtResource("2") -icon = 0 -glow_color = Color(0.5, 0.75, 1, 1) - -[node name="EditorLabel" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -200 -offset_right = 200 -offset_top = 120 -offset_bottom = 186 -theme_override_font_sizes/font_size = 32 -horizontal_alignment = 1 -text = "Map Editor" - -[node name="StatusLabel" type="Label" parent="UI"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -260 -offset_right = 260 -offset_top = 200 -offset_bottom = 260 -theme_override_font_sizes/font_size = 26 -horizontal_alignment = 1 -autowrap_mode = 2 -text = "" - -[node name="QuitButton" type="Control" parent="UI"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -64 -offset_right = 64 -offset_top = 320 -offset_bottom = 448 -script = ExtResource("2") -icon = 1 -glow_color = Color(0.9, 0.35, 0.35, 1) - -[node name="QuitLabel" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = -160 -offset_right = 160 -offset_top = 460 -offset_bottom = 526 -theme_override_font_sizes/font_size = 32 -horizontal_alignment = 1 -text = "Quit" diff --git a/PlanetSetup.gd b/PlanetSetup.gd deleted file mode 100644 index fd81af1..0000000 --- a/PlanetSetup.gd +++ /dev/null @@ -1,16 +0,0 @@ -class_name PlanetSetup -extends Resource - -# A hand-authored capture-point/enemy layout for a planet surface. Chosen -# deterministically per planet (see PlanetSetups.setup_for_planet) rather -# than generated - #40 wants curated setups, not procedural ones - and -# stored as a committed .tres under res://PlanetSetups/, not in a save. - -@export var setup_id: String = "" -@export var display_name: String = "" -# Which PlanetSurfaceProfiles id (e.g. "rocky", "gas") this setup was -# authored for; only setups matching a planet's own profile are eligible. -@export var surface_profile_id: String = "rocky" -# Surface-local positions (the fleet lands at Vector2.ZERO). -@export var capture_points: PackedVector2Array = PackedVector2Array() -@export var enemy_spawns: Array[Resource] = [] diff --git a/PlanetSetup.gd.uid b/PlanetSetup.gd.uid deleted file mode 100644 index 8e9aef7..0000000 --- a/PlanetSetup.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d1govxko60n03 diff --git a/PlanetSetupHost.gd b/PlanetSetupHost.gd deleted file mode 100644 index f2df9c5..0000000 --- a/PlanetSetupHost.gd +++ /dev/null @@ -1,33 +0,0 @@ -class_name PlanetSetupHost -extends Node2D - -# Instantiates the contents of a PlanetSetup onto a landed surface. Capture -# points are placeholder markers here; #43 replaces them with the real -# CapturePoint scene through the same apply_setup() entry point, and enemy -# spawning is filled in by the enemy-foundation ticket (#44). Keeping this -# seam stable is the point of this ticket. - -const MARKER_COLOR: Color = Color(0.55, 0.85, 1.0) -const MARKER_RADIUS: float = 36.0 - -var setup: Resource - - -func apply_setup(new_setup: Resource) -> void: - setup = new_setup - for child in get_children(): - child.queue_free() - if setup == null: - return - for point in setup.capture_points: - _add_capture_point_marker(point) - - -func _add_capture_point_marker(at: Vector2) -> void: - var marker: NeonPiece = NeonPiece.create(self, at, MARKER_COLOR) - var points: PackedVector2Array = PackedVector2Array() - for i in range(16): - var angle: float = float(i) / 16.0 * TAU - points.append(Vector2(cos(angle), sin(angle)) * MARKER_RADIUS) - marker.add_polygon(points, 0.6) - marker.queue_redraw() diff --git a/PlanetSetupHost.gd.uid b/PlanetSetupHost.gd.uid deleted file mode 100644 index f65f628..0000000 --- a/PlanetSetupHost.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ckxmhx54fi5s1 diff --git a/PlanetSetupHost.tscn b/PlanetSetupHost.tscn deleted file mode 100644 index f87cc83..0000000 --- a/PlanetSetupHost.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://PlanetSetupHost.gd" id="1"] - -[node name="PlanetSetupHost" type="Node2D"] -script = ExtResource("1") diff --git a/PlanetSetupSpawn.gd b/PlanetSetupSpawn.gd deleted file mode 100644 index 5918b8a..0000000 --- a/PlanetSetupSpawn.gd +++ /dev/null @@ -1,7 +0,0 @@ -class_name PlanetSetupSpawn -extends Resource - -# One authored enemy placement within a PlanetSetup. - -@export var enemy_id: String = "chaser" -@export var position: Vector2 = Vector2.ZERO diff --git a/PlanetSetupSpawn.gd.uid b/PlanetSetupSpawn.gd.uid deleted file mode 100644 index 29438be..0000000 --- a/PlanetSetupSpawn.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c8spn4nio735d diff --git a/PlanetSetups.gd b/PlanetSetups.gd deleted file mode 100644 index 81105a4..0000000 --- a/PlanetSetups.gd +++ /dev/null @@ -1,69 +0,0 @@ -class_name PlanetSetups -extends RefCounted - -# Registry of authored PlanetSetup resources. Deliberately scans -# res://PlanetSetups/ at first use instead of a hardcoded preload -# dictionary (contrast PlanetDefinitions.gd), so dropping a new .tres in -# that folder is the only step needed to add a setup - the map editor -# (#48) writes there directly. - -const SETUPS_DIR: String = "res://PlanetSetups" -# Compared against get_script() rather than an `is PlanetSetup` check - the -# same load-order hazard as PlanetSurfaceController's setup_host: a global -# class_name identifier can be looked up before an import pass has -# registered it, while a direct script preload never depends on that cache. -const PLANET_SETUP_SCRIPT: Script = preload("res://PlanetSetup.gd") - -static var _all: Array = [] -static var _scanned: bool = false - - -static func _ensure_scanned() -> void: - if _scanned: - return - _scanned = true - var dir: DirAccess = DirAccess.open(SETUPS_DIR) - if dir == null: - return - dir.list_dir_begin() - var file_name: String = dir.get_next() - while file_name != "": - if not dir.current_is_dir() and file_name.ends_with(".tres"): - var res: Resource = load(SETUPS_DIR + "/" + file_name) - if res != null and res.get_script() == PLANET_SETUP_SCRIPT: - _all.append(res) - file_name = dir.get_next() - dir.list_dir_end() - - -static func rescan() -> void: - # The map editor calls this after saving so a freshly written setup is - # visible without restarting the process. - _all = [] - _scanned = false - _ensure_scanned() - - -static func all() -> Array: - _ensure_scanned() - return _all - - -static func setups_for_profile(profile_id: String) -> Array: - var result: Array = [] - for setup in all(): - if setup.surface_profile_id == profile_id: - result.append(setup) - return result - - -static func setup_for_planet(planet_id: String, profile_id: String) -> Resource: - # Same pattern as WorldGen.planet_data_at_chunk: seeded purely from the - # planet's own stable id, so the same planet always resolves to the - # same setup from anywhere, with nothing stored. - var candidates: Array = setups_for_profile(profile_id) - if candidates.is_empty(): - return null - var rng: RandomNumberGenerator = RandomNumberGenerator.new() - rng.seed = hash(planet_id) - return candidates[rng.randi() % candidates.size()] diff --git a/PlanetSetups.gd.uid b/PlanetSetups.gd.uid deleted file mode 100644 index 403182c..0000000 --- a/PlanetSetups.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5ndxvompklip diff --git a/PlanetSetups/gas_default.tres b/PlanetSetups/gas_default.tres deleted file mode 100644 index bafb1de..0000000 --- a/PlanetSetups/gas_default.tres +++ /dev/null @@ -1,27 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetSetup" load_steps=4 format=3] - -[ext_resource type="Script" path="res://PlanetSetup.gd" id="1"] -[ext_resource type="Script" path="res://PlanetSetupSpawn.gd" id="2"] - -[sub_resource type="Resource" id="Resource_g1"] -script = ExtResource("2") -enemy_id = "chaser" -position = Vector2(500, -300) - -[sub_resource type="Resource" id="Resource_g2"] -script = ExtResource("2") -enemy_id = "chaser" -position = Vector2(-450, 400) - -[sub_resource type="Resource" id="Resource_g3"] -script = ExtResource("2") -enemy_id = "chaser" -position = Vector2(200, 700) - -[resource] -script = ExtResource("1") -setup_id = "gas_default" -display_name = "Gas Default" -surface_profile_id = "gas" -capture_points = PackedVector2Array(600, -200, -550, -250, 0, 650, -700, 450) -enemy_spawns = Array[Resource]([SubResource("Resource_g1"), SubResource("Resource_g2"), SubResource("Resource_g3")]) diff --git a/PlanetSetups/rocky_default.tres b/PlanetSetups/rocky_default.tres deleted file mode 100644 index cd4767b..0000000 --- a/PlanetSetups/rocky_default.tres +++ /dev/null @@ -1,22 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetSetup" load_steps=4 format=3] - -[ext_resource type="Script" path="res://PlanetSetup.gd" id="1"] -[ext_resource type="Script" path="res://PlanetSetupSpawn.gd" id="2"] - -[sub_resource type="Resource" id="Resource_r1"] -script = ExtResource("2") -enemy_id = "chaser" -position = Vector2(450, 250) - -[sub_resource type="Resource" id="Resource_r2"] -script = ExtResource("2") -enemy_id = "chaser" -position = Vector2(-600, -350) - -[resource] -script = ExtResource("1") -setup_id = "rocky_default" -display_name = "Rocky Default" -surface_profile_id = "rocky" -capture_points = PackedVector2Array(700, 100, -650, 300, 50, -750) -enemy_spawns = Array[Resource]([SubResource("Resource_r1"), SubResource("Resource_r2")]) diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 7569796..294f5f1 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -2,8 +2,6 @@ extends Node2D const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd") const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") -const PLANET_SETUPS = preload("res://PlanetSetups.gd") -const SETUP_HOST_SCENE: PackedScene = preload("res://PlanetSetupHost.tscn") # Bootstraps the surviving fleet, including each vessel type, on this scene's # own startup (from GameState, since this is a fresh scene tree - nothing here @@ -48,21 +46,10 @@ func _compose_surface(profile: Resource) -> void: var caption: Label = get_node(caption_path) caption.add_theme_color_override("font_color", profile.caption_color) caption.add_theme_color_override("font_outline_color", profile.caption_outline_color) - if profile.features_scene != null: - var features: Node = profile.features_scene.instantiate() - feature_host.add_child(features) - - var setup: Resource = PLANET_SETUPS.setup_for_planet(GameState.planet_id, profile.profile_id) - if setup != null: - # Untyped on purpose: a static PlanetSetupHost annotation here would - # need the global class_name cache to already know about it, which - # is only true after an editor/import pass has run (see #31/#35's - # load-order notes) - a plain Node reference plus duck-typed - # apply_setup() sidesteps that entirely, same as BuildingDefinitions - # and VesselDefinitions already do for their own resource types. - var setup_host: Node = SETUP_HOST_SCENE.instantiate() - feature_host.add_child(setup_host) - setup_host.apply_setup(setup) + if profile.features_scene == null: + return + var features: Node = profile.features_scene.instantiate() + feature_host.add_child(features) func _on_exit_pressed() -> void: diff --git a/ResourceGlyph.gd.uid b/ResourceGlyph.gd.uid deleted file mode 100644 index 52c084d..0000000 --- a/ResourceGlyph.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://beff2kjshg4vf diff --git a/ResourceTransferList.gd.uid b/ResourceTransferList.gd.uid deleted file mode 100644 index 53f117f..0000000 --- a/ResourceTransferList.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bv6u4t3oabews diff --git a/Ship.gd b/Ship.gd index cdaa542..1c2727c 100644 --- a/Ship.gd +++ b/Ship.gd @@ -115,27 +115,17 @@ func _physics_process(delta: float) -> void: rotation = lerp_angle(rotation, target_angle, rotation_speed * delta) -func take_damage(_amount: int) -> bool: - # Ships have no hit points - any unabsorbed hit is lethal. A shield - # charge absorbs a hit entirely regardless of its amount, matching - # VesselShieldComponent's existing all-or-nothing semantics; `_amount` - # is accepted (not `int`-clamped away) so callers like EnemyProjectile - # have a normal damage parameter to pass even though it isn't graded here. - for child in get_children(): - if child.has_method("absorb_hit") and child.absorb_hit(): - return true - ShipExplosionVFX.spawn(get_tree().root, position) - destroyed.emit() - queue_free() - return false - - func _on_area_entered(area: Area2D) -> void: if not (area.is_in_group("enemies") or area.is_in_group("asteroids")): return - var absorbed: bool = take_damage(1) - if absorbed and area.is_in_group("asteroids") and is_instance_valid(area): - area.queue_free() + for child in get_children(): + if child.has_method("absorb_hit") and child.absorb_hit(): + if area.is_in_group("asteroids") and is_instance_valid(area): + area.queue_free() + return + ShipExplosionVFX.spawn(get_tree().root, position) + destroyed.emit() + queue_free() func _draw() -> void: diff --git a/VesselDelivery.gd.uid b/VesselDelivery.gd.uid deleted file mode 100644 index 1f4b96e..0000000 --- a/VesselDelivery.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ft0puvels7lg diff --git a/VesselDeliveryField.gd.uid b/VesselDeliveryField.gd.uid deleted file mode 100644 index 2c5afe0..0000000 --- a/VesselDeliveryField.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://clgmyishafevr diff --git a/project.godot b/project.godot index 0dcc344..05d44a8 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Spaceship Test" -run/main_scene="res://MainMenu.tscn" +run/main_scene="res://Main.tscn" config/features=PackedStringArray("4.7") config/icon="res://icon.svg" diff --git a/tests/_gas_bomb_aim_preview.gd b/tests/_gas_bomb_aim_preview.gd index cb1922c..8a38362 100644 --- a/tests/_gas_bomb_aim_preview.gd +++ b/tests/_gas_bomb_aim_preview.gd @@ -39,5 +39,5 @@ func _ready() -> void: for offset in [Vector2(-65, 0), Vector2(45, -45), Vector2(55, 65)]: var enemy: Node = load("res://Enemy.tscn").instantiate() enemy.position = skill.aim_target + offset + enemy.speed = 0.0 main.add_child(enemy) - enemy.speed = 0.0 # after add_child - _ready() sets it from the definition diff --git a/tests/_gas_surface_preview.gd.uid b/tests/_gas_surface_preview.gd.uid deleted file mode 100644 index 0bb3aef..0000000 --- a/tests/_gas_surface_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://x0rvhwfnneqp diff --git a/tests/_setup_markers_preview.gd b/tests/_setup_markers_preview.gd deleted file mode 100644 index 8069d45..0000000 --- a/tests/_setup_markers_preview.gd +++ /dev/null @@ -1,12 +0,0 @@ -extends Node2D - - -func _ready() -> void: - GameState.planet_name = "Kepler's Rest" - GameState.planet_id = "chunk:3:7" - GameState.planet_definition_id = "rocky" - GameState.surviving_ship_count = 3 - GameState.rocks = 128 - var surface: Node = load("res://PlanetSurface.tscn").instantiate() - surface.get_node("SurfaceController").fade_duration = 0.0 - add_child(surface) diff --git a/tests/_setup_markers_preview.gd.uid b/tests/_setup_markers_preview.gd.uid deleted file mode 100644 index 8df3b02..0000000 --- a/tests/_setup_markers_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dtpta85omp8cm diff --git a/tests/_setup_markers_preview.tscn b/tests/_setup_markers_preview.tscn deleted file mode 100644 index 99ed228..0000000 --- a/tests/_setup_markers_preview.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://tests/_setup_markers_preview.gd" id="1"] - -[node name="SetupMarkersPreview" type="Node2D"] -script = ExtResource("1") diff --git a/tests/test_enemy_composition.gd b/tests/test_enemy_composition.gd deleted file mode 100644 index a7660d3..0000000 --- a/tests/test_enemy_composition.gd +++ /dev/null @@ -1,78 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_enemy_composition.gd -# Verifies: -# 1. an Enemy resolves its stats/hull/color from EnemyDefinitions, not -# hardcoded fields -# 2. an enemy instantiated under a node that is NOT a sibling of Fleet -# still finds the fleet and pursues it (the old `../Fleet` path would -# break here - this is exactly the planet-surface hazard #44 exists -# to close before #45/#46 land) - -var main_scene: Node -var fleet: Node -var nested_host: Node2D -var enemy: Node -var elapsed: float = 0.0 -var stage: int = 0 - - -func _initialize() -> void: - var chaser_def: Resource = EnemyDefinitions.get_definition("chaser") - if chaser_def.enemy_id != "chaser" or chaser_def.max_hp != 3: - return _fail("EnemyDefinitions did not resolve the chaser definition") - - main_scene = load("res://Main.tscn").instantiate() - root.add_child(main_scene) - fleet = main_scene.get_node("Fleet") - main_scene.get_node("EnemySpawner").set_process(false) - - # Deliberately NOT a sibling of Fleet - two levels deep under an - # unrelated node, the way a planet surface's SurfaceFeatures parents - # things. - nested_host = Node2D.new() - nested_host.name = "NestedHost" - main_scene.add_child(nested_host) - var inner: Node2D = Node2D.new() - inner.name = "Inner" - nested_host.add_child(inner) - - enemy = load("res://Enemy.tscn").instantiate() - enemy.position = fleet.get_centroid() + Vector2(600, 0) - inner.add_child(enemy) - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - if elapsed < 0.1: - return false - if enemy.definition == null or enemy.definition.enemy_id != "chaser": - return _fail("enemy did not resolve its definition on _ready") - if enemy.hp != enemy.definition.max_hp: - return _fail("enemy hp did not come from the definition") - if not enemy.has_capability("pursuit"): - return _fail("chaser did not compose a pursuit component") - elapsed = 0.0 - stage = 1 - return false - 1: - # Give the pursuit component a few physics ticks to close distance. - if elapsed < 1.0: - return false - var distance: float = enemy.position.distance_to(fleet.get_centroid()) - print("distance_after_1s=%.1f (started at 600)" % distance) - if distance >= 550.0: - return _fail("enemy under a non-sibling host never approached the fleet") - print("TEST_ENEMY_COMPOSITION: PASS") - quit(0) - return true - return false - - -func _fail(message: String) -> bool: - print("TEST_ENEMY_COMPOSITION: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_enemy_composition.gd.uid b/tests/test_enemy_composition.gd.uid deleted file mode 100644 index 002eb3d..0000000 --- a/tests/test_enemy_composition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dltu3a0n318lo diff --git a/tests/test_enemy_projectile_damage.gd b/tests/test_enemy_projectile_damage.gd deleted file mode 100644 index a72fb36..0000000 --- a/tests/test_enemy_projectile_damage.gd +++ /dev/null @@ -1,91 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_enemy_projectile_damage.gd -# Verifies the ranged damage path #44 adds: an EnemyProjectile destroys an -# unshielded ship on contact, and is absorbed by a shielded ship, removing -# exactly one charge rather than destroying it. - -var game_state: Node -var scene: Node -var fleet: Node -var interceptor: Node -var shield_ship: Node -var stage: int = 0 -var elapsed: float = 0.0 - - -func _initialize() -> void: - game_state = root.get_node("GameState") - game_state.buildings.clear() - game_state.extractors.clear() - game_state.transporters.clear() - game_state.stored_vessels.clear() - game_state.build_time_scale = 0.02 - game_state.reset_progression() - game_state.rocks = 2000 - scene = load("res://Main.tscn").instantiate() - root.add_child(scene) - fleet = scene.get_node("Fleet") - - -func _fire_at(ship: Node) -> void: - var projectile: Area2D = load("res://EnemyProjectile.tscn").instantiate() - projectile.global_position = ship.global_position - projectile.launch(Vector2.ZERO) # already on top of the target - scene.add_child(projectile) - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - scene.get_node("AsteroidField").set_process(false) - scene.get_node("EnemySpawner").set_process(false) - fleet.spawn_ships(1, Vector2.ZERO) - interceptor = fleet.ships[0] - fleet.add_ship(GameState.VESSEL_SHIELD) - shield_ship = fleet.ships[1] - shield_ship.position = Vector2(2000.0, 0.0) - elapsed = 0.0 - stage = 1 - return false - 1: - if elapsed < 0.2: - return false - if not shield_ship.has_shield(): - return _fail("a fresh shield vessel has no shield") - _fire_at(interceptor) - elapsed = 0.0 - stage = 2 - return false - 2: - if elapsed < 0.4: - return false - if is_instance_valid(interceptor): - return _fail("an unshielded ship survived an enemy projectile") - print("unshielded interceptor destroyed by an enemy projectile") - - _fire_at(shield_ship) - elapsed = 0.0 - stage = 3 - return false - 3: - if elapsed < 0.4: - return false - if not is_instance_valid(shield_ship): - return _fail("a shielded ship was destroyed by an enemy projectile") - if shield_ship.shield_charges != 0: - return _fail("shield charges did not drop to 0 after absorbing one hit (charges=%d)" % - shield_ship.shield_charges) - print("shielded ship absorbed the hit and lost exactly one charge") - print("TEST_ENEMY_PROJECTILE_DAMAGE: PASS") - quit(0) - return true - return false - - -func _fail(message: String) -> bool: - print("TEST_ENEMY_PROJECTILE_DAMAGE: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_enemy_projectile_damage.gd.uid b/tests/test_enemy_projectile_damage.gd.uid deleted file mode 100644 index 592713a..0000000 --- a/tests/test_enemy_projectile_damage.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cx7f3e82jkumb diff --git a/tests/test_extractor_building.gd b/tests/test_extractor_building.gd index 656c2c5..0e52c21 100644 --- a/tests/test_extractor_building.gd +++ b/tests/test_extractor_building.gd @@ -42,12 +42,6 @@ func _initialize() -> void: game_state.rocks = 200 game_state.buildings.clear() game_state.extractors.clear() - game_state.planet_capture.clear() - # This test is about the planet-proximity/duplicate rules, not conquest - # (see test_planet_conquest_gate), so pre-conquer the target planet. - var target_planet_id: String = "chunk:%d:%d" % [target_chunk.x, target_chunk.y] - game_state.register_capture_points(target_planet_id, 1) - game_state.mark_point_captured(target_planet_id, 0) # Extractors are gated behind research (see test_skill_tree). This test # is about the planet-proximity rule, so grant the research up front. game_state.reset_progression() diff --git a/tests/test_gas_bomber.gd b/tests/test_gas_bomber.gd index cda2a2f..8bd1210 100644 --- a/tests/test_gas_bomber.gd +++ b/tests/test_gas_bomber.gd @@ -107,19 +107,16 @@ func _process(delta: float) -> bool: if main.get_node("Camera2D").zoom.x >= original_zoom: return _fail_now("long aim did not zoom out") - # speed is set AFTER add_child(): _ready() copies it from the - # enemy's definition, which would otherwise clobber a - # pre-add_child override the instant the enemy entered the tree. for offset in [Vector2(-60, 0), Vector2(70, 35)]: var enemy: Node = load("res://Enemy.tscn").instantiate() enemy.position = skill.aim_target + offset - main.add_child(enemy) enemy.speed = 0.0 + main.add_child(enemy) inside_enemies.append(enemy) outside_enemy = load("res://Enemy.tscn").instantiate() outside_enemy.position = skill.aim_target + Vector2(GasBombController.AOE_RADIUS + 80.0, 0) - main.add_child(outside_enemy) outside_enemy.speed = 0.0 + main.add_child(outside_enemy) var release := InputEventScreenTouch.new() release.index = 7 diff --git a/tests/test_gas_system.gd.uid b/tests/test_gas_system.gd.uid deleted file mode 100644 index b71cec5..0000000 --- a/tests/test_gas_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ce1d4f81i7iqp diff --git a/tests/test_lab_resources.gd.uid b/tests/test_lab_resources.gd.uid deleted file mode 100644 index 6bc8efd..0000000 --- a/tests/test_lab_resources.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dbp0236qseh1x diff --git a/tests/test_main_menu.gd b/tests/test_main_menu.gd deleted file mode 100644 index 5193f33..0000000 --- a/tests/test_main_menu.gd +++ /dev/null @@ -1,95 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_main_menu.gd -# Verifies: -# 1. the menu loads with Play/Map Editor/Quit, the editor button starts -# disabled and explains itself instead of silently doing nothing -# 2. Quit's visibility follows OS.has_feature("web") (this platform is -# not a web export, so this run exercises the "visible" branch; the -# condition itself is what's under test, not the concrete boolean) -# 3. Play resets GameState to a fresh game AND reaches Main.tscn - -var menu: Node -var game_state: Node -var elapsed: float = 0.0 -var stage: int = 0 - - -func _initialize() -> void: - game_state = root.get_node("GameState") - menu = load("res://MainMenu.tscn").instantiate() - root.add_child(menu) - current_scene = menu - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - # A scene added to the tree inside _initialize() has not had its - # _ready() flushed yet - see test_planet_setups.gd for the same - # hazard - so the menu's @onready buttons aren't valid until a - # frame has actually processed. - if elapsed < 0.1: - return false - if menu.editor_button.enabled: - return _fail("map editor button is not disabled ahead of #48") - if menu.quit_button.visible != (not OS.has_feature("web")): - return _fail("quit button visibility did not follow OS.has_feature('web')") - - _press(menu.editor_button) - elapsed = 0.0 - stage = 1 - return false - 1: - if elapsed < 0.1: - return false - if "coming soon" not in menu.status_label.text.to_lower(): - return _fail("disabled editor button press did not explain itself") - if current_scene != menu: - return _fail("pressing the disabled editor button changed scenes") - - # Dirty the game state exactly like an in-progress run would, - # so a genuine reset is distinguishable from a coincidental match. - game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 999) - game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) - game_state.surviving_ship_count = 1 - game_state.planet_capture["chunk:1:1"] = {"captured": [true], "conquered": true} - - _press(menu.play_button) - elapsed = 0.0 - stage = 2 - return false - 2: - if elapsed < 0.3: - return false - var on_main: bool = current_scene != null and current_scene.name == "Main" - var reset_ok: bool = game_state.rocks == 500 and game_state.buildings.is_empty() \ - and game_state.surviving_ship_count == GameState.BASE_FLEET_SIZE \ - and game_state.planet_capture.is_empty() - print("on_main=%s reset_ok=%s rocks=%d buildings=%d ship_count=%d" % [ - on_main, reset_ok, game_state.rocks, game_state.buildings.size(), - game_state.surviving_ship_count - ]) - if not on_main or not reset_ok: - return _fail("Play did not reset to a fresh game before reaching Main.tscn") - print("TEST_MAIN_MENU: PASS") - quit(0) - return true - return false - - -func _press(button: IconButton) -> void: - var down: InputEventScreenTouch = InputEventScreenTouch.new() - down.pressed = true - button._gui_input(down) - var up: InputEventScreenTouch = InputEventScreenTouch.new() - up.pressed = false - button._gui_input(up) - - -func _fail(message: String) -> bool: - print("TEST_MAIN_MENU: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_main_menu.gd.uid b/tests/test_main_menu.gd.uid deleted file mode 100644 index 984875e..0000000 --- a/tests/test_main_menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dgf2ichdb7d4j diff --git a/tests/test_planet_conquest_gate.gd b/tests/test_planet_conquest_gate.gd deleted file mode 100644 index 93b69ba..0000000 --- a/tests/test_planet_conquest_gate.gd +++ /dev/null @@ -1,157 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_planet_conquest_gate.gd -# Verifies: -# 1. GameState starts every planet unconquered, and register_capture_points -# is idempotent (a second call with the same total doesn't wipe progress) -# 2. mark_point_captured only conquers once every point is captured, and -# the planet_conquered signal fires exactly once -# 3. over an unconquered planet, the extractor build option is disabled, -# the info popup explains why, and confirming spends nothing and builds -# nothing -# 4. once conquered, the same build flow works and charges the cost - -var main_scene: Node -var fleet: Node -var game_state: Node -var controller: Node -var button: IconButton -var popup: Control -var target_chunk: Vector2i -var stage: int = 0 -var elapsed: float = 0.0 -var conquered_signal_count: int = 0 - - -func _initialize() -> void: - var found: bool = false - for x in range(-15, 15): - for y in range(-15, 15): - var chunk: Vector2i = Vector2i(x, y) - if WorldGen.has_planet_at_chunk(chunk): - target_chunk = chunk - found = true - break - if found: - break - if not found: - return _fail("no planet chunk to test with") - - game_state = root.get_node("GameState") - game_state.rocks = 200 - game_state.buildings.clear() - game_state.extractors.clear() - game_state.planet_capture.clear() - game_state.reset_progression() - for node in GameState.SKILL_TREE: - game_state.unlocked.append(node.id) - - var target_planet_id: String = "chunk:%d:%d" % [target_chunk.x, target_chunk.y] - - # Unit-level checks on GameState itself, before any scene involvement. - if game_state.is_planet_conquered(target_planet_id): - return _fail("a fresh planet reported conquered") - game_state.register_capture_points(target_planet_id, 3) - game_state.mark_point_captured(target_planet_id, 0) - game_state.register_capture_points(target_planet_id, 3) # idempotent re-registration - if game_state.captured_point_count(target_planet_id) != 1: - return _fail("re-registering wiped already-captured progress") - game_state.planet_conquered.connect(func(_id): conquered_signal_count += 1) - game_state.mark_point_captured(target_planet_id, 1) - if game_state.is_planet_conquered(target_planet_id): - return _fail("conquered with points still missing") - game_state.mark_point_captured(target_planet_id, 2) - if not game_state.is_planet_conquered(target_planet_id): - return _fail("all points captured but planet not marked conquered") - # Marking an already-captured point again must not double-fire the signal. - game_state.mark_point_captured(target_planet_id, 2) - if conquered_signal_count != 1: - return _fail("planet_conquered fired %d times, expected exactly 1" % conquered_signal_count) - - # Reset conquest for the scene-level gate check below - this run tests - # the unconquered path first, then conquers it. - game_state.planet_capture.clear() - - main_scene = load("res://Main.tscn").instantiate() - root.add_child(main_scene) - fleet = main_scene.get_node("Fleet") - controller = main_scene.get_node("BuildController") - button = main_scene.get_node("UI/BuildMenu/ExtractorButton") - popup = main_scene.get_node("UI/BuildMenu/InfoPopup") - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - main_scene.get_node("AsteroidField").set_process(false) - main_scene.get_node("EnemySpawner").set_process(false) - var near: Vector2 = WorldGen.chunk_origin(target_chunk) + Vector2.ONE * WorldGen.CHUNK_SIZE * 0.5 - for ship in fleet.ships: - ship.position = near - fleet.move_to(near) - elapsed = 0.0 - stage = 1 - return false - 1: - var planet_field: Node = main_scene.get_node("PlanetField") - if elapsed < planet_field.check_interval + 0.2: - return false - if not planet_field.populated_chunks.has(target_chunk): - return _fail("planet never spawned") - var planet: Node2D = planet_field.populated_chunks[target_chunk][0] - for ship in fleet.ships: - ship.position = planet.position - fleet.move_to(planet.position) - elapsed = 0.0 - stage = 2 - return false - 2: - if elapsed < 0.3: - return false - var planet_field: Node = main_scene.get_node("PlanetField") - var planet: Node2D = planet_field.populated_chunks[target_chunk][0] - - controller._on_build_pressed() - if button.enabled: - return _fail("extractor option enabled over an unconquered planet") - controller._show_building_info(GameState.BUILDING_EXTRACTOR) - var explains_capture: bool = popup.visible \ - and "captur" in popup.get_node("Panel/MessageLabel").text.to_lower() - var offered: bool = popup.get_node("Panel/BuildButton").visible - if not explains_capture or offered: - return _fail("unconquered popup did not explain itself, or offered a build") - var rocks_before: int = game_state.rocks - controller._confirm_build() # must do nothing even if somehow pressed - if game_state.rocks != rocks_before or not game_state.extractors.is_empty(): - return _fail("build proceeded over an unconquered planet") - popup.visible = false - - # Now conquer it and confirm the same flow succeeds. - game_state.register_capture_points(planet.planet_id, 2) - game_state.mark_point_captured(planet.planet_id, 0) - game_state.mark_point_captured(planet.planet_id, 1) - - controller._on_build_pressed() - if not button.enabled: - return _fail("extractor option stayed disabled after conquest") - controller._show_building_info(GameState.BUILDING_EXTRACTOR) - if not popup.get_node("Panel/BuildButton").visible: - return _fail("conquered planet did not offer a build") - controller._confirm_build() - var built: bool = game_state.has_extractor_at(planet.planet_id) - var charged: bool = game_state.rocks == rocks_before - GameState.EXTRACTOR_COST - if not built or not charged: - return _fail("conquered planet did not build/charge correctly") - - print("TEST_PLANET_CONQUEST_GATE: PASS") - quit(0) - return true - return false - - -func _fail(message: String) -> bool: - print("TEST_PLANET_CONQUEST_GATE: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_planet_conquest_gate.gd.uid b/tests/test_planet_conquest_gate.gd.uid deleted file mode 100644 index 236fedc..0000000 --- a/tests/test_planet_conquest_gate.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://k4l7u3r84jch diff --git a/tests/test_planet_setups.gd b/tests/test_planet_setups.gd deleted file mode 100644 index 9a735e7..0000000 --- a/tests/test_planet_setups.gd +++ /dev/null @@ -1,129 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_planet_setups.gd -# Verifies: -# 1. setup_for_planet is deterministic per planet_id and only returns -# setups matching the requested surface profile -# 2. dropping a new .tres into res://PlanetSetups/ makes it appear via -# PlanetSetups.rescan() with no other code change -# 3. landing on a rocky/gas planet composes a PlanetSetupHost carrying the -# resolved setup, with one marker per capture point - -const SCRATCH_SETUP_PATH: String = "res://PlanetSetups/_test_scratch_setup.tres" - -var game_state: Node -var rocky_setup: Resource -var stage: int = 0 -var elapsed: float = 0.0 -var pending_surface: Node - - -func _initialize() -> void: - # --- 1. determinism and profile filtering --- - rocky_setup = PlanetSetups.setup_for_planet("chunk:3:7", "rocky") - if rocky_setup == null: - return _fail("no rocky setup resolved for a rocky planet id") - if rocky_setup.surface_profile_id != "rocky": - return _fail("resolved setup does not match the requested profile") - var rocky_setup_again: Resource = PlanetSetups.setup_for_planet("chunk:3:7", "rocky") - if rocky_setup != rocky_setup_again: - return _fail("setup_for_planet was not deterministic for the same planet id") - - var gas_setup: Resource = PlanetSetups.setup_for_planet("chunk:3:7", "gas") - if gas_setup == null: - return _fail("no gas setup resolved for the same planet id under the gas profile") - if gas_setup.surface_profile_id != "gas": - return _fail("gas request returned a mismatched-profile setup") - - for setup in PlanetSetups.setups_for_profile("rocky"): - if setup.surface_profile_id != "rocky": - return _fail("setups_for_profile('rocky') leaked a non-rocky setup") - - # --- 2. dropping a new .tres is picked up by rescan() --- - var before_count: int = PlanetSetups.setups_for_profile("rocky").size() - var scratch: Resource = PlanetSetup.new() - scratch.setup_id = "_test_scratch_setup" - scratch.display_name = "Scratch" - scratch.surface_profile_id = "rocky" - scratch.capture_points = PackedVector2Array([Vector2(100, 100)]) - var save_err: int = ResourceSaver.save(scratch, SCRATCH_SETUP_PATH) - if save_err != OK: - return _fail("could not save the scratch setup for the pickup check (err %d)" % save_err) - PlanetSetups.rescan() - var after_count: int = PlanetSetups.setups_for_profile("rocky").size() - DirAccess.remove_absolute(ProjectSettings.globalize_path(SCRATCH_SETUP_PATH)) - var import_file: String = SCRATCH_SETUP_PATH + ".import" - if FileAccess.file_exists(import_file): - DirAccess.remove_absolute(ProjectSettings.globalize_path(import_file)) - PlanetSetups.rescan() - if after_count != before_count + 1: - return _fail("registry did not pick up a newly saved setup (before=%d after=%d)" % [ - before_count, after_count - ]) - - # --- 3. landing composes a PlanetSetupHost with the resolved setup --- - # Deferred to _process(): a scene instantiated and added to the tree - # inside _initialize() has not had its _ready() chain flushed yet (this - # runs before the engine's first frame), so children added during - # PlanetSurfaceController._ready() are not observable until a frame has - # actually processed - see test_gas_system.gd for the same pattern. - game_state = root.get_node("GameState") - game_state.planet_name = "Setup Test" - game_state.planet_id = "chunk:3:7" - game_state.planet_definition_id = "rocky" - pending_surface = load("res://PlanetSurface.tscn").instantiate() - root.add_child(pending_surface) - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - if elapsed < 0.1: - return false - var host: Node = pending_surface.get_node_or_null("SurfaceFeatures/PlanetSetupHost") - if host == null: - pending_surface.free() - return _fail("landing did not compose a PlanetSetupHost") - if host.setup != rocky_setup: - pending_surface.free() - return _fail("host did not receive the deterministically resolved setup") - var marker_count: int = 0 - for child in host.get_children(): - if child is NeonPiece: - marker_count += 1 - if marker_count != rocky_setup.capture_points.size(): - pending_surface.free() - return _fail("host placed %d markers for %d capture points" % [ - marker_count, rocky_setup.capture_points.size() - ]) - pending_surface.free() - - game_state.planet_definition_id = "gas_giant" - pending_surface = load("res://PlanetSurface.tscn").instantiate() - root.add_child(pending_surface) - elapsed = 0.0 - stage = 1 - return false - 1: - if elapsed < 0.1: - return false - var gas_host: Node = pending_surface.get_node_or_null("SurfaceFeatures/PlanetSetupHost") - if gas_host == null or gas_host.setup.surface_profile_id != "gas": - pending_surface.free() - return _fail("gas landing did not compose a matching-profile setup host") - pending_surface.free() - - print("TEST_PLANET_SETUPS: PASS") - quit(0) - return true - return false - - -func _fail(message: String) -> bool: - print("TEST_PLANET_SETUPS: FAIL (%s)" % message) - if FileAccess.file_exists(SCRATCH_SETUP_PATH): - DirAccess.remove_absolute(ProjectSettings.globalize_path(SCRATCH_SETUP_PATH)) - quit(1) - return true diff --git a/tests/test_planet_setups.gd.uid b/tests/test_planet_setups.gd.uid deleted file mode 100644 index 0f9cd41..0000000 --- a/tests/test_planet_setups.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d2ekw3y7ipgy2 diff --git a/tests/test_vessel_delivery.gd.uid b/tests/test_vessel_delivery.gd.uid deleted file mode 100644 index 57266e2..0000000 --- a/tests/test_vessel_delivery.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bbqmyl26uqrbj