From 0504b24d8d04cb497beb6ae3daad0e21c1cd019f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Fri, 14 Aug 2026 21:44:28 +0200 Subject: [PATCH] Compose buildings from definitions and capabilities --- BuildController.gd | 218 +++------ Building.gd | 669 +-------------------------- Building.tscn | 2 + BuildingActionProvider.gd | 3 + BuildingActionProvider.gd.uid | 1 + BuildingDefinition.gd | 20 + BuildingDefinition.gd.uid | 1 + BuildingDefinitions.gd | 21 + BuildingDefinitions.gd.uid | 1 + BuildingField.gd | 2 +- BuildingVisualBase.gd | 606 ++++++++++++++++++++++++ BuildingVisualBase.gd.uid | 1 + CommandBuildingDefinition.tres | 20 + CommandBuildingVisual.gd | 9 + CommandBuildingVisual.gd.uid | 1 + CommandBuildingVisual.tscn | 11 + CrafterBuildingDefinition.tres | 19 + CrafterBuildingVisual.gd | 14 + CrafterBuildingVisual.gd.uid | 1 + CrafterBuildingVisual.tscn | 11 + ExtractorBuildingDefinition.tres | 16 + GameState.gd | 39 +- MothershipBuildingDefinition.tres | 21 + MothershipBuildingVisual.gd | 12 + MothershipBuildingVisual.gd.uid | 1 + MothershipBuildingVisual.tscn | 11 + ShipLabBuildingDefinition.tres | 20 + ShipLabBuildingVisual.gd | 12 + ShipLabBuildingVisual.gd.uid | 1 + ShipLabBuildingVisual.tscn | 11 + TransportHubBuildingDefinition.tres | 20 + TransportHubBuildingVisual.gd | 12 + TransportHubBuildingVisual.gd.uid | 1 + TransportHubBuildingVisual.tscn | 11 + WorldMapContent.gd | 46 +- tests/_build_popup_preview.gd | 2 +- tests/_crafter_preview.gd | 6 +- tests/test_animation_continuity.gd | 2 +- tests/test_build_info_popup.gd | 10 +- tests/test_building_composition.gd | 35 ++ tests/test_building_redraw_cost.gd | 8 +- tests/test_crafter_animation.gd | 11 +- tests/test_extractor_building.gd | 6 +- tests/test_mothership_replacement.gd | 8 +- tests/test_skill_tree.gd | 4 +- tests/test_transport_hub_building.gd | 12 +- 46 files changed, 1091 insertions(+), 878 deletions(-) create mode 100644 BuildingActionProvider.gd create mode 100644 BuildingActionProvider.gd.uid create mode 100644 BuildingDefinition.gd create mode 100644 BuildingDefinition.gd.uid create mode 100644 BuildingDefinitions.gd create mode 100644 BuildingDefinitions.gd.uid create mode 100644 BuildingVisualBase.gd create mode 100644 BuildingVisualBase.gd.uid create mode 100644 CommandBuildingDefinition.tres create mode 100644 CommandBuildingVisual.gd create mode 100644 CommandBuildingVisual.gd.uid create mode 100644 CommandBuildingVisual.tscn create mode 100644 CrafterBuildingDefinition.tres create mode 100644 CrafterBuildingVisual.gd create mode 100644 CrafterBuildingVisual.gd.uid create mode 100644 CrafterBuildingVisual.tscn create mode 100644 ExtractorBuildingDefinition.tres create mode 100644 MothershipBuildingDefinition.tres create mode 100644 MothershipBuildingVisual.gd create mode 100644 MothershipBuildingVisual.gd.uid create mode 100644 MothershipBuildingVisual.tscn create mode 100644 ShipLabBuildingDefinition.tres create mode 100644 ShipLabBuildingVisual.gd create mode 100644 ShipLabBuildingVisual.gd.uid create mode 100644 ShipLabBuildingVisual.tscn create mode 100644 TransportHubBuildingDefinition.tres create mode 100644 TransportHubBuildingVisual.gd create mode 100644 TransportHubBuildingVisual.gd.uid create mode 100644 TransportHubBuildingVisual.tscn create mode 100644 tests/test_building_composition.gd diff --git a/BuildController.gd b/BuildController.gd index 114575f..b598703 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -1,5 +1,7 @@ extends Node2D +const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") + # Space-side building UI. Owns: # - the build menu (place a mothership, ship crafter or command centre) # - the shared ACTION button, which adapts to whatever the fleet is @@ -22,14 +24,8 @@ extends Node2D @onready var building_action_button: IconButton = get_node("../UI/BuildingActionButton") @onready var build_menu: Control = get_node("../UI/BuildMenu") -@onready var build_mothership_button: IconButton = get_node("../UI/BuildMenu/MothershipButton") -@onready var build_crafter_button: IconButton = get_node("../UI/BuildMenu/CrafterButton") -@onready var build_command_button: IconButton = get_node("../UI/BuildMenu/CommandButton") @onready var build_close_button: IconButton = get_node("../UI/BuildMenu/CloseButton") @onready var build_status: Label = get_node("../UI/BuildMenu/StatusLabel") -@onready var build_extractor_button: IconButton = get_node("../UI/BuildMenu/ExtractorButton") -@onready var build_hub_button: IconButton = get_node("../UI/BuildMenu/TransportHubButton") -@onready var build_lab_button: IconButton = get_node("../UI/BuildMenu/ShipLabButton") @onready var lab_menu: Control = get_node("../UI/LabMenu") @onready var lab_vessel_buttons: Array = [ get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"), @@ -78,20 +74,7 @@ extends Node2D get_node("../UI/HangarMenu/FleetShieldLabel"), get_node("../UI/HangarMenu/FleetTransporterLabel"), ] -@onready var build_menu_entries: Array = [ - [GameState.BUILDING_MOTHERSHIP, build_mothership_button, - get_node("../UI/BuildMenu/MothershipLabel"), get_node("../UI/BuildMenu/MothershipCost")], - [GameState.BUILDING_SHIP_CRAFTER, build_crafter_button, - get_node("../UI/BuildMenu/CrafterLabel"), get_node("../UI/BuildMenu/CrafterCost")], - [GameState.BUILDING_COMMAND_CENTER, build_command_button, - get_node("../UI/BuildMenu/CommandLabel"), get_node("../UI/BuildMenu/CommandCost")], - [GameState.BUILDING_EXTRACTOR, build_extractor_button, - get_node("../UI/BuildMenu/ExtractorLabel"), get_node("../UI/BuildMenu/ExtractorCost")], - [GameState.BUILDING_TRANSPORT_HUB, build_hub_button, - get_node("../UI/BuildMenu/TransportHubLabel"), get_node("../UI/BuildMenu/TransportHubCost")], - [GameState.BUILDING_SHIP_LAB, build_lab_button, - get_node("../UI/BuildMenu/ShipLabLabel"), get_node("../UI/BuildMenu/ShipLabCost")], -] +var build_menu_entries: Array = [] # Which vessel the lab screen is showing, and which visible upgrade card is # selected. Each vessel gets its own small set of direct cards; the detail and @@ -148,41 +131,6 @@ var lab_building_id: int = 0 @onready var formation_close_button: IconButton = get_node("../UI/FormationMenu/CloseButton") @onready var formation_reset_button: IconButton = get_node("../UI/FormationMenu/ResetButton") -# What each option is and does, shown before anything is spent. Picking a -# structure is a real commitment - it costs ore, and a mothership placement -# can destroy the one you already have - so no option builds straight from -# the menu; each one explains itself first and waits for a confirmation. -# -# The bodies are kept short on purpose: at the UI's font size the popup holds -# roughly eight lines, and a placement condition or a price warning is -# appended below them and needs some of those lines. -const BUILDING_INFO: Dictionary = { - GameState.BUILDING_MOTHERSHIP: { - "title": "Mothership", - "body": "Fleet home port. A wiped-out fleet rebuilds here, and extractor haulers deliver ore here.", - }, - GameState.BUILDING_SHIP_CRAFTER: { - "title": "Ship Crafter", - "body": "Fly the fleet onto it, then use the action button to build extra vessels.", - }, - GameState.BUILDING_COMMAND_CENTER: { - "title": "Command Center", - "body": "Fly the fleet onto it to drag your fleet into a new formation. Rebuilt fleets keep it.", - }, - GameState.BUILDING_SHIP_LAB: { - "title": "Ship Lab", - "body": "Fly the fleet onto it to upgrade each vessel type, and to unlock the shield vessel.", - }, - GameState.BUILDING_TRANSPORT_HUB: { - "title": "Transport Hub", - "body": "Stations transport vessels. They fetch ore from your mothership and deliver it to whichever building is waiting on it.", - }, - GameState.BUILDING_EXTRACTOR: { - "title": "Extractor", - "body": "Mines a planet and runs a hauler to your mothership. It keeps earning while you are away.", - }, -} - # Which structure the popup is currently offering to build, or "" when it is # only explaining something that cannot be built right now. var pending_build_type: String = "" @@ -210,12 +158,7 @@ var context_actions_expanded: bool = false func _ready() -> void: build_button.pressed.connect(_on_build_pressed) build_close_button.pressed.connect(_close_build_menu) - build_mothership_button.pressed.connect(_on_place_mothership) - build_crafter_button.pressed.connect(_on_place_crafter) - build_command_button.pressed.connect(_on_place_command_center) - build_extractor_button.pressed.connect(_on_place_extractor) - build_hub_button.pressed.connect(_on_place_transport_hub) - build_lab_button.pressed.connect(_on_place_ship_lab) + _configure_build_menu_entries() craft_shield_button.pressed.connect(_on_craft_shield) lab_close_button.pressed.connect(func(): lab_menu.visible = false) lab_buy_button.pressed.connect(_on_lab_buy) @@ -267,6 +210,21 @@ func _ready() -> void: _layout_context_buttons() +func _configure_build_menu_entries() -> void: + build_menu_entries.clear() + for definition in BUILDING_DEFINITIONS.all(): + if definition.menu_button_name == "": + continue + var button: IconButton = get_node("../UI/BuildMenu/%s" % definition.menu_button_name) + var label: Label = get_node("../UI/BuildMenu/%s" % definition.menu_label_name) + var cost_number: NeonNumber = get_node("../UI/BuildMenu/%s" % definition.menu_cost_name) + button.icon = definition.menu_icon + button.pressed.connect(_show_building_info.bind(String(definition.definition_id))) + label.text = definition.display_name + cost_number.amount = definition.cost + build_menu_entries.append([definition.definition_id, button, label, cost_number, definition]) + + # True while a building prompt or panel owns the shared action slot, so the # enter-atmosphere prompt knows to stay out of the way. func is_showing_building_prompt() -> bool: @@ -318,9 +276,7 @@ func _process(delta: float) -> void: if build_menu.visible: # Keep the greyed state live while the menu is open, so flying onto # a planet lights the option up without reopening it. - build_extractor_button.enabled = _planet_under_fleet() != null \ - and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR) - build_extractor_button.queue_redraw() + _refresh_build_option_states() if craft_menu.visible: # Live, so a delivery arriving while the menu is open visibly works @@ -359,10 +315,7 @@ func _process(delta: float) -> void: current_building = pending_building context_actions_expanded = false - # The hangar is where finished vessels are delivered (#23), so it belongs - # at the mothership and nowhere else. - hangar_button.visible = current_building != null \ - and current_building.building_type == GameState.BUILDING_MOTHERSHIP + hangar_button.visible = false # Every building can hold ore, so the transfer button rides alongside the # action button rather than being one of the things the action button # turns into - the two are independent choices at the same structure. @@ -371,7 +324,7 @@ func _process(delta: float) -> void: action_button.visible = current_building != null if current_building != null: var actions: Array = _context_action_buttons() - building_action_button.icon = _action_icon_for(current_building.building_type) + building_action_button.icon = int(current_building.definition.action_icon) building_action_button.queue_redraw() action_button.icon = building_action_button.icon if actions.size() == 1 else IconButton.Icon.ACTION_MENU action_button.queue_redraw() @@ -403,13 +356,20 @@ func _layout_context_buttons() -> void: func _context_action_buttons() -> Array: if current_building == null: return [] - var actions: Array = [transfer_button] - if current_building.building_type in [GameState.BUILDING_MOTHERSHIP, GameState.BUILDING_SHIP_CRAFTER, - GameState.BUILDING_SHIP_LAB, GameState.BUILDING_COMMAND_CENTER]: - actions.push_front(building_action_button) - if current_building.building_type == GameState.BUILDING_MOTHERSHIP: - actions.append(hangar_button) - return actions + var buttons: Array = [] + for action in current_building.available_actions(): + var button: IconButton = _button_for_building_action(action) + if button != null and button not in buttons: + buttons.append(button) + return buttons + + +func _button_for_building_action(action: String) -> IconButton: + if action == "transfer": + return transfer_button + if action == "hangar": + return hangar_button + return building_action_button func _on_context_action_pressed() -> void: @@ -433,20 +393,6 @@ func _collapse_context_actions() -> void: _layout_context_buttons() -func _action_icon_for(type: String) -> int: - match type: - GameState.BUILDING_SHIP_CRAFTER: - return IconButton.Icon.SHIP - GameState.BUILDING_TRANSPORT_HUB: - return IconButton.Icon.TRANSPORT_HUB - GameState.BUILDING_SHIP_LAB: - return IconButton.Icon.SHIP_LAB - GameState.BUILDING_MOTHERSHIP: - return IconButton.Icon.MOTHERSHIP - _: - return IconButton.Icon.COMMAND_CENTER - - func _check_fleet_wipe() -> void: if not fleet.ships.is_empty(): return @@ -465,11 +411,12 @@ func _on_action_pressed() -> void: for bar in resource_bars: bar.building_id = current_building.building_id - if current_building.building_type == GameState.BUILDING_MOTHERSHIP: + var action: String = current_building.primary_action() + if action == "research": skill_menu.visible = true skill_tree.reset_scroll() _refresh_skill_menu() - elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER: + elif action == "craft": # Remembered explicitly: _process() clears current_building while any # menu is open, so by the next frame the panel would no longer know # which crafter it belongs to. Losing this line makes every button in @@ -478,11 +425,11 @@ func _on_action_pressed() -> void: craft_building_id = current_building.building_id craft_menu.visible = true _refresh_craft_status() - elif current_building.building_type == GameState.BUILDING_SHIP_LAB: + elif action == "lab": lab_building_id = current_building.building_id lab_menu.visible = true _refresh_lab() - elif current_building.building_type == GameState.BUILDING_COMMAND_CENTER: + elif action == "formation": formation_menu.visible = true formation_editor.queue_redraw() @@ -551,8 +498,7 @@ func _refresh_build_status() -> void: # Researched but not usable right here is still shown, greyed: an # extractor needs a planet under the fleet, which is a "not now" rather # than a "not yet". - build_extractor_button.enabled = _planet_under_fleet() != null \ - and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR) + _refresh_build_option_states() # Prices are shown under each option now, so this only needs to say # whether the fleet is somewhere an extractor could go. @@ -562,16 +508,14 @@ func _refresh_build_status() -> void: build_status.text = "" -func _on_place_mothership() -> void: - _show_building_info(GameState.BUILDING_MOTHERSHIP) - - -func _on_place_crafter() -> void: - _show_building_info(GameState.BUILDING_SHIP_CRAFTER) - - -func _on_place_command_center() -> void: - _show_building_info(GameState.BUILDING_COMMAND_CENTER) +func _refresh_build_option_states() -> void: + for entry in build_menu_entries: + var definition: Resource = entry[4] + var enabled: bool = GameState.is_building_unlocked(definition.definition_id) + if definition.placement_mode == "planet": + enabled = enabled and _planet_under_fleet() != null + entry[1].enabled = enabled + entry[1].queue_redraw() func _close_build_menu() -> void: @@ -582,40 +526,17 @@ func _close_build_menu() -> void: build_menu.visible = false -func _on_place_ship_lab() -> void: - _show_building_info(GameState.BUILDING_SHIP_LAB) - - -func _on_place_transport_hub() -> void: - _show_building_info(GameState.BUILDING_TRANSPORT_HUB) - - -func _on_place_extractor() -> void: - # The option stays pressable while greyed, so this is also how a - # not-over-a-planet extractor explains itself. - _show_building_info(GameState.BUILDING_EXTRACTOR) - - func cost_of(type: String) -> int: - match type: - GameState.BUILDING_TRANSPORT_HUB: - return GameState.TRANSPORT_HUB_COST - GameState.BUILDING_SHIP_LAB: - return GameState.SHIP_LAB_COST - GameState.BUILDING_MOTHERSHIP: - return GameState.MOTHERSHIP_COST - GameState.BUILDING_SHIP_CRAFTER: - return GameState.SHIP_CRAFTER_COST - GameState.BUILDING_COMMAND_CENTER: - return GameState.COMMAND_CENTER_COST - _: - return GameState.EXTRACTOR_COST + var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) + return int(definition.cost) if definition != null else 0 func _show_building_info(type: String) -> void: - var info: Dictionary = BUILDING_INFO[type] - var cost: int = cost_of(type) - var lines: Array = [info.body] + var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) + if definition == null: + return + var cost: int = int(definition.cost) + var lines: Array = [definition.description] var can_build: bool = true # Locked structures still list themselves, greyed, so the player can see @@ -626,7 +547,7 @@ func _show_building_info(type: String) -> void: # Anything that would stop this particular placement is spelled out in # the same popup, rather than silently failing after the press. - if can_build and type == GameState.BUILDING_EXTRACTOR: + if can_build and definition.placement_mode == "planet": var planet: Node2D = _planet_under_fleet() if planet == null: lines.append("No planet in reach - fly over one.") @@ -635,15 +556,15 @@ func _show_building_info(type: String) -> void: lines.append("%s already has an extractor." % planet.planet_name) can_build = false else: - lines.append("Will mine %s from %s." % ["gas" if planet.resource_type == "gas" else "ore", planet.planet_name]) - elif can_build and type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership(): + lines.append("Will mine %s from %s." % [GameState.resource_name(planet.resource_type), planet.planet_name]) + elif can_build and definition.unique and GameState.has_building_of_type(type): lines.append("WARNING: only one may exist - this destroys your current one.") if can_build and not GameState.can_afford(cost): lines.append("Not enough rocks - needs %d, you have %d." % [cost, GameState.rocks]) can_build = false - info_title.text = info.title + info_title.text = definition.display_name info_message.text = "\n\n".join(lines) info_cost.amount = cost info_build_button.visible = can_build @@ -668,28 +589,31 @@ func _confirm_build() -> void: # planet an extractor was going to mine. if not GameState.is_building_unlocked(type): return + var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) + if definition == null: + return - if type == GameState.BUILDING_EXTRACTOR: + if definition.placement_mode == "planet": var planet: Node2D = _planet_under_fleet() if planet == null or GameState.has_extractor_at(planet.planet_name): build_status.text = "No planet in reach any more" return - if not GameState.spend(GameState.EXTRACTOR_COST): - build_status.text = "Not enough rocks (need %d, have %d)" % [GameState.EXTRACTOR_COST, GameState.rocks] + if not GameState.spend(definition.cost): + build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks] return GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type) build_menu.visible = false return - if type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership(): - if not GameState.can_afford(GameState.MOTHERSHIP_COST): + if definition.unique and GameState.has_building_of_type(type): + if not GameState.can_afford(definition.cost): build_status.text = "Not enough rocks (need %d, have %d)" % [ - GameState.MOTHERSHIP_COST, GameState.rocks + definition.cost, GameState.rocks ] return # Only now is the old one destroyed - never before the new one is # known to be affordable, or the player would be left with none. - GameState.remove_buildings_of_type(GameState.BUILDING_MOTHERSHIP) + GameState.remove_buildings_of_type(type) building_field.rebuild_all() _try_place(type, cost_of(type)) diff --git a/Building.gd b/Building.gd index d4387d7..b9dea7e 100644 --- a/Building.gd +++ b/Building.gd @@ -1,665 +1,42 @@ extends Node2D -# A player-placed structure in space. Three kinds for now: -# mothership - where a wiped-out fleet is rebuilt -# ship_crafter - fly the fleet onto it to buy additional ships -# command_center - reconfigure the fleet formation -# -# Buildings are stationary and harmless; whether the fleet is "on" one is -# decided by BuildingField comparing the fleet CENTROID against -# interact_radius, the same stable single-number test Planet uses (see -# Planet.gd for why per-ship overlap was a poor fit). -# -# Nothing here rests on ground or vents into an atmosphere - these are -# orbital structures, so they are built out of trusses, radiator fins, -# reactor cores and open cradles rather than walls and roofs. -# -# The crafter and the mothership animate, so they run _process. The command -# centre is drawn once and never redrawn - per-frame redraws of art that -# never changes were a real framerate cost earlier on. +const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") -@export var building_type: String = "mothership" -# Which GameState.buildings record this node is showing. Set by -# BuildingField when it spawns, so a node can be traced back to the stock -# and production queue that survive the scene swap (#16). +@export var definition_id: String = "mothership" @export var building_id: int = 0 @export var interact_radius: float = 150.0 -const MOTHERSHIP_COLOR: Color = Color(0.45, 0.9, 0.55) -const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25) -const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0) -const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85) -const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9) +var definition: Resource +var visual: Node2D -# Ship lab: a test frame with a vessel held in it and instruments sweeping -# over it. Reads as a workshop rather than a factory - the crafter already -# owns "assembly line", so this is measurement, not production. -const LAB_FRAME_W: float = 78.0 -const LAB_FRAME_H: float = 58.0 -const LAB_MOUNT_RADIUS: float = 15.0 -const LAB_SCANNER_ORBIT: float = 96.0 -const LAB_SCAN_SPEED: float = 0.9 - -# Transport hub: an octagonal dock ring with berths around it and a freight -# core in the middle. Deliberately radial-but-flat, so it does not read as a -# small mothership - the mothership is the only thing with swept arms. -const HUB_RING_RADIUS: float = 74.0 -const HUB_DECK_RADIUS: float = 54.0 -const HUB_CORE_RADIUS: float = 22.0 -const HUB_BERTH_COUNT: int = 8 -const HUB_BERTH_LENGTH: float = 20.0 -const HUB_DOCKED_COUNT: int = 3 -const HUB_DOCKED_ORBIT: float = 96.0 -const HUB_SPIN_SPEED: float = 0.22 -# The vessel on the assembly line is drawn in the fleet's own colour, so it -# reads as "one of yours being built" rather than as shipyard machinery. -const HULL_COLOR: Color = Color(0.25, 0.85, 1.0) - -# One assembly cycle: parts are welded on, the finished hull flashes, then -# it clears the cradle and the line starts over. -const CYCLE: float = 3.6 -const ASSEMBLE_END: float = 0.66 # fraction of CYCLE spent welding -const FLASH_END: float = 0.76 # ... then the completion flash - # ... then launch, to 1.0 - -const CORE_CENTRE: Vector2 = Vector2(-58.0, 12.0) -const BAY_CENTRE: Vector2 = Vector2(46.0, 12.0) -const RIB_RADIUS: float = 36.0 -const RAIL_Y: float = -32.0 -const FLASH_RING_RADIUS: float = 20.0 - -# The hull in four pieces, attached in this order. Each carries the point -# the crane welds it on at, which is where the sparks fly from. -const SHIP_PARTS: Array = [ - {"at": 0.04, "weld": Vector2(0, -12), "points": [Vector2(0, -26), Vector2(-8, 4), Vector2(8, 4)]}, - {"at": 0.28, "weld": Vector2(-13, 7), "points": [Vector2(-8, -4), Vector2(-22, 14), Vector2(-8, 10)]}, - {"at": 0.48, "weld": Vector2(13, 7), "points": [Vector2(8, -4), Vector2(22, 14), Vector2(8, 10)]}, - {"at": 0.70, "weld": Vector2(0, 10), "points": [Vector2(-7, 4), Vector2(7, 4), Vector2(5, 14), Vector2(-5, 14)]}, -] - -# The assembly clock, wrapped at CYCLE because the whole build sequence is -# expressed as a fraction of it. -var anim_time: float = 0.0 - -# Free-running seconds for everything that is merely cyclic - reactor pulses, -# orbiting satellites, blinking beacons. These must NOT share anim_time: -# CYCLE is 3.6s, which is not a whole number of periods for any of them, so -# wrapping made every one of them leap at the wrap. The satellites were the -# obvious casualty, snapping back 66 degrees every 3.6 seconds. -# -# Deliberately never wrapped. GDScript floats are 64-bit, so even after hours -# of play the argument to sin() keeps far more precision than a frame needs, -# and any wrap point would reintroduce exactly the discontinuity above. -var phase_time: float = 0.0 - -var _hull_parts: Array[PackedVector2Array] = [] - -# Every moving element is its own NeonPiece, rasterised once when the -# building is created and thereafter only moved, scaled, faded or hidden. -# Nothing here ever calls queue_redraw() again: the building's own _draw() -# lays down the fixed structure a single time, and the animation is pure -# node-property work from then on. -var _core_cell: NeonPiece = null -var _satellites: Node2D = null -var _beacons: Array = [] -var _clamps: NeonPiece = null -var _crane: Node2D = null -var _hoist: NeonPiece = null -var _sparks: Node2D = null -var _hull_holder: Node2D = null -var _hull_pieces: Array = [] -# Rotating holder for the transports stationed at a transport hub (#18). -var _hub_traffic: Node2D = null -# Sweeping instrument booms at a ship lab (#5). -var _lab_scanners: Node2D = null -var _flash_ring: NeonPiece = null -var _wash: NeonPiece = null +var building_type: String: + get: return definition_id + set(value): definition_id = value func _ready() -> void: add_to_group("buildings") z_index = -1 - # The crafter runs an assembly line and the mothership holds a live - # reactor with satellites in orbit, so both animate. The command centre is - # entirely static and needs no per-frame work at all. - var animated: bool = building_type != GameState.BUILDING_COMMAND_CENTER - set_process(animated) - if building_type == GameState.BUILDING_SHIP_CRAFTER: - for part in SHIP_PARTS: - _hull_parts.append(PackedVector2Array(part.points)) - _build_crafter_pieces() - elif building_type == GameState.BUILDING_MOTHERSHIP: - _build_mothership_pieces() - elif building_type == GameState.BUILDING_TRANSPORT_HUB: - _build_hub_pieces() - elif building_type == GameState.BUILDING_SHIP_LAB: - _build_lab_pieces() - queue_redraw() + definition = BUILDING_DEFINITIONS.get_definition(definition_id) + if definition == null or definition.visual_scene == null: + return + visual = definition.visual_scene.instantiate() + get_node("VisualHost").add_child(visual) -func _process(delta: float) -> void: - anim_time = fmod(anim_time + delta, CYCLE) - phase_time += delta - apply_animation() +func available_actions() -> PackedStringArray: + if visual == null: + return PackedStringArray() + var provider: Node = visual.get_node_or_null("ActionProvider") + return provider.actions if provider != null else PackedStringArray() -# Pushes the current clock onto the movable pieces. Normally driven from -# _process; public so a preview or a test can pin the clock and see exactly -# that frame without having to run the animation up to it in real time. -func apply_animation() -> void: - match building_type: - GameState.BUILDING_MOTHERSHIP: - _animate_mothership() - GameState.BUILDING_SHIP_CRAFTER: - _animate_crafter() - GameState.BUILDING_TRANSPORT_HUB: - _animate_transport_hub() - GameState.BUILDING_SHIP_LAB: - _animate_ship_lab() - - -# Where the satellites are, and how open the reactor core is. Both are read -# by _draw and by the test that checks they stay continuous across the -# assembly clock's wrap. -func orbit_angle() -> float: - return phase_time * ORBIT_SPEED - - -func core_pulse() -> float: - return 0.5 + 0.5 * sin(phase_time * PULSE_SPEED) +func primary_action() -> String: + for action in available_actions(): + if action != "transfer" and action != "hangar": + return action + return "" func get_display_name() -> String: - match building_type: - GameState.BUILDING_MOTHERSHIP: - return "Mothership" - GameState.BUILDING_SHIP_CRAFTER: - return "Ship Crafter" - _: - return "Command Center" - - -# How many times this building's geometry has actually been rasterised. The -# whole point of the NeonPiece decomposition is that this stays at 1 for the -# building's lifetime however long it animates, so it is worth being able to -# assert rather than assume. -var draw_count: int = 0 - - -func _draw() -> void: - draw_count += 1 - # Runs exactly once per building: this is the fixed structure. Everything - # that moves is a NeonPiece child, animated through node properties, so - # nothing here ever asks for a redraw again. - match building_type: - GameState.BUILDING_COMMAND_CENTER: - _draw_command_center() - GameState.BUILDING_MOTHERSHIP: - _draw_mothership_static(self) - GameState.BUILDING_TRANSPORT_HUB: - _draw_transport_hub_static(self) - GameState.BUILDING_SHIP_LAB: - _draw_ship_lab_static(self) - _: - _draw_crafter_static(self) - - -# --- shared helpers ------------------------------------------------------- - -func _ring(centre: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array: - var pts: PackedVector2Array = PackedVector2Array() - for i in range(sides): - var a: float = (float(i) / float(sides)) * TAU + phase - pts.append(centre + Vector2(cos(a), sin(a)) * radius) - return pts - - -func _arc(centre: Vector2, radius: float, from_a: float, to_a: float, steps: int) -> PackedVector2Array: - var pts: PackedVector2Array = PackedVector2Array() - for i in range(steps + 1): - var a: float = lerpf(from_a, to_a, float(i) / float(steps)) - pts.append(centre + Vector2(cos(a), sin(a)) * radius) - return pts - - - - -# --- command centre: a tactical plotting hub ------------------------------ -# The mothership is radial-and-swept, the crafter is an asymmetric assembly -# line, so the command centre reads best as the odd one out: rigid, hexagonal, -# a wheel rather than a claw. Six spokes tie a hex rim to a central diamond -# readout, and every spoke ends in a formation port - half of them holding a -# token, half left open - so the structure itself shows fleet slots waiting -# to be filled, which is exactly what the drag-and-drop editor inside does. - -const CMD_HEX_RADIUS: float = 88.0 -const CMD_DECK_RADIUS: float = 66.0 -const CMD_HUB_RADIUS: float = 30.0 -const CMD_SCREEN_RADIUS: float = 14.0 -const CMD_PORT_COUNT: int = 6 -const CMD_PORT_LENGTH: float = 16.0 -const CMD_TOKEN_RADIUS: float = 9.0 -const CMD_STUD_RADIUS: float = 7.0 - - -# --- ship lab ------------------------------------------------------------- - -func _draw_ship_lab_static(target: CanvasItem) -> void: - var c: Color = SHIP_LAB_COLOR - - # Outer test frame, open at the corners so it reads as a rig rather than - # a solid hull. - for sx in [-1.0, 1.0]: - for sy in [-1.0, 1.0]: - var corner: Vector2 = Vector2(sx * LAB_FRAME_W, sy * LAB_FRAME_H) - NeonDraw.draw_glowing_line(target, corner, - corner - Vector2(sx * LAB_FRAME_W * 0.45, 0.0), c, 0.9, 0.42) - NeonDraw.draw_glowing_line(target, corner, - corner - Vector2(0.0, sy * LAB_FRAME_H * 0.5), c, 0.9, 0.42) - - # The vessel under test, held nose-up in the middle - deliberately the - # fleet's own silhouette, so it is obvious what the lab is for. - NeonDraw.draw_glowing_polygon(target, PackedVector2Array([ - Vector2(0, -26), Vector2(13, 12), Vector2(0, 5), Vector2(-13, 12), - ]), HULL_COLOR, 0.95) - - # Mount ring and its bracing. - NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, LAB_MOUNT_RADIUS, 6, PI / 6.0), c, 0.5, - NeonDraw.DEFAULT_CORE, 0.7) - for i in range(4): - var a: float = (float(i) / 4.0) * TAU + PI / 4.0 - var dir: Vector2 = Vector2(cos(a), sin(a)) - NeonDraw.draw_glowing_line(target, dir * LAB_MOUNT_RADIUS, dir * 40.0, c, 0.45, 0.3) - - -func _build_lab_pieces() -> void: - # Two instrument booms sweeping round the vessel. One holder, rasterised - # once, rotated per frame (#10). - _lab_scanners = Node2D.new() - add_child(_lab_scanners) - for i in range(2): - var a: float = float(i) * PI - var at: Vector2 = Vector2(cos(a), sin(a)) * LAB_SCANNER_ORBIT - var piece: NeonPiece = NeonPiece.create(_lab_scanners, at, SHIP_LAB_COLOR) - piece.add_polygon(PackedVector2Array([ - Vector2(-4, -12), Vector2(4, -12), Vector2(4, 12), Vector2(-4, 12), - ])) - piece.add_line(Vector2(0, 0), Vector2(-cos(a), -sin(a)) * 26.0, 0.3, 0.5) - piece.rotation = a - piece.queue_redraw() - - -func _animate_ship_lab() -> void: - _lab_scanners.rotation = phase_time * LAB_SCAN_SPEED - - -# --- transport hub -------------------------------------------------------- - -func _draw_transport_hub_static(target: CanvasItem) -> void: - var c: Color = TRANSPORT_HUB_COLOR - - # Dock ring and the deck nested inside it. - NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_RING_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 1.0) - NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_DECK_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 0.45, NeonDraw.DEFAULT_CORE, 0.7) - - # Freight core: a squared-off hold rather than a reactor, so it reads as - # storage and not as another power plant. - NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_CORE_RADIUS, 4, PI / 4.0), c, 0.95) - - for i in range(HUB_BERTH_COUNT): - var a: float = (float(i) / HUB_BERTH_COUNT) * TAU + PI / 8.0 - var dir: Vector2 = Vector2(cos(a), sin(a)) - NeonDraw.draw_glowing_line(target, dir * HUB_CORE_RADIUS, dir * HUB_DECK_RADIUS, c, 0.5, 0.32) - # Berth arm standing off the ring, where a transporter parks. - NeonDraw.draw_glowing_line(target, dir * HUB_RING_RADIUS, - dir * (HUB_RING_RADIUS + HUB_BERTH_LENGTH), c, 0.75, 0.38) - - -func _build_hub_pieces() -> void: - # Stationed transports, as NeonPiece children of one holder that turns. - # Per #10 the geometry is rasterised once and only the holder's rotation - # changes, so the traffic costs no redraws at all. - _hub_traffic = Node2D.new() - add_child(_hub_traffic) - for i in range(HUB_DOCKED_COUNT): - var a: float = (float(i) / HUB_DOCKED_COUNT) * TAU - var at: Vector2 = Vector2(cos(a), sin(a)) * HUB_DOCKED_ORBIT - var piece: NeonPiece = NeonPiece.create(_hub_traffic, at, TRANSPORT_HUB_COLOR) - # A stubby freighter: blunt, boxy, nothing like the fleet's darts. - piece.add_polygon(PackedVector2Array([ - Vector2(-9, -5), Vector2(9, -5), Vector2(12, 0), Vector2(9, 5), Vector2(-9, 5), - ])) - piece.rotation = a + PI / 2.0 - piece.queue_redraw() - - -func _animate_transport_hub() -> void: - _hub_traffic.rotation = phase_time * HUB_SPIN_SPEED - - -func _diamond(centre: Vector2, r: float) -> PackedVector2Array: - return PackedVector2Array([ - centre + Vector2(0, -r), centre + Vector2(r, 0), - centre + Vector2(0, r), centre + Vector2(-r, 0), - ]) - - -func _draw_command_center() -> void: - var c: Color = COMMAND_COLOR - var readout: Color = Color(0.82, 0.86, 1.0) - - # The rim: a hex table holding the whole layout together, with a fainter - # deck ring nested just inside it - the same "plating has layers" read the - # mothership gets from its nested reactor rings. - NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_HEX_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 1.0) - NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_DECK_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 0.5, NeonDraw.DEFAULT_CORE, 0.7) - - for i in range(CMD_PORT_COUNT): - var a: float = (float(i) / CMD_PORT_COUNT) * TAU + PI / 6.0 - var dir: Vector2 = Vector2(cos(a), sin(a)) - # Spoke bracing the hub to the rim. - NeonDraw.draw_glowing_line(self, dir * CMD_HUB_RADIUS, dir * CMD_HEX_RADIUS, c, 0.65, 0.36) - # Socket where the mast plugs into the rim. - NeonDraw.draw_glowing_polygon(self, _ring(dir * CMD_HEX_RADIUS, CMD_STUD_RADIUS, 6, a), c, 0.85) - # Port mast standing proud of the rim; every other one is manned. - var port_tip: Vector2 = dir * (CMD_HEX_RADIUS + CMD_PORT_LENGTH) - NeonDraw.draw_glowing_line(self, dir * CMD_HEX_RADIUS, port_tip, c, 0.8, 0.4) - if i % 2 == 0: - NeonDraw.draw_glowing_polygon(self, _diamond(port_tip, CMD_TOKEN_RADIUS), c, 1.0) - - # Command core: a diamond hub with a lit readout set inside it. - NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_HUB_RADIUS), c, 1.0) - NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9) - - -# --- mothership: a radial flagship ---------------------------------------- -# Not a big fighter. A mothership is the thing everything else orbits, so it -# is built radially around a reactor core rather than pointed in a direction: -# six swept talons, crystal panels set between them, and satellites circling -# the whole assembly. A forward-facing hull, however large, just reads as -# another ship. - -const ARM_COUNT: int = 6 -const ARM_INNER: float = 42.0 -const ARM_OUTER: float = 106.0 -const ARM_SWEEP: float = 0.72 -const SATELLITE_COUNT: int = 4 -const SATELLITE_ORBIT: float = 126.0 -const ORBIT_SPEED: float = 0.32 -const PULSE_SPEED: float = 2.4 - - -func _talon(base_angle: float) -> PackedVector2Array: - # One swept claw: the outer edge runs from the core to the tip, and the - # inner edge comes back a little behind it, closing to a point so the arm - # tapers instead of ending in a blunt stub. - var steps: int = 7 - var pts: PackedVector2Array = PackedVector2Array() - for i in range(steps + 1): - var f: float = float(i) / float(steps) - var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) - pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f)) - for i in range(steps, -1, -1): - var f: float = float(i) / float(steps) - var trail: float = 0.34 * (1.0 - f) + 0.04 - var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) - trail - pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f)) - return pts - - -func _draw_mothership_static(canvas: CanvasItem) -> void: - var c: Color = MOTHERSHIP_COLOR - for i in range(ARM_COUNT): - var base: float = (float(i) / float(ARM_COUNT)) * TAU - NeonDraw.draw_glowing_polygon(canvas, _talon(base), c, 0.95) - # Crystal panel set into the gap behind each arm. - var pa: float = base - 0.30 - NeonDraw.draw_glowing_polygon(canvas, PackedVector2Array([ - Vector2(cos(pa - 0.11), sin(pa - 0.11)) * 54.0, - Vector2(cos(pa + 0.11), sin(pa + 0.11)) * 54.0, - Vector2(cos(pa + 0.15), sin(pa + 0.15)) * 86.0, - Vector2(cos(pa - 0.15), sin(pa - 0.15)) * 86.0, - ]), c, 0.7) - - # Reactor housing: the nested rings around the cell that breathes. - NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 38.0, 6, PI / 6.0), c, 1.0) - NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 24.0, 6, 0.0), c, 0.85) - - -const CORE_CELL_RADIUS: float = 12.0 - - -func _build_mothership_pieces() -> void: - var core_color: Color = Color(0.75, 1.0, 0.85) - - # The breathing cell: drawn at a reference size and then scaled, rather - # than rebuilt at a new radius every frame. - _core_cell = NeonPiece.create(self, Vector2.ZERO, core_color) - _core_cell.add_polygon(_ring(Vector2.ZERO, CORE_CELL_RADIUS, 6, PI / 6.0)) - - # Satellites sit at fixed offsets inside a holder; the orbit is the - # holder's rotation, so four hexagon pairs become one transform update. - _satellites = Node2D.new() - add_child(_satellites) - for i in range(SATELLITE_COUNT): - var a: float = (float(i) / float(SATELLITE_COUNT)) * TAU - var at: Vector2 = Vector2(cos(a), sin(a)) * SATELLITE_ORBIT - var shell: NeonPiece = NeonPiece.create(_satellites, at, MOTHERSHIP_COLOR) - shell.add_polygon(_ring(Vector2.ZERO, 10.0, 6, PI / 6.0), 0.85) - var cell: NeonPiece = NeonPiece.create(_satellites, at, core_color) - cell.add_polygon(_ring(Vector2.ZERO, 4.5, 6, PI / 6.0), 0.8) - - -func _animate_mothership() -> void: - var breath: float = core_pulse() - var radius: float = lerpf(9.0, 15.0, breath) - _core_cell.scale = Vector2.ONE * (radius / CORE_CELL_RADIUS) - _core_cell.modulate.a = lerpf(0.55, 1.0, breath) - _satellites.rotation = orbit_angle() - - -# --- ship crafter: an orbital yard with a hull in the cradle -------------- - -func _draw_crafter_static(canvas: CanvasItem) -> void: - var c: Color = CRAFTER_COLOR - - # Fabricator core housing. This replaces the chimney - nothing vents - # smoke in vacuum. - NeonDraw.draw_glowing_polygon(canvas, _ring(CORE_CENTRE, 26.0, 6, PI / 6.0), c, 1.0) - - # Radiator fins above and below the core, slatted like real ones. - for dir_y in [-1.0, 1.0]: - var fin: PackedVector2Array = PackedVector2Array([ - CORE_CENTRE + Vector2(-16, 30 * dir_y), CORE_CENTRE + Vector2(18, 34 * dir_y), - CORE_CENTRE + Vector2(24, 54 * dir_y), CORE_CENTRE + Vector2(-10, 50 * dir_y), - ]) - NeonDraw.draw_glowing_polygon(canvas, fin, c, 0.75) - for f in [0.35, 0.68]: - NeonDraw.draw_glowing_line(canvas, - fin[0].lerp(fin[3], f), fin[1].lerp(fin[2], f), c, 0.5, 0.28) - - # Truss spine linking the core to the cradle: two rails with X-bracing, - # the standard "this was assembled in orbit" read. - var spine_top: float = 4.0 - var spine_bottom: float = 22.0 - NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_top), Vector2(14, spine_top), c, 0.9, 0.4) - NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_bottom), Vector2(14, spine_bottom), c, 0.9, 0.4) - for i in range(3): - var x0: float = -32.0 + float(i) * 15.0 - var x1: float = x0 + 15.0 - NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_top), Vector2(x1, spine_bottom), c, 0.55, 0.28) - NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_bottom), Vector2(x1, spine_top), c, 0.55, 0.28) - - # Construction cradle: two open ribs holding the hull, with the crane - # rail overhead. Open at the top so the finished hull can clear it. - NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, 2.10, 4.18, 7), c, 1.0, 0.7) - NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, -1.04, 1.04, 7), c, 1.0, 0.7) - NeonDraw.draw_glowing_line(canvas, Vector2(10, RAIL_Y), Vector2(82, RAIL_Y), c, 0.9, 0.55) - for side in [-1.0, 1.0]: - NeonDraw.draw_glowing_line(canvas, - BAY_CENTRE + Vector2(RIB_RADIUS * side, -6.0), Vector2(BAY_CENTRE.x + 44.0 * side, RAIL_Y), c, 0.7, 0.3) - - - -func _build_crafter_pieces() -> void: - var c: Color = CRAFTER_COLOR - - # The core's inner cell: fades with the reactor rather than being redrawn - # at a new brightness each frame. - _core_cell = NeonPiece.create(self, CORE_CENTRE, Color(1.0, 0.9, 0.6)) - _core_cell.add_polygon(_ring(Vector2.ZERO, 12.0, 6, PI / 6.0)) - - # Docking beacons, blinking out of step with each other. - for i in range(2): - var lamp: Vector2 = BAY_CENTRE + Vector2(-RIB_RADIUS + 2.0 + float(i) * (RIB_RADIUS * 2.0 - 4.0), 30.0) - var beacon: NeonPiece = NeonPiece.create(self, lamp, Color(1.0, 0.95, 0.75)) - beacon.add_polygon(_ring(Vector2.ZERO, 3.0, 4)) - _beacons.append(beacon) - - # Clamp arms reaching in from each rib. They let go as the hull launches, - # which is a fade rather than a redraw. - _clamps = NeonPiece.create(self, Vector2.ZERO, c) - for side in [-1.0, 1.0]: - var outer: Vector2 = BAY_CENTRE + Vector2(RIB_RADIUS * side, 6.0) - var inner: Vector2 = BAY_CENTRE + Vector2(24.0 * side, 6.0) - _clamps.add_line(outer, inner, 0.35) - _clamps.add_line(inner + Vector2(0, -5), inner + Vector2(0, 5), 0.3) - - # Crane: the head rides the rail, so only its x moves. The hoist is drawn - # one unit long and stretched with scale.y to reach whatever seam is - # being welded - stretching a line needs no redraw, redrawing it would. - _crane = Node2D.new() - add_child(_crane) - var head: NeonPiece = NeonPiece.create(_crane, Vector2.ZERO, c) - head.add_polygon(PackedVector2Array([ - Vector2(-10, -7), Vector2(10, -7), Vector2(6, 6), Vector2(-6, 6), - ])) - head.add_line(Vector2(0, -7), Vector2(0, RAIL_Y - (RAIL_Y + 13.0)), 0.35, 0.8) - _hoist = NeonPiece.create(_crane, Vector2(0, 6), c, 0.5) - _hoist.add_line(Vector2.ZERO, Vector2(0, 1), 0.3) - - # Weld sparks: four spokes on a holder that spins and stretches. The - # jitter is genuinely procedural, but rotating and scaling a holder gives - # the same flicker without re-tessellating the spokes every frame. - _sparks = Node2D.new() - add_child(_sparks) - for i in range(4): - var a: float = float(i) * TAU / 4.0 - var spark: NeonPiece = NeonPiece.create(_sparks, Vector2.ZERO, Color(1.0, 0.95, 0.7)) - spark.add_line(Vector2.ZERO, Vector2(cos(a), sin(a)) * 11.0, 0.3) - - # The hull under construction. Each part is its own piece so attaching one - # is a `visible` flip, and the launch is the holder's position and alpha. - _hull_holder = Node2D.new() - add_child(_hull_holder) - for i in range(_hull_parts.size()): - var part: NeonPiece = NeonPiece.create(_hull_holder, Vector2.ZERO, HULL_COLOR) - part.add_polygon(_hull_parts[i]) - part.visible = false - _hull_pieces.append(part) - - # Engine wash under the hull, and the completion ring. The ring is drawn - # at a reference radius and scaled outwards. - _wash = NeonPiece.create(_hull_holder, Vector2.ZERO, Color(1.0, 0.85, 0.45), 0.0) - for dx in [-4.0, 4.0]: - _wash.add_line(Vector2(dx, 14), Vector2(dx * 1.6, 30.0), 0.45) - - _flash_ring = NeonPiece.create(self, BAY_CENTRE, HULL_COLOR, 0.0) - _flash_ring.add_polygon(_ring(Vector2.ZERO, FLASH_RING_RADIUS, 6, PI / 6.0)) - - -func _animate_crafter() -> void: - var t: float = anim_time / CYCLE - - _core_cell.modulate.a = 0.45 + 0.55 * (0.5 + 0.5 * sin(phase_time * 4.0)) - for i in range(_beacons.size()): - _beacons[i].modulate.a = 0.15 + 0.45 * (0.5 + 0.5 * sin(phase_time * 6.0 + float(i) * PI)) - - if t < ASSEMBLE_END: - _animate_assembly(t / ASSEMBLE_END) - elif t < FLASH_END: - _animate_completion_flash((t - ASSEMBLE_END) / (FLASH_END - ASSEMBLE_END)) - else: - _animate_launch((t - FLASH_END) / (1.0 - FLASH_END)) - - -# How many pieces are on the hull at this point through the assembly phase. -# Parts attach in order, so this is just how many thresholds have passed. -func attached_part_count(p: float) -> int: - var n: int = 0 - for part in SHIP_PARTS: - if p >= float(part.at): - n += 1 - return n - - -func _place_hull(centre: Vector2, alpha: float) -> void: - # The parts are defined around their own origin, so the holder goes - # straight to the point they should be centred on. - _hull_holder.position = centre - _hull_holder.modulate.a = alpha - - -func _animate_assembly(p: float) -> void: - _clamps.visible = true - _clamps.modulate.a = 0.8 - _flash_ring.modulate.a = 0.0 - _wash.modulate.a = 0.0 - _place_hull(BAY_CENTRE, 1.0) - - var next_part: int = attached_part_count(p) - for i in range(_hull_pieces.size()): - var attached: bool = i < next_part - _hull_pieces[i].visible = attached - if attached: - # Freshly attached parts flare, then settle to full brightness. - var age: float = p - float(SHIP_PARTS[i].at) - _hull_pieces[i].modulate.a = 1.0 if age > 0.08 else lerpf(0.3, 1.0, age / 0.08) - - # The crane rides the rail over whichever seam is being worked, and - # retracts once there is nothing left to attach. - var weld: Vector2 = BAY_CENTRE + (SHIP_PARTS[mini(next_part, SHIP_PARTS.size() - 1)].weld as Vector2) - if next_part >= _hull_pieces.size(): - weld.y = RAIL_Y + 16.0 - _crane.position = Vector2(weld.x, RAIL_Y + 13.0) - _hoist.scale.y = maxf(weld.y - (RAIL_Y + 19.0), 1.0) - - var welding: bool = next_part < _hull_pieces.size() - _sparks.visible = welding - if welding: - var flicker: float = 0.45 + 0.55 * absf(sin(phase_time * 34.0)) - _sparks.position = weld - _sparks.rotation = phase_time * 7.0 - _sparks.scale = Vector2.ONE * (0.55 + 0.45 * flicker) - _sparks.modulate.a = flicker - - -func _animate_completion_flash(p: float) -> void: - # Whole hull, clamps springing open, and a ring pushing outwards to mark - # it as finished. - _clamps.modulate.a = 1.0 - p - _sparks.visible = false - _place_hull(BAY_CENTRE, 1.0) - for piece in _hull_pieces: - piece.visible = true - piece.modulate.a = 1.0 - # Retracted, so it does not hang over the finished hull. - _crane.position = Vector2(BAY_CENTRE.x, RAIL_Y + 13.0) - _hoist.scale.y = 1.0 - _flash_ring.modulate.a = 1.0 - p - _flash_ring.scale = Vector2.ONE * (lerpf(18.0, 56.0, p) / FLASH_RING_RADIUS) - - -func _animate_launch(p: float) -> void: - # Clears the cradle and fades out; the next cycle starts from bare ribs. - _clamps.modulate.a = 0.0 - _sparks.visible = false - _flash_ring.modulate.a = 0.0 - var eased: float = p * p - var fade: float = clampf(1.0 - p * 1.15, 0.0, 1.0) - _place_hull(BAY_CENTRE + Vector2(0.0, -124.0 * eased), fade) - for piece in _hull_pieces: - piece.visible = true - piece.modulate.a = 1.0 - # Engine wash, strongest just after it breaks free of the clamps. - _wash.modulate.a = 0.4 + 0.6 * sin(minf(p * 3.2, PI)) + return String(definition.display_name) if definition != null else definition_id.capitalize() diff --git a/Building.tscn b/Building.tscn index 174c302..3929101 100644 --- a/Building.tscn +++ b/Building.tscn @@ -4,3 +4,5 @@ [node name="Building" type="Node2D"] script = ExtResource("1") + +[node name="VisualHost" type="Node2D" parent="."] diff --git a/BuildingActionProvider.gd b/BuildingActionProvider.gd new file mode 100644 index 0000000..1a9dba8 --- /dev/null +++ b/BuildingActionProvider.gd @@ -0,0 +1,3 @@ +extends Node + +@export var actions: PackedStringArray = [] diff --git a/BuildingActionProvider.gd.uid b/BuildingActionProvider.gd.uid new file mode 100644 index 0000000..e5e07f8 --- /dev/null +++ b/BuildingActionProvider.gd.uid @@ -0,0 +1 @@ +uid://c4dah6xluvenl diff --git a/BuildingDefinition.gd b/BuildingDefinition.gd new file mode 100644 index 0000000..fdc990e --- /dev/null +++ b/BuildingDefinition.gd @@ -0,0 +1,20 @@ +class_name BuildingDefinition +extends Resource + +@export var definition_id: String +@export var display_name: String +@export_multiline var description: String +@export var cost: int +@export var action_icon: int +@export var menu_icon: int +@export var visual_scene: PackedScene +@export var capabilities: PackedStringArray = [] +@export var map_color: Color = Color.WHITE +@export var map_sides: int = 4 +@export var map_radius: float = 7.0 +@export var map_phase: float = 0.0 +@export var menu_button_name: String +@export var menu_label_name: String +@export var menu_cost_name: String +@export_enum("space", "planet") var placement_mode: String = "space" +@export var unique: bool = false diff --git a/BuildingDefinition.gd.uid b/BuildingDefinition.gd.uid new file mode 100644 index 0000000..7473bd0 --- /dev/null +++ b/BuildingDefinition.gd.uid @@ -0,0 +1 @@ +uid://d10s0frtxiw62 diff --git a/BuildingDefinitions.gd b/BuildingDefinitions.gd new file mode 100644 index 0000000..f05bf8e --- /dev/null +++ b/BuildingDefinitions.gd @@ -0,0 +1,21 @@ +class_name BuildingDefinitions +extends RefCounted + +const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "extractor", "transport_hub", "ship_lab"] +const DEFINITIONS: Dictionary = { + "mothership": preload("res://MothershipBuildingDefinition.tres"), + "ship_crafter": preload("res://CrafterBuildingDefinition.tres"), + "command_center": preload("res://CommandBuildingDefinition.tres"), + "extractor": preload("res://ExtractorBuildingDefinition.tres"), + "transport_hub": preload("res://TransportHubBuildingDefinition.tres"), + "ship_lab": preload("res://ShipLabBuildingDefinition.tres"), +} + +static func get_definition(definition_id: String) -> Resource: + return DEFINITIONS.get(definition_id) + +static func all() -> Array: + var result: Array = [] + for definition_id in ORDER: + result.append(DEFINITIONS[definition_id]) + return result diff --git a/BuildingDefinitions.gd.uid b/BuildingDefinitions.gd.uid new file mode 100644 index 0000000..0dead32 --- /dev/null +++ b/BuildingDefinitions.gd.uid @@ -0,0 +1 @@ +uid://cpm0u3gu0exke diff --git a/BuildingField.gd b/BuildingField.gd index 496c26a..09b37bd 100644 --- a/BuildingField.gd +++ b/BuildingField.gd @@ -50,7 +50,7 @@ func get_building_fleet_is_on() -> Node2D: func _spawn(record: Dictionary) -> void: var b: Node2D = building_scene.instantiate() b.position = record.position - b.building_type = record.type + b.definition_id = record.type b.building_id = int(record.get("id", 0)) # Deferred for the same reason as the other field spawners: this can run # from inside a parent's _ready() chain, where a direct add_child() is diff --git a/BuildingVisualBase.gd b/BuildingVisualBase.gd new file mode 100644 index 0000000..cd339b3 --- /dev/null +++ b/BuildingVisualBase.gd @@ -0,0 +1,606 @@ +class_name BuildingVisualBase +extends Node2D + +# A player-placed structure in space. Three kinds for now: +# mothership - where a wiped-out fleet is rebuilt +# ship_crafter - fly the fleet onto it to buy additional ships +# command_center - reconfigure the fleet formation +# +# Buildings are stationary and harmless; whether the fleet is "on" one is +# decided by BuildingField comparing the fleet CENTROID against +# interact_radius, the same stable single-number test Planet uses (see +# Planet.gd for why per-ship overlap was a poor fit). +# +# Nothing here rests on ground or vents into an atmosphere - these are +# orbital structures, so they are built out of trusses, radiator fins, +# reactor cores and open cradles rather than walls and roofs. +# +# The crafter and the mothership animate, so they run _process. The command +# centre is drawn once and never redrawn - per-frame redraws of art that +# never changes were a real framerate cost earlier on. + +const MOTHERSHIP_COLOR: Color = Color(0.45, 0.9, 0.55) +const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25) +const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0) +const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85) +const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9) + +# Ship lab: a test frame with a vessel held in it and instruments sweeping +# over it. Reads as a workshop rather than a factory - the crafter already +# owns "assembly line", so this is measurement, not production. +const LAB_FRAME_W: float = 78.0 +const LAB_FRAME_H: float = 58.0 +const LAB_MOUNT_RADIUS: float = 15.0 +const LAB_SCANNER_ORBIT: float = 96.0 +const LAB_SCAN_SPEED: float = 0.9 + +# Transport hub: an octagonal dock ring with berths around it and a freight +# core in the middle. Deliberately radial-but-flat, so it does not read as a +# small mothership - the mothership is the only thing with swept arms. +const HUB_RING_RADIUS: float = 74.0 +const HUB_DECK_RADIUS: float = 54.0 +const HUB_CORE_RADIUS: float = 22.0 +const HUB_BERTH_COUNT: int = 8 +const HUB_BERTH_LENGTH: float = 20.0 +const HUB_DOCKED_COUNT: int = 3 +const HUB_DOCKED_ORBIT: float = 96.0 +const HUB_SPIN_SPEED: float = 0.22 +# The vessel on the assembly line is drawn in the fleet's own colour, so it +# reads as "one of yours being built" rather than as shipyard machinery. +const HULL_COLOR: Color = Color(0.25, 0.85, 1.0) + +# One assembly cycle: parts are welded on, the finished hull flashes, then +# it clears the cradle and the line starts over. +const CYCLE: float = 3.6 +const ASSEMBLE_END: float = 0.66 # fraction of CYCLE spent welding +const FLASH_END: float = 0.76 # ... then the completion flash + # ... then launch, to 1.0 + +const CORE_CENTRE: Vector2 = Vector2(-58.0, 12.0) +const BAY_CENTRE: Vector2 = Vector2(46.0, 12.0) +const RIB_RADIUS: float = 36.0 +const RAIL_Y: float = -32.0 +const FLASH_RING_RADIUS: float = 20.0 + +# The hull in four pieces, attached in this order. Each carries the point +# the crane welds it on at, which is where the sparks fly from. +const SHIP_PARTS: Array = [ + {"at": 0.04, "weld": Vector2(0, -12), "points": [Vector2(0, -26), Vector2(-8, 4), Vector2(8, 4)]}, + {"at": 0.28, "weld": Vector2(-13, 7), "points": [Vector2(-8, -4), Vector2(-22, 14), Vector2(-8, 10)]}, + {"at": 0.48, "weld": Vector2(13, 7), "points": [Vector2(8, -4), Vector2(22, 14), Vector2(8, 10)]}, + {"at": 0.70, "weld": Vector2(0, 10), "points": [Vector2(-7, 4), Vector2(7, 4), Vector2(5, 14), Vector2(-5, 14)]}, +] + +# The assembly clock, wrapped at CYCLE because the whole build sequence is +# expressed as a fraction of it. +var anim_time: float = 0.0 + +# Free-running seconds for everything that is merely cyclic - reactor pulses, +# orbiting satellites, blinking beacons. These must NOT share anim_time: +# CYCLE is 3.6s, which is not a whole number of periods for any of them, so +# wrapping made every one of them leap at the wrap. The satellites were the +# obvious casualty, snapping back 66 degrees every 3.6 seconds. +# +# Deliberately never wrapped. GDScript floats are 64-bit, so even after hours +# of play the argument to sin() keeps far more precision than a frame needs, +# and any wrap point would reintroduce exactly the discontinuity above. +var phase_time: float = 0.0 + +var _hull_parts: Array[PackedVector2Array] = [] + +# Every moving element is its own NeonPiece, rasterised once when the +# building is created and thereafter only moved, scaled, faded or hidden. +# Nothing here ever calls queue_redraw() again: the building's own _draw() +# lays down the fixed structure a single time, and the animation is pure +# node-property work from then on. +var _core_cell: NeonPiece = null +var _satellites: Node2D = null +var _beacons: Array = [] +var _clamps: NeonPiece = null +var _crane: Node2D = null +var _hoist: NeonPiece = null +var _sparks: Node2D = null +var _hull_holder: Node2D = null +var _hull_pieces: Array = [] +# Rotating holder for the transports stationed at a transport hub (#18). +var _hub_traffic: Node2D = null +# Sweeping instrument booms at a ship lab (#5). +var _lab_scanners: Node2D = null +var _flash_ring: NeonPiece = null +var _wash: NeonPiece = null + + +func _process(delta: float) -> void: + anim_time = fmod(anim_time + delta, CYCLE) + phase_time += delta + apply_animation() + + +# Pushes the current clock onto the movable pieces. Normally driven from +# _process; public so a preview or a test can pin the clock and see exactly +# that frame without having to run the animation up to it in real time. +func apply_animation() -> void: + pass + + +# Where the satellites are, and how open the reactor core is. Both are read +# by _draw and by the test that checks they stay continuous across the +# assembly clock's wrap. +func orbit_angle() -> float: + return phase_time * ORBIT_SPEED + + +func core_pulse() -> float: + return 0.5 + 0.5 * sin(phase_time * PULSE_SPEED) + + +# How many times this building's geometry has actually been rasterised. The +# whole point of the NeonPiece decomposition is that this stays at 1 for the +# building's lifetime however long it animates, so it is worth being able to +# assert rather than assume. +var draw_count: int = 0 + + +func register_draw() -> void: + draw_count += 1 + + +# --- shared helpers ------------------------------------------------------- + +func _ring(centre: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array: + var pts: PackedVector2Array = PackedVector2Array() + for i in range(sides): + var a: float = (float(i) / float(sides)) * TAU + phase + pts.append(centre + Vector2(cos(a), sin(a)) * radius) + return pts + + +func _arc(centre: Vector2, radius: float, from_a: float, to_a: float, steps: int) -> PackedVector2Array: + var pts: PackedVector2Array = PackedVector2Array() + for i in range(steps + 1): + var a: float = lerpf(from_a, to_a, float(i) / float(steps)) + pts.append(centre + Vector2(cos(a), sin(a)) * radius) + return pts + + + + +# --- command centre: a tactical plotting hub ------------------------------ +# The mothership is radial-and-swept, the crafter is an asymmetric assembly +# line, so the command centre reads best as the odd one out: rigid, hexagonal, +# a wheel rather than a claw. Six spokes tie a hex rim to a central diamond +# readout, and every spoke ends in a formation port - half of them holding a +# token, half left open - so the structure itself shows fleet slots waiting +# to be filled, which is exactly what the drag-and-drop editor inside does. + +const CMD_HEX_RADIUS: float = 88.0 +const CMD_DECK_RADIUS: float = 66.0 +const CMD_HUB_RADIUS: float = 30.0 +const CMD_SCREEN_RADIUS: float = 14.0 +const CMD_PORT_COUNT: int = 6 +const CMD_PORT_LENGTH: float = 16.0 +const CMD_TOKEN_RADIUS: float = 9.0 +const CMD_STUD_RADIUS: float = 7.0 + + +# --- ship lab ------------------------------------------------------------- + +func _draw_ship_lab_static(target: CanvasItem) -> void: + var c: Color = SHIP_LAB_COLOR + + # Outer test frame, open at the corners so it reads as a rig rather than + # a solid hull. + for sx in [-1.0, 1.0]: + for sy in [-1.0, 1.0]: + var corner: Vector2 = Vector2(sx * LAB_FRAME_W, sy * LAB_FRAME_H) + NeonDraw.draw_glowing_line(target, corner, + corner - Vector2(sx * LAB_FRAME_W * 0.45, 0.0), c, 0.9, 0.42) + NeonDraw.draw_glowing_line(target, corner, + corner - Vector2(0.0, sy * LAB_FRAME_H * 0.5), c, 0.9, 0.42) + + # The vessel under test, held nose-up in the middle - deliberately the + # fleet's own silhouette, so it is obvious what the lab is for. + NeonDraw.draw_glowing_polygon(target, PackedVector2Array([ + Vector2(0, -26), Vector2(13, 12), Vector2(0, 5), Vector2(-13, 12), + ]), HULL_COLOR, 0.95) + + # Mount ring and its bracing. + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, LAB_MOUNT_RADIUS, 6, PI / 6.0), c, 0.5, + NeonDraw.DEFAULT_CORE, 0.7) + for i in range(4): + var a: float = (float(i) / 4.0) * TAU + PI / 4.0 + var dir: Vector2 = Vector2(cos(a), sin(a)) + NeonDraw.draw_glowing_line(target, dir * LAB_MOUNT_RADIUS, dir * 40.0, c, 0.45, 0.3) + + +func _build_lab_pieces() -> void: + # Two instrument booms sweeping round the vessel. One holder, rasterised + # once, rotated per frame (#10). + _lab_scanners = Node2D.new() + add_child(_lab_scanners) + for i in range(2): + var a: float = float(i) * PI + var at: Vector2 = Vector2(cos(a), sin(a)) * LAB_SCANNER_ORBIT + var piece: NeonPiece = NeonPiece.create(_lab_scanners, at, SHIP_LAB_COLOR) + piece.add_polygon(PackedVector2Array([ + Vector2(-4, -12), Vector2(4, -12), Vector2(4, 12), Vector2(-4, 12), + ])) + piece.add_line(Vector2(0, 0), Vector2(-cos(a), -sin(a)) * 26.0, 0.3, 0.5) + piece.rotation = a + piece.queue_redraw() + + +func _animate_ship_lab() -> void: + _lab_scanners.rotation = phase_time * LAB_SCAN_SPEED + + +# --- transport hub -------------------------------------------------------- + +func _draw_transport_hub_static(target: CanvasItem) -> void: + var c: Color = TRANSPORT_HUB_COLOR + + # Dock ring and the deck nested inside it. + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_RING_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 1.0) + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_DECK_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 0.45, NeonDraw.DEFAULT_CORE, 0.7) + + # Freight core: a squared-off hold rather than a reactor, so it reads as + # storage and not as another power plant. + NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_CORE_RADIUS, 4, PI / 4.0), c, 0.95) + + for i in range(HUB_BERTH_COUNT): + var a: float = (float(i) / HUB_BERTH_COUNT) * TAU + PI / 8.0 + var dir: Vector2 = Vector2(cos(a), sin(a)) + NeonDraw.draw_glowing_line(target, dir * HUB_CORE_RADIUS, dir * HUB_DECK_RADIUS, c, 0.5, 0.32) + # Berth arm standing off the ring, where a transporter parks. + NeonDraw.draw_glowing_line(target, dir * HUB_RING_RADIUS, + dir * (HUB_RING_RADIUS + HUB_BERTH_LENGTH), c, 0.75, 0.38) + + +func _build_hub_pieces() -> void: + # Stationed transports, as NeonPiece children of one holder that turns. + # Per #10 the geometry is rasterised once and only the holder's rotation + # changes, so the traffic costs no redraws at all. + _hub_traffic = Node2D.new() + add_child(_hub_traffic) + for i in range(HUB_DOCKED_COUNT): + var a: float = (float(i) / HUB_DOCKED_COUNT) * TAU + var at: Vector2 = Vector2(cos(a), sin(a)) * HUB_DOCKED_ORBIT + var piece: NeonPiece = NeonPiece.create(_hub_traffic, at, TRANSPORT_HUB_COLOR) + # A stubby freighter: blunt, boxy, nothing like the fleet's darts. + piece.add_polygon(PackedVector2Array([ + Vector2(-9, -5), Vector2(9, -5), Vector2(12, 0), Vector2(9, 5), Vector2(-9, 5), + ])) + piece.rotation = a + PI / 2.0 + piece.queue_redraw() + + +func _animate_transport_hub() -> void: + _hub_traffic.rotation = phase_time * HUB_SPIN_SPEED + + +func _diamond(centre: Vector2, r: float) -> PackedVector2Array: + return PackedVector2Array([ + centre + Vector2(0, -r), centre + Vector2(r, 0), + centre + Vector2(0, r), centre + Vector2(-r, 0), + ]) + + +func _draw_command_center() -> void: + var c: Color = COMMAND_COLOR + var readout: Color = Color(0.82, 0.86, 1.0) + + # The rim: a hex table holding the whole layout together, with a fainter + # deck ring nested just inside it - the same "plating has layers" read the + # mothership gets from its nested reactor rings. + NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_HEX_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 1.0) + NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_DECK_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 0.5, NeonDraw.DEFAULT_CORE, 0.7) + + for i in range(CMD_PORT_COUNT): + var a: float = (float(i) / CMD_PORT_COUNT) * TAU + PI / 6.0 + var dir: Vector2 = Vector2(cos(a), sin(a)) + # Spoke bracing the hub to the rim. + NeonDraw.draw_glowing_line(self, dir * CMD_HUB_RADIUS, dir * CMD_HEX_RADIUS, c, 0.65, 0.36) + # Socket where the mast plugs into the rim. + NeonDraw.draw_glowing_polygon(self, _ring(dir * CMD_HEX_RADIUS, CMD_STUD_RADIUS, 6, a), c, 0.85) + # Port mast standing proud of the rim; every other one is manned. + var port_tip: Vector2 = dir * (CMD_HEX_RADIUS + CMD_PORT_LENGTH) + NeonDraw.draw_glowing_line(self, dir * CMD_HEX_RADIUS, port_tip, c, 0.8, 0.4) + if i % 2 == 0: + NeonDraw.draw_glowing_polygon(self, _diamond(port_tip, CMD_TOKEN_RADIUS), c, 1.0) + + # Command core: a diamond hub with a lit readout set inside it. + NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_HUB_RADIUS), c, 1.0) + NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9) + + +# --- mothership: a radial flagship ---------------------------------------- +# Not a big fighter. A mothership is the thing everything else orbits, so it +# is built radially around a reactor core rather than pointed in a direction: +# six swept talons, crystal panels set between them, and satellites circling +# the whole assembly. A forward-facing hull, however large, just reads as +# another ship. + +const ARM_COUNT: int = 6 +const ARM_INNER: float = 42.0 +const ARM_OUTER: float = 106.0 +const ARM_SWEEP: float = 0.72 +const SATELLITE_COUNT: int = 4 +const SATELLITE_ORBIT: float = 126.0 +const ORBIT_SPEED: float = 0.32 +const PULSE_SPEED: float = 2.4 + + +func _talon(base_angle: float) -> PackedVector2Array: + # One swept claw: the outer edge runs from the core to the tip, and the + # inner edge comes back a little behind it, closing to a point so the arm + # tapers instead of ending in a blunt stub. + var steps: int = 7 + var pts: PackedVector2Array = PackedVector2Array() + for i in range(steps + 1): + var f: float = float(i) / float(steps) + var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) + pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f)) + for i in range(steps, -1, -1): + var f: float = float(i) / float(steps) + var trail: float = 0.34 * (1.0 - f) + 0.04 + var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) - trail + pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f)) + return pts + + +func _draw_mothership_static(canvas: CanvasItem) -> void: + var c: Color = MOTHERSHIP_COLOR + for i in range(ARM_COUNT): + var base: float = (float(i) / float(ARM_COUNT)) * TAU + NeonDraw.draw_glowing_polygon(canvas, _talon(base), c, 0.95) + # Crystal panel set into the gap behind each arm. + var pa: float = base - 0.30 + NeonDraw.draw_glowing_polygon(canvas, PackedVector2Array([ + Vector2(cos(pa - 0.11), sin(pa - 0.11)) * 54.0, + Vector2(cos(pa + 0.11), sin(pa + 0.11)) * 54.0, + Vector2(cos(pa + 0.15), sin(pa + 0.15)) * 86.0, + Vector2(cos(pa - 0.15), sin(pa - 0.15)) * 86.0, + ]), c, 0.7) + + # Reactor housing: the nested rings around the cell that breathes. + NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 38.0, 6, PI / 6.0), c, 1.0) + NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 24.0, 6, 0.0), c, 0.85) + + +const CORE_CELL_RADIUS: float = 12.0 + + +func _build_mothership_pieces() -> void: + var core_color: Color = Color(0.75, 1.0, 0.85) + + # The breathing cell: drawn at a reference size and then scaled, rather + # than rebuilt at a new radius every frame. + _core_cell = NeonPiece.create(self, Vector2.ZERO, core_color) + _core_cell.add_polygon(_ring(Vector2.ZERO, CORE_CELL_RADIUS, 6, PI / 6.0)) + + # Satellites sit at fixed offsets inside a holder; the orbit is the + # holder's rotation, so four hexagon pairs become one transform update. + _satellites = Node2D.new() + add_child(_satellites) + for i in range(SATELLITE_COUNT): + var a: float = (float(i) / float(SATELLITE_COUNT)) * TAU + var at: Vector2 = Vector2(cos(a), sin(a)) * SATELLITE_ORBIT + var shell: NeonPiece = NeonPiece.create(_satellites, at, MOTHERSHIP_COLOR) + shell.add_polygon(_ring(Vector2.ZERO, 10.0, 6, PI / 6.0), 0.85) + var cell: NeonPiece = NeonPiece.create(_satellites, at, core_color) + cell.add_polygon(_ring(Vector2.ZERO, 4.5, 6, PI / 6.0), 0.8) + + +func _animate_mothership() -> void: + var breath: float = core_pulse() + var radius: float = lerpf(9.0, 15.0, breath) + _core_cell.scale = Vector2.ONE * (radius / CORE_CELL_RADIUS) + _core_cell.modulate.a = lerpf(0.55, 1.0, breath) + _satellites.rotation = orbit_angle() + + +# --- ship crafter: an orbital yard with a hull in the cradle -------------- + +func _draw_crafter_static(canvas: CanvasItem) -> void: + var c: Color = CRAFTER_COLOR + + # Fabricator core housing. This replaces the chimney - nothing vents + # smoke in vacuum. + NeonDraw.draw_glowing_polygon(canvas, _ring(CORE_CENTRE, 26.0, 6, PI / 6.0), c, 1.0) + + # Radiator fins above and below the core, slatted like real ones. + for dir_y in [-1.0, 1.0]: + var fin: PackedVector2Array = PackedVector2Array([ + CORE_CENTRE + Vector2(-16, 30 * dir_y), CORE_CENTRE + Vector2(18, 34 * dir_y), + CORE_CENTRE + Vector2(24, 54 * dir_y), CORE_CENTRE + Vector2(-10, 50 * dir_y), + ]) + NeonDraw.draw_glowing_polygon(canvas, fin, c, 0.75) + for f in [0.35, 0.68]: + NeonDraw.draw_glowing_line(canvas, + fin[0].lerp(fin[3], f), fin[1].lerp(fin[2], f), c, 0.5, 0.28) + + # Truss spine linking the core to the cradle: two rails with X-bracing, + # the standard "this was assembled in orbit" read. + var spine_top: float = 4.0 + var spine_bottom: float = 22.0 + NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_top), Vector2(14, spine_top), c, 0.9, 0.4) + NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_bottom), Vector2(14, spine_bottom), c, 0.9, 0.4) + for i in range(3): + var x0: float = -32.0 + float(i) * 15.0 + var x1: float = x0 + 15.0 + NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_top), Vector2(x1, spine_bottom), c, 0.55, 0.28) + NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_bottom), Vector2(x1, spine_top), c, 0.55, 0.28) + + # Construction cradle: two open ribs holding the hull, with the crane + # rail overhead. Open at the top so the finished hull can clear it. + NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, 2.10, 4.18, 7), c, 1.0, 0.7) + NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, -1.04, 1.04, 7), c, 1.0, 0.7) + NeonDraw.draw_glowing_line(canvas, Vector2(10, RAIL_Y), Vector2(82, RAIL_Y), c, 0.9, 0.55) + for side in [-1.0, 1.0]: + NeonDraw.draw_glowing_line(canvas, + BAY_CENTRE + Vector2(RIB_RADIUS * side, -6.0), Vector2(BAY_CENTRE.x + 44.0 * side, RAIL_Y), c, 0.7, 0.3) + + + +func _build_crafter_pieces() -> void: + var c: Color = CRAFTER_COLOR + + # The core's inner cell: fades with the reactor rather than being redrawn + # at a new brightness each frame. + _core_cell = NeonPiece.create(self, CORE_CENTRE, Color(1.0, 0.9, 0.6)) + _core_cell.add_polygon(_ring(Vector2.ZERO, 12.0, 6, PI / 6.0)) + + # Docking beacons, blinking out of step with each other. + for i in range(2): + var lamp: Vector2 = BAY_CENTRE + Vector2(-RIB_RADIUS + 2.0 + float(i) * (RIB_RADIUS * 2.0 - 4.0), 30.0) + var beacon: NeonPiece = NeonPiece.create(self, lamp, Color(1.0, 0.95, 0.75)) + beacon.add_polygon(_ring(Vector2.ZERO, 3.0, 4)) + _beacons.append(beacon) + + # Clamp arms reaching in from each rib. They let go as the hull launches, + # which is a fade rather than a redraw. + _clamps = NeonPiece.create(self, Vector2.ZERO, c) + for side in [-1.0, 1.0]: + var outer: Vector2 = BAY_CENTRE + Vector2(RIB_RADIUS * side, 6.0) + var inner: Vector2 = BAY_CENTRE + Vector2(24.0 * side, 6.0) + _clamps.add_line(outer, inner, 0.35) + _clamps.add_line(inner + Vector2(0, -5), inner + Vector2(0, 5), 0.3) + + # Crane: the head rides the rail, so only its x moves. The hoist is drawn + # one unit long and stretched with scale.y to reach whatever seam is + # being welded - stretching a line needs no redraw, redrawing it would. + _crane = Node2D.new() + add_child(_crane) + var head: NeonPiece = NeonPiece.create(_crane, Vector2.ZERO, c) + head.add_polygon(PackedVector2Array([ + Vector2(-10, -7), Vector2(10, -7), Vector2(6, 6), Vector2(-6, 6), + ])) + head.add_line(Vector2(0, -7), Vector2(0, RAIL_Y - (RAIL_Y + 13.0)), 0.35, 0.8) + _hoist = NeonPiece.create(_crane, Vector2(0, 6), c, 0.5) + _hoist.add_line(Vector2.ZERO, Vector2(0, 1), 0.3) + + # Weld sparks: four spokes on a holder that spins and stretches. The + # jitter is genuinely procedural, but rotating and scaling a holder gives + # the same flicker without re-tessellating the spokes every frame. + _sparks = Node2D.new() + add_child(_sparks) + for i in range(4): + var a: float = float(i) * TAU / 4.0 + var spark: NeonPiece = NeonPiece.create(_sparks, Vector2.ZERO, Color(1.0, 0.95, 0.7)) + spark.add_line(Vector2.ZERO, Vector2(cos(a), sin(a)) * 11.0, 0.3) + + # The hull under construction. Each part is its own piece so attaching one + # is a `visible` flip, and the launch is the holder's position and alpha. + _hull_holder = Node2D.new() + add_child(_hull_holder) + for i in range(_hull_parts.size()): + var part: NeonPiece = NeonPiece.create(_hull_holder, Vector2.ZERO, HULL_COLOR) + part.add_polygon(_hull_parts[i]) + part.visible = false + _hull_pieces.append(part) + + # Engine wash under the hull, and the completion ring. The ring is drawn + # at a reference radius and scaled outwards. + _wash = NeonPiece.create(_hull_holder, Vector2.ZERO, Color(1.0, 0.85, 0.45), 0.0) + for dx in [-4.0, 4.0]: + _wash.add_line(Vector2(dx, 14), Vector2(dx * 1.6, 30.0), 0.45) + + _flash_ring = NeonPiece.create(self, BAY_CENTRE, HULL_COLOR, 0.0) + _flash_ring.add_polygon(_ring(Vector2.ZERO, FLASH_RING_RADIUS, 6, PI / 6.0)) + + +func _animate_crafter() -> void: + var t: float = anim_time / CYCLE + + _core_cell.modulate.a = 0.45 + 0.55 * (0.5 + 0.5 * sin(phase_time * 4.0)) + for i in range(_beacons.size()): + _beacons[i].modulate.a = 0.15 + 0.45 * (0.5 + 0.5 * sin(phase_time * 6.0 + float(i) * PI)) + + if t < ASSEMBLE_END: + _animate_assembly(t / ASSEMBLE_END) + elif t < FLASH_END: + _animate_completion_flash((t - ASSEMBLE_END) / (FLASH_END - ASSEMBLE_END)) + else: + _animate_launch((t - FLASH_END) / (1.0 - FLASH_END)) + + +# How many pieces are on the hull at this point through the assembly phase. +# Parts attach in order, so this is just how many thresholds have passed. +func attached_part_count(p: float) -> int: + var n: int = 0 + for part in SHIP_PARTS: + if p >= float(part.at): + n += 1 + return n + + +func _place_hull(centre: Vector2, alpha: float) -> void: + # The parts are defined around their own origin, so the holder goes + # straight to the point they should be centred on. + _hull_holder.position = centre + _hull_holder.modulate.a = alpha + + +func _animate_assembly(p: float) -> void: + _clamps.visible = true + _clamps.modulate.a = 0.8 + _flash_ring.modulate.a = 0.0 + _wash.modulate.a = 0.0 + _place_hull(BAY_CENTRE, 1.0) + + var next_part: int = attached_part_count(p) + for i in range(_hull_pieces.size()): + var attached: bool = i < next_part + _hull_pieces[i].visible = attached + if attached: + # Freshly attached parts flare, then settle to full brightness. + var age: float = p - float(SHIP_PARTS[i].at) + _hull_pieces[i].modulate.a = 1.0 if age > 0.08 else lerpf(0.3, 1.0, age / 0.08) + + # The crane rides the rail over whichever seam is being worked, and + # retracts once there is nothing left to attach. + var weld: Vector2 = BAY_CENTRE + (SHIP_PARTS[mini(next_part, SHIP_PARTS.size() - 1)].weld as Vector2) + if next_part >= _hull_pieces.size(): + weld.y = RAIL_Y + 16.0 + _crane.position = Vector2(weld.x, RAIL_Y + 13.0) + _hoist.scale.y = maxf(weld.y - (RAIL_Y + 19.0), 1.0) + + var welding: bool = next_part < _hull_pieces.size() + _sparks.visible = welding + if welding: + var flicker: float = 0.45 + 0.55 * absf(sin(phase_time * 34.0)) + _sparks.position = weld + _sparks.rotation = phase_time * 7.0 + _sparks.scale = Vector2.ONE * (0.55 + 0.45 * flicker) + _sparks.modulate.a = flicker + + +func _animate_completion_flash(p: float) -> void: + # Whole hull, clamps springing open, and a ring pushing outwards to mark + # it as finished. + _clamps.modulate.a = 1.0 - p + _sparks.visible = false + _place_hull(BAY_CENTRE, 1.0) + for piece in _hull_pieces: + piece.visible = true + piece.modulate.a = 1.0 + # Retracted, so it does not hang over the finished hull. + _crane.position = Vector2(BAY_CENTRE.x, RAIL_Y + 13.0) + _hoist.scale.y = 1.0 + _flash_ring.modulate.a = 1.0 - p + _flash_ring.scale = Vector2.ONE * (lerpf(18.0, 56.0, p) / FLASH_RING_RADIUS) + + +func _animate_launch(p: float) -> void: + # Clears the cradle and fades out; the next cycle starts from bare ribs. + _clamps.modulate.a = 0.0 + _sparks.visible = false + _flash_ring.modulate.a = 0.0 + var eased: float = p * p + var fade: float = clampf(1.0 - p * 1.15, 0.0, 1.0) + _place_hull(BAY_CENTRE + Vector2(0.0, -124.0 * eased), fade) + for piece in _hull_pieces: + piece.visible = true + piece.modulate.a = 1.0 + # Engine wash, strongest just after it breaks free of the clamps. + _wash.modulate.a = 0.4 + 0.6 * sin(minf(p * 3.2, PI)) diff --git a/BuildingVisualBase.gd.uid b/BuildingVisualBase.gd.uid new file mode 100644 index 0000000..8827724 --- /dev/null +++ b/BuildingVisualBase.gd.uid @@ -0,0 +1 @@ +uid://p7vuv1xsksc diff --git a/CommandBuildingDefinition.tres b/CommandBuildingDefinition.tres new file mode 100644 index 0000000..727a6f4 --- /dev/null +++ b/CommandBuildingDefinition.tres @@ -0,0 +1,20 @@ +[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://CommandBuildingVisual.tscn" id="2"] +[resource] +script = ExtResource("1") +definition_id = "command_center" +display_name = "Command Center" +description = "Fly the fleet onto it to drag your fleet into a new formation. Rebuilt fleets keep it." +cost = 15 +action_icon = 10 +menu_icon = 10 +visual_scene = ExtResource("2") +capabilities = PackedStringArray("formation") +map_color = Color(0.55, 0.6, 1, 1) +map_sides = 4 +map_radius = 6.0 +map_phase = 0.785398 +menu_button_name = "CommandButton" +menu_label_name = "CommandLabel" +menu_cost_name = "CommandCost" diff --git a/CommandBuildingVisual.gd b/CommandBuildingVisual.gd new file mode 100644 index 0000000..713b407 --- /dev/null +++ b/CommandBuildingVisual.gd @@ -0,0 +1,9 @@ +extends "res://BuildingVisualBase.gd" + +func _ready() -> void: + set_process(false) + queue_redraw() + +func _draw() -> void: + register_draw() + _draw_command_center() diff --git a/CommandBuildingVisual.gd.uid b/CommandBuildingVisual.gd.uid new file mode 100644 index 0000000..9a270e2 --- /dev/null +++ b/CommandBuildingVisual.gd.uid @@ -0,0 +1 @@ +uid://bpb5pq8g56ywn diff --git a/CommandBuildingVisual.tscn b/CommandBuildingVisual.tscn new file mode 100644 index 0000000..048b3f3 --- /dev/null +++ b/CommandBuildingVisual.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://CommandBuildingVisual.gd" id="1"] +[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") + +[node name="ActionProvider" type="Node" parent="."] +script = ExtResource("2") +actions = PackedStringArray("formation", "transfer") diff --git a/CrafterBuildingDefinition.tres b/CrafterBuildingDefinition.tres new file mode 100644 index 0000000..2a3f91f --- /dev/null +++ b/CrafterBuildingDefinition.tres @@ -0,0 +1,19 @@ +[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://CrafterBuildingVisual.tscn" id="2"] +[resource] +script = ExtResource("1") +definition_id = "ship_crafter" +display_name = "Ship Crafter" +description = "Fly the fleet onto it, then use the action button to build extra vessels." +cost = 12 +action_icon = 2 +menu_icon = 9 +visual_scene = ExtResource("2") +capabilities = PackedStringArray("production") +map_color = Color(1, 0.72, 0.25, 1) +map_sides = 4 +map_radius = 7.0 +menu_button_name = "CrafterButton" +menu_label_name = "CrafterLabel" +menu_cost_name = "CrafterCost" diff --git a/CrafterBuildingVisual.gd b/CrafterBuildingVisual.gd new file mode 100644 index 0000000..ddd41a8 --- /dev/null +++ b/CrafterBuildingVisual.gd @@ -0,0 +1,14 @@ +extends "res://BuildingVisualBase.gd" + +func _ready() -> void: + for part in SHIP_PARTS: + _hull_parts.append(PackedVector2Array(part.points)) + _build_crafter_pieces() + queue_redraw() + +func apply_animation() -> void: + _animate_crafter() + +func _draw() -> void: + register_draw() + _draw_crafter_static(self) diff --git a/CrafterBuildingVisual.gd.uid b/CrafterBuildingVisual.gd.uid new file mode 100644 index 0000000..05576b0 --- /dev/null +++ b/CrafterBuildingVisual.gd.uid @@ -0,0 +1 @@ +uid://dbwebd1vf7biy diff --git a/CrafterBuildingVisual.tscn b/CrafterBuildingVisual.tscn new file mode 100644 index 0000000..c3bb670 --- /dev/null +++ b/CrafterBuildingVisual.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://CrafterBuildingVisual.gd" id="1"] +[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") + +[node name="ActionProvider" type="Node" parent="."] +script = ExtResource("2") +actions = PackedStringArray("craft", "transfer") diff --git a/ExtractorBuildingDefinition.tres b/ExtractorBuildingDefinition.tres new file mode 100644 index 0000000..0584758 --- /dev/null +++ b/ExtractorBuildingDefinition.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"] +[resource] +script = ExtResource("1") +definition_id = "extractor" +display_name = "Extractor" +description = "Mines resources into local storage for transport vessels to collect. It keeps extracting while you are away." +cost = 25 +action_icon = 11 +menu_icon = 11 +capabilities = PackedStringArray("extraction") +map_color = Color(1, 0.85, 0.3, 1) +menu_button_name = "ExtractorButton" +menu_label_name = "ExtractorLabel" +menu_cost_name = "ExtractorCost" +placement_mode = "planet" diff --git a/GameState.gd b/GameState.gd index 500ec5d..56f773f 100644 --- a/GameState.gd +++ b/GameState.gd @@ -2,6 +2,7 @@ extends Node const INVENTORY = preload("res://Inventory.gd") const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") +const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") # Autoload singleton - the one thing that survives change_scene_to_file() @@ -104,7 +105,7 @@ func building_inventory(id: int) -> RefCounted: var building: Dictionary = find_building(id) if building.is_empty(): return null - if building.type == BUILDING_MOTHERSHIP: + if building_has_capability(building, "bank"): return bank_inventory return _inventory_for_record(building) @@ -313,11 +314,6 @@ func unlock(id: String) -> bool: func is_building_unlocked(type: String) -> bool: - # The mothership is always buildable: it is where research happens, so - # locking it would leave a player with no mothership unable to ever get - # one. - if type == BUILDING_MOTHERSHIP: - return true for node in SKILL_TREE: if node.get("unlocks", "") == type: return is_unlocked(node.id) @@ -392,6 +388,13 @@ func find_building(id: int) -> Dictionary: return {} +func building_has_capability(building: Dictionary, capability: String) -> bool: + if building.is_empty(): + return false + var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(building.type)) + return definition != null and capability in definition.capabilities + + # --- vessel types and their upgrades (#5) --------------------------------- # The three things that can be flying around. The first and last double as @@ -494,7 +497,7 @@ signal vessel_upgraded(upgrade_id: String) func has_ship_lab() -> bool: for b in buildings: - if b.type == BUILDING_SHIP_LAB: + if building_has_capability(b, "vessel_research"): return true return false @@ -627,9 +630,8 @@ func product_cost(product: String) -> int: func can_produce(type: String) -> bool: - # Only the ship crafter builds anything. The hub STATIONS transporters, - # it does not make them (#12). - return type == BUILDING_SHIP_CRAFTER + var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) + return definition != null and "production" in definition.capabilities func building_stock(id: int) -> int: @@ -777,7 +779,7 @@ func _advance_production(delta: float) -> void: func queue_lab_research(lab_id: int, upgrade_id: String) -> bool: var lab: Dictionary = find_building(lab_id) - if lab.is_empty() or lab.type != BUILDING_SHIP_LAB or upgrade_id == "": + if not building_has_capability(lab, "vessel_research") or upgrade_id == "": return false var orders: Array = lab.get("research_orders", []) if not orders.is_empty() or (upgrade_id != UNLOCK_SHIELD_VESSEL and upgrade_maxed(upgrade_id)): @@ -795,7 +797,7 @@ func lab_research_order(lab_id: int) -> Dictionary: func _advance_lab_research(delta: float) -> void: for lab in buildings: - if lab.type != BUILDING_SHIP_LAB: + if not building_has_capability(lab, "vessel_research"): continue var orders: Array = lab.get("research_orders", []) if orders.is_empty(): @@ -841,6 +843,13 @@ func has_mothership() -> bool: return get_mothership_position() != null +func has_building_of_type(type: String) -> bool: + for building in buildings: + if building.type == type: + return true + return false + + func remove_buildings_of_type(type: String) -> int: var kept: Array = [] var removed: int = 0 @@ -857,7 +866,7 @@ func get_mothership_position() -> Variant: # Where a wiped-out fleet gets rebuilt. Returns null when no mothership # has been built yet, so the caller can fall back to the world origin. for b in buildings: - if b.type == BUILDING_MOTHERSHIP: + if building_has_capability(b, "bank"): return b.position return null @@ -1017,7 +1026,7 @@ func hub_capacity() -> int: func hub_count() -> int: var n: int = 0 for b in buildings: - if b.type == BUILDING_TRANSPORT_HUB: + if building_has_capability(b, "transport_hub"): n += 1 return n @@ -1056,7 +1065,7 @@ func nearest_hub_to(at: Vector2) -> Dictionary: var best: Dictionary = {} var best_d: float = INF for b in buildings: - if b.type != BUILDING_TRANSPORT_HUB: + if not building_has_capability(b, "transport_hub"): continue var d: float = at.distance_squared_to(b.position) if d < best_d: diff --git a/MothershipBuildingDefinition.tres b/MothershipBuildingDefinition.tres new file mode 100644 index 0000000..a032777 --- /dev/null +++ b/MothershipBuildingDefinition.tres @@ -0,0 +1,21 @@ +[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://MothershipBuildingVisual.tscn" id="2"] +[resource] +script = ExtResource("1") +definition_id = "mothership" +display_name = "Mothership" +description = "Fleet home port. A wiped-out fleet rebuilds here, and extractor transports deliver resources here." +cost = 20 +action_icon = 8 +menu_icon = 8 +visual_scene = ExtResource("2") +capabilities = PackedStringArray("bank", "research", "hangar") +map_color = Color(0.45, 0.9, 0.55, 1) +map_sides = 6 +map_radius = 8.0 +map_phase = 0.523599 +menu_button_name = "MothershipButton" +menu_label_name = "MothershipLabel" +menu_cost_name = "MothershipCost" +unique = true diff --git a/MothershipBuildingVisual.gd b/MothershipBuildingVisual.gd new file mode 100644 index 0000000..ba2f617 --- /dev/null +++ b/MothershipBuildingVisual.gd @@ -0,0 +1,12 @@ +extends "res://BuildingVisualBase.gd" + +func _ready() -> void: + _build_mothership_pieces() + queue_redraw() + +func apply_animation() -> void: + _animate_mothership() + +func _draw() -> void: + register_draw() + _draw_mothership_static(self) diff --git a/MothershipBuildingVisual.gd.uid b/MothershipBuildingVisual.gd.uid new file mode 100644 index 0000000..7ac3669 --- /dev/null +++ b/MothershipBuildingVisual.gd.uid @@ -0,0 +1 @@ +uid://m43bwdwhqgy4 diff --git a/MothershipBuildingVisual.tscn b/MothershipBuildingVisual.tscn new file mode 100644 index 0000000..0cc3615 --- /dev/null +++ b/MothershipBuildingVisual.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://MothershipBuildingVisual.gd" id="1"] +[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") + +[node name="ActionProvider" type="Node" parent="."] +script = ExtResource("2") +actions = PackedStringArray("research", "transfer", "hangar") diff --git a/ShipLabBuildingDefinition.tres b/ShipLabBuildingDefinition.tres new file mode 100644 index 0000000..0823f52 --- /dev/null +++ b/ShipLabBuildingDefinition.tres @@ -0,0 +1,20 @@ +[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://ShipLabBuildingVisual.tscn" id="2"] +[resource] +script = ExtResource("1") +definition_id = "ship_lab" +display_name = "Ship Lab" +description = "Fly the fleet onto it to upgrade each vessel type and unlock new vessel designs." +cost = 35 +action_icon = 13 +menu_icon = 13 +visual_scene = ExtResource("2") +capabilities = PackedStringArray("vessel_research") +map_color = Color(0.55, 0.95, 0.9, 1) +map_sides = 3 +map_radius = 7.5 +map_phase = -1.570796 +menu_button_name = "ShipLabButton" +menu_label_name = "ShipLabLabel" +menu_cost_name = "ShipLabCost" diff --git a/ShipLabBuildingVisual.gd b/ShipLabBuildingVisual.gd new file mode 100644 index 0000000..9655162 --- /dev/null +++ b/ShipLabBuildingVisual.gd @@ -0,0 +1,12 @@ +extends "res://BuildingVisualBase.gd" + +func _ready() -> void: + _build_lab_pieces() + queue_redraw() + +func apply_animation() -> void: + _animate_ship_lab() + +func _draw() -> void: + register_draw() + _draw_ship_lab_static(self) diff --git a/ShipLabBuildingVisual.gd.uid b/ShipLabBuildingVisual.gd.uid new file mode 100644 index 0000000..fbb5203 --- /dev/null +++ b/ShipLabBuildingVisual.gd.uid @@ -0,0 +1 @@ +uid://b0p7p0fs0th0b diff --git a/ShipLabBuildingVisual.tscn b/ShipLabBuildingVisual.tscn new file mode 100644 index 0000000..e71fdf2 --- /dev/null +++ b/ShipLabBuildingVisual.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://ShipLabBuildingVisual.gd" id="1"] +[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") + +[node name="ActionProvider" type="Node" parent="."] +script = ExtResource("2") +actions = PackedStringArray("lab", "transfer") diff --git a/TransportHubBuildingDefinition.tres b/TransportHubBuildingDefinition.tres new file mode 100644 index 0000000..59ad15d --- /dev/null +++ b/TransportHubBuildingDefinition.tres @@ -0,0 +1,20 @@ +[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://TransportHubBuildingVisual.tscn" id="2"] +[resource] +script = ExtResource("1") +definition_id = "transport_hub" +display_name = "Transport Hub" +description = "Stations transport vessels. They collect extracted resources and supply buildings that are waiting on materials." +cost = 30 +action_icon = 12 +menu_icon = 12 +visual_scene = ExtResource("2") +capabilities = PackedStringArray("transport_hub") +map_color = Color(0.95, 0.55, 0.85, 1) +map_sides = 8 +map_radius = 7.5 +map_phase = 0.392699 +menu_button_name = "TransportHubButton" +menu_label_name = "TransportHubLabel" +menu_cost_name = "TransportHubCost" diff --git a/TransportHubBuildingVisual.gd b/TransportHubBuildingVisual.gd new file mode 100644 index 0000000..a8a0d44 --- /dev/null +++ b/TransportHubBuildingVisual.gd @@ -0,0 +1,12 @@ +extends "res://BuildingVisualBase.gd" + +func _ready() -> void: + _build_hub_pieces() + queue_redraw() + +func apply_animation() -> void: + _animate_transport_hub() + +func _draw() -> void: + register_draw() + _draw_transport_hub_static(self) diff --git a/TransportHubBuildingVisual.gd.uid b/TransportHubBuildingVisual.gd.uid new file mode 100644 index 0000000..b1ca4cb --- /dev/null +++ b/TransportHubBuildingVisual.gd.uid @@ -0,0 +1 @@ +uid://6qyl5qnhw0ho diff --git a/TransportHubBuildingVisual.tscn b/TransportHubBuildingVisual.tscn new file mode 100644 index 0000000..1426761 --- /dev/null +++ b/TransportHubBuildingVisual.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://TransportHubBuildingVisual.gd" id="1"] +[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") + +[node name="ActionProvider" type="Node" parent="."] +script = ExtResource("2") +actions = PackedStringArray("transfer") diff --git a/WorldMapContent.gd b/WorldMapContent.gd index 33212a0..5d4667b 100644 --- a/WorldMapContent.gd +++ b/WorldMapContent.gd @@ -1,20 +1,10 @@ extends Node2D const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") +const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") const WORLD_TO_LOCAL: float = 0.04 -# Mirrors Building.gd's own MOTHERSHIP_COLOR/CRAFTER_COLOR/COMMAND_COLOR so a -# marker on the map reads as "the same building" at a glance. Duplicated -# rather than cross-referenced - the same colour is already independently -# duplicated between Ship.gd, Fleet.gd and Building.gd's HULL_COLOR, so this -# follows the codebase's existing convention rather than inventing a new one. -const MOTHERSHIP_MARKER_COLOR: Color = Color(0.45, 0.9, 0.55) -const CRAFTER_MARKER_COLOR: Color = Color(1.0, 0.72, 0.25) -const COMMAND_MARKER_COLOR: Color = Color(0.55, 0.6, 1.0) -const TRANSPORT_HUB_MARKER_COLOR: Color = Color(0.95, 0.55, 0.85) -const SHIP_LAB_MARKER_COLOR: Color = Color(0.55, 0.95, 0.9) - # A structure built on a planet's surface (see GameState.extractors) is not # itself a map marker - it is a small badge on the existing planet marker, # in a colour distinct from both the planets and the space buildings so it @@ -40,28 +30,20 @@ func _process(_delta: float) -> void: func building_markers() -> Array: var markers: Array = [] for b in GameState.buildings: + var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(b.type)) + if definition == null: + continue markers.append({ "position": b.position * WORLD_TO_LOCAL, "type": b.type, - "color": _building_marker_color(b.type), + "color": definition.map_color, + "sides": definition.map_sides, + "radius": definition.map_radius, + "phase": definition.map_phase, }) return markers -func _building_marker_color(type: String) -> Color: - match type: - GameState.BUILDING_MOTHERSHIP: - return MOTHERSHIP_MARKER_COLOR - GameState.BUILDING_SHIP_CRAFTER: - return CRAFTER_MARKER_COLOR - GameState.BUILDING_TRANSPORT_HUB: - return TRANSPORT_HUB_MARKER_COLOR - GameState.BUILDING_SHIP_LAB: - return SHIP_LAB_MARKER_COLOR - _: - return COMMAND_MARKER_COLOR - - # Whether the planet in this (explored) chunk has an extractor on it, per # GameState.has_extractor_at(). A separate predicate for the same # GPU-less-testability reason as building_markers(). @@ -119,17 +101,7 @@ func _draw() -> void: # Buildings are placed freely in space rather than tied to a chunk, so # they get their own pass rather than living inside the chunk loop above. for marker in building_markers(): - match marker.type: - GameState.BUILDING_MOTHERSHIP: - _draw_marker_polygon(_regular_polygon(marker.position, 8.0, 6, PI / 6.0), marker.color) - GameState.BUILDING_SHIP_CRAFTER: - _draw_marker_polygon(_regular_polygon(marker.position, 7.0, 4, 0.0), marker.color) - GameState.BUILDING_TRANSPORT_HUB: - _draw_marker_polygon(_regular_polygon(marker.position, 7.5, 8, PI / 8.0), marker.color) - GameState.BUILDING_SHIP_LAB: - _draw_marker_polygon(_regular_polygon(marker.position, 7.5, 3, -PI / 2.0), marker.color) - _: - _draw_marker_polygon(_regular_polygon(marker.position, 6.0, 4, PI / 4.0), marker.color) + _draw_marker_polygon(_regular_polygon(marker.position, marker.radius, marker.sides, marker.phase), marker.color) var fleet_local: Vector2 = fleet.get_centroid() * WORLD_TO_LOCAL var marker_points: PackedVector2Array = PackedVector2Array([ diff --git a/tests/_build_popup_preview.gd b/tests/_build_popup_preview.gd index 3bb938e..d3c6b1d 100644 --- a/tests/_build_popup_preview.gd +++ b/tests/_build_popup_preview.gd @@ -28,4 +28,4 @@ func _ready() -> void: # Flip SHOW_POPUP to check the menu underneath # instead of the popup that covers it. if SHOW_POPUP: - controller._on_place_mothership() + controller._show_building_info(GameState.BUILDING_MOTHERSHIP) diff --git a/tests/_crafter_preview.gd b/tests/_crafter_preview.gd index dc89fe4..52888aa 100644 --- a/tests/_crafter_preview.gd +++ b/tests/_crafter_preview.gd @@ -31,9 +31,9 @@ func _ready() -> void: b.position = Vector2(-240.0 + float(i % 3) * 240.0, -105.0 + float(i / 3) * 215.0) add_child(b) if cell.has("phase"): - b.set_process(false) - b.anim_time = float(cell.phase) * b.CYCLE - b.apply_animation() + b.visual.set_process(false) + b.visual.anim_time = float(cell.phase) * b.visual.CYCLE + b.visual.apply_animation() var cam: Camera2D = Camera2D.new() cam.zoom = Vector2(1.65, 1.65) diff --git a/tests/test_animation_continuity.gd b/tests/test_animation_continuity.gd index d373a25..3919aa6 100644 --- a/tests/test_animation_continuity.gd +++ b/tests/test_animation_continuity.gd @@ -53,7 +53,7 @@ func _process(delta: float) -> bool: var ship: Node2D = null for b in field.spawned: if is_instance_valid(b) and b.building_type == GameState.BUILDING_MOTHERSHIP: - ship = b + ship = b.visual if ship == null: print("TEST_ANIMATION_CONTINUITY: FAIL (mothership never spawned)") quit(1) diff --git a/tests/test_build_info_popup.gd b/tests/test_build_info_popup.gd index ef1d708..c67d45c 100644 --- a/tests/test_build_info_popup.gd +++ b/tests/test_build_info_popup.gd @@ -28,9 +28,9 @@ var elapsed: float = 0.0 # Extractor is excluded: it needs a planet underneath, which # test_extractor_building covers in full. const OPTIONS: Array = [ - {"press": "_on_place_mothership", "type": "mothership", "title": "Mothership"}, - {"press": "_on_place_crafter", "type": "ship_crafter", "title": "Ship Crafter"}, - {"press": "_on_place_command_center", "type": "command_center", "title": "Command Center"}, + {"type": "mothership", "title": "Mothership"}, + {"type": "ship_crafter", "title": "Ship Crafter"}, + {"type": "command_center", "title": "Command Center"}, ] @@ -84,7 +84,7 @@ func _process(delta: float) -> bool: var built_before: int = _count(type) controller._on_build_pressed() - controller.call(option.press) + controller._show_building_info(type) var informed: bool = popup.visible \ and title.text == option.title \ @@ -111,7 +111,7 @@ func _process(delta: float) -> bool: # --- reopening and confirming builds it ------------------ controller._on_build_pressed() - controller.call(option.press) + controller._show_building_info(type) controller._confirm_build() var built: bool = _count(type) == built_before + 1 var charged: bool = game_state.rocks == rocks_before - expected_cost diff --git a/tests/test_building_composition.gd b/tests/test_building_composition.gd new file mode 100644 index 0000000..7cadf5d --- /dev/null +++ b/tests/test_building_composition.gd @@ -0,0 +1,35 @@ +extends SceneTree + +const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") + + +func _initialize() -> void: + var scene: PackedScene = load("res://Building.tscn") + for definition in BUILDING_DEFINITIONS.all(): + if definition.placement_mode != "space": + continue + var building: Node2D = scene.instantiate() + building.definition_id = definition.definition_id + building._ready() + if building.definition == null \ + or building.definition.definition_id != definition.definition_id \ + or building.visual == null: + _fail("%s did not compose its definition and visual (resolved=%s visual=%s)" % [ + definition.definition_id, + "null" if building.definition == null else building.definition.definition_id, + building.visual, + ]) + return + var provider: Node = building.visual.get_node_or_null("ActionProvider") + if provider == null or not provider.actions.has("transfer"): + _fail("%s has no composed action provider" % definition.definition_id) + return + building.free() + + print("TEST_BUILDING_COMPOSITION: PASS") + quit(0) + + +func _fail(message: String) -> void: + print("TEST_BUILDING_COMPOSITION: FAIL (%s)" % message) + quit(1) diff --git a/tests/test_building_redraw_cost.gd b/tests/test_building_redraw_cost.gd index c083754..1c81451 100644 --- a/tests/test_building_redraw_cost.gd +++ b/tests/test_building_redraw_cost.gd @@ -61,11 +61,11 @@ func _process(delta: float) -> bool: continue match b.building_type: GameState.BUILDING_SHIP_CRAFTER: - crafter = b + crafter = b.visual GameState.BUILDING_MOTHERSHIP: - mothership = b + mothership = b.visual GameState.BUILDING_COMMAND_CENTER: - command = b + command = b.visual if crafter == null or mothership == null or command == null: print("TEST_BUILDING_REDRAW_COST: FAIL (buildings never spawned)") quit(1) @@ -88,7 +88,7 @@ func _process(delta: float) -> bool: for b in [crafter, mothership, command]: if b.draw_count != 1: print("TEST_BUILDING_REDRAW_COST: FAIL (%s redrew its geometry %d times)" % [ - b.building_type, b.draw_count + b.name, b.draw_count ]) quit(1) return true diff --git a/tests/test_crafter_animation.gd b/tests/test_crafter_animation.gd index 7ddab14..9b5d57e 100644 --- a/tests/test_crafter_animation.gd +++ b/tests/test_crafter_animation.gd @@ -57,16 +57,19 @@ func _process(delta: float) -> bool: # Spawning is deferred, so give the tree a frame to attach them. if elapsed < 0.2: return false - crafter = _find(GameState.BUILDING_SHIP_CRAFTER) - if crafter == null: + var crafter_building: Node2D = _find(GameState.BUILDING_SHIP_CRAFTER) + if crafter_building == null: print("TEST_CRAFTER_ANIMATION: FAIL (crafter never spawned)") quit(1) return true + crafter = crafter_building.visual var animated: bool = crafter.is_processing() - var mothership: Node2D = _find(GameState.BUILDING_MOTHERSHIP) + var mothership_building: Node2D = _find(GameState.BUILDING_MOTHERSHIP) + var mothership: Node2D = mothership_building.visual if mothership_building != null else null var mothership_animates: bool = mothership != null and mothership.is_processing() - var command: Node2D = _find(GameState.BUILDING_COMMAND_CENTER) + var command_building: Node2D = _find(GameState.BUILDING_COMMAND_CENTER) + var command: Node2D = command_building.visual if command_building != null else null var command_static: bool = command != null and not command.is_processing() print("crafter animates=%s mothership animates=%s command centre static=%s" % [ animated, mothership_animates, command_static diff --git a/tests/test_extractor_building.gd b/tests/test_extractor_building.gd index 13b2567..0a4b769 100644 --- a/tests/test_extractor_building.gd +++ b/tests/test_extractor_building.gd @@ -69,7 +69,7 @@ func _process(delta: float) -> bool: print("TEST_EXTRACTOR_BUILDING: FAIL (enabled with no planet in reach)") quit(1) return true - controller._on_place_extractor() + controller._show_building_info(GameState.BUILDING_EXTRACTOR) var explained: bool = popup.visible and "planet" in popup.get_node("Panel/MessageLabel").text.to_lower() var offered: bool = popup.get_node("Panel/BuildButton").visible print("no planet: greyed=%s popup_shown=%s confirm_offered=%s" % [ @@ -118,7 +118,7 @@ func _process(delta: float) -> bool: return true var rocks_before: int = game_state.rocks - controller._on_place_extractor() + controller._show_building_info(GameState.BUILDING_EXTRACTOR) var planet_field2: Node = main_scene.get_node("PlanetField") var planet2: Node2D = planet_field2.populated_chunks[target_chunk][0] @@ -148,7 +148,7 @@ func _process(delta: float) -> bool: # A second one on the same planet must be refused, not charged. var rocks_now: int = game_state.rocks controller._on_build_pressed() - controller._on_place_extractor() + controller._show_building_info(GameState.BUILDING_EXTRACTOR) var re_offered: bool = popup.get_node("Panel/BuildButton").visible controller._confirm_build() # must do nothing even if pressed var still_one: bool = game_state.extractors.size() == 1 and not re_offered diff --git a/tests/test_mothership_replacement.gd b/tests/test_mothership_replacement.gd index 6ebe817..576f039 100644 --- a/tests/test_mothership_replacement.gd +++ b/tests/test_mothership_replacement.gd @@ -67,7 +67,7 @@ func _process(delta: float) -> bool: return false # --- first one: informed, then built ------------------------- controller._on_build_pressed() - controller._on_place_mothership() + controller._show_building_info(GameState.BUILDING_MOTHERSHIP) var first_text: String = popup.get_node("Panel/MessageLabel").text.to_lower() var first_warned: bool = "destroy" in first_text print("first: popup=%s warned=%s built_before_confirm=%d" % [ @@ -86,7 +86,7 @@ func _process(delta: float) -> bool: # --- second: warns, builds nothing --------------------------- _place_fleet(second_pos) controller._on_build_pressed() - controller._on_place_mothership() + controller._show_building_info(GameState.BUILDING_MOTHERSHIP) var warned: bool = popup.visible and "destroy" in popup.get_node("Panel/MessageLabel").text.to_lower() print("second press: warned=%s motherships=%d" % [warned, _count_motherships()]) if not warned or _count_motherships() != 1: @@ -107,7 +107,7 @@ func _process(delta: float) -> bool: # --- confirming replaces ------------------------------------- var rocks_before: int = game_state.rocks - controller._on_place_mothership() + controller._show_building_info(GameState.BUILDING_MOTHERSHIP) controller._confirm_build() var only_one: bool = _count_motherships() == 1 var moved: bool = game_state.get_mothership_position().distance_to(second_pos) < 60.0 @@ -124,7 +124,7 @@ func _process(delta: float) -> bool: game_state.rocks = GameState.MOTHERSHIP_COST - 1 _place_fleet(Vector2(2500.0, -800.0)) controller._on_build_pressed() - controller._on_place_mothership() + controller._show_building_info(GameState.BUILDING_MOTHERSHIP) # With nothing affordable there is no confirm button to press; # pressing it anyway must still do nothing. var offered: bool = popup.get_node("Panel/BuildButton").visible diff --git a/tests/test_skill_tree.gd b/tests/test_skill_tree.gd index 7b2a887..5c9d21c 100644 --- a/tests/test_skill_tree.gd +++ b/tests/test_skill_tree.gd @@ -72,9 +72,9 @@ func _process(delta: float) -> bool: # Since #21 an unresearched structure is not listed at all, rather # than listed and greyed. It is therefore unreachable from the # menu - but the popup must still refuse if something calls - # _on_place_crafter() by another route. + # the generic definition entry point by another route. var hidden: bool = not scene.get_node("UI/BuildMenu/CrafterButton").visible - controller._on_place_crafter() + controller._show_building_info(GameState.BUILDING_SHIP_CRAFTER) var offered: bool = popup.get_node("Panel/BuildButton").visible print("locked crafter: hidden_from_menu=%s confirm_offered=%s" % [hidden, offered]) if not hidden or offered: diff --git a/tests/test_transport_hub_building.gd b/tests/test_transport_hub_building.gd index b2cc4eb..dcb03a6 100644 --- a/tests/test_transport_hub_building.gd +++ b/tests/test_transport_hub_building.gd @@ -1,5 +1,7 @@ extends SceneTree +const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") + # Headless gameplay test: run with # godot --headless -s res://tests/test_transport_hub_building.gd # Forgejo #18 (part 5 of #12): the Transport Hub is a placeable building of @@ -67,7 +69,7 @@ func _process(delta: float) -> bool: var icons: Dictionary = {} for type in [GameState.BUILDING_MOTHERSHIP, GameState.BUILDING_SHIP_CRAFTER, GameState.BUILDING_COMMAND_CENTER, GameState.BUILDING_TRANSPORT_HUB]: - var icon: int = controller._action_icon_for(type) + var icon: int = BUILDING_DEFINITIONS.get_definition(type).action_icon if icons.has(icon): print("TEST_TRANSPORT_HUB_BUILDING: FAIL (%s and %s share an icon)" % [type, icons[icon]]) quit(1) @@ -99,7 +101,7 @@ func _process(delta: float) -> bool: return false for b in field.spawned: if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB: - hub = b + hub = b.visual if hub == null: print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)") quit(1) @@ -120,9 +122,9 @@ func _process(delta: float) -> bool: for m in markers: if m.type == GameState.BUILDING_TRANSPORT_HUB: found = true - if m.color == map_content.CRAFTER_MARKER_COLOR \ - or m.color == map_content.MOTHERSHIP_MARKER_COLOR \ - or m.color == map_content.COMMAND_MARKER_COLOR: + if m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_SHIP_CRAFTER).map_color \ + or m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_MOTHERSHIP).map_color \ + or m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_COMMAND_CENTER).map_color: print("TEST_TRANSPORT_HUB_BUILDING: FAIL (map marker reuses another building's colour)") quit(1) return true