diff --git a/AimedFleetAbilityController.gd b/AimedFleetAbilityController.gd deleted file mode 100644 index f8cb2e5..0000000 --- a/AimedFleetAbilityController.gd +++ /dev/null @@ -1,191 +0,0 @@ -class_name AimedFleetAbilityController -extends Node2D - -# Shared press-drag-release input and camera behavior for manually aimed fleet -# abilities. Subclasses provide the component, ammunition, target calculation, -# shot effect, and effect visuals. - -const AIM_DEADZONE: float = 22.0 - -@export var minimum_zoom: float = 0.4 -@onready var fleet: Node2D = get_node("../Fleet") -@onready var camera: Camera2D = get_node("../Camera2D") - -var button: IconButton -var status: Label -var status_message_until: int = 0 -var aiming: bool = false -var aim_touch: int = -1 -var aim_start_screen: Vector2 = Vector2.ZERO -var aim_screen: Vector2 = Vector2.ZERO -var aim_target: Vector2 = Vector2.ZERO -var saved_zoom: float = 1.0 -var restoring_zoom: bool = false - - -func _ready() -> void: - button = get_node(_button_path()) - status = get_node(_status_path()) - button.visible = false - status.text = "" - queue_redraw() - - -func _process(delta: float) -> void: - var had_visuals: bool = aiming or _has_effect_visuals() - button.visible = fleet.has_capability(_component_id()) - button.enabled = _next_ready_ship() != null and _can_afford_shot() - if not button.visible and aiming: - _cancel_aim() - if status.text != "" and Time.get_ticks_msec() >= status_message_until: - status.text = "" - - if restoring_zoom and not aiming: - var zoom: float = move_toward(camera.zoom.x, saved_zoom, - maxf(saved_zoom, 0.1) * delta * 5.0) - camera.zoom = Vector2(zoom, zoom) - if is_equal_approx(zoom, saved_zoom): - restoring_zoom = false - - _advance_effects(delta) - if aiming or _has_effect_visuals() or had_visuals: - queue_redraw() - - -func can_begin_aim() -> bool: - # An unaffordable shot still claims the press so the status label can - # explain what is missing instead of silently passing the touch through. - return button.visible and not button._blocked_by_modal() \ - and not aiming and _next_ready_ship() != null - - -func handle_touch(event: InputEventScreenTouch) -> bool: - if event.pressed: - if not can_begin_aim() \ - or not Rect2(button.global_position, button.size).has_point(event.position): - return false - if not _can_afford_shot(): - _say(_unaffordable_message()) - return true - aiming = true - aim_touch = event.index - aim_start_screen = event.position - aim_screen = event.position - saved_zoom = camera.zoom.x - restoring_zoom = false - button.pressed_down = true - button.queue_redraw() - _update_aim() - return true - if event.index != aim_touch: - return false - if aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE: - _fire() - else: - _cancel_aim() - return true - - -func handle_drag(event: InputEventScreenDrag) -> bool: - if not aiming or event.index != aim_touch: - return false - aim_screen = event.position - _update_aim() - return true - - -func _update_aim() -> void: - var origin: Vector2 = _aim_origin() - aim_target = _calculate_aim_target(aim_screen - aim_start_screen, origin) - _update_aim_zoom(origin, aim_target) - queue_redraw() - - -func _update_aim_zoom(origin: Vector2, target: Vector2) -> void: - var displacement: Vector2 = target - origin - var half_screen: Vector2 = get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0) - var padding: float = _aim_padding() - var fit_x: float = saved_zoom if absf(displacement.x) < 1.0 \ - else half_screen.x / (absf(displacement.x) + padding) - var fit_y: float = saved_zoom if absf(displacement.y) < 1.0 \ - else half_screen.y / (absf(displacement.y) + padding) - var desired: float = clampf(minf(saved_zoom, minf(fit_x, fit_y)), - minimum_zoom, saved_zoom) - camera.zoom = Vector2(desired, desired) - - -func _finish_aim() -> void: - aiming = false - aim_touch = -1 - button.pressed_down = false - button.queue_redraw() - restoring_zoom = true - - -func _cancel_aim() -> void: - _finish_aim() - queue_redraw() - - -func _fire() -> void: - var ship: Node2D = _next_ready_ship() - if ship == null: - _cancel_aim() - return - if not _can_afford_shot(): - _say(_unaffordable_message()) - _cancel_aim() - return - var component: Node = ship.get_component(_component_id()) - if component == null or not _consume_shot(component): - _cancel_aim() - return - GameState.withdraw_fleet_resource(_ammo_resource(), _ammo_cost()) - _perform_shot(ship, component) - _finish_aim() - button.enabled = _next_ready_ship() != null and _can_afford_shot() - - -func _next_ready_ship() -> Node2D: - for ship in fleet.ships: - if not is_instance_valid(ship): - continue - var component: Node = ship.get_component(_component_id()) - if component != null and component.is_ready(): - return ship - return null - - -func _can_afford_shot() -> bool: - return GameState.fleet_resource(_ammo_resource()) >= _ammo_cost() - - -func _unaffordable_message() -> String: - var resource_type: String = _ammo_resource() - return "Need %d %s in the fleet hold to fire (have %d)" % [ - _ammo_cost(), GameState.resource_name(resource_type).to_lower(), - GameState.fleet_resource(resource_type), - ] - - -func _say(message: String) -> void: - status.text = message - status_message_until = Time.get_ticks_msec() + 3000 - - -func _aim_origin() -> Vector2: - return fleet.get_centroid() - - -# Subclass hooks. -func _button_path() -> NodePath: return NodePath() -func _status_path() -> NodePath: return NodePath() -func _component_id() -> String: return "" -func _ammo_resource() -> String: return "" -func _ammo_cost() -> int: return 0 -func _aim_padding() -> float: return 0.0 -func _has_effect_visuals() -> bool: return false -func _advance_effects(_delta: float) -> void: pass -func _consume_shot(_component: Node) -> bool: return false -func _perform_shot(_ship: Node2D, _component: Node) -> void: pass -func _calculate_aim_target(_drag: Vector2, origin: Vector2) -> Vector2: return origin diff --git a/AimedFleetAbilityController.gd.uid b/AimedFleetAbilityController.gd.uid deleted file mode 100644 index a455e17..0000000 --- a/AimedFleetAbilityController.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dbahjmvwru7bp diff --git a/BuildController.gd b/BuildController.gd index 6a45e1e..02de479 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -32,14 +32,12 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"), get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"), get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselF"), - get_node("../UI/LabMenu/VesselG"), ] @onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel") @onready var lab_vessel_labels: Array = [ get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"), get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"), get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselFLabel"), - get_node("../UI/LabMenu/VesselGLabel"), ] @onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel") @onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel") @@ -69,7 +67,6 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/AvailableGasBomber"), get_node("../UI/HangarMenu/AvailableCarrier"), get_node("../UI/HangarMenu/AvailableLaser"), - get_node("../UI/HangarMenu/AvailableDistorder"), ] @onready var hangar_available_labels: Array = [ get_node("../UI/HangarMenu/AvailableInterceptorLabel"), @@ -77,7 +74,6 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/AvailableGasBomberLabel"), get_node("../UI/HangarMenu/AvailableCarrierLabel"), get_node("../UI/HangarMenu/AvailableLaserLabel"), - get_node("../UI/HangarMenu/AvailableDistorderLabel"), ] @onready var hangar_fleet_slots: Array = [ get_node("../UI/HangarMenu/FleetInterceptor"), @@ -85,7 +81,6 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/FleetGasBomber"), get_node("../UI/HangarMenu/FleetCarrier"), get_node("../UI/HangarMenu/FleetLaser"), - get_node("../UI/HangarMenu/FleetDistorder"), ] @onready var hangar_fleet_labels: Array = [ get_node("../UI/HangarMenu/FleetInterceptorLabel"), @@ -93,7 +88,6 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/FleetGasBomberLabel"), get_node("../UI/HangarMenu/FleetCarrierLabel"), get_node("../UI/HangarMenu/FleetLaserLabel"), - get_node("../UI/HangarMenu/FleetDistorderLabel"), ] var build_menu_entries: Array = [] @@ -123,15 +117,13 @@ var lab_building_id: int = 0 @onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel") @onready var craft_laser_button: IconButton = get_node("../UI/CraftMenu/LaserButton") @onready var craft_laser_label: Label = get_node("../UI/CraftMenu/LaserLabel") -@onready var craft_distorder_button: IconButton = get_node("../UI/CraftMenu/DistorderButton") -@onready var craft_distorder_label: Label = get_node("../UI/CraftMenu/DistorderLabel") @onready var craft_vessel_buttons: Array = [ craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button, - craft_laser_button, craft_distorder_button, + craft_laser_button, ] @onready var craft_vessel_labels: Array = [ craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label, - craft_laser_label, craft_distorder_label, + craft_laser_label, ] # --- the transfer panel (#24) --------------------------------------------- @@ -680,13 +672,6 @@ func _confirm_build() -> void: if not GameState.is_planet_conquered(planet.planet_id): build_status.text = "%s is not captured yet" % planet.planet_name return - if String(planet.resource_type) == "": - # #63: a research_lab planet's whole point is that it yields - # nothing to mine - an extractor there would sit on an empty - # resource id with no ResourceDefinition, breaking its own - # glyph/HUD rendering the moment it produced anything. - build_status.text = "%s has nothing to extract" % 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 @@ -730,18 +715,14 @@ func _refresh_skill_menu(id: String = "") -> void: # leaving the panel blank, which reads as broken. skill_detail.text = "Select a node to see what it unlocks." skill_cost.amount = 0 - skill_status.text = "Fleet capacity %d · Research points %d" \ - % [GameState.max_fleet_size(), GameState.research_points] + skill_status.text = "Fleet capacity %d" % GameState.max_fleet_size() skill_unlock_button.visible = false return var node: Dictionary = GameState.skill_node(id) if node.is_empty(): return - var points_needed: int = GameState.skill_research_point_cost(id) skill_detail.text = "%s\n%s" % [node.name, node.desc] - if points_needed > 0: - skill_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points_needed] skill_cost.amount = int(node.cost) if GameState.is_unlocked(id): @@ -753,12 +734,6 @@ func _refresh_skill_menu(id: String = "") -> void: elif not GameState.can_afford(int(node.cost)): skill_status.text = "Not enough rocks - you have %d" % GameState.rocks skill_unlock_button.visible = false - elif GameState.research_points < points_needed: - # #63: a distinct reason from the ore one above - conflating them - # would leave the player trying to fix the wrong shortage. - skill_status.text = "Not enough research points - you have %d, need %d" \ - % [GameState.research_points, points_needed] - skill_unlock_button.visible = false else: skill_status.text = "" skill_unlock_button.visible = true @@ -919,8 +894,7 @@ func _refresh_lab() -> void: lab_vessel_labels[i].text = definition.display_name lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel lab_vessel_buttons[i].queue_redraw() - lab_vessel_label.text = "%s research · %d RP" % [ - GameState.vessel_name(lab_vessel), GameState.research_points] + lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel) var options: Array = _lab_options() for i in range(lab_upgrade_buttons.size()): @@ -932,9 +906,6 @@ func _refresh_lab() -> void: var option: Dictionary = options[i] var price_text: String = _format_costs(option.costs if bool(option.unlock) \ else GameState.research_costs(option.id)) - var point_cost: int = GameState.research_point_cost(String(option.id)) - if point_cost > 0: - price_text += " · %d RP" % point_cost if not bool(option.unlock) and GameState.upgrade_maxed(option.id): price_text = "MAX" var level_note: String = "" if bool(option.unlock) else " %d/%d" % [ @@ -953,22 +924,11 @@ func _refresh_lab() -> void: lab_choice = clampi(lab_choice, 0, options.size() - 1) var choice: Dictionary = options[lab_choice] - var choice_points: int = GameState.research_point_cost(String(choice.id)) lab_detail.text = "%s\n%s" % [choice.name, choice.desc] - if choice_points > 0: - lab_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, choice_points] var queued: Dictionary = GameState.lab_research_order(lab_building_id) lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id)) if not queued.is_empty(): - var queued_points: int = GameState.research_point_cost(String(queued.id)) - if not bool(queued.started) and GameState.research_points < queued_points: - # #63: distinct from "Waiting for resources" - flying more ore/gas - # to this lab cannot fix a research-point shortfall, only - # conquering a research lab elsewhere can. - lab_status.text = "Need %d research point%s (have %d)" \ - % [queued_points, "" if queued_points == 1 else "s", GameState.research_points] - else: - lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] + lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] lab_buy_button.queue_redraw() @@ -1043,7 +1003,7 @@ func _transfer_place_name() -> String: var b: Dictionary = GameState.find_building(transfer_building_id) if b.is_empty(): return "this building" - if GameState.building_has_capability(b, "bank"): + if b.type == GameState.BUILDING_MOTHERSHIP: # Worth naming as the bank rather than as a structure: this is the # one building where unloading makes the ore spendable everywhere. return "the bank" @@ -1056,7 +1016,7 @@ func _refresh_transfer() -> void: return var building: Dictionary = GameState.find_building(transfer_building_id) var heading: String = "This building" - if GameState.building_has_capability(building, "bank"): + if not building.is_empty() and building.type == GameState.BUILDING_MOTHERSHIP: heading = "Mothership bank" transfer_list.set_destination(transfer_building_id, heading) diff --git a/BuildingDefinition.gd b/BuildingDefinition.gd index 7b3f1fd..fdc990e 100644 --- a/BuildingDefinition.gd +++ b/BuildingDefinition.gd @@ -9,7 +9,6 @@ extends Resource @export var menu_icon: int @export var visual_scene: PackedScene @export var capabilities: PackedStringArray = [] -@export var logistics: Resource @export var map_color: Color = Color.WHITE @export var map_sides: int = 4 @export var map_radius: float = 7.0 diff --git a/BuildingLogisticsProfile.gd b/BuildingLogisticsProfile.gd deleted file mode 100644 index fdd6706..0000000 --- a/BuildingLogisticsProfile.gd +++ /dev/null @@ -1,13 +0,0 @@ -class_name BuildingLogisticsProfile -extends Resource - -# Optional logistics and conversion behavior composed into a building -# definition. GameState consumes this data generically, so adding another -# converter does not require another resource-type branch in transport code. - -@export var input_buffers: Dictionary = {} -@export var collected_outputs: PackedStringArray = [] -@export var conversion_input: String = "" -@export var conversion_output: String = "" -@export var conversion_rate: float = 0.0 -@export var conversion_yield: int = 1 diff --git a/BuildingLogisticsProfile.gd.uid b/BuildingLogisticsProfile.gd.uid deleted file mode 100644 index b6480eb..0000000 --- a/BuildingLogisticsProfile.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cydp18abylrgd diff --git a/CarrierCapacityUpgrade.tres b/CarrierCapacityUpgrade.tres index 99c01ac..8736c1b 100644 --- a/CarrierCapacityUpgrade.tres +++ b/CarrierCapacityUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Reinforced Hold" description = "+30 cargo capacity per level." base_cost = 40 max_level = 3 -research_point_cost = 1 icon = 21 additive_modifiers = {"cargo_capacity": 30.0} diff --git a/CarrierVesselDefinition.tres b/CarrierVesselDefinition.tres index 91d2743..6d7012b 100644 --- a/CarrierVesselDefinition.tres +++ b/CarrierVesselDefinition.tres @@ -22,5 +22,4 @@ unlock_name = "Unlock Carrier" unlock_description = "Research a dedicated hauler hull that trades weapons for a much larger cargo hold." unlock_cost = 0 unlock_resource_costs = {"ore": 60} -research_point_cost = 1 unlock_icon = -1 diff --git a/DistorderVesselDefinition.tres b/DistorderVesselDefinition.tres deleted file mode 100644 index c9bc3e7..0000000 --- a/DistorderVesselDefinition.tres +++ /dev/null @@ -1,25 +0,0 @@ -[gd_resource type="Resource" script_class="VesselDefinition" load_steps=3 format=3] -[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://VesselDistorderComponent.tscn" id="2"] -[resource] -script = ExtResource("1") -definition_id = "distorder" -display_name = "Distorder" -description = "Unarmed space-time distortion hull. While one flies with the fleet, a fader lets you push cruise speed beyond its default." -build_cost = 14 -build_resource_costs = {"ore": 14, "energy": 8} -build_time = 16.0 -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) -hull_points = PackedVector2Array(0, -24, -14, -2, -6, 18, 10, 10, 12, -10) -fleet_eligible = true -capabilities = PackedStringArray("distorder") -component_scenes = Array[PackedScene]([ExtResource("2")]) -base_stats = {"max_speed": 430.0, "acceleration": 410.0, "delivery_speed": 180.0} -unlock_id = "unlock_distorder" -unlock_name = "Unlock Distorder" -unlock_description = "Research a space-time distortion hull - while one flies with the fleet, a fader lets you push cruise speed beyond its default." -unlock_cost = 0 -unlock_resource_costs = {"energy": 50} -research_point_cost = 1 -unlock_icon = -1 diff --git a/EnemyBigLaserBeam.gd.uid b/EnemyBigLaserBeam.gd.uid deleted file mode 100644 index 7e2bd8a..0000000 --- a/EnemyBigLaserBeam.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://coopf1u1lnb8b diff --git a/EnemyBigLaserComponent.gd.uid b/EnemyBigLaserComponent.gd.uid deleted file mode 100644 index 31a1aba..0000000 --- a/EnemyBigLaserComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6oj7ia3em4x0 diff --git a/EnemySpawnerComponent.gd.uid b/EnemySpawnerComponent.gd.uid deleted file mode 100644 index 043b98d..0000000 --- a/EnemySpawnerComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://eqkp0coeoh4d diff --git a/EnergyResourceGlyph.gd.uid b/EnergyResourceGlyph.gd.uid deleted file mode 100644 index c8fb069..0000000 --- a/EnergyResourceGlyph.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b3b64pvw4y53x diff --git a/Fleet.gd b/Fleet.gd index ef17702..78e4627 100644 --- a/Fleet.gd +++ b/Fleet.gd @@ -80,7 +80,7 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO, ship.destroyed.connect(_on_ship_destroyed.bind(ship)) fleet_target = center - _sync_composition_state() + GameState.sync_fleet_cargo_capacity(vessel_types()) func vessel_types() -> PackedStringArray: @@ -91,15 +91,6 @@ func vessel_types() -> PackedStringArray: return result -func _sync_composition_state() -> void: - GameState.sync_fleet_cargo_capacity(vessel_types()) - # Composition changes are authoritative; scene teardown is not. Keeping - # this out of SpeedFader prevents an outgoing scene's empty fleet from - # resetting a boost while the same vessels are being restored on a surface. - if not has_capability("distorder"): - GameState.set_fleet_speed_multiplier(1.0) - - func has_vessel_type(vessel_type: String) -> bool: for ship in ships: if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type: @@ -148,7 +139,7 @@ func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool: if ship.has_signal("destroyed"): ship.destroyed.connect(_on_ship_destroyed.bind(ship)) ship.set_move_target(fleet_target + offset) - _sync_composition_state() + GameState.sync_fleet_cargo_capacity(vessel_types()) return true @@ -166,7 +157,7 @@ func store_ship(index: int) -> String: formation_offsets.remove_at(index) ship.queue_free() apply_formation() - _sync_composition_state() + GameState.sync_fleet_cargo_capacity(vessel_types()) return vessel_type @@ -209,25 +200,18 @@ func _process(delta: float) -> void: if steer_direction == Vector2.ZERO: return # The anchor - the fleet's formation centre - travels at cruise_speed. - # Ships may exceed it individually to reach their slots. #59: multiplied - # at the point of use, not baked into cruise_speed itself, so the base - # value stays stable regardless of the current boost. - anchor += steer_direction * _effective_cruise_speed() * delta + # Ships may exceed it individually to reach their slots. + anchor += steer_direction * cruise_speed * delta fleet_target = anchor _assign_slot_targets() -func _effective_cruise_speed() -> float: - return cruise_speed * GameState.fleet_speed_multiplier - - func _assign_slot_targets() -> void: - var effective_cruise_speed: float = _effective_cruise_speed() for i in range(ships.size()): ships[i].set_move_target(fleet_target + formation_offsets[i]) # Context, not a command: the ship adds this to its own velocity to # work out how it is really moving. See Ship.fleet_velocity. - ships[i].fleet_velocity = steer_direction * effective_cruise_speed + ships[i].fleet_velocity = steer_direction * cruise_speed func stop() -> void: @@ -292,7 +276,7 @@ func _on_ship_destroyed(ship: Node) -> void: if idx != -1: ships.remove_at(idx) formation_offsets.remove_at(idx) - _sync_composition_state() + GameState.sync_fleet_cargo_capacity(vessel_types()) func get_centroid() -> Vector2: diff --git a/GameController.gd b/GameController.gd index 957914b..e3cb346 100644 --- a/GameController.gd +++ b/GameController.gd @@ -28,7 +28,6 @@ extends Node2D # indirectly - e.g. Main.tscn loaded from inside a _screenshot_tool.gd # preview - for a class_name this new. Node + duck-typed calls sidesteps it. @onready var laser_controller: Node = get_node_or_null("LaserController") -@onready var speed_fader: Node = get_node_or_null("UI/SpeedFader") var touches: Dictionary = {} var pinch_active: bool = false @@ -264,8 +263,6 @@ func _handle_touch(event: InputEventScreenTouch) -> void: return if laser_controller != null and laser_controller.handle_touch(event): return - if speed_fader != null and speed_fader.handle_touch(event): - return if _handle_pivot_touch(event): return if joystick != null and joystick.visible and joystick.handle_touch(event): @@ -290,8 +287,6 @@ func _handle_drag(event: InputEventScreenDrag) -> void: return if laser_controller != null and laser_controller.handle_drag(event): return - if speed_fader != null and speed_fader.handle_drag(event): - return if joystick != null and joystick.visible and joystick.handle_drag(event): return if not touches.has(event.index): diff --git a/GameState.gd b/GameState.gd index 35c4bbc..ced08da 100644 --- a/GameState.gd +++ b/GameState.gd @@ -4,7 +4,6 @@ const INVENTORY = preload("res://Inventory.gd") const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") -const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") # Autoload singleton - the one thing that survives change_scene_to_file() @@ -23,35 +22,6 @@ var planet_name: String = "" var planet_id: String = "" var planet_definition_id: String = "rocky" -# #59: 1.0 is today's behaviour. Applied at the point of use (Fleet's cruise -# anchor, Ship's own max_speed/acceleration) rather than baked into stored -# stats, so a speed upgrade researched while boosted can't corrupt the base -# value - see Fleet.gd/Ship.gd for the multiply sites. -const MAX_FLEET_SPEED_MULTIPLIER: float = 3.0 -var fleet_speed_multiplier: float = 1.0 - -# #62: energy cost of sustaining a boost, scaled by how far above 1.0 the -# multiplier is - a small boost costs proportionally less, full boost costs -# ENERGY_DRAIN_PER_SECOND. Sitting at exactly 1.0 costs nothing. -const ENERGY_DRAIN_PER_SECOND: float = 2.0 -var _distorder_drain_progress: float = 0.0 -# One-shot notice, consumed and cleared by SpeedFader.gd: true the instant -# a sustained boost is force-reset for running the fleet hold's energy -# to zero, so the control (which may not even be part of the current -# scene tree at that exact frame) can show why on its next _process(). -var boost_depleted_notice: bool = false - - -func set_fleet_speed_multiplier(value: float) -> void: - fleet_speed_multiplier = clampf(value, 1.0, MAX_FLEET_SPEED_MULTIPLIER) - - -# #63: currency gated behind conquering a research_lab planet - required by -# both progression paths (skill tree nodes, vessel/upgrade research) on top -# of their existing ore/resource costs. -signal research_points_changed(total: int) -var research_points: int = 0 - # Explored-chunk history for the world map's fog-of-war reveal. Lives # here (not on ExplorationTracker, which gets destroyed and recreated on # every scene swap) so exploration progress isn't lost every time the @@ -123,26 +93,6 @@ func fleet_resource(resource_type: String) -> int: return fleet_inventory.amount(resource_type) -# #60: consumed outright (ammunition), not moved to another inventory like -# transfer_resource_to_building/_fleet - returns the amount actually taken. -func withdraw_fleet_resource(resource_type: String, amount: int) -> int: - return fleet_inventory.withdraw(resource_type, amount) - - -# #61: generic counterparts, for resources (energy) with no dedicated -# fleet_cargo/fleet_gas_cargo-style property of their own. Deliberately NOT -# the same as deposit_resource_to_building() on a "bank" building - that -# deposits into the building's OWN per-record inventory, not bank_inventory -# itself (only building_resource()/building_inventory() special-case "bank" -# capability to redirect there). -func deposit_fleet_resource(resource_type: String, amount: int) -> int: - return fleet_inventory.deposit(resource_type, amount) - - -func deposit_bank_resource(resource_type: String, amount: int) -> int: - return bank_inventory.deposit(resource_type, amount) - - # #56: total across every resource type in the hold, and the combined cap # that limits it - a HUD readout wants both, not any one resource's amount. func fleet_cargo_used() -> int: @@ -280,13 +230,6 @@ var buildings: Array = [] # tree always reads root-at-the-bottom and cannot drift out of step with the # dependencies when nodes are added. `requires` is a single prerequisite id # ("" for a root). -# #63: research_points (default 0 where omitted) is a SEPARATE currency from -# cost (ore), spent only from GameState.research_points - earned by -# conquering a research_lab planet, never bought. Balance, decided -# deliberately: shipyards and logistics stay at 0 so a fresh game can still -# reach a ship crafter and an extractor with zero research points (see the -# closing comment on #63 for the reasoning); every other node - none of them -# required for that opening - costs at least 1. const SKILL_TREE: Array = [ { "id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "", @@ -300,42 +243,42 @@ const SKILL_TREE: Array = [ }, { "id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards", - "x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "research_points": 1, + "x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "desc": "Build command centers, where the fleet formation is arranged.", }, { "id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics", - "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1, + "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "desc": "Build transport hubs. Each hub includes its own automatic transporters.", }, { "id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards", - "x": 0.82, "unlocks": BUILDING_SHIP_LAB, "research_points": 1, + "x": 0.82, "unlocks": BUILDING_SHIP_LAB, "desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.", }, { "id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight", - "x": 0.18, "hub_bonus": 1, "research_points": 2, + "x": 0.18, "hub_bonus": 1, "desc": "Permanent: +1 transporter stationed at every hub, including ones already built.", }, { "id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight", - "x": 0.50, "unlocks": BUILDING_GAS_BURNER, "research_points": 2, + "x": 0.50, "unlocks": BUILDING_GAS_BURNER, "desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.", }, { "id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination", - "x": 0.82, "fleet_bonus": 1, "research_points": 1, + "x": 0.82, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing", - "x": 0.50, "fleet_bonus": 1, "research_points": 2, + "x": 0.50, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron", - "x": 0.50, "fleet_bonus": 1, "research_points": 3, + "x": 0.50, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, ] @@ -385,27 +328,16 @@ func prerequisite_met(id: String) -> bool: func can_unlock(id: String) -> bool: - var node: Dictionary = skill_node(id) - return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(node.cost)) \ - and research_points >= skill_research_point_cost(id) - - -func skill_research_point_cost(id: String) -> int: - return int(skill_node(id).get("research_points", 0)) + return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost)) func unlock(id: String) -> bool: if not can_unlock(id): return false - var node: Dictionary = skill_node(id) - if not spend(int(node.cost)): + if not spend(int(skill_node(id).cost)): return false - var points_needed: int = skill_research_point_cost(id) - if points_needed > 0: - research_points -= points_needed - research_points_changed.emit(research_points) unlocked.append(id) - if int(node.get("hub_bonus", 0)) > 0: + if int(skill_node(id).get("hub_bonus", 0)) > 0: _sync_hub_transporters() return true @@ -449,10 +381,6 @@ func reset_new_game() -> void: planet_name = "" planet_id = "" planet_definition_id = "rocky" - fleet_speed_multiplier = 1.0 - _distorder_drain_progress = 0.0 - boost_depleted_notice = false - research_points = 0 explored_chunks.clear() bank_inventory = INVENTORY.new({RESOURCE_ORE: 500}) fleet_inventory = INVENTORY.new() @@ -526,13 +454,6 @@ func building_has_capability(building: Dictionary, capability: String) -> bool: return definition != null and capability in definition.capabilities -func building_logistics_profile(building: Dictionary) -> Resource: - if building.is_empty(): - return null - var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(building.type)) - return null if definition == null else definition.logistics - - # --- vessel types and their upgrades (#5) --------------------------------- # Combat vessels can join the player fleet. Transporter metadata lives in the @@ -544,7 +465,6 @@ const VESSEL_GAS_BOMBER: String = "gas_bomber" const VESSEL_TRANSPORTER: String = "transporter" const VESSEL_CARRIER: String = "carrier" const VESSEL_LASER: String = "laser" -const VESSEL_DISTORDER: String = "distorder" # Compatibility constants for callers and old tests. Runtime metadata is # resolved from VesselDefinition below. @@ -636,14 +556,10 @@ func unlock_vessel(vessel: String) -> bool: if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab(): return false var costs: Dictionary = research_costs(definition.unlock_id) - var points_needed: int = int(definition.research_point_cost) - if not can_afford_resources(costs) or research_points < points_needed: + if not can_afford_resources(costs): return false for resource_type in costs: bank_inventory.withdraw(String(resource_type), int(costs[resource_type])) - if points_needed > 0: - research_points -= points_needed - research_points_changed.emit(research_points) vessel_levels[definition.unlock_id] = 1 vessel_upgraded.emit(definition.unlock_id) return true @@ -690,8 +606,7 @@ func upgrade_cost(id: String) -> int: func can_buy_upgrade(id: String) -> bool: - return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) \ - and research_points >= research_point_cost(id) + return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) func buy_upgrade(id: String) -> bool: @@ -699,10 +614,6 @@ func buy_upgrade(id: String) -> bool: return false if not spend(upgrade_cost(id)): return false - var points_needed: int = research_point_cost(id) - if points_needed > 0: - research_points -= points_needed - research_points_changed.emit(research_points) vessel_levels[id] = upgrade_level(id) + 1 vessel_upgraded.emit(id) return true @@ -965,14 +876,6 @@ func research_cost(id: String) -> int: return int(research_costs(id).get(RESOURCE_ORE, 0)) -func research_point_cost(id: String) -> int: - var unlock_definition: Resource = vessel_for_unlock(id) - if unlock_definition != null: - return int(unlock_definition.research_point_cost) - var d: Dictionary = upgrade_def(id) - return int(d.resource.research_point_cost) if not d.is_empty() else 0 - - func research_costs(id: String) -> Dictionary: var unlock_definition: Resource = vessel_for_unlock(id) if unlock_definition != null: @@ -1001,20 +904,10 @@ func _advance_lab_research(delta: float) -> void: if inventory.amount(String(resource_type)) < int(costs[resource_type]): affordable = false break - # #63: research points are a fleet-wide currency, not delivered - # to the lab like ore/gas - checked and spent here too so a - # shortfall stalls the order the same way an undelivered - # resource does, rather than letting it start regardless. - var points_needed: int = research_point_cost(id) - if points_needed > research_points: - affordable = false if not affordable: continue for resource_type in costs: inventory.withdraw(String(resource_type), int(costs[resource_type])) - if points_needed > 0: - research_points -= points_needed - research_points_changed.emit(research_points) order.started = true order.elapsed = float(order.elapsed) + delta if float(order.elapsed) < lab_research_time(): @@ -1057,20 +950,19 @@ func has_building_of_type(type: String) -> bool: func remove_buildings_of_type(type: String) -> int: var kept: Array = [] - var removed_hub_ids: Array = [] + var removed_ids: Array = [] var removed: int = 0 for b in buildings: if b.type == type: removed += 1 - if building_has_capability(b, "transport_hub"): - removed_hub_ids.append(int(b.id)) + removed_ids.append(int(b.id)) else: kept.append(b) buildings = kept - if not removed_hub_ids.is_empty(): + if type == BUILDING_TRANSPORT_HUB and not removed_ids.is_empty(): var kept_transporters: Array = [] for transporter in transporters: - if int(transporter.get("hub_id", 0)) not in removed_hub_ids: + if int(transporter.get("hub_id", 0)) not in removed_ids: kept_transporters.append(transporter) transporters = kept_transporters return removed @@ -1200,76 +1092,44 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted: return inventory -# --- building logistics and resource conversion --------------------------- +# --- gas burners (#54) ----------------------------------------------------- +# +# Converts gas into energy at the building's own site, simulated here +# alongside extractor mining and production so it keeps running whether or +# not the player is nearby. Deliberately withdraws only from the building's +# OWN inventory, never the bank - a burner with no local gas simply stalls, +# exactly like a lab or crafter waiting on materials, rather than conjuring +# energy for free. -func building_logistics_deficit(id: int, resource_type: String) -> int: - var building: Dictionary = find_building(id) - var profile: Resource = building_logistics_profile(building) - if profile == null: +const GAS_BURN_RATE: float = 1.0 # gas/second +const ENERGY_PER_GAS: int = 2 +# Local gas reserve a burner tries to keep topped up. Transporters treat +# falling short of this the same way they treat a queued production/research +# order (see _dispatch_transporters()'s gas_burner_gas_deficit() use). +const GAS_BURNER_BUFFER: int = 40 + + +func gas_burner_gas_deficit(id: int) -> int: + var b: Dictionary = find_building(id) + if not building_has_capability(b, "gas_burner"): return 0 - var target: int = int(profile.input_buffers.get(resource_type, 0)) - return maxi(0, target - building_resource(id, resource_type)) + return maxi(0, GAS_BURNER_BUFFER - building_resource(id, RESOURCE_GAS)) -func building_requested_resource(id: int, resource_type: String) -> int: - return maxi(outstanding_resource_cost(id, resource_type), - building_logistics_deficit(id, resource_type)) - - -func _collectable_building_resource(building: Dictionary, minimum_amount: int) -> String: - var profile: Resource = building_logistics_profile(building) - if profile == null: - return "" - for resource_type in profile.collected_outputs: - if building_resource(int(building.id), resource_type) >= minimum_amount: - return resource_type - return "" - - -func _advance_resource_converters(delta: float) -> void: - for building in buildings: - var profile: Resource = building_logistics_profile(building) - if profile == null or profile.conversion_input == "" \ - or profile.conversion_output == "" or profile.conversion_rate <= 0.0 \ - or profile.conversion_yield <= 0: +func _advance_gas_burners(delta: float) -> void: + for b in buildings: + if not building_has_capability(b, "gas_burner"): continue - var inventory: RefCounted = _inventory_for_record(building) - if inventory.amount(profile.conversion_input) <= 0: - continue - var progress: float = float(building.get("conversion_progress", 0.0)) \ - + delta * profile.conversion_rate - var whole: int = mini(int(floor(progress)), inventory.amount(profile.conversion_input)) + var inventory: RefCounted = _inventory_for_record(b) + if inventory.amount(RESOURCE_GAS) <= 0: + continue # stalled - no local gas to burn + var progress: float = float(b.get("gas_burn_progress", 0.0)) + delta * GAS_BURN_RATE + var whole: int = mini(int(floor(progress)), inventory.amount(RESOURCE_GAS)) if whole > 0: - inventory.withdraw(profile.conversion_input, whole) - inventory.deposit(profile.conversion_output, whole * profile.conversion_yield) + inventory.withdraw(RESOURCE_GAS, whole) + inventory.deposit(RESOURCE_ENERGY, whole * ENERGY_PER_GAS) progress -= whole - building.conversion_progress = progress - - -func _advance_distorder_drain(delta: float) -> void: - if fleet_speed_multiplier <= 1.0: - # Nothing owed at 1.0x - and no stale fractional progress should - # carry over into a LATER, unrelated boost. - _distorder_drain_progress = 0.0 - return - var rate: float = (fleet_speed_multiplier - 1.0) / (MAX_FLEET_SPEED_MULTIPLIER - 1.0) \ - * ENERGY_DRAIN_PER_SECOND - _distorder_drain_progress += rate * delta - var whole: int = int(floor(_distorder_drain_progress)) - if whole <= 0: - return - _distorder_drain_progress -= whole - var available: int = fleet_inventory.amount(RESOURCE_ENERGY) - if available >= whole: - fleet_inventory.withdraw(RESOURCE_ENERGY, whole) - return - # Ran out mid-drain: take whatever is left (down to exactly zero) and - # force the boost back to its default, rather than let the fleet keep - # travelling fast on credit. - fleet_inventory.withdraw(RESOURCE_ENERGY, available) - fleet_speed_multiplier = 1.0 - _distorder_drain_progress = 0.0 - boost_depleted_notice = true + b.gas_burn_progress = progress # Planet conquest: a planet must have every one of its capture points held @@ -1282,7 +1142,7 @@ var planet_capture: Dictionary = {} signal planet_conquered(planet_id: String) -func register_capture_points(planet_key: String, total: int, definition_id: String = "") -> void: +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): @@ -1293,13 +1153,8 @@ func register_capture_points(planet_key: String, total: int, definition_id: Stri # A planet demanding zero points is vacuously already held (#55: a # black hole's "void" profile has no authored PlanetSetup at all, so # this is the only registration it ever gets) - every planet WITH real - # points still starts unconquered, exactly as before. The planet definition - # supplies an optional conquest reward, avoiding planet-id special cases in - # the capture system. - var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) - var point_reward: int = 0 if definition == null else int(definition.conquest_research_points) - planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0, - "research_point_reward": point_reward, "research_awarded": false} + # points still starts unconquered, exactly as before. + planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0} func capture_state(planet_key: String) -> Dictionary: @@ -1326,16 +1181,6 @@ func mark_point_captured(planet_key: String, index: int) -> void: if captured.all(func(c): return bool(c)): state.conquered = true planet_conquered.emit(planet_key) - # #63: definition-authored research points, exactly once per planet. - # state.conquered flipping above already makes a second call to this - # whole function a no-op via the early return above it, but - # research_awarded is its own explicit guard on the award itself - # regardless of how conquest was reached. - var reward: int = int(state.get("research_point_reward", 0)) - if reward > 0 and not bool(state.get("research_awarded", false)): - state.research_awarded = true - research_points += reward - research_points_changed.emit(research_points) func is_planet_conquered(planet_key: String) -> bool: @@ -1361,8 +1206,7 @@ func _process(delta: float) -> void: _advance_production(delta) _advance_lab_research(delta) _advance_vessel_deliveries(delta) - _advance_resource_converters(delta) - _advance_distorder_drain(delta) + _advance_gas_burners(delta) for e in extractors: e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE var whole: int = int(floor(float(e.mining_progress))) @@ -1520,7 +1364,13 @@ func _dispatch_transporters() -> void: for b in buildings: var needed_resource := "" for resource_type in resource_types(): - var need: int = building_requested_resource(int(b.id), resource_type) + # A gas burner's ongoing gas deficit is folded in alongside + # queued production/research costs (#54) - both are just "this + # building wants more of resource_type", so the one dispatch + # path already below serves both without any new transporter + # state. + var need: int = maxi(outstanding_resource_cost(int(b.id), resource_type), + gas_burner_gas_deficit(int(b.id)) if resource_type == RESOURCE_GAS else 0) if need > 0 and bank_inventory.amount(resource_type) > 0: needed_resource = resource_type break @@ -1573,11 +1423,14 @@ func _dispatch_transporters() -> void: _begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position, TRANSPORTER_TO_EXTRACTOR) - # Collect building outputs last: queued deliveries and extractor collection - # both outrank resources that are merely ready for pickup. + # Collecting a gas burner's energy output, last: queued deliveries and + # extractor collection both outrank it, same "supply what's needed + # before what's merely ready" ordering _dispatch_transporters() already + # holds for extractors over queued orders (f5c60f8). for b in buildings: - var pickup_resource: String = _collectable_building_resource(b, load) - if pickup_resource == "" or _transporter_inbound_for_pickup(int(b.id)): + if not building_has_capability(b, "gas_burner"): + continue + if building_resource(int(b.id), RESOURCE_ENERGY) < load or _transporter_inbound_for_pickup(int(b.id)): continue var chosen_pickup: Dictionary = {} var best_pickup_distance: float = INF @@ -1591,7 +1444,7 @@ func _dispatch_transporters() -> void: if chosen_pickup.is_empty(): return chosen_pickup.target_id = int(b.id) - chosen_pickup.resource_type = pickup_resource + chosen_pickup.resource_type = RESOURCE_ENERGY chosen_pickup.extractor_index = -1 _begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position, TRANSPORTER_TO_BUILDING_PICKUP) @@ -1621,7 +1474,8 @@ func _arrive(t: Dictionary) -> void: TRANSPORTER_TO_MOTHERSHIP: var target: Dictionary = find_building(int(t.target_id)) var resource_type: String = String(t.get("resource_type", RESOURCE_ORE)) - var want: int = building_requested_resource(int(t.target_id), resource_type) + var want: int = maxi(outstanding_resource_cost(int(t.target_id), resource_type), + gas_burner_gas_deficit(int(t.target_id)) if resource_type == RESOURCE_GAS else 0) var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit()) if take <= 0 or target.is_empty(): # Nothing to carry, or the building is gone. Go home empty. diff --git a/GasBombController.gd b/GasBombController.gd index 43e784a..4a5b213 100644 --- a/GasBombController.gd +++ b/GasBombController.gd @@ -1,64 +1,61 @@ class_name GasBombController -extends "res://AimedFleetAbilityController.gd" +extends Node2D const BOMB_COLOR := Color(0.96, 0.22, 0.72) const TARGET_COLOR := Color(0.78, 0.32, 1.0) const MAX_RANGE := 950.0 const AOE_RADIUS := 180.0 const DRAG_RADIUS := 260.0 +const AIM_DEADZONE := 22.0 const PROJECTILE_SPEED := 720.0 const DAMAGE := 3 const COOLDOWN := 5.0 -const GAS_PER_SHOT: int = 5 +@export var minimum_zoom: float = 0.4 +@onready var fleet: Node2D = get_node("../Fleet") +@onready var camera: Camera2D = get_node("../Camera2D") +@onready var button: IconButton = get_node("../UI/GasBombButton") + +var aiming := false +var aim_touch := -1 +var aim_start_screen := Vector2.ZERO +var aim_screen := Vector2.ZERO +var aim_target := Vector2.ZERO +var saved_zoom := 1.0 +var restoring_zoom := false var projectiles: Array = [] var explosions: Array = [] -var last_hit_count: int = 0 +var last_hit_count := 0 var projectile_active: bool: get: return not projectiles.is_empty() var explosion_time: float: get: - var longest: float = 0.0 + var longest := 0.0 for explosion in explosions: longest = maxf(longest, float(explosion.time)) return longest -func _button_path() -> NodePath: return "../UI/GasBombButton" -func _status_path() -> NodePath: return "../UI/GasBombStatus" -func _component_id() -> String: return "gas_bomber" -func _ammo_resource() -> String: return GameState.RESOURCE_GAS -func _ammo_cost() -> int: return GAS_PER_SHOT -func _aim_padding() -> float: return AOE_RADIUS -func _has_effect_visuals() -> bool: return not projectiles.is_empty() or not explosions.is_empty() +func _ready() -> void: + button.visible = false + queue_redraw() -func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2: - if drag.length() < 0.001: - return origin - var distance: float = minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE - return origin + drag.normalized() * distance +func _process(delta: float) -> void: + var had_visuals := aiming or not projectiles.is_empty() or not explosions.is_empty() + button.visible = fleet.has_capability("gas_bomber") + button.enabled = _next_ready_bomber() != null + if not button.visible and aiming: + _cancel_aim() + if restoring_zoom and not aiming: + var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0) + camera.zoom = Vector2(z, z) + if is_equal_approx(z, saved_zoom): + restoring_zoom = false -func _consume_shot(component: Node) -> bool: - return component.consume_shot(COOLDOWN) - - -func _perform_shot(ship: Node2D, _component: Node) -> void: - projectiles.append({ - "position": ship.global_position, - "target": aim_target, - "trail": PackedVector2Array([ship.global_position]), - }) - - -func _next_ready_bomber() -> Node2D: - return _next_ready_ship() - - -func _advance_effects(delta: float) -> void: for i in range(projectiles.size() - 1, -1, -1): var projectile: Dictionary = projectiles[i] var distance: float = (projectile.position as Vector2).distance_to(projectile.target) @@ -67,8 +64,8 @@ func _advance_effects(delta: float) -> void: _explode(projectile.target) projectiles.remove_at(i) else: - projectile.position = (projectile.position as Vector2).move_toward( - projectile.target, PROJECTILE_SPEED * delta) + projectile.position = (projectile.position as Vector2).move_toward(projectile.target, + PROJECTILE_SPEED * delta) var trail: PackedVector2Array = projectile.trail trail.append(projectile.position) while trail.size() > 12: @@ -79,6 +76,107 @@ func _advance_effects(delta: float) -> void: explosion.time = maxf(float(explosion.time) - delta, 0.0) if float(explosion.time) <= 0.0: explosions.remove_at(i) + if aiming or not projectiles.is_empty() or not explosions.is_empty() or had_visuals: + queue_redraw() + + +func can_begin_aim() -> bool: + return button.visible and button.enabled and not button._blocked_by_modal() \ + and not aiming + + +func handle_touch(event: InputEventScreenTouch) -> bool: + if event.pressed: + if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position): + return false + aiming = true + aim_touch = event.index + aim_start_screen = event.position + aim_screen = event.position + saved_zoom = camera.zoom.x + restoring_zoom = false + button.pressed_down = true + button.queue_redraw() + _update_aim() + return true + if event.index != aim_touch: + return false + var should_fire := aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE + if should_fire: + _fire() + else: + _cancel_aim() + return true + + +func handle_drag(event: InputEventScreenDrag) -> bool: + if not aiming or event.index != aim_touch: + return false + aim_screen = event.position + _update_aim() + return true + + +func _update_aim() -> void: + var drag := aim_screen - aim_start_screen + var origin: Vector2 = fleet.get_centroid() + if drag.length() < 0.001: + aim_target = origin + else: + var distance := minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE + aim_target = origin + drag.normalized() * distance + _update_aim_zoom(origin, aim_target) + queue_redraw() + + +func _update_aim_zoom(origin: Vector2, target: Vector2) -> void: + var displacement := target - origin + var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0) + var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + AOE_RADIUS) + var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + AOE_RADIUS) + var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom) + camera.zoom = Vector2(desired, desired) + + +func _finish_aim() -> void: + aiming = false + aim_touch = -1 + button.pressed_down = false + button.queue_redraw() + restoring_zoom = true + + +func _cancel_aim() -> void: + _finish_aim() + queue_redraw() + + +func _fire() -> void: + var bomber: Node2D = _next_ready_bomber() + if bomber == null: + _cancel_aim() + return + var component: Node = bomber.get_component("gas_bomber") + if not component.consume_shot(COOLDOWN): + _cancel_aim() + return + projectiles.append({ + "position": bomber.global_position, + "target": aim_target, + "trail": PackedVector2Array([bomber.global_position]), + }) + _finish_aim() + button.enabled = _next_ready_bomber() != null + + +func _next_ready_bomber() -> Node2D: + for ship in fleet.ships: + if not is_instance_valid(ship): + continue + var component: Node = ship.get_component("gas_bomber") + if component != null and component.is_ready(): + return ship + return null func _explode(at: Vector2) -> void: @@ -93,29 +191,28 @@ func _explode(at: Vector2) -> void: func _draw() -> void: if aiming: - NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.8) + var origin: Vector2 = fleet.get_centroid() + NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8) _draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8) _draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0) for projectile in projectiles: var trail: PackedVector2Array = projectile.trail if trail.size() >= 2: for i in range(1, trail.size()): - var alpha: float = float(i) / trail.size() - draw_line(trail[i - 1], trail[i], Color(BOMB_COLOR, alpha * 0.65), - 4.0 + alpha * 4.0, true) + var alpha := float(i) / trail.size() + draw_line(trail[i - 1], trail[i], + Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true) draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35)) NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0) for explosion in explosions: - var progress: float = 1.0 - float(explosion.time) / 0.8 - var radius: float = lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0)) + var progress := 1.0 - float(explosion.time) / 0.8 + var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0)) draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16)) _draw_ring(explosion.position, radius, BOMB_COLOR, 1.0 - progress) for i in range(7): - var angle: float = TAU * float(i) / 7.0 + progress - var cloud_at: Vector2 = explosion.position \ - + Vector2(cos(angle), sin(angle)) * radius * 0.55 - draw_circle(cloud_at, 18.0 + 14.0 * progress, - Color(BOMB_COLOR, (1.0 - progress) * 0.22)) + var angle := TAU * float(i) / 7.0 + progress + var cloud_at: Vector2 = explosion.position + Vector2(cos(angle), sin(angle)) * radius * 0.55 + draw_circle(cloud_at, 18.0 + 14.0 * progress, Color(BOMB_COLOR, (1.0 - progress) * 0.22)) func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void: diff --git a/GasBomberVesselDefinition.tres b/GasBomberVesselDefinition.tres index aad85ae..d464a4e 100644 --- a/GasBomberVesselDefinition.tres +++ b/GasBomberVesselDefinition.tres @@ -23,5 +23,4 @@ unlock_name = "Unlock Gas Bomber" unlock_description = "Research volatile-gas containment and unlock the Gas Bomber for ship crafters." unlock_cost = 0 unlock_resource_costs = {"gas": 30} -research_point_cost = 1 unlock_icon = 31 diff --git a/GasBurnerBuildingDefinition.tres b/GasBurnerBuildingDefinition.tres index a0d3058..09d2240 100644 --- a/GasBurnerBuildingDefinition.tres +++ b/GasBurnerBuildingDefinition.tres @@ -1,19 +1,6 @@ -[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=4 format=3] +[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3] [ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"] [ext_resource type="PackedScene" path="res://GasBurnerBuildingVisual.tscn" id="2"] -[ext_resource type="Script" path="res://BuildingLogisticsProfile.gd" id="3"] - -[sub_resource type="Resource" id="Resource_logistics"] -script = ExtResource("3") -input_buffers = { -"gas": 40 -} -collected_outputs = PackedStringArray("energy") -conversion_input = "gas" -conversion_output = "energy" -conversion_rate = 1.0 -conversion_yield = 2 - [resource] script = ExtResource("1") definition_id = "gas_burner" @@ -24,7 +11,6 @@ action_icon = 33 menu_icon = 33 visual_scene = ExtResource("2") capabilities = PackedStringArray("gas_burner") -logistics = SubResource("Resource_logistics") map_color = Color(0.95, 0.35, 0.15, 1) map_sides = 5 map_radius = 7.0 diff --git a/GasBurnerBuildingVisual.gd.uid b/GasBurnerBuildingVisual.gd.uid deleted file mode 100644 index 20dc35e..0000000 --- a/GasBurnerBuildingVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5a74rb5s4q21 diff --git a/IconButton.gd b/IconButton.gd index 5ba2701..62878a0 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -41,8 +41,6 @@ enum Icon { # #58: LASER_VESSEL is the ship glyph, LASER_SHOT the action button - # same two-icon split as GAS_BOMBER/GAS_BOMB above. LASER_VESSEL, LASER_SHOT, - # #59: a warped ring reads as a space-time distortion rather than a hull. - DISTORDER_VESSEL, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -241,8 +239,6 @@ func _draw() -> void: _draw_laser_vessel(center, pulse, s) Icon.LASER_SHOT: _draw_laser_shot(center, pulse, s) - Icon.DISTORDER_VESSEL: - _draw_distorder_vessel(center, pulse, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -281,18 +277,6 @@ func _draw_carrier(center: Vector2, pulse: float, s: float) -> void: NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color) -func _draw_distorder_vessel(center: Vector2, pulse: float, s: float) -> void: - # A ring whose radius wobbles with angle instead of a plain circle - - # reads as warped space rather than a hull silhouette. - var points: PackedVector2Array = PackedVector2Array() - for i in range(17): - var a: float = TAU * float(i) / 16.0 - var r: float = (10.0 + 3.0 * sin(a * 3.0)) * s - points.append(center + Vector2(cos(a), sin(a)) * r) - NeonDraw.draw_glowing_path(self, points, _active_color(), pulse, 0.8, core_color) - NeonDraw.draw_glowing_circle(self, center, 2.5 * s, _active_color(), pulse, core_color) - - func _draw_laser_vessel(center: Vector2, pulse: float, s: float) -> void: # A slim dart rather than the interceptor's plain triangle - the notch at # the nose reads as a fixed forward-facing emitter, not a hull point. diff --git a/InterceptorDamageUpgrade.tres b/InterceptorDamageUpgrade.tres index a0845fc..835851a 100644 --- a/InterceptorDamageUpgrade.tres +++ b/InterceptorDamageUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Heavy Slugs" description = "+1 damage per shot." base_cost = 30 max_level = 3 -research_point_cost = 1 icon = 15 additive_modifiers = {"damage": 1.0} diff --git a/InterceptorRangeUpgrade.tres b/InterceptorRangeUpgrade.tres index b986ba0..ce01efe 100644 --- a/InterceptorRangeUpgrade.tres +++ b/InterceptorRangeUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Targeting Array" description = "+60 firing range per level." base_cost = 20 max_level = 3 -research_point_cost = 1 icon = 17 additive_modifiers = {"fire_range": 60.0} diff --git a/InterceptorRateUpgrade.tres b/InterceptorRateUpgrade.tres index 57dabe6..4add45e 100644 --- a/InterceptorRateUpgrade.tres +++ b/InterceptorRateUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Autoloader" description = "Fires 15% faster per level." base_cost = 25 max_level = 3 -research_point_cost = 1 icon = 16 multiplicative_modifiers = {"fire_cooldown": 0.85} diff --git a/InterceptorSpeedUpgrade.tres b/InterceptorSpeedUpgrade.tres index 16ac1fe..5549971 100644 --- a/InterceptorSpeedUpgrade.tres +++ b/InterceptorSpeedUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Thrusters" description = "+40 top speed, +60 acceleration per level." base_cost = 25 max_level = 3 -research_point_cost = 1 icon = 18 additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0} diff --git a/LaserController.gd b/LaserController.gd index b9c9cfc..78f8e81 100644 --- a/LaserController.gd +++ b/LaserController.gd @@ -1,60 +1,164 @@ class_name LaserController -extends "res://AimedFleetAbilityController.gd" +extends Node2D + +# Same press-drag-release aiming plumbing as GasBombController, adapted for +# an instant beam rather than a lobbed, timed projectile: there is no travel +# time or explosion delay - the beam strikes the instant the shot fires, and +# only a brief flash lingers for feedback. Damage/range/cooldown are read +# straight off the firing ship's own VesselLaserComponent (already resolved +# from vessel_stat/upgrades) rather than hardcoded here, unlike the bomber's +# AOE_RADIUS/DAMAGE/COOLDOWN. +# +# Reused as-is by both Main.tscn and PlanetSurface.tscn (same relative +# ../Fleet, ../Camera2D, ../UI/LaserButton paths as GasBombController), so +# it works identically in space and on a landed surface with no branching. const BEAM_COLOR := Color(0.3, 0.9, 1.0) const TARGET_COLOR := Color(0.6, 0.95, 1.0) const BEAM_HALF_WIDTH := 22.0 +const DRAG_DEADZONE := 22.0 const FLASH_DURATION := 0.22 -const ENERGY_PER_SHOT: int = 8 +@export var minimum_zoom: float = 0.4 +@onready var fleet: Node2D = get_node("../Fleet") +@onready var camera: Camera2D = get_node("../Camera2D") +@onready var button: IconButton = get_node("../UI/LaserButton") + +var aiming := false +var aim_touch := -1 +var aim_start_screen := Vector2.ZERO +var aim_screen := Vector2.ZERO +var aim_target := Vector2.ZERO +var saved_zoom := 1.0 +var restoring_zoom := false var beams: Array = [] -var last_hit_count: int = 0 +var last_hit_count := 0 -func _button_path() -> NodePath: return "../UI/LaserButton" -func _status_path() -> NodePath: return "../UI/LaserStatus" -func _component_id() -> String: return "laser" -func _ammo_resource() -> String: return GameState.RESOURCE_ENERGY -func _ammo_cost() -> int: return ENERGY_PER_SHOT -func _aim_padding() -> float: return BEAM_HALF_WIDTH -func _has_effect_visuals() -> bool: return not beams.is_empty() +func _ready() -> void: + button.visible = false + queue_redraw() -func _aim_max_range() -> float: - var ship: Node2D = _next_ready_ship() - if ship == null: - return 950.0 - var component: Node = ship.get_component(_component_id()) - return component.beam_range if component != null else 950.0 +func _process(delta: float) -> void: + var had_visuals := aiming or not beams.is_empty() + button.visible = fleet.has_capability("laser") + button.enabled = _next_ready_laser_ship() != null + if not button.visible and aiming: + _cancel_aim() + if restoring_zoom and not aiming: + var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0) + camera.zoom = Vector2(z, z) + if is_equal_approx(z, saved_zoom): + restoring_zoom = false -func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2: - # A beam always reaches full range; dragging chooses its direction only. - var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP - return origin + direction * _aim_max_range() - - -func _consume_shot(component: Node) -> bool: - return component.consume_shot() - - -func _perform_shot(ship: Node2D, component: Node) -> void: - var direction: Vector2 = aim_target - _aim_origin() - direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP - var beam_to: Vector2 = ship.global_position + direction * component.beam_range - _strike(ship.global_position, beam_to, direction, int(component.damage)) - - -func _next_ready_laser_ship() -> Node2D: - return _next_ready_ship() - - -func _advance_effects(delta: float) -> void: for i in range(beams.size() - 1, -1, -1): var beam: Dictionary = beams[i] beam.time = maxf(float(beam.time) - delta, 0.0) if float(beam.time) <= 0.0: beams.remove_at(i) + if aiming or not beams.is_empty() or had_visuals: + queue_redraw() + + +func can_begin_aim() -> bool: + return button.visible and button.enabled and not button._blocked_by_modal() \ + and not aiming + + +func handle_touch(event: InputEventScreenTouch) -> bool: + if event.pressed: + if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position): + return false + aiming = true + aim_touch = event.index + aim_start_screen = event.position + aim_screen = event.position + saved_zoom = camera.zoom.x + restoring_zoom = false + button.pressed_down = true + button.queue_redraw() + _update_aim() + return true + if event.index != aim_touch: + return false + var should_fire := aim_start_screen.distance_to(aim_screen) >= DRAG_DEADZONE + if should_fire: + _fire() + else: + _cancel_aim() + return true + + +func handle_drag(event: InputEventScreenDrag) -> bool: + if not aiming or event.index != aim_touch: + return false + aim_screen = event.position + _update_aim() + return true + + +func _aim_origin() -> Vector2: + return fleet.get_centroid() + + +func _aim_max_range() -> float: + var ship: Node2D = _next_ready_laser_ship() + if ship == null: + return 950.0 + var component: Node = ship.get_component("laser") + return component.beam_range if component != null else 950.0 + + +func _update_aim() -> void: + # A beam always reaches its full range - only the DIRECTION is aimed, + # unlike the bomb's drag-controlled distance. + var drag := aim_screen - aim_start_screen + var origin: Vector2 = _aim_origin() + var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP + aim_target = origin + direction * _aim_max_range() + _update_aim_zoom(origin, aim_target) + queue_redraw() + + +func _update_aim_zoom(origin: Vector2, target: Vector2) -> void: + var displacement := target - origin + var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0) + var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + BEAM_HALF_WIDTH) + var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + BEAM_HALF_WIDTH) + var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom) + camera.zoom = Vector2(desired, desired) + + +func _finish_aim() -> void: + aiming = false + aim_touch = -1 + button.pressed_down = false + button.queue_redraw() + restoring_zoom = true + + +func _cancel_aim() -> void: + _finish_aim() + queue_redraw() + + +func _fire() -> void: + var shooter: Node2D = _next_ready_laser_ship() + if shooter == null: + _cancel_aim() + return + var component: Node = shooter.get_component("laser") + if not component.consume_shot(): + _cancel_aim() + return + var direction: Vector2 = (aim_target - _aim_origin()) + direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP + var beam_to: Vector2 = shooter.global_position + direction * component.beam_range + _strike(shooter.global_position, beam_to, direction, int(component.damage)) + _finish_aim() + button.enabled = _next_ready_laser_ship() != null func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> void: @@ -74,12 +178,20 @@ func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> voi last_hit_count += 1 +func _next_ready_laser_ship() -> Node2D: + for ship in fleet.ships: + if not is_instance_valid(ship): + continue + var component: Node = ship.get_component("laser") + if component != null and component.is_ready(): + return ship + return null + + func _draw() -> void: if aiming: - NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, - TARGET_COLOR, 0.7, 0.6) + NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.6) for beam in beams: var progress: float = 1.0 - float(beam.time) / FLASH_DURATION var alpha: float = 1.0 - progress - NeonDraw.draw_glowing_line(self, beam.from, beam.to, - BEAM_COLOR, alpha, 1.2 - progress * 0.6) + NeonDraw.draw_glowing_line(self, beam.from, beam.to, BEAM_COLOR, alpha, 1.2 - progress * 0.6) diff --git a/LaserController.gd.uid b/LaserController.gd.uid deleted file mode 100644 index 303a679..0000000 --- a/LaserController.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cdqg3isy1lbuf diff --git a/LaserCooldownUpgrade.tres b/LaserCooldownUpgrade.tres index b89ee4a..71ad6aa 100644 --- a/LaserCooldownUpgrade.tres +++ b/LaserCooldownUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Capacitor Bank" description = "Recharges 0.6s sooner per level." base_cost = 30 max_level = 3 -research_point_cost = 1 icon = 20 additive_modifiers = {"beam_cooldown": -0.6} diff --git a/LaserDamageUpgrade.tres b/LaserDamageUpgrade.tres index e9c4c97..daa76ab 100644 --- a/LaserDamageUpgrade.tres +++ b/LaserDamageUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Focusing Lens" description = "+1 damage per shot." base_cost = 30 max_level = 3 -research_point_cost = 1 icon = 15 additive_modifiers = {"damage": 1.0} diff --git a/LaserRangeUpgrade.tres b/LaserRangeUpgrade.tres index 755e585..447c58e 100644 --- a/LaserRangeUpgrade.tres +++ b/LaserRangeUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Beam Collimator" description = "+150 beam range per level." base_cost = 25 max_level = 3 -research_point_cost = 1 icon = 17 additive_modifiers = {"beam_range": 150.0} diff --git a/LaserVesselDefinition.tres b/LaserVesselDefinition.tres index ea4b7bc..370a0d5 100644 --- a/LaserVesselDefinition.tres +++ b/LaserVesselDefinition.tres @@ -26,5 +26,4 @@ unlock_name = "Unlock Laser Ship" unlock_description = "Research a directed-energy hull for the ship crafter - manually aimed, no ammo, cools down between shots." unlock_cost = 0 unlock_resource_costs = {"energy": 40} -research_point_cost = 1 unlock_icon = -1 diff --git a/Main.tscn b/Main.tscn index e7a1427..7eaf569 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=29 format=3] +[gd_scene load_steps=28 format=3] [ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Fleet.gd" id="2"] @@ -26,7 +26,6 @@ [ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"] [ext_resource type="Script" path="res://GasBombController.gd" id="29"] [ext_resource type="Script" path="res://LaserController.gd" id="30"] -[ext_resource type="Script" path="res://SpeedFader.gd" id="31"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -222,28 +221,6 @@ offset_top = -424 offset_bottom = -24 script = ExtResource("13") -[node name="SpeedFader" type="Control" parent="UI"] -visible = false -mouse_filter = 2 -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -94 -offset_right = -24 -offset_top = 740 -offset_bottom = 920 -script = ExtResource("31") - -[node name="Label" type="Label" parent="UI/SpeedFader"] -mouse_filter = 2 -offset_left = -20 -offset_right = 90 -offset_top = -32 -offset_bottom = -4 -theme_override_font_sizes/font_size = 20 -theme_override_colors/font_color = Color(0.7, 0.3, 1, 1) -horizontal_alignment = 1 -text = "x1.0" - [node name="LaserButton" type="Control" parent="UI"] visible = false mouse_filter = 2 @@ -287,38 +264,6 @@ script = ExtResource("10") icon = 32 glow_color = Color(0.96, 0.22, 0.72, 1) -[node name="GasBombStatus" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 1.0 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -344 -offset_right = -24 -offset_top = -480 -offset_bottom = -430 -theme_override_font_sizes/font_size = 18 -theme_override_colors/font_color = Color(0.96, 0.22, 0.72, 1) -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="LaserStatus" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 1.0 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -344 -offset_right = -24 -offset_top = -540 -offset_bottom = -490 -theme_override_font_sizes/font_size = 18 -theme_override_colors/font_color = Color(0.3, 0.9, 1, 1) -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - [node name="PivotCWButton" type="Control" parent="UI"] mouse_filter = 2 anchor_left = 1.0 @@ -894,7 +839,7 @@ horizontal_alignment = 1 anchor_left = 0.5 anchor_right = 0.5 offset_left = -290 -offset_right = -215 +offset_right = -198 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -905,7 +850,7 @@ mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 -offset_right = -175 +offset_right = -158 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -914,8 +859,8 @@ horizontal_alignment = 1 [node name="AvailableShield" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -189 -offset_right = -114 +offset_left = -168 +offset_right = -76 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -926,8 +871,8 @@ glow_color = Color(0.45, 0.8, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -229 -offset_right = -74 +offset_left = -208 +offset_right = -36 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -936,8 +881,8 @@ horizontal_alignment = 1 [node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -88 -offset_right = -13 +offset_left = -46 +offset_right = 46 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -948,8 +893,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -128 -offset_right = 27 +offset_left = -86 +offset_right = 86 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -958,8 +903,8 @@ horizontal_alignment = 1 [node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 13 -offset_right = 88 +offset_left = 76 +offset_right = 168 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -970,8 +915,8 @@ glow_color = Color(1, 0.78, 0.3, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -27 -offset_right = 128 +offset_left = 36 +offset_right = 208 offset_top = 380 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -980,8 +925,8 @@ horizontal_alignment = 1 [node name="AvailableLaser" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 114 -offset_right = 189 +offset_left = 198 +offset_right = 290 offset_top = 255 offset_bottom = 375 script = ExtResource("10") @@ -992,29 +937,7 @@ glow_color = Color(0.3, 0.9, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 74 -offset_right = 229 -offset_top = 380 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="AvailableDistorder" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 215 -offset_right = 290 -offset_top = 255 -offset_bottom = 375 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="AvailableDistorderLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 175 +offset_left = 158 offset_right = 330 offset_top = 380 offset_bottom = 460 @@ -1034,7 +957,7 @@ horizontal_alignment = 1 anchor_left = 0.5 anchor_right = 0.5 offset_left = -290 -offset_right = -215 +offset_right = -198 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1045,7 +968,7 @@ mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 -offset_right = -175 +offset_right = -158 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -1054,8 +977,8 @@ horizontal_alignment = 1 [node name="FleetShield" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -189 -offset_right = -114 +offset_left = -168 +offset_right = -76 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1066,8 +989,8 @@ glow_color = Color(0.45, 0.8, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -229 -offset_right = -74 +offset_left = -208 +offset_right = -36 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -1076,8 +999,8 @@ horizontal_alignment = 1 [node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -88 -offset_right = -13 +offset_left = -46 +offset_right = 46 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1088,8 +1011,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -128 -offset_right = 27 +offset_left = -86 +offset_right = 86 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -1098,8 +1021,8 @@ horizontal_alignment = 1 [node name="FleetCarrier" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 13 -offset_right = 88 +offset_left = 76 +offset_right = 168 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1110,8 +1033,8 @@ glow_color = Color(1, 0.78, 0.3, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -27 -offset_right = 128 +offset_left = 36 +offset_right = 208 offset_top = 690 offset_bottom = 770 theme_override_font_sizes/font_size = 22 @@ -1120,8 +1043,8 @@ horizontal_alignment = 1 [node name="FleetLaser" type="Control" parent="UI/HangarMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 114 -offset_right = 189 +offset_left = 198 +offset_right = 290 offset_top = 565 offset_bottom = 685 script = ExtResource("10") @@ -1132,29 +1055,7 @@ glow_color = Color(0.3, 0.9, 1, 1) mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 74 -offset_right = 229 -offset_top = 690 -offset_bottom = 770 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 - -[node name="FleetDistorder" type="Control" parent="UI/HangarMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 215 -offset_right = 290 -offset_top = 565 -offset_bottom = 685 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="FleetDistorderLabel" type="Label" parent="UI/HangarMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 175 +offset_left = 158 offset_right = 330 offset_top = 690 offset_bottom = 770 @@ -1206,7 +1107,7 @@ script = ExtResource("24") anchor_left = 0.5 anchor_right = 0.5 offset_left = -300 -offset_right = -240 +offset_right = -230 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1216,8 +1117,8 @@ glow_color = Color(0.25, 0.85, 1, 1) [node name="VesselB" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -210 -offset_right = -150 +offset_left = -194 +offset_right = -124 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1227,8 +1128,8 @@ glow_color = Color(0.45, 0.8, 1, 1) [node name="VesselC" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -120 -offset_right = -60 +offset_left = -88 +offset_right = -18 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1238,8 +1139,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1) [node name="VesselD" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = -30 -offset_right = 30 +offset_left = 18 +offset_right = 88 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1249,8 +1150,8 @@ glow_color = Color(0.95, 0.55, 0.85, 1) [node name="VesselE" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 60 -offset_right = 120 +offset_left = 124 +offset_right = 194 offset_top = 274 offset_bottom = 394 script = ExtResource("10") @@ -1260,25 +1161,14 @@ glow_color = Color(1, 0.78, 0.3, 1) [node name="VesselF" type="Control" parent="UI/LabMenu"] anchor_left = 0.5 anchor_right = 0.5 -offset_left = 150 -offset_right = 210 +offset_left = 230 +offset_right = 300 offset_top = 274 offset_bottom = 394 script = ExtResource("10") icon = 35 glow_color = Color(0.3, 0.9, 1, 1) -[node name="VesselG" type="Control" parent="UI/LabMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 240 -offset_right = 300 -offset_top = 274 -offset_bottom = 394 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - [node name="VesselLabel" type="Label" parent="UI/LabMenu"] visible = false mouse_filter = 2 @@ -1296,7 +1186,7 @@ mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 -offset_right = -210 +offset_right = -200 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1307,8 +1197,8 @@ text = "Interceptor" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -240 -offset_right = -120 +offset_left = -224 +offset_right = -94 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1319,8 +1209,8 @@ text = "Shield" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -150 -offset_right = -30 +offset_left = -118 +offset_right = 12 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1331,8 +1221,8 @@ text = "Gas Bomber" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -60 -offset_right = 60 +offset_left = 12 +offset_right = 118 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1343,8 +1233,8 @@ text = "Transporter" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 30 -offset_right = 150 +offset_left = 94 +offset_right = 224 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 @@ -1355,25 +1245,13 @@ text = "Carrier" mouse_filter = 2 anchor_left = 0.5 anchor_right = 0.5 -offset_left = 120 -offset_right = 240 -offset_top = 400 -offset_bottom = 460 -theme_override_font_sizes/font_size = 22 -horizontal_alignment = 1 -text = "Laser Ship" - -[node name="VesselGLabel" type="Label" parent="UI/LabMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 210 +offset_left = 200 offset_right = 330 offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 horizontal_alignment = 1 -text = "Distorder" +text = "Laser Ship" [node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"] mouse_filter = 2 @@ -1620,7 +1498,7 @@ anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -310 -offset_right = -245 +offset_right = -232 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1633,7 +1511,7 @@ anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 offset_left = -330 -offset_right = -225 +offset_right = -212 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1645,8 +1523,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -221 -offset_right = -156 +offset_left = -202 +offset_right = -124 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1659,8 +1537,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -241 -offset_right = -136 +offset_left = -222 +offset_right = -104 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1673,8 +1551,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -132 -offset_right = -67 +offset_left = -94 +offset_right = -16 offset_top = -100 offset_bottom = 28 script = ExtResource("10") @@ -1687,8 +1565,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -152 -offset_right = -47 +offset_left = -114 +offset_right = 4 offset_top = 56 offset_bottom = 150 theme_override_font_sizes/font_size = 24 @@ -1701,8 +1579,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -43 -offset_right = 22 +offset_left = 14 +offset_right = 92 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1715,8 +1593,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = -63 -offset_right = 42 +offset_left = -6 +offset_right = 112 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 @@ -1729,8 +1607,8 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 46 -offset_right = 111 +offset_left = 122 +offset_right = 200 offset_top = -100 offset_bottom = 48 script = ExtResource("10") @@ -1743,41 +1621,13 @@ anchor_left = 0.5 anchor_right = 0.5 anchor_top = 0.5 anchor_bottom = 0.5 -offset_left = 26 -offset_right = 131 -offset_top = 56 -offset_bottom = 136 -theme_override_font_sizes/font_size = 24 -autowrap_mode = 2 -text = "Laser Ship" -horizontal_alignment = 1 - -[node name="DistorderButton" type="Control" parent="UI/CraftMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 135 -offset_right = 200 -offset_top = -100 -offset_bottom = 48 -script = ExtResource("10") -icon = 37 -glow_color = Color(0.7, 0.3, 1, 1) - -[node name="DistorderLabel" type="Label" parent="UI/CraftMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -anchor_top = 0.5 -anchor_bottom = 0.5 -offset_left = 115 +offset_left = 102 offset_right = 220 offset_top = 56 offset_bottom = 136 theme_override_font_sizes/font_size = 24 autowrap_mode = 2 -text = "Distorder" +text = "Laser Ship" horizontal_alignment = 1 [node name="QueueLabel" type="Label" parent="UI/CraftMenu"] @@ -2086,9 +1936,9 @@ anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 offset_left = -64.0 -offset_top = -400.0 +offset_top = -456.0 offset_right = 76.0 -offset_bottom = -336.0 +offset_bottom = -392.0 script = ExtResource("19") digit_height = 52.0 @@ -2098,9 +1948,9 @@ anchor_right = 1.0 anchor_top = 1.0 anchor_bottom = 1.0 offset_left = 32.0 -offset_top = -326.0 +offset_top = -386.0 offset_right = -32.0 -offset_bottom = -250.0 +offset_bottom = -318.0 theme_override_font_sizes/font_size = 32 autowrap_mode = 2 horizontal_alignment = 1 @@ -2112,9 +1962,9 @@ anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 offset_left = -74.0 -offset_top = -236.0 +offset_top = -304.0 offset_right = 74.0 -offset_bottom = -88.0 +offset_bottom = -156.0 script = ExtResource("10") icon = 5 glow_color = Color(0.45, 0.9, 0.55, 1) diff --git a/PlanetDefinition.gd b/PlanetDefinition.gd index 6678109..e184241 100644 --- a/PlanetDefinition.gd +++ b/PlanetDefinition.gd @@ -8,7 +8,3 @@ extends Resource @export var surface_profile_id: String = "rocky" @export var resource_id: String = "ore" @export var map_color: Color = Color(0.7, 0.65, 0.55) -# Optional reward granted once when every authored capture point is claimed. -# Keeping it on the definition lets conquest remain generic as new planet -# rewards are introduced. -@export var conquest_research_points: int = 0 diff --git a/PlanetDefinitions.gd b/PlanetDefinitions.gd index 197202e..bada655 100644 --- a/PlanetDefinitions.gd +++ b/PlanetDefinitions.gd @@ -4,13 +4,11 @@ extends RefCounted const ROCKY_ID: String = "rocky" const GAS_GIANT_ID: String = "gas_giant" const BLACK_HOLE_ID: String = "black_hole" -const RESEARCH_LAB_ID: String = "research_lab" const DEFAULT_DEFINITION_ID: String = ROCKY_ID const DEFINITIONS: Dictionary = { ROCKY_ID: preload("res://RockyPlanetDefinition.tres"), GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"), BLACK_HOLE_ID: preload("res://BlackHolePlanetDefinition.tres"), - RESEARCH_LAB_ID: preload("res://ResearchLabPlanetDefinition.tres"), } diff --git a/PlanetSetupHost.gd b/PlanetSetupHost.gd index 99e145e..1b79167 100644 --- a/PlanetSetupHost.gd +++ b/PlanetSetupHost.gd @@ -19,8 +19,7 @@ func apply_setup(new_setup: Resource) -> void: if setup == null: return - GameState.register_capture_points(GameState.planet_id, setup.capture_points.size(), - GameState.planet_definition_id) + GameState.register_capture_points(GameState.planet_id, setup.capture_points.size()) for i in range(setup.capture_points.size()): _add_capture_point(i, setup.capture_points[i]) for spawn in setup.enemy_spawns: diff --git a/PlanetSetups/ResearchLabSetup.tres b/PlanetSetups/ResearchLabSetup.tres deleted file mode 100644 index 20b4075..0000000 --- a/PlanetSetups/ResearchLabSetup.tres +++ /dev/null @@ -1,42 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetSetup" format=3] - -[ext_resource type="Script" path="res://PlanetSetupSpawn.gd" id="1"] -[ext_resource type="Script" path="res://PlanetSetup.gd" id="2"] - -[sub_resource type="Resource" id="Resource_ta1"] -script = ExtResource("1") -enemy_id = "turret_spawner" -position = Vector2(-620.0, -180.0) - -[sub_resource type="Resource" id="Resource_ta2"] -script = ExtResource("1") -enemy_id = "turret_spawner" -position = Vector2(640.0, -160.0) - -[sub_resource type="Resource" id="Resource_ta3"] -script = ExtResource("1") -enemy_id = "artillery" -position = Vector2(0.0, -720.0) - -[sub_resource type="Resource" id="Resource_ta4"] -script = ExtResource("1") -enemy_id = "sniper" -position = Vector2(-380.0, 420.0) - -[sub_resource type="Resource" id="Resource_ta5"] -script = ExtResource("1") -enemy_id = "sniper" -position = Vector2(400.0, 440.0) - -[sub_resource type="Resource" id="Resource_ta6"] -script = ExtResource("1") -enemy_id = "chaser" -position = Vector2(120.0, 60.0) - -[resource] -script = ExtResource("2") -setup_id = "research_lab_1" -display_name = "Research Lab Garrison" -surface_profile_id = "research_lab" -capture_points = PackedVector2Array(-450.0, -60.0, 460.0, -40.0, 0.0, 480.0) -enemy_spawns = Array[Resource]([SubResource("Resource_ta1"), SubResource("Resource_ta2"), SubResource("Resource_ta3"), SubResource("Resource_ta4"), SubResource("Resource_ta5"), SubResource("Resource_ta6")]) diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index 709872e..afdeb49 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3] +[gd_scene load_steps=15 format=3] [ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"] @@ -10,7 +10,6 @@ [ext_resource type="Script" path="res://ResourceReadout.gd" id="9"] [ext_resource type="Script" path="res://GasBombController.gd" id="14"] [ext_resource type="Script" path="res://LaserController.gd" id="16"] -[ext_resource type="Script" path="res://SpeedFader.gd" id="17"] [ext_resource type="Script" path="res://CaptureLabel.gd" id="15"] [sub_resource type="ShaderMaterial" id="1"] @@ -158,30 +157,6 @@ script = ExtResource("7") glow_color = Color(0.06, 0.32, 0.5, 1) core_color = Color(0.02, 0.09, 0.15, 1) -[node name="SpeedFader" type="Control" parent="UI"] -visible = false -mouse_filter = 2 -anchor_left = 1.0 -anchor_right = 1.0 -offset_left = -94 -offset_right = -24 -offset_top = 740 -offset_bottom = 920 -script = ExtResource("17") -glow_color = Color(0.5, 0.18, 0.6, 1) -core_color = Color(0.02, 0.09, 0.15, 1) - -[node name="Label" type="Label" parent="UI/SpeedFader"] -mouse_filter = 2 -offset_left = -20 -offset_right = 90 -offset_top = -32 -offset_bottom = -4 -theme_override_font_sizes/font_size = 20 -theme_override_colors/font_color = Color(0.7, 0.3, 1, 1) -horizontal_alignment = 1 -text = "x1.0" - [node name="LaserButton" type="Control" parent="UI"] visible = false mouse_filter = 2 @@ -229,38 +204,6 @@ icon = 32 glow_color = Color(0.82, 0.08, 0.48, 1) core_color = Color(0.02, 0.09, 0.15, 1) -[node name="GasBombStatus" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 1.0 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -344 -offset_right = -24 -offset_top = -480 -offset_bottom = -430 -theme_override_font_sizes/font_size = 18 -theme_override_colors/font_color = Color(0.82, 0.08, 0.48, 1) -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - -[node name="LaserStatus" type="Label" parent="UI"] -mouse_filter = 2 -anchor_left = 1.0 -anchor_right = 1.0 -anchor_top = 1.0 -anchor_bottom = 1.0 -offset_left = -344 -offset_right = -24 -offset_top = -540 -offset_bottom = -490 -theme_override_font_sizes/font_size = 18 -theme_override_colors/font_color = Color(0.2, 0.6, 0.68, 1) -autowrap_mode = 2 -horizontal_alignment = 1 -text = "" - [node name="PivotCWButton" type="Control" parent="UI"] mouse_filter = 2 anchor_left = 1.0 diff --git a/PlanetSurfaceProfiles.gd b/PlanetSurfaceProfiles.gd index 9d42e5e..5624fae 100644 --- a/PlanetSurfaceProfiles.gd +++ b/PlanetSurfaceProfiles.gd @@ -6,7 +6,6 @@ const PROFILES: Dictionary = { "rocky": preload("res://RockySurfaceProfile.tres"), "gas": preload("res://GasSurfaceProfile.tres"), "void": preload("res://VoidSurfaceProfile.tres"), - "research_lab": preload("res://ResearchLabSurfaceProfile.tres"), } diff --git a/ResearchLabPlanetDefinition.tres b/ResearchLabPlanetDefinition.tres deleted file mode 100644 index ac195df..0000000 --- a/ResearchLabPlanetDefinition.tres +++ /dev/null @@ -1,15 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] - -[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://ResearchLabPlanetVisual.tscn" id="2"] - -[resource] -script = ExtResource("1") -definition_id = "research_lab" -display_name = "Research Lab" -names = PackedStringArray("Blacksite Zero", "Cordoned Array", "Dead Signal Station", "The Vault", "Interdicted Post") -visual_scene = ExtResource("2") -surface_profile_id = "research_lab" -resource_id = "" -map_color = Color(0.7, 0.2, 0.15, 1) -conquest_research_points = 1 diff --git a/ResearchLabPlanetVisual.gd b/ResearchLabPlanetVisual.gd deleted file mode 100644 index 7f832a9..0000000 --- a/ResearchLabPlanetVisual.gd +++ /dev/null @@ -1,55 +0,0 @@ -extends Node2D - -# Same informal interface as GasGiantVisual/RockyPlanetVisual - configure(), -# extractor_anchor(), animates via its own _process(). Deliberately angular -# and armoured rather than a natural silhouette (#63: this is an ENEMY -# installation, not a celestial body), with a rotating dish and blinking -# warning lights to read as occupied/hostile at a glance. - -var phase: float = 0.0 - -const RADIUS: float = 108.0 -const HULL_COLOR: Color = Color(0.55, 0.1, 0.08) -const CORE_COLOR: Color = Color(0.9, 0.25, 0.18) -const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15) -const DARK_CORE: Color = Color(0.14, 0.02, 0.02) - - -func configure(_shape_seed: int) -> void: - queue_redraw() - - -func extractor_anchor() -> Vector2: - return Vector2(0.0, -(RADIUS + 40.0)) - - -func _process(delta: float) -> void: - phase += delta - queue_redraw() - - -func _draw() -> void: - var hull: PackedVector2Array = PackedVector2Array() - for i in range(6): - var angle: float = TAU * float(i) / 6.0 - hull.append(Vector2(cos(angle), sin(angle)) * RADIUS) - NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 0.85, DARK_CORE) - - var inner: PackedVector2Array = PackedVector2Array() - for i in range(6): - var angle: float = TAU * float(i) / 6.0 + PI / 6.0 - inner.append(Vector2(cos(angle), sin(angle)) * RADIUS * 0.55) - NeonDraw.draw_glowing_polygon(self, inner, CORE_COLOR, 0.9, DARK_CORE) - - # A slow-rotating dish - reads as actively transmitting, not derelict. - var dish_angle: float = phase * 0.6 - var dish_tip: Vector2 = Vector2(cos(dish_angle), sin(dish_angle)) * RADIUS * 1.35 - NeonDraw.draw_glowing_line(self, Vector2.ZERO, dish_tip, CORE_COLOR, 0.8, 0.55, DARK_CORE) - NeonDraw.draw_glowing_circle(self, dish_tip, 11.0, CORE_COLOR, 1.0, DARK_CORE) - - # Blinking hull-corner warning lights. - var blink: float = 0.4 + 0.6 * absf(sin(phase * 4.0)) - for i in range(6): - var angle: float = TAU * float(i) / 6.0 - var at: Vector2 = Vector2(cos(angle), sin(angle)) * RADIUS - NeonDraw.draw_glowing_circle(self, at, 5.0, WARNING_COLOR, blink, DARK_CORE) diff --git a/ResearchLabPlanetVisual.gd.uid b/ResearchLabPlanetVisual.gd.uid deleted file mode 100644 index a16d73f..0000000 --- a/ResearchLabPlanetVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6nasspc8ewwj diff --git a/ResearchLabPlanetVisual.tscn b/ResearchLabPlanetVisual.tscn deleted file mode 100644 index bb2ebaa..0000000 --- a/ResearchLabPlanetVisual.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://ResearchLabPlanetVisual.gd" id="1"] - -[node name="Visual" type="Node2D"] -script = ExtResource("1") diff --git a/ResearchLabSurfaceProfile.tres b/ResearchLabSurfaceProfile.tres deleted file mode 100644 index 98e83c8..0000000 --- a/ResearchLabSurfaceProfile.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=2 format=3] - -[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] - -[resource] -script = ExtResource("1") -profile_id = "research_lab" -base_color = Color(0.13, 0.1, 0.1, 1) -crater_color = Color(0.05, 0.03, 0.03, 1) -highlight_color = Color(0.55, 0.15, 0.1, 1) -caption_color = Color(0.9, 0.6, 0.55, 1) -caption_outline_color = Color(0.05, 0.02, 0.02, 0.9) diff --git a/ShieldHitsUpgrade.tres b/ShieldHitsUpgrade.tres index bbbd180..2530597 100644 --- a/ShieldHitsUpgrade.tres +++ b/ShieldHitsUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Layered Emitters" description = "+1 hit absorbed before the shield drops." base_cost = 60 max_level = 2 -research_point_cost = 1 icon = 19 additive_modifiers = {"shield_charges": 1.0} diff --git a/ShieldRechargeUpgrade.tres b/ShieldRechargeUpgrade.tres index 756523c..d73cf09 100644 --- a/ShieldRechargeUpgrade.tres +++ b/ShieldRechargeUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Fast Cycling" description = "Shield returns 1.2s sooner per level." base_cost = 35 max_level = 3 -research_point_cost = 1 icon = 20 additive_modifiers = {"shield_recharge": -1.2} diff --git a/ShieldSpeedUpgrade.tres b/ShieldSpeedUpgrade.tres index ef2b8a4..fe5a4b6 100644 --- a/ShieldSpeedUpgrade.tres +++ b/ShieldSpeedUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Thrusters" description = "+40 top speed, +60 acceleration per level." base_cost = 25 max_level = 3 -research_point_cost = 1 icon = 18 additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0} diff --git a/ShieldVesselDefinition.tres b/ShieldVesselDefinition.tres index 0a99ba1..aa4c440 100644 --- a/ShieldVesselDefinition.tres +++ b/ShieldVesselDefinition.tres @@ -24,5 +24,4 @@ unlock_id = "unlock_shield" unlock_name = "Unlock Shield Vessel" unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up." unlock_cost = 45 -research_point_cost = 1 unlock_icon = 23 diff --git a/Ship.gd b/Ship.gd index 074c6b4..cdaa542 100644 --- a/Ship.gd +++ b/Ship.gd @@ -97,29 +97,16 @@ func _physics_process(delta: float) -> void: if child.has_method("tick"): child.tick(delta) - # #59: multiplied here, at the point of use, rather than baked into the - # cached max_speed/acceleration fields apply_vessel_stats() sets - those - # stay the true base values, so a stat upgrade researched mid-boost can't - # corrupt them, and the fader takes effect the instant it moves with no - # re-apply needed. arrive_radius is scaled too: at N physics ticks/sec a - # ship's per-tick step is `speed * delta`, and once that step outgrows - # the (otherwise fixed) arrive_radius it can overshoot the snap-to-target - # zone every tick while chasing a target that is ALSO now receding N - # times faster - a small, boost-proportional radius keeps the same - # "snaps onto its slot" formation tightness at any boost level that - # 1.0x already has. - var boost: float = GameState.fleet_speed_multiplier var to_target: Vector2 = move_target - position var distance: float = to_target.length() - if distance <= arrive_radius * boost: + if distance <= arrive_radius: position = move_target velocity = Vector2.ZERO else: - var effective_acceleration: float = acceleration * boost - var braking_speed: float = sqrt(2.0 * effective_acceleration * distance) - var desired_speed: float = min(max_speed * boost, braking_speed) + var braking_speed: float = sqrt(2.0 * acceleration * distance) + var desired_speed: float = min(max_speed, braking_speed) var desired_velocity: Vector2 = to_target.normalized() * desired_speed - velocity = velocity.move_toward(desired_velocity, effective_acceleration * delta) + velocity = velocity.move_toward(desired_velocity, acceleration * delta) position += velocity * delta var motion: Vector2 = velocity + fleet_velocity diff --git a/SpeedFader.gd b/SpeedFader.gd deleted file mode 100644 index 46edf9e..0000000 --- a/SpeedFader.gd +++ /dev/null @@ -1,125 +0,0 @@ -class_name SpeedFader -extends Control - -# Vertical drag control for GameState.fleet_speed_multiplier. Same raw-touch -# idiom as TouchJoystick (handle_touch/handle_drag offered first by -# GameController, not _gui_input) and the same NeonDraw look, but a fader -# rather than a stick: the knob's Y position along a fixed vertical track IS -# the value, no spring-back to centre. -# -# Self-contained: writes straight to GameState.fleet_speed_multiplier rather -# than reporting a value for something else to apply, since the multiplier -# already lives in the one place (the autoload) that needs to survive scene -# swaps. - -@export var glow_color: Color = Color(0.7, 0.3, 1.0) -@export var core_color: Color = Color(1.0, 1.0, 1.0) -@export var track_margin: float = 28.0 - -@onready var fleet: Node2D = get_node("../../Fleet") -@onready var label: Label = get_node("Label") - -var active_touch_index: int = -1 -# 0 = bottom of the track (1.0x), 1 = top (GameState.MAX_FLEET_SPEED_MULTIPLIER). -var knob_t: float = 0.0 -var status_message_until: int = 0 - - -func _ready() -> void: - visible = false - queue_redraw() - - -func _process(_delta: float) -> void: - var has_distorder: bool = fleet.has_capability("distorder") - if has_distorder != visible: - visible = has_distorder - if not has_distorder: - # Fleet owns persistent composition state. This outgoing UI node can - # briefly observe an empty fleet during a scene swap, so it must only - # clear its own interaction state here. - active_touch_index = -1 - knob_t = 0.0 - return - knob_t = (GameState.fleet_speed_multiplier - 1.0) \ - / maxf(GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0, 0.001) - # #62: GameState._process() (not this node's own _process()) is what - # drains energy and force-resets the multiplier, since it has to keep - # running across the space<->surface swap and while this control might - # not even be in the current scene tree. It leaves a one-shot flag for - # whichever SpeedFader picks it up next. - if GameState.boost_depleted_notice: - GameState.boost_depleted_notice = false - # Still holding the drag at the moment energy ran out would - # otherwise re-boost on the very next drag event, from a knob - # position the finger never actually moved away from. - active_touch_index = -1 - label.text = "Out of energy!" - status_message_until = Time.get_ticks_msec() + 3000 - elif Time.get_ticks_msec() >= status_message_until: - label.text = "x%.1f" % GameState.fleet_speed_multiplier - queue_redraw() - - -func is_active() -> bool: - return active_touch_index != -1 - - -func _blocked_by_modal() -> bool: - # Same check as IconButton._blocked_by_modal() - panels stay transparent - # to raw input (the skill tree/formation editor need _unhandled_input), - # so every raw-touch control checks for itself instead. - for menu in get_tree().get_nodes_in_group("modal_menus"): - if menu.is_visible_in_tree() and not menu.is_ancestor_of(self): - return true - return false - - -func handle_touch(event: InputEventScreenTouch) -> bool: - var local: Vector2 = event.position - global_position - if event.pressed: - if not visible or active_touch_index != -1 or _blocked_by_modal(): - return false - if not Rect2(Vector2.ZERO, size).has_point(local): - return false - active_touch_index = event.index - _update_from_local(local) - return true - if event.index != active_touch_index: - return false - active_touch_index = -1 - queue_redraw() - return true - - -func handle_drag(event: InputEventScreenDrag) -> bool: - if event.index != active_touch_index: - return false - _update_from_local(event.position - global_position) - return true - - -func _update_from_local(local: Vector2) -> void: - var usable: float = maxf(size.y - track_margin * 2.0, 1.0) - # Up is 1.0 (top of the track), matching how a real fader/throttle reads. - var t: float = 1.0 - clampf((local.y - track_margin) / usable, 0.0, 1.0) - knob_t = t - GameState.set_fleet_speed_multiplier(1.0 + t * (GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0)) - label.text = "x%.1f" % GameState.fleet_speed_multiplier - queue_redraw() - - -func _draw() -> void: - var top: Vector2 = Vector2(size.x * 0.5, track_margin) - var bottom: Vector2 = Vector2(size.x * 0.5, size.y - track_margin) - NeonDraw.draw_glowing_line(self, bottom, top, glow_color, 0.55, 0.5, core_color) - - # Tick marks at the extremes read as "min" and "max" at a glance. - for t in [0.0, 1.0]: - var y: float = lerpf(bottom.y, top.y, t) - NeonDraw.draw_glowing_line(self, Vector2(size.x * 0.5 - 12.0, y), - Vector2(size.x * 0.5 + 12.0, y), glow_color, 0.4, 0.35, core_color) - - var knob_pos: Vector2 = bottom.lerp(top, clampf(knob_t, 0.0, 1.0)) - NeonDraw.draw_glowing_circle(self, knob_pos, 16.0, glow_color, - 1.0 if is_active() else 0.8, core_color) diff --git a/SpeedFader.gd.uid b/SpeedFader.gd.uid deleted file mode 100644 index 3dd9dd3..0000000 --- a/SpeedFader.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://m16rgs2ed7w1 diff --git a/TransportCapacityUpgrade.tres b/TransportCapacityUpgrade.tres index 87b331c..2c2b920 100644 --- a/TransportCapacityUpgrade.tres +++ b/TransportCapacityUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Expanded Hold" description = "Carries 8 more resources per supply run." base_cost = 30 max_level = 3 -research_point_cost = 1 icon = 21 additive_modifiers = {"cargo_capacity": 8.0} diff --git a/TransportSpeedUpgrade.tres b/TransportSpeedUpgrade.tres index 06c3f42..43c3c96 100644 --- a/TransportSpeedUpgrade.tres +++ b/TransportSpeedUpgrade.tres @@ -7,6 +7,5 @@ display_name = "Freight Engines" description = "+70 speed on supply runs." base_cost = 30 max_level = 3 -research_point_cost = 1 icon = 22 additive_modifiers = {"transport_speed": 70.0} diff --git a/VesselCooldownFillComponent.gd b/VesselCooldownFillComponent.gd deleted file mode 100644 index 8c009c1..0000000 --- a/VesselCooldownFillComponent.gd +++ /dev/null @@ -1,84 +0,0 @@ -class_name VesselCooldownFillComponent -extends Node2D - -# Shared per-vessel cooldown state and hull-fill feedback for abilities that -# grant one independently recharging shot per ship. - -var cooldown_duration: float = 1.0 -var cooldown_left: float = 0.0 - - -func setup() -> void: - show_behind_parent = true - queue_redraw() - - -func is_ready() -> bool: - return cooldown_left <= 0.0 - - -func _begin_cooldown(duration: float) -> bool: - if not is_ready(): - return false - cooldown_duration = maxf(duration, 0.001) - cooldown_left = cooldown_duration - queue_redraw() - return true - - -func tick(delta: float) -> void: - if cooldown_left <= 0.0: - return - cooldown_left = maxf(cooldown_left - delta, 0.0) - queue_redraw() - - -func cooldown_progress() -> float: - return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001) - - -func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array: - if points.size() < 3 or progress <= 0.001: - return PackedVector2Array() - if progress >= 0.999: - return points.duplicate() - var top: float = INF - var bottom: float = -INF - for point in points: - top = minf(top, point.y) - bottom = maxf(bottom, point.y) - var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0)) - var clipped := PackedVector2Array() - var previous: Vector2 = points[-1] - var previous_inside: bool = previous.y >= fill_y - for current in points: - var current_inside: bool = current.y >= fill_y - if current_inside != previous_inside: - var amount: float = (fill_y - previous.y) / (current.y - previous.y) - clipped.append(previous.lerp(current, amount)) - if current_inside: - clipped.append(current) - previous = current - previous_inside = current_inside - return clipped - - -func _draw() -> void: - var ship: Node = get_parent() - if ship == null or ship.definition == null: - return - var hull: PackedVector2Array = ship.definition.hull_points - var ready_color: Color = _ready_color() - if is_ready(): - draw_colored_polygon(hull, Color(ready_color, 0.42)) - NeonDraw.draw_glowing_polygon(self, hull, ready_color, 0.75, - ship.core_color, 0.75) - return - draw_colored_polygon(hull, Color(_cooling_color(), 0.58)) - var filled: PackedVector2Array = _filled_hull(hull, cooldown_progress()) - if filled.size() >= 3: - draw_colored_polygon(filled, Color(ready_color, 0.68)) - - -func _ready_color() -> Color: return Color.WHITE -func _cooling_color() -> Color: return Color(0.2, 0.2, 0.2) diff --git a/VesselCooldownFillComponent.gd.uid b/VesselCooldownFillComponent.gd.uid deleted file mode 100644 index 2897948..0000000 --- a/VesselCooldownFillComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://drsagaj2wr0ni diff --git a/VesselDefinition.gd b/VesselDefinition.gd index f7af0a0..1aecbfb 100644 --- a/VesselDefinition.gd +++ b/VesselDefinition.gd @@ -23,10 +23,6 @@ extends Resource @export_multiline var unlock_description: String @export var unlock_cost: int @export var unlock_resource_costs: Dictionary = {} -# #63: spent from GameState.research_points, alongside unlock_resource_costs -# rather than instead of it. 0 (the default) means this unlock needs no -# research points at all. -@export var research_point_cost: int = 0 @export var unlock_icon: int = -1 diff --git a/VesselDefinitions.gd b/VesselDefinitions.gd index 33ca763..a037eb7 100644 --- a/VesselDefinitions.gd +++ b/VesselDefinitions.gd @@ -1,7 +1,7 @@ class_name VesselDefinitions extends RefCounted -const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser", "distorder"] +const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser"] const DEFINITIONS: Dictionary = { "interceptor": preload("res://InterceptorVesselDefinition.tres"), "shield": preload("res://ShieldVesselDefinition.tres"), @@ -9,7 +9,6 @@ const DEFINITIONS: Dictionary = { "transporter": preload("res://TransporterVesselDefinition.tres"), "carrier": preload("res://CarrierVesselDefinition.tres"), "laser": preload("res://LaserVesselDefinition.tres"), - "distorder": preload("res://DistorderVesselDefinition.tres"), } static func get_definition(definition_id: String) -> Resource: diff --git a/VesselDistorderComponent.gd b/VesselDistorderComponent.gd deleted file mode 100644 index 25a31d7..0000000 --- a/VesselDistorderComponent.gd +++ /dev/null @@ -1,9 +0,0 @@ -class_name VesselDistorderComponent -extends Node - -# Pure presence marker (#59) - no per-shot behaviour, no tick(). Its only -# job is to make Ship.has_capability("distorder") true, which is what -# SpeedFader.gd/Fleet.gd key off to reveal and apply the speed boost. -# Energy drain while boosted is a later ticket (#62). - -var capability_id := "distorder" diff --git a/VesselDistorderComponent.gd.uid b/VesselDistorderComponent.gd.uid deleted file mode 100644 index b9a9147..0000000 --- a/VesselDistorderComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c2una2fh0f06b diff --git a/VesselDistorderComponent.tscn b/VesselDistorderComponent.tscn deleted file mode 100644 index 862f0d3..0000000 --- a/VesselDistorderComponent.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://VesselDistorderComponent.gd" id="1"] - -[node name="DistorderComponent" type="Node"] -script = ExtResource("1") diff --git a/VesselGasBomberComponent.gd b/VesselGasBomberComponent.gd index a109188..dc44815 100644 --- a/VesselGasBomberComponent.gd +++ b/VesselGasBomberComponent.gd @@ -1,19 +1,84 @@ class_name VesselGasBomberComponent -extends "res://VesselCooldownFillComponent.gd" +extends Node2D var capability_id := "gas_bomber" +var cooldown_duration := 5.0 +var cooldown_left := 0.0 const READY_COLOR := Color(1.0, 0.25, 0.78) const COOLING_COLOR := Color(0.38, 0.16, 0.32) -func _init() -> void: - cooldown_duration = 5.0 +func setup() -> void: + # Keep the ship's normal neon outline above the charge fill. + show_behind_parent = true + queue_redraw() + + +func is_ready() -> bool: + return cooldown_left <= 0.0 func consume_shot(duration: float) -> bool: - return _begin_cooldown(duration) + if not is_ready(): + return false + cooldown_duration = maxf(duration, 0.001) + cooldown_left = cooldown_duration + queue_redraw() + return true -func _ready_color() -> Color: return READY_COLOR -func _cooling_color() -> Color: return COOLING_COLOR +func tick(delta: float) -> void: + if cooldown_left <= 0.0: + return + cooldown_left = maxf(cooldown_left - delta, 0.0) + queue_redraw() + + +func cooldown_progress() -> float: + return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001) + + +func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array: + if points.size() < 3 or progress <= 0.001: + return PackedVector2Array() + if progress >= 0.999: + return points.duplicate() + var top := INF + var bottom := -INF + for point in points: + top = minf(top, point.y) + bottom = maxf(bottom, point.y) + var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0)) + var clipped := PackedVector2Array() + var previous: Vector2 = points[-1] + var previous_inside := previous.y >= fill_y + for current in points: + var current_inside := current.y >= fill_y + if current_inside != previous_inside: + var amount: float = (fill_y - previous.y) / (current.y - previous.y) + clipped.append(previous.lerp(current, amount)) + if current_inside: + clipped.append(current) + previous = current + previous_inside = current_inside + return clipped + + +func _draw() -> void: + var parent_ship: Node = get_parent() + if parent_ship == null or parent_ship.definition == null: + return + var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE + var hull: PackedVector2Array = parent_ship.definition.hull_points + if is_ready(): + # A fully lit hull means this individual bomber owns an available shot. + draw_colored_polygon(hull, Color(READY_COLOR, 0.42)) + NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75) + return + # Rearming is shown inside the bomber instead of as a shield-like ring: + # the muted silhouette fills from its engines toward its nose. + draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58)) + var filled := _filled_hull(hull, cooldown_progress()) + if filled.size() >= 3: + draw_colored_polygon(filled, Color(READY_COLOR, 0.68)) diff --git a/VesselLaserComponent.gd b/VesselLaserComponent.gd index a004277..fb9379e 100644 --- a/VesselLaserComponent.gd +++ b/VesselLaserComponent.gd @@ -1,16 +1,29 @@ class_name VesselLaserComponent -extends "res://VesselCooldownFillComponent.gd" +extends Node2D + +# Same shape as VesselGasBomberComponent (per-ship cooldown, ready/cooling +# hull fill) - #58's own note: two laser vessels must give two shots, so +# this lives on the SHIP, not the fleet-wide LaserController. +# +# Unlike the gas bomber, damage/beam_range/cooldown are resolved from the +# vessel's own base_stats/upgrades (apply_stats(), the same pattern +# VesselWeaponComponent uses) rather than hardcoded in the controller, so +# ship-lab upgrades actually reach them. var capability_id := "laser" var damage: int = 2 var beam_range: float = 950.0 +var cooldown_duration: float = 4.0 +var cooldown_left: float = 0.0 const READY_COLOR := Color(0.3, 0.9, 1.0) const COOLING_COLOR := Color(0.08, 0.28, 0.34) -func _init() -> void: - cooldown_duration = 4.0 +func setup() -> void: + # Keep the ship's normal neon outline above the charge fill. + show_behind_parent = true + queue_redraw() func apply_stats() -> void: @@ -22,9 +35,66 @@ func apply_stats() -> void: queue_redraw() +func is_ready() -> bool: + return cooldown_left <= 0.0 + + func consume_shot() -> bool: - return _begin_cooldown(cooldown_duration) + if not is_ready(): + return false + cooldown_left = maxf(cooldown_duration, 0.001) + queue_redraw() + return true -func _ready_color() -> Color: return READY_COLOR -func _cooling_color() -> Color: return COOLING_COLOR +func tick(delta: float) -> void: + if cooldown_left <= 0.0: + return + cooldown_left = maxf(cooldown_left - delta, 0.0) + queue_redraw() + + +func cooldown_progress() -> float: + return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001) + + +func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array: + if points.size() < 3 or progress <= 0.001: + return PackedVector2Array() + if progress >= 0.999: + return points.duplicate() + var top := INF + var bottom := -INF + for point in points: + top = minf(top, point.y) + bottom = maxf(bottom, point.y) + var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0)) + var clipped := PackedVector2Array() + var previous: Vector2 = points[-1] + var previous_inside := previous.y >= fill_y + for current in points: + var current_inside := current.y >= fill_y + if current_inside != previous_inside: + var amount: float = (fill_y - previous.y) / (current.y - previous.y) + clipped.append(previous.lerp(current, amount)) + if current_inside: + clipped.append(current) + previous = current + previous_inside = current_inside + return clipped + + +func _draw() -> void: + var parent_ship: Node = get_parent() + if parent_ship == null or parent_ship.definition == null: + return + var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE + var hull: PackedVector2Array = parent_ship.definition.hull_points + if is_ready(): + draw_colored_polygon(hull, Color(READY_COLOR, 0.42)) + NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75) + return + draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58)) + var filled := _filled_hull(hull, cooldown_progress()) + if filled.size() >= 3: + draw_colored_polygon(filled, Color(READY_COLOR, 0.68)) diff --git a/VesselLaserComponent.gd.uid b/VesselLaserComponent.gd.uid deleted file mode 100644 index 40baaf0..0000000 --- a/VesselLaserComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bds5gccvd8dj8 diff --git a/VesselUpgradeDefinition.gd b/VesselUpgradeDefinition.gd index 0bc3872..083e092 100644 --- a/VesselUpgradeDefinition.gd +++ b/VesselUpgradeDefinition.gd @@ -9,7 +9,3 @@ extends Resource @export var icon: int @export var additive_modifiers: Dictionary = {} @export var multiplicative_modifiers: Dictionary = {} -# #63: spent from GameState.research_points per level bought, alongside -# base_cost (ore) rather than instead of it. 0 (the default) means this -# level needs no research points at all. -@export var research_point_cost: int = 0 diff --git a/WorldGen.gd b/WorldGen.gd index b95837d..3238051 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -22,10 +22,6 @@ const GAS_GIANT_THRESHOLD: float = 0.55 # chance independent of biome - not a second smooth region to tune against # the first. const BLACK_HOLE_CHANCE: float = 0.05 -# #63: findable without excessive exploration - research points gate mid/ -# late skill-tree nodes and vessel research, so a player needs to run into -# one of these reasonably often, not just as a rare curiosity. -const RESEARCH_LAB_CHANCE: float = 0.08 # A predictable early gas source for play-testing. The origin chunk is # deterministically normal space. Its explicit in-chunk position puts it @@ -99,8 +95,6 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: # roll doesn't shift which random values any LATER draw (planet_name, # below) consumes for chunks that would have existed before it. var black_hole_roll: float = rng.randf() - # Same principle, its own independent roll (#63). - var research_lab_roll: float = rng.randf() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE @@ -110,8 +104,6 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: # that chunk's whole purpose is to always be the same reachable source. if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE: definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID - elif not guaranteed_nearby_giant and research_lab_roll < RESEARCH_LAB_CHANCE: - definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) var names: PackedStringArray = definition.names var planet_name: String = names[rng.randi() % names.size()] diff --git a/gas_surface.gdshader.uid b/gas_surface.gdshader.uid deleted file mode 100644 index d4176be..0000000 --- a/gas_surface.gdshader.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bhxcvtw6tcshl diff --git a/tests/_black_hole_space_preview.gd.uid b/tests/_black_hole_space_preview.gd.uid deleted file mode 100644 index eb96047..0000000 --- a/tests/_black_hole_space_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://25xjfqi2qk7a diff --git a/tests/_carrier_crafter_preview.gd.uid b/tests/_carrier_crafter_preview.gd.uid deleted file mode 100644 index b6cefcc..0000000 --- a/tests/_carrier_crafter_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://f5a8wm4wr2i4 diff --git a/tests/_carrier_hangar_preview.gd.uid b/tests/_carrier_hangar_preview.gd.uid deleted file mode 100644 index 3f59a52..0000000 --- a/tests/_carrier_hangar_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b5ah2jh0i16du diff --git a/tests/_fleet_cargo_limit_preview.gd.uid b/tests/_fleet_cargo_limit_preview.gd.uid deleted file mode 100644 index 82353c4..0000000 --- a/tests/_fleet_cargo_limit_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://brt82kn8lb3u5 diff --git a/tests/_gas_burner_build_menu_preview.gd.uid b/tests/_gas_burner_build_menu_preview.gd.uid deleted file mode 100644 index 965e80c..0000000 --- a/tests/_gas_burner_build_menu_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bs6hguk0tkc6c diff --git a/tests/_gas_burner_transfer_preview.gd.uid b/tests/_gas_burner_transfer_preview.gd.uid deleted file mode 100644 index b665754..0000000 --- a/tests/_gas_burner_transfer_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://fd1m4excjdbn diff --git a/tests/_laser_aim_preview.gd.uid b/tests/_laser_aim_preview.gd.uid deleted file mode 100644 index 9064f70..0000000 --- a/tests/_laser_aim_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c0q640rosb53o diff --git a/tests/_mothership_beam_preview.gd.uid b/tests/_mothership_beam_preview.gd.uid deleted file mode 100644 index def6f77..0000000 --- a/tests/_mothership_beam_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c8lgxg63d8s52 diff --git a/tests/_resource_readout_surface_preview.gd.uid b/tests/_resource_readout_surface_preview.gd.uid deleted file mode 100644 index 27eb816..0000000 --- a/tests/_resource_readout_surface_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cy6n62kcp7q1y diff --git a/tests/_rocky_surface_preview.gd.uid b/tests/_rocky_surface_preview.gd.uid deleted file mode 100644 index 09a037c..0000000 --- a/tests/_rocky_surface_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c0yjf3qdv31eo diff --git a/tests/_skill_menu_preview.gd b/tests/_skill_menu_preview.gd index 6999bdf..96d4581 100644 --- a/tests/_skill_menu_preview.gd +++ b/tests/_skill_menu_preview.gd @@ -8,13 +8,7 @@ extends Node2D # ... and again at 720x960 to check a shorter screen format. -var configured: bool = false - - -func _process(_delta: float) -> void: - if configured: - return - configured = true +func _ready() -> void: GameState.buildings.clear() GameState.extractors.clear() GameState.transporters.clear() @@ -22,12 +16,9 @@ func _process(_delta: float) -> void: GameState.gas = 36 GameState.fleet_cargo = 24 GameState.fleet_gas_cargo = 12 - GameState.research_points = 0 - GameState.unlocked = ["shipyards"] + GameState.unlocked = ["shipyards", "logistics", "freight"] var main: Node = load("res://Main.tscn").instantiate() add_child(main) - main.get_node("EnemySpawner").set_process(false) - main.get_node("AsteroidField").set_process(false) main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0)) var mothership_id: int = int(GameState.buildings[-1].id) var bc: Node = main.get_node("BuildController") @@ -36,5 +27,5 @@ func _process(_delta: float) -> void: main.get_node("UI/SkillMenu").visible = true # Select a node so the description, price and status all have text - an # empty description cannot collide with anything. - bc.skill_tree.selected_id = "coordination" - bc._refresh_skill_menu("coordination") + bc.skill_tree.selected_id = "freight" + bc._refresh_skill_menu("freight") diff --git a/tests/_turret_vs_chaser_preview.gd.uid b/tests/_turret_vs_chaser_preview.gd.uid deleted file mode 100644 index f71bd01..0000000 --- a/tests/_turret_vs_chaser_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dfi84x2tcwqwy diff --git a/tests/_void_surface_preview.gd.uid b/tests/_void_surface_preview.gd.uid deleted file mode 100644 index 0e500ff..0000000 --- a/tests/_void_surface_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://clf1bitxms0ti diff --git a/tests/test_black_hole.gd.uid b/tests/test_black_hole.gd.uid deleted file mode 100644 index 75397b6..0000000 --- a/tests/test_black_hole.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://vlcbg3juhpy3 diff --git a/tests/test_carrier_vessel.gd b/tests/test_carrier_vessel.gd index a5deec4..adf159b 100644 --- a/tests/test_carrier_vessel.gd +++ b/tests/test_carrier_vessel.gd @@ -48,7 +48,6 @@ func _initialize() -> void: game_state.reset_progression() game_state.build_time_scale = 0.02 game_state.rocks = 500 - game_state.research_points = 1 scene = load("res://Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") diff --git a/tests/test_carrier_vessel.gd.uid b/tests/test_carrier_vessel.gd.uid deleted file mode 100644 index 5af74b7..0000000 --- a/tests/test_carrier_vessel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gj02xu88lxpm diff --git a/tests/test_distorder_energy.gd b/tests/test_distorder_energy.gd deleted file mode 100644 index ea73206..0000000 --- a/tests/test_distorder_energy.gd +++ /dev/null @@ -1,150 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_distorder_energy.gd -# Forgejo #62: sustaining a boost above the fleet's default speed drains -# energy from the fleet hold, continuously, proportional to how far above -# 1.0x the multiplier is. -# -# Most of this is pure GameState math (GameState._advance_distorder_drain, -# called directly the same way other tests call _advance_lab_research/ -# _advance_production) - no Main.tscn/Fleet needed for it, which is itself -# proof the drain is scene-independent (it lives in the autoload's own -# _process(), unlike a component's tick() which only runs while its ship's -# scene is loaded) and so "continues correctly across a landing and -# return" without a scene-swap simulation to re-prove that. -# -# Covers: -# A. sustained boost drains energy at the configured rate; half boost -# drains proportionally less than full boost -# B. a small multiplier still drains a nonzero amount over time - no -# fractional rounding loss -# D. at exactly 1.0x, energy is never consumed -# C. hitting zero energy forces the multiplier back to 1.0, and the -# SpeedFader visibly follows it down with the reason shown - -var game_state: Node -var main: Node -var fleet: Node -var fader: Node -var stage: int = 0 -var elapsed: float = 0.0 - - -func _initialize() -> void: - game_state = root.get_node("GameState") - game_state.reset_progression() - game_state.fleet_speed_multiplier = 1.0 - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 1000) - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - # --- D. nothing drains at exactly 1.0x ---------------------------- - var before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) - for i in range(60): - game_state._advance_distorder_drain(1.0) - if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != before: - return _fail_now("energy drained at 1.0x: %d -> %d" \ - % [before, game_state.fleet_resource(GameState.RESOURCE_ENERGY)]) - print("no drain at 1.0x, even over 60 simulated seconds") - - # --- A. full boost drains at the configured rate ------------------ - game_state.fleet_speed_multiplier = GameState.MAX_FLEET_SPEED_MULTIPLIER - var full_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) - for i in range(10): - game_state._advance_distorder_drain(1.0) - var full_drained: int = full_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY) - if full_drained != 20: - return _fail_now("full boost drained %d over 10s, expected 20 (ENERGY_DRAIN_PER_SECOND=2.0)" \ - % full_drained) - print("full boost drained %d over 10s, matches ENERGY_DRAIN_PER_SECOND" % full_drained) - - # --- A continued: half boost drains proportionally less ----------- - game_state.fleet_speed_multiplier = 2.0 - var half_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) - for i in range(10): - game_state._advance_distorder_drain(1.0) - var half_drained: int = half_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY) - if half_drained <= 0 or half_drained >= full_drained: - return _fail_now("half boost drained %d over 10s, expected between 0 and full's %d" \ - % [half_drained, full_drained]) - print("half boost drained %d over 10s - less than full's %d, still nonzero" \ - % [half_drained, full_drained]) - - # --- B. a small multiplier still drains a nonzero amount ---------- - game_state.fleet_speed_multiplier = 1.05 - var small_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) - for i in range(600): - game_state._advance_distorder_drain(0.1) - var small_drained: int = small_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY) - if small_drained <= 0: - return _fail_now("a small (1.05x) sustained boost drained nothing over 60s - fractional rounding loss") - print("small boost (1.05x) still drained %d over 60s (no rounding loss)" % small_drained) - - # --- C. running out resets the multiplier and raises the notice --- - game_state.withdraw_fleet_resource(GameState.RESOURCE_ENERGY, - game_state.fleet_resource(GameState.RESOURCE_ENERGY)) - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 5) - game_state.fleet_speed_multiplier = GameState.MAX_FLEET_SPEED_MULTIPLIER - game_state.boost_depleted_notice = false - for i in range(10): - game_state._advance_distorder_drain(1.0) - if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != 0: - return _fail_now("fleet energy did not reach exactly 0, got %d" \ - % game_state.fleet_resource(GameState.RESOURCE_ENERGY)) - if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0): - return _fail_now("multiplier was not forced back to 1.0 after running out: %f" \ - % game_state.fleet_speed_multiplier) - if not game_state.boost_depleted_notice: - return _fail_now("no depleted notice was raised when energy ran out") - print("running out of energy dropped the multiplier to 1.0 and raised the notice") - - stage = 1 - elapsed = 0.0 - return false - 1: - # --- C continued: the SpeedFader visibly follows it down ---------- - if elapsed < 0.2: - return false - main = load("res://Main.tscn").instantiate() - root.add_child(main) - fleet = main.get_node("Fleet") - fader = main.get_node("UI/SpeedFader") - fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_DISTORDER])) - fader._process(0.0) - if not fader.visible: - return _fail_now("fader hidden with a distorder ship in the fleet") - - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 4) - game_state.set_fleet_speed_multiplier(GameState.MAX_FLEET_SPEED_MULTIPLIER) - fader._process(0.0) - if fader.knob_t < 0.9: - return _fail_now("fader knob did not reflect the boosted multiplier before draining: %f" \ - % fader.knob_t) - - for i in range(10): - game_state._advance_distorder_drain(1.0) - if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0): - return _fail_now("multiplier was not forced back to 1.0 after running out (with a live fleet)") - fader._process(0.0) - if fader.knob_t > 0.05: - return _fail_now("fader knob did not snap down after the boost was depleted: %f" % fader.knob_t) - if fader.label.text != "Out of energy!": - return _fail_now("fader did not show the depleted reason, text was '%s'" % fader.label.text) - if game_state.boost_depleted_notice: - return _fail_now("boost_depleted_notice was not consumed by the fader") - print("fader visibly snapped its knob down and explained the depletion") - - print("TEST_DISTORDER_ENERGY: PASS") - quit(0) - return true - return false - - -func _fail_now(message: String) -> bool: - print("TEST_DISTORDER_ENERGY: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_distorder_energy.gd.uid b/tests/test_distorder_energy.gd.uid deleted file mode 100644 index e954283..0000000 --- a/tests/test_distorder_energy.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bistn7dgiv7lw diff --git a/tests/test_enemy_mothership.gd.uid b/tests/test_enemy_mothership.gd.uid deleted file mode 100644 index 9679052..0000000 --- a/tests/test_enemy_mothership.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c18rxosfduxoa diff --git a/tests/test_extractor_income.gd b/tests/test_extractor_income.gd index f72ac4d..b89037e 100644 --- a/tests/test_extractor_income.gd +++ b/tests/test_extractor_income.gd @@ -21,7 +21,6 @@ func _initialize() -> void: game_state.extractors.clear() game_state.transporters.clear() game_state.reset_progression() - game_state.research_points = 1 game_state.add_extractor("Test Planet", EXTRACTOR_AT) extractor_index = 0 diff --git a/tests/test_fleet_cargo_limit.gd.uid b/tests/test_fleet_cargo_limit.gd.uid deleted file mode 100644 index 58069e9..0000000 --- a/tests/test_fleet_cargo_limit.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b1fyqy5i8mqf0 diff --git a/tests/test_gas_bomber.gd b/tests/test_gas_bomber.gd index 8ba50e7..cda2a2f 100644 --- a/tests/test_gas_bomber.gd +++ b/tests/test_gas_bomber.gd @@ -3,10 +3,7 @@ extends SceneTree var game_state: Node var main: Node var fleet: Node -var skill: Node -# Mirrors GasBombController.AOE_RADIUS's value rather than referencing the -# class constant directly - see test_gas_bomber_cooldowns.gd's identical fix. -const AOE_RADIUS_FOR_TEST: float = 180.0 +var skill: GasBombController var inside_enemies: Array = [] var outside_enemy: Node var stage := 0 @@ -27,10 +24,6 @@ func _initialize() -> void: game_state.fleet_cargo = 0 game_state.fleet_gas_cargo = 0 game_state.build_time_scale = 0.01 - game_state.research_points = 1 - # #60: after research/crafting resets the fleet hold below, top it up - # with fleet-carried gas for the shot itself (ammunition, not the ore/gas - # spent researching or crafting the bomber). # Research is gas-only: any amount of ore and 29 gas must still leave it # waiting, then the thirtieth unit starts and completes the unlock. @@ -76,7 +69,6 @@ func _initialize() -> void: _fail_now("crafting did not produce a typed Gas Bomber") return - game_state.fleet_gas_cargo = 20 main = load("res://Main.tscn").instantiate() root.add_child(main) fleet = main.get_node("Fleet") @@ -125,7 +117,7 @@ func _process(delta: float) -> bool: enemy.speed = 0.0 inside_enemies.append(enemy) outside_enemy = load("res://Enemy.tscn").instantiate() - outside_enemy.position = skill.aim_target + Vector2(AOE_RADIUS_FOR_TEST + 80.0, 0) + outside_enemy.position = skill.aim_target + Vector2(GasBombController.AOE_RADIUS + 80.0, 0) main.add_child(outside_enemy) outside_enemy.speed = 0.0 diff --git a/tests/test_gas_bomber_ammo.gd b/tests/test_gas_bomber_ammo.gd deleted file mode 100644 index 54aa122..0000000 --- a/tests/test_gas_bomber_ammo.gd +++ /dev/null @@ -1,142 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_gas_bomber_ammo.gd -# Forgejo #60: gas bomb shots draw ammunition from the FLEET HOLD -# (GameState.fleet_inventory), not the bank - the fleet has to be carrying -# its own gas. -# -# Covers: -# A. gas sitting in the bank does not enable firing - only the fleet -# hold counts -# B. with insufficient fleet gas the button is disabled, pressing it -# explains why, and no projectile is created / no cooldown consumed -# C. firing drains exactly GAS_PER_SHOT from the fleet hold and nothing -# from the bank - -var game_state: Node -var main: Node -var fleet: Node -var skill: Node -var bomber_component: Node -var stage: int = 0 -var elapsed: float = 0.0 - -# Mirrors GasBombController.GAS_PER_SHOT's value rather than referencing -# the class constant directly - a bare ClassName.CONST reference elsewhere -# in a test -# script has been observed to corrupt method resolution for the WHOLE -# script (an unrelated skill._process() call earlier in the same file then -# fails with "Nonexistent function '_process' in base 'Nil'"), root cause -# unconfirmed. See tests/test_gas_bomber_cooldowns.gd's identical fix. -const GAS_PER_SHOT: int = 5 - - -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.reset_progression() - game_state.vessel_levels["unlock_gas_bomber"] = 1 - game_state.rocks = 0 - game_state.gas = 0 - game_state.fleet_gas_cargo = 0 - main = load("res://Main.tscn").instantiate() - root.add_child(main) - fleet = main.get_node("Fleet") - skill = main.get_node("GasBombController") - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - if elapsed < 0.2: - return false - main.get_node("EnemySpawner").set_process(false) - main.get_node("AsteroidField").set_process(false) - # An interceptor rides along purely for its "cargo" capability - # (#56): a fleet made only of a gas bomber has zero cargo - # capacity, which would silently clamp every fleet_gas_cargo - # assignment below to 0. - fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([ - GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_GAS_BOMBER])) - bomber_component = fleet.ships[1].get_component("gas_bomber") - skill._process(0.0) - - # --- A. bank gas does not count ----------------------------------- - game_state.gas = 500 - game_state.fleet_gas_cargo = 0 - skill._process(0.0) - if skill.button.enabled: - return _fail_now("500 banked gas (0 in the fleet hold) left the control enabled") - print("500 banked gas, 0 fleet gas: control correctly disabled") - - # --- B. insufficient fleet gas -------------------------------------- - game_state.fleet_gas_cargo = GAS_PER_SHOT - 1 - skill._process(0.0) - if skill.button.enabled: - return _fail_now("fleet gas one short of GAS_PER_SHOT left the control enabled") - - var press := InputEventScreenTouch.new() - press.index = 3 - press.position = skill.button.global_position + skill.button.size * 0.5 - press.pressed = true - main._handle_touch(press) - if skill.aiming: - return _fail_now("aiming began despite insufficient fleet gas") - if skill.status.text == "": - return _fail_now("an unaffordable press did not explain itself") - print("insufficient fleet gas: disabled, press explains itself, no aiming began") - if not bomber_component.is_ready(): - return _fail_now("a refused press consumed the bomber's cooldown") - if game_state.fleet_resource(GameState.RESOURCE_GAS) != GAS_PER_SHOT - 1: - return _fail_now("a refused press spent fleet gas anyway") - print("no cooldown consumed, no gas spent on a refused press") - - # --- C. exact ammo cost on a real shot ------------------------------ - game_state.fleet_gas_cargo = GAS_PER_SHOT + 7 - skill._process(0.0) - if not skill.button.enabled: - return _fail_now("enough fleet gas still left the control disabled") - - var press2 := InputEventScreenTouch.new() - press2.index = 4 - press2.position = skill.button.global_position + skill.button.size * 0.5 - press2.pressed = true - main._handle_touch(press2) - if not skill.aiming: - return _fail_now("an affordable press did not begin aiming") - var drag := InputEventScreenDrag.new() - drag.index = 4 - drag.position = press2.position + Vector2(-300.0, 0.0) - main._handle_drag(drag) - var release := InputEventScreenTouch.new() - release.index = 4 - release.position = drag.position - release.pressed = false - main._handle_touch(release) - - if skill.projectiles.size() != 1: - return _fail_now("a paid-for shot did not launch a projectile") - if bomber_component.is_ready(): - return _fail_now("firing did not consume the bomber's cooldown") - if game_state.fleet_resource(GameState.RESOURCE_GAS) != 7: - return _fail_now("fleet gas after firing is %d, expected 7 (paid exactly %d)" \ - % [game_state.fleet_resource(GameState.RESOURCE_GAS), GAS_PER_SHOT]) - if game_state.gas != 500: - return _fail_now("firing touched the bank (%d), expected it untouched at 500" % game_state.gas) - print("firing drained exactly %d from the fleet hold, bank untouched" % GAS_PER_SHOT) - - print("TEST_GAS_BOMBER_AMMO: PASS") - quit(0) - return true - return false - - -func _fail_now(message: String) -> bool: - print("TEST_GAS_BOMBER_AMMO: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_gas_bomber_ammo.gd.uid b/tests/test_gas_bomber_ammo.gd.uid deleted file mode 100644 index c31651b..0000000 --- a/tests/test_gas_bomber_ammo.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cwcw77qvykk35 diff --git a/tests/test_gas_bomber_cooldowns.gd b/tests/test_gas_bomber_cooldowns.gd index 968d0da..2a25403 100644 --- a/tests/test_gas_bomber_cooldowns.gd +++ b/tests/test_gas_bomber_cooldowns.gd @@ -2,7 +2,7 @@ extends SceneTree var main: Node var fleet: Node -var skill: Node +var skill: GasBombController var elapsed := 0.0 @@ -12,8 +12,6 @@ func _initialize() -> void: state.extractors.clear() state.transporters.clear() state.vessel_levels["unlock_gas_bomber"] = 1 - # #60: shots draw from the fleet hold now - stock enough for both fires. - state.fleet_gas_cargo = 20 main = load("res://Main.tscn").instantiate() root.add_child(main) fleet = main.get_node("Fleet") @@ -55,7 +53,7 @@ func _process(delta: float) -> bool: # Rearming one bomber restores one available shot without touching the # other bomber's cooldown. - first.tick(5.0) # matches GasBombController.COOLDOWN + first.tick(GasBombController.COOLDOWN) skill._process(0.0) if not first.is_ready() or second.is_ready() or not skill.button.enabled: return _fail("one completed cooldown did not restore exactly one shot") diff --git a/tests/test_gas_bomber_cooldowns.gd.uid b/tests/test_gas_bomber_cooldowns.gd.uid index 41c349e..d03a550 100644 --- a/tests/test_gas_bomber_cooldowns.gd.uid +++ b/tests/test_gas_bomber_cooldowns.gd.uid @@ -1 +1 @@ -uid://tguuracp1ewt +uid://cwo6ake5taias diff --git a/tests/test_gas_burner.gd b/tests/test_gas_burner.gd index 4b97dc5..4dfffd0 100644 --- a/tests/test_gas_burner.gd +++ b/tests/test_gas_burner.gd @@ -30,8 +30,6 @@ var hub_id: int = 0 var burner_id: int = 0 var load_limit: int = 0 var energy_before_stall: int = 0 -var conversion_rate: float = 0.0 -var conversion_yield: int = 0 const MOTHERSHIP_AT := Vector2.ZERO const HUB_AT := Vector2(150.0, 0.0) @@ -50,9 +48,6 @@ func _initialize() -> void: mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT) burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT) game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20) - var logistics: Resource = game_state.building_logistics_profile(game_state.find_building(burner_id)) - conversion_rate = logistics.conversion_rate - conversion_yield = logistics.conversion_yield func _process(delta: float) -> bool: @@ -78,10 +73,10 @@ func _process(delta: float) -> bool: var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY) var gas_burned: int = 20 - gas_left if gas_burned < 2: - return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, conversion_rate]) - if energy_made != gas_burned * conversion_yield: + return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, GameState.GAS_BURN_RATE]) + if energy_made != gas_burned * GameState.ENERGY_PER_GAS: return _fail("burner made %d energy for %d gas burned, expected %dx" % [ - energy_made, gas_burned, conversion_yield + energy_made, gas_burned, GameState.ENERGY_PER_GAS ]) if game_state.rocks != 0 or game_state.gas != 0: return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [ diff --git a/tests/test_gas_burner.gd.uid b/tests/test_gas_burner.gd.uid deleted file mode 100644 index 0188f66..0000000 --- a/tests/test_gas_burner.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dlx2eu6lw3ps6 diff --git a/tests/test_hub_capacity.gd b/tests/test_hub_capacity.gd index d9005de..0bc68ae 100644 --- a/tests/test_hub_capacity.gd +++ b/tests/test_hub_capacity.gd @@ -12,7 +12,6 @@ func _initialize() -> void: state.transporters.clear() state.reset_progression() state.rocks = 900 - state.research_points = state.skill_research_point_cost("freight_yards") state.unlocked = ["shipyards", "logistics", "freight"] var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0)) diff --git a/tests/test_lab_resources.gd b/tests/test_lab_resources.gd index 57eabcb..e4cbeb7 100644 --- a/tests/test_lab_resources.gd +++ b/tests/test_lab_resources.gd @@ -10,7 +10,6 @@ func _initialize() -> void: game.vessel_levels.clear() game.build_time_scale = 0.02 game.rocks = 500 - game.research_points = 1 lab_id = game.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO) if not game.queue_lab_research(lab_id, "int_damage"): _fail("could not queue research") diff --git a/tests/test_lab_transporter_delivery.gd b/tests/test_lab_transporter_delivery.gd index 6c1fe94..dff33b2 100644 --- a/tests/test_lab_transporter_delivery.gd +++ b/tests/test_lab_transporter_delivery.gd @@ -12,7 +12,6 @@ func _initialize() -> void: state.transporters.clear() state.reset_progression() state.rocks = 100 - state.research_points = 1 state.build_time_scale = 0.02 state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO) state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0)) diff --git a/tests/test_laser_ammo.gd b/tests/test_laser_ammo.gd deleted file mode 100644 index a38b9ea..0000000 --- a/tests/test_laser_ammo.gd +++ /dev/null @@ -1,141 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_laser_ammo.gd -# Forgejo #61: laser shots draw ammunition from the FLEET HOLD -# (GameState.fleet_inventory), the same way gas bomb shots do (#60) - the -# fleet has to be carrying its own energy. -# -# Covers: -# A. banked energy does not enable firing - only the fleet hold counts -# B. with insufficient fleet energy the button is disabled, pressing it -# explains why, and no beam is fired / no cooldown consumed -# C. firing drains exactly ENERGY_PER_SHOT from the fleet hold and -# nothing from the bank - -var game_state: Node -var main: Node -var fleet: Node -var skill: Node -var laser_component: Node -var stage: int = 0 -var elapsed: float = 0.0 - -# Mirrors LaserController.ENERGY_PER_SHOT's value rather than referencing -# the class constant directly - a bare ClassName.CONST reference elsewhere -# in a test script has been observed to corrupt method resolution for the -# WHOLE script (an unrelated skill._process() call earlier in the same -# file then fails with "Nonexistent function '_process' in base 'Nil'"), -# root cause unconfirmed. See tests/test_gas_bomber_cooldowns.gd's -# identical fix (#60). -const ENERGY_PER_SHOT: int = 8 - - -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.reset_progression() - game_state.vessel_levels["unlock_laser"] = 1 - game_state.rocks = 0 - game_state.gas = 0 - game_state.fleet_gas_cargo = 0 - main = load("res://Main.tscn").instantiate() - root.add_child(main) - fleet = main.get_node("Fleet") - skill = main.get_node("LaserController") - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - if elapsed < 0.2: - return false - main.get_node("EnemySpawner").set_process(false) - main.get_node("AsteroidField").set_process(false) - # An interceptor rides along purely for its "cargo" capability - # (#56): a fleet made only of a laser ship has zero cargo - # capacity, which would silently clamp every fleet energy - # deposit below to 0. - fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([ - GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_LASER])) - laser_component = fleet.ships[1].get_component("laser") - skill._process(0.0) - - # --- A. bank energy does not count ---------------------------- - game_state.deposit_bank_resource(GameState.RESOURCE_ENERGY, 500) - skill._process(0.0) - if skill.button.enabled: - return _fail_now("500 banked energy (0 in the fleet hold) left the control enabled") - print("500 banked energy, 0 fleet energy: control correctly disabled") - - # --- B. insufficient fleet energy ------------------------------- - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, ENERGY_PER_SHOT - 1) - skill._process(0.0) - if skill.button.enabled: - return _fail_now("fleet energy one short of ENERGY_PER_SHOT left the control enabled") - - var press := InputEventScreenTouch.new() - press.index = 3 - press.position = skill.button.global_position + skill.button.size * 0.5 - press.pressed = true - main._handle_touch(press) - if skill.aiming: - return _fail_now("aiming began despite insufficient fleet energy") - if skill.status.text == "": - return _fail_now("an unaffordable press did not explain itself") - print("insufficient fleet energy: disabled, press explains itself, no aiming began") - if not laser_component.is_ready(): - return _fail_now("a refused press consumed the laser ship's cooldown") - if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != ENERGY_PER_SHOT - 1: - return _fail_now("a refused press spent fleet energy anyway") - print("no cooldown consumed, no energy spent on a refused press") - - # --- C. exact ammo cost on a real shot ----------------------------- - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 8) - skill._process(0.0) - if not skill.button.enabled: - return _fail_now("enough fleet energy still left the control disabled") - var fleet_energy_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) - - var press2 := InputEventScreenTouch.new() - press2.index = 4 - press2.position = skill.button.global_position + skill.button.size * 0.5 - press2.pressed = true - main._handle_touch(press2) - if not skill.aiming: - return _fail_now("an affordable press did not begin aiming") - var drag := InputEventScreenDrag.new() - drag.index = 4 - drag.position = press2.position + Vector2(-300.0, 0.0) - main._handle_drag(drag) - var release := InputEventScreenTouch.new() - release.index = 4 - release.position = drag.position - release.pressed = false - main._handle_touch(release) - - if laser_component.is_ready(): - return _fail_now("firing did not consume the laser ship's cooldown") - var fleet_energy_after: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY) - if fleet_energy_before - fleet_energy_after != ENERGY_PER_SHOT: - return _fail_now("firing spent %d fleet energy, expected exactly %d" \ - % [fleet_energy_before - fleet_energy_after, ENERGY_PER_SHOT]) - if game_state.banked_resource(GameState.RESOURCE_ENERGY) != 500: - return _fail_now("firing touched the bank (%d), expected it untouched at 500" \ - % game_state.banked_resource(GameState.RESOURCE_ENERGY)) - print("firing drained exactly %d from the fleet hold, bank untouched" % ENERGY_PER_SHOT) - - print("TEST_LASER_AMMO: PASS") - quit(0) - return true - return false - - -func _fail_now(message: String) -> bool: - print("TEST_LASER_AMMO: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_laser_ammo.gd.uid b/tests/test_laser_ammo.gd.uid deleted file mode 100644 index c314dea..0000000 --- a/tests/test_laser_ammo.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d24p5etger56g diff --git a/tests/test_laser_vessel.gd b/tests/test_laser_vessel.gd index 03819d4..4c3001e 100644 --- a/tests/test_laser_vessel.gd +++ b/tests/test_laser_vessel.gd @@ -20,12 +20,7 @@ extends SceneTree var game_state: Node var main: Node var fleet: Node -var skill: Node -# Mirrors LaserController.BEAM_HALF_WIDTH's value rather than referencing -# the class constant directly - a bare ClassName.CONST reference elsewhere -# in a test script has been observed to corrupt method resolution for the -# WHOLE script. See tests/test_gas_bomber_cooldowns.gd's identical fix (#60). -const BEAM_HALF_WIDTH: float = 22.0 +var skill: LaserController var on_line_enemies: Array = [] var off_line_enemy: Node var beyond_range_enemy: Node @@ -47,13 +42,6 @@ func _initialize() -> void: # this ticket is about the laser's own behaviour, not re-proving the # ship-lab research flow (already covered generically elsewhere). game_state.vessel_levels["unlock_laser"] = 1 - # #61: shots draw energy from the fleet hold now - stock enough for two - # fires. Set BEFORE the first spawn_ships() call (inside Main.tscn's own - # _ready(), triggered by instantiate() below): the fleet hold has no - # capacity cap yet at this point (#56's cap is only computed once a - # fleet composition exists), so this deposit can't be clamped by the - # soon-to-exist all-laser fleet having zero "cargo" capability. - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 20) main = load("res://Main.tscn").instantiate() root.add_child(main) fleet = main.get_node("Fleet") @@ -118,7 +106,7 @@ func _process(delta: float) -> bool: off_line_enemy = load("res://Enemy.tscn").instantiate() off_line_enemy.position = first_shooter.global_position \ - + Vector2(200.0, BEAM_HALF_WIDTH + 60.0) + + Vector2(200.0, LaserController.BEAM_HALF_WIDTH + 60.0) main.add_child(off_line_enemy) off_line_enemy.speed = 0.0 diff --git a/tests/test_laser_vessel.gd.uid b/tests/test_laser_vessel.gd.uid deleted file mode 100644 index df5f9c4..0000000 --- a/tests/test_laser_vessel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dp0e71h2124c8 diff --git a/tests/test_multi_resource_supply.gd b/tests/test_multi_resource_supply.gd index ecf956c..d29b574 100644 --- a/tests/test_multi_resource_supply.gd +++ b/tests/test_multi_resource_supply.gd @@ -10,7 +10,6 @@ func _initialize() -> void: state.reset_progression() state.rocks = 0 state.gas = 10 - state.research_points = 1 state.build_time_scale = 0.01 state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) diff --git a/tests/test_research_points.gd b/tests/test_research_points.gd deleted file mode 100644 index 72adc94..0000000 --- a/tests/test_research_points.gd +++ /dev/null @@ -1,143 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_research_points.gd -# Forgejo #63: a rare enemy planet type that yields no resources but grants -# one research point when conquered - a required currency for mid/late -# skill-tree nodes and vessel/lab research, gated alongside their existing -# ore/resource costs. -# -# Covers: -# A. research labs generate deterministically and are rare -# (structural) the definition yields no resources and its surface has no -# resource provider at all -# F. a fresh game can still reach a ship crafter and an extractor with -# zero research points -# D. a research-point-gated skill node is refused while unaffordable, and -# explains why -# E. conquering a research_lab awards exactly one point, once (guarded -# even against a redundant re-mark), emits the signal once, and a -# non-research_lab planet's conquest never awards one -# D continued: unlocking spends the points and the balance updates -# G. the ship-lab research path gates and spends research points the -# same way, independent of the skill tree - -var game_state: Node - - -func _initialize() -> void: - game_state = root.get_node("GameState") - - # --- A. deterministic and rare ------------------------------------------- - var lab_count: int = 0 - var total_planets: int = 0 - for x in range(-40, 40): - for y in range(-40, 40): - var chunk: Vector2i = Vector2i(x, y) - if not WorldGen.has_planet_at_chunk(chunk): - continue - total_planets += 1 - var first: Dictionary = WorldGen.planet_data_at_chunk(chunk) - var second: Dictionary = WorldGen.planet_data_at_chunk(chunk) - if first.definition_id != second.definition_id or first.local_pos != second.local_pos: - return _fail("planet_data_at_chunk(%s) is not deterministic" % chunk) - if first.definition_id == "research_lab": - lab_count += 1 - print("scanned %d planet chunks: %d research labs" % [total_planets, lab_count]) - if lab_count == 0: - return _fail("no research labs generated across a wide scan - the rare outcome never fires") - if lab_count * 3 >= total_planets: - return _fail("research labs (%d of %d) are not rare" % [lab_count, total_planets]) - print("research labs are deterministic and rare (%d of %d planet chunks)" % [lab_count, total_planets]) - - # --- structural: yields nothing at all ------------------------------------ - var lab_def: Resource = load("res://PlanetDefinitions.gd").get_definition("research_lab") - if String(lab_def.resource_id) != "": - return _fail("research_lab planet's resource_id is '%s', expected empty" % lab_def.resource_id) - var profile: Resource = load("res://PlanetSurfaceProfiles.gd").get_profile("research_lab") - if profile.features_scene != null: - return _fail("research_lab surface profile has a features_scene - it must yield nothing") - print("research_lab yields no resource id and no surface feature provider") - - # --- F. a fresh game still reaches a crafter and an extractor ------------- - game_state.reset_new_game() - if not game_state.can_unlock("shipyards"): - return _fail("shipyards is not reachable with 0 research points") - game_state.unlock("shipyards") - if not game_state.can_unlock("logistics"): - return _fail("logistics is not reachable with 0 research points after shipyards") - game_state.unlock("logistics") - print("fresh game reaches a ship crafter (shipyards) and an extractor (logistics) at 0 research points") - - # --- D. a points-gated node is refused and explains why ------------------- - if game_state.can_unlock("coordination"): - return _fail("coordination (costs research points) was unlockable with 0 research points") - print("a research-point-gated node (coordination) correctly refused at 0 points") - - # --- E. earning exactly one point, once, with the signal ------------------ - var totals_seen: Array = [] - game_state.research_points_changed.connect(func(total): totals_seen.append(total)) - game_state.planet_capture.clear() - game_state.register_capture_points("test_lab_planet", 2, "research_lab") - if game_state.is_planet_conquered("test_lab_planet"): - return _fail("a lab with 2 real capture points started conquered") - game_state.mark_point_captured("test_lab_planet", 0) - if game_state.research_points != 0: - return _fail("a research point was awarded before every point was captured") - game_state.mark_point_captured("test_lab_planet", 1) - if game_state.research_points != 1: - return _fail("conquering a research_lab did not award exactly 1 point: %d" % game_state.research_points) - if totals_seen != [1]: - return _fail("research_points_changed did not fire exactly once with the new total: %s" % [totals_seen]) - print("conquering a research_lab awarded exactly 1 point and fired the signal once") - - game_state.mark_point_captured("test_lab_planet", 0) # already captured - redundant re-mark - if game_state.research_points != 1: - return _fail("re-marking an already-captured point re-awarded a research point: %d" % game_state.research_points) - print("double-award guarded against") - - game_state.register_capture_points("test_rocky_planet", 1, "rocky") - game_state.mark_point_captured("test_rocky_planet", 0) - if game_state.research_points != 1: - return _fail("conquering a non-research_lab planet awarded a research point: %d" % game_state.research_points) - print("a non-research_lab planet's conquest never awards a point") - - # --- D continued: unlocking spends the points, balance updates ------------ - if not game_state.can_unlock("coordination"): - return _fail("coordination is still not unlockable with 1 research point and enough ore") - if not game_state.unlock("coordination"): - return _fail("unlock(coordination) failed despite can_unlock() being true") - if game_state.research_points != 0: - return _fail("unlocking a 1-point node did not spend the point: %d remaining" % game_state.research_points) - print("unlocking a research-point-gated skill node spent the point, balance now 0") - - # --- G. the ship-lab research path gates and spends the same currency ----- - var carrier_def: Resource = game_state.vessel_definition(GameState.VESSEL_CARRIER) - var point_cost: int = int(carrier_def.research_point_cost) - if point_cost <= 0: - return _fail("carrier production data has no research-point cost") - game_state.vessel_levels.erase(carrier_def.unlock_id) - var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO) - if not game_state.queue_lab_research(lab_id, carrier_def.unlock_id): - return _fail("could not queue carrier research") - game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, 1000) - game_state._advance_lab_research(100.0) - if game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER): - return _fail("carrier research completed despite 0 research points") - print("lab research correctly stalled on a research-point shortfall") - - game_state.research_points = point_cost - game_state._advance_lab_research(100.0) - if not game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER): - return _fail("carrier research did not complete once research points were available") - if game_state.research_points != 0: - return _fail("lab research did not spend the research points: %d remaining" % game_state.research_points) - print("lab research spent the research points once affordable, and completed") - - print("TEST_RESEARCH_POINTS: PASS") - quit(0) - - -func _fail(message: String) -> void: - print("TEST_RESEARCH_POINTS: FAIL (%s)" % message) - quit(1) diff --git a/tests/test_research_points.gd.uid b/tests/test_research_points.gd.uid deleted file mode 100644 index fe90658..0000000 --- a/tests/test_research_points.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://x08iyhs2lrrr diff --git a/tests/test_resource_readout.gd.uid b/tests/test_resource_readout.gd.uid deleted file mode 100644 index 37f0f14..0000000 --- a/tests/test_resource_readout.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bhad0657luiec diff --git a/tests/test_ship_lab.gd b/tests/test_ship_lab.gd index 1342cce..df2c168 100644 --- a/tests/test_ship_lab.gd +++ b/tests/test_ship_lab.gd @@ -81,7 +81,6 @@ func _process(delta: float) -> bool: print("no lab -> no upgrades") game_state.unlocked = ["shipyards", "vessel_science", "coordination", "wing", "squadron"] - game_state.research_points = 10 field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0)) field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(700.0, 0.0)) crafter_id = int(game_state.buildings[-1].id) diff --git a/tests/test_skill_tree.gd b/tests/test_skill_tree.gd index e7dc526..5c9d21c 100644 --- a/tests/test_skill_tree.gd +++ b/tests/test_skill_tree.gd @@ -155,7 +155,6 @@ func _process(delta: float) -> bool: # --- fleet capacity ------------------------------------------- var base_cap: int = game_state.max_fleet_size() - game_state.research_points = 2 game_state.unlock("coordination") var wing_ok: bool = game_state.unlock("wing") var raised_cap: int = game_state.max_fleet_size() diff --git a/tests/test_space_time_distorder.gd b/tests/test_space_time_distorder.gd deleted file mode 100644 index 6890399..0000000 --- a/tests/test_space_time_distorder.gd +++ /dev/null @@ -1,239 +0,0 @@ -extends SceneTree - -# Headless gameplay test: run with -# godot --headless -s res://tests/test_space_time_distorder.gd -# Forgejo #59: a researchable vessel that lets the whole fleet travel faster -# than its default cruise speed, via a fader driving -# game_state.fleet_speed_multiplier. -# -# Covers: -# A. the fader is absent with no distorder ship, appears once one joins -# B. dragging it up measurably increases distance travelled per second; -# at 1.0 the fleet moves exactly as it does today -# C. formation holds together under boost - ships stay near their slot -# D. losing the vessel resets the multiplier and hides the fader -# E. the multiplier survives the space -> surface scene swap (it lives -# in the autoload) - -var game_state: Node -var main: Node -var fleet: Node -var fader: Node -var stage: int = 0 -var elapsed: float = 0.0 -var baseline_distance: float = 0.0 -var boosted_distance: float = 0.0 -var start_centroid: Vector2 = Vector2.ZERO -var unboosted_lag: float = 0.0 - -const MEASURE_TIME: float = 1.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.reset_progression() - game_state.vessel_levels["unlock_distorder"] = 1 - # #62: sustaining a boost now drains fleet energy continuously - stock - # plenty so this test's own boosted phases aren't cut short by running - # out mid-measurement. Set BEFORE the first spawn_ships() call (inside - # Main.tscn's own _ready(), triggered by instantiate() below): the fleet - # hold has no capacity cap yet at this point, so this can't be clamped - # by the soon-to-exist fleet's cargo capability. - game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500) - main = load("res://Main.tscn").instantiate() - root.add_child(main) - fleet = main.get_node("Fleet") - fader = main.get_node("UI/SpeedFader") - - -func _process(delta: float) -> bool: - elapsed += delta - match stage: - 0: - if elapsed < 0.2: - return false - main.get_node("EnemySpawner").set_process(false) - main.get_node("AsteroidField").set_process(false) - - # --- A. absent, then appears ------------------------------------- - if fader.visible: - return _fail_now("fader appeared without a distorder ship") - - fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([ - GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER])) - fader._process(0.0) - if not fader.visible: - return _fail_now("a distorder ship in the fleet did not reveal the fader") - print("fader hidden with no distorder ship, visible once one joins") - - # --- B. baseline @ 1.0x ------------------------------------------- - if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0): - return _fail_now("multiplier is not 1.0 by default: %f" % game_state.fleet_speed_multiplier) - start_centroid = fleet.get_centroid() - fleet.move_in_direction(Vector2(1.0, 0.0)) - elapsed = 0.0 - stage = 1 - return false - 1: - if elapsed < MEASURE_TIME: - return false - baseline_distance = fleet.get_centroid().distance_to(start_centroid) - # Reference for the formation check below: pure pursuit (a ship - # always aiming straight at its CURRENT, still-moving target - # rather than leading it) has its own inherent non-zero steady- - # state following distance even at 1.0x - it scales with the - # speeds involved, so "holds together under boost" has to mean - # staying proportional to this, not shrinking to some fixed - # absolute number regardless of how fast the fleet is going. - for i in range(fleet.ships.size()): - var ship: Node2D = fleet.ships[i] - if not is_instance_valid(ship): - continue - var expected: Vector2 = fleet.fleet_target + fleet.formation_offsets[i] - unboosted_lag = maxf(unboosted_lag, ship.position.distance_to(expected)) - fleet.stop() - print("baseline (1.0x): travelled %.1f over %.1fs, formation lag %.1f" \ - % [baseline_distance, MEASURE_TIME, unboosted_lag]) - elapsed = 0.0 - stage = 2 - return false - 2: - # Let the braking manoeuvre from stop() fully settle before - # measuring the boosted leg, so leftover momentum from the - # baseline pass can't pad the comparison either way. - if elapsed < 0.5: - return false - - var press := InputEventScreenTouch.new() - press.index = 5 - press.position = fader.global_position + Vector2(fader.size.x * 0.5, fader.size.y - 4.0) - press.pressed = true - main._handle_touch(press) - if not fader.is_active(): - return _fail_now("pressing the fader did not begin dragging") - var drag := InputEventScreenDrag.new() - drag.index = 5 - # All the way to the top of the track = MAX_FLEET_SPEED_MULTIPLIER. - drag.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0) - main._handle_drag(drag) - if not is_equal_approx(game_state.fleet_speed_multiplier, GameState.MAX_FLEET_SPEED_MULTIPLIER): - return _fail_now("dragging to the top did not reach the max multiplier: %f" \ - % game_state.fleet_speed_multiplier) - print("fader dragged to %.1fx" % game_state.fleet_speed_multiplier) - - start_centroid = fleet.get_centroid() - fleet.move_in_direction(Vector2(1.0, 0.0)) - elapsed = 0.0 - stage = 3 - return false - 3: - if elapsed < MEASURE_TIME: - return false - boosted_distance = fleet.get_centroid().distance_to(start_centroid) - print("boosted (%.1fx): travelled %.1f over %.1fs" \ - % [game_state.fleet_speed_multiplier, boosted_distance, MEASURE_TIME]) - if boosted_distance <= baseline_distance * 1.3: - return _fail_now("boosted travel (%.1f) was not measurably more than baseline (%.1f)" \ - % [boosted_distance, baseline_distance]) - elapsed = 0.0 - stage = 4 - return false - 4: - # --- C. formation holds together under boost ---------------------- - # Both the anchor's advance and the ships' own catch-up speed are - # scaled by the SAME multiplier, so the initial accelerate-from- - # rest transient covers a proportionally larger absolute distance - # under a 3x boost - a moment right after the boost begins isn't - # "trailing", it just hasn't finished the same (time-invariant) - # catch-up burst yet. Keep steering a while longer so that burst - # is over before judging whether formation holds. - if elapsed < 1.5: - return false - # Allow the lag to scale with the boost (it inherently does, even - # with both the anchor and ship speeds multiplied by the exact - # same factor - see the note by unboosted_lag above) but not - # beyond that, with slack for measurement noise. - var allowed_lag: float = maxf(unboosted_lag, 20.0) * game_state.fleet_speed_multiplier * 1.5 - for i in range(fleet.ships.size()): - var ship: Node2D = fleet.ships[i] - if not is_instance_valid(ship): - continue - var expected: Vector2 = fleet.fleet_target + fleet.formation_offsets[i] - var strayed: float = ship.position.distance_to(expected) - if strayed > allowed_lag: - return _fail_now("a ship strayed %.1f from its formation slot under sustained boost (unboosted lag was %.1f, allowed %.1f)" \ - % [strayed, unboosted_lag, allowed_lag]) - print("formation held together under sustained boost (lag stayed proportional to the boost)") - - fleet.stop() - var release := InputEventScreenTouch.new() - release.index = 5 - release.position = fader.global_position + Vector2(fader.size.x * 0.5, 0.0) - release.pressed = false - main._handle_touch(release) - - # --- E. survives the space -> surface scene swap ------------------- - # Done here, with the distorder ship still crewed and the fleet - # still boosted: tearing this scene down while nothing else - # changes the fleet's own composition proves the multiplier - # lives in the autoload rather than being reset as a side effect - # of the old scene's own SpeedFader still ticking once more - # during teardown (it would, correctly, self-reset the instant - # it next saw a fleet with no distorder ship - not a real - # scenario here since this fleet keeps its distorder ship the - # whole way across the swap, via GameState.surviving_vessel_types - # exactly like every other vessel type). - game_state.surviving_ship_count = fleet.ships.size() - game_state.surviving_vessel_types = fleet.vessel_types() - game_state.planet_id = "test_space_time_distorder" - game_state.planet_name = "Distorder Test" - main.queue_free() - elapsed = 0.0 - stage = 5 - return false - 5: - if elapsed < 0.3: - return false - main = load("res://PlanetSurface.tscn").instantiate() - root.add_child(main) - current_scene = main - fleet = main.get_node("Fleet") - fader = main.get_node("UI/SpeedFader") - elapsed = 0.0 - stage = 6 - return false - 6: - if elapsed < 0.3: - return false - if not is_equal_approx(game_state.fleet_speed_multiplier, 3.0): - return _fail_now("multiplier did not survive the space -> surface swap: %f" \ - % game_state.fleet_speed_multiplier) - fader._process(0.0) - if not fader.visible: - return _fail_now("the surface scene's fader did not show the surviving distorder ship") - print("multiplier and fader both survived the space -> surface swap") - - # --- D. losing the vessel resets the multiplier ------------------- - fleet.spawn_ships(1, fleet.get_centroid(), PackedStringArray([GameState.VESSEL_INTERCEPTOR])) - fader._process(0.0) - if fader.visible: - return _fail_now("fader stayed visible after losing the last distorder ship") - if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0): - return _fail_now("multiplier was not reset after losing the distorder ship: %f" \ - % game_state.fleet_speed_multiplier) - print("losing the distorder ship reset the multiplier and hid the fader") - - print("TEST_SPACE_TIME_DISTORDER: PASS") - quit(0) - return true - return false - - -func _fail_now(message: String) -> bool: - print("TEST_SPACE_TIME_DISTORDER: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_space_time_distorder.gd.uid b/tests/test_space_time_distorder.gd.uid deleted file mode 100644 index 5f7ecd6..0000000 --- a/tests/test_space_time_distorder.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://btmmwkkgw6qwi diff --git a/tests/test_surface_profiles.gd.uid b/tests/test_surface_profiles.gd.uid deleted file mode 100644 index 2de5318..0000000 --- a/tests/test_surface_profiles.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://crbidmy4oqat7 diff --git a/tests/test_turret_spawner_enemy.gd.uid b/tests/test_turret_spawner_enemy.gd.uid deleted file mode 100644 index dbd423d..0000000 --- a/tests/test_turret_spawner_enemy.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://du2i8ayup3qkd diff --git a/tests/test_vessel_composition.gd b/tests/test_vessel_composition.gd index 226d9a1..67bf6c3 100644 --- a/tests/test_vessel_composition.gd +++ b/tests/test_vessel_composition.gd @@ -23,7 +23,8 @@ func _process(delta: float) -> bool: return false var definitions: Array = VESSEL_DEFINITIONS.all() var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet() - if fleet_definitions.any(func(definition): return definition.has_capability("transport")): + if fleet_definitions.size() != 5 \ + or fleet_definitions.any(func(definition): return definition.has_capability("transport")): return _fail("logistics transporters leaked into the combat-fleet catalog") if ships.size() != definitions.size(): return _fail("catalog and composed fleet differ in size")