diff --git a/BuildController.gd b/BuildController.gd index 1d33801..114575f 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -1,8 +1,5 @@ extends Node2D -const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") -const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.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 @@ -25,18 +22,20 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") @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"), get_node("../UI/LabMenu/VesselC"), ] @onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel") -@onready var lab_vessel_labels: Array = [ - get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"), - get_node("../UI/LabMenu/VesselCLabel"), -] @onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel") @onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel") @onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton") @@ -79,7 +78,20 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") get_node("../UI/HangarMenu/FleetShieldLabel"), get_node("../UI/HangarMenu/FleetTransporterLabel"), ] -var build_menu_entries: Array = [] +@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")], +] # 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 @@ -103,12 +115,6 @@ var lab_building_id: int = 0 @onready var craft_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel") @onready var craft_transporter_label: Label = get_node("../UI/CraftMenu/TransporterLabel") @onready var craft_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel") -@onready var craft_vessel_buttons: Array = [ - craft_interceptor_button, craft_shield_button, craft_transporter_button, -] -@onready var craft_vessel_labels: Array = [ - craft_interceptor_label, craft_shield_label, craft_transporter_label, -] # --- the transfer panel (#24) --------------------------------------------- # Ore used to be moved from inside whichever panel happened to be open: "load @@ -142,6 +148,41 @@ 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 = "" @@ -169,7 +210,13 @@ var context_actions_expanded: bool = false func _ready() -> void: build_button.pressed.connect(_on_build_pressed) build_close_button.pressed.connect(_close_build_menu) - _configure_build_menu_entries() + 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) + 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) for i in range(lab_vessel_buttons.size()): @@ -182,9 +229,8 @@ func _ready() -> void: action_button.pressed.connect(_on_context_action_pressed) building_action_button.pressed.connect(_on_action_pressed) - for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.all().size())): - var definition: Resource = VESSEL_DEFINITIONS.all()[i] - craft_vessel_buttons[i].pressed.connect(_on_craft_vessel.bind(definition.definition_id)) + craft_interceptor_button.pressed.connect(_on_craft_interceptor) + craft_transporter_button.pressed.connect(_on_craft_transporter) craft_close_button.pressed.connect(func(): craft_menu.visible = false) transfer_button.pressed.connect(_on_transfer_pressed) @@ -221,21 +267,6 @@ 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: @@ -287,7 +318,9 @@ 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. - _refresh_build_option_states() + build_extractor_button.enabled = _planet_under_fleet() != null \ + and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR) + build_extractor_button.queue_redraw() if craft_menu.visible: # Live, so a delivery arriving while the menu is open visibly works @@ -326,7 +359,10 @@ func _process(delta: float) -> void: current_building = pending_building context_actions_expanded = false - hangar_button.visible = 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 # 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. @@ -335,7 +371,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 = int(current_building.definition.action_icon) + building_action_button.icon = _action_icon_for(current_building.building_type) 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() @@ -367,20 +403,13 @@ func _layout_context_buttons() -> void: func _context_action_buttons() -> Array: if current_building == null: return [] - 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 + 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 func _on_context_action_pressed() -> void: @@ -404,6 +433,20 @@ 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 @@ -422,12 +465,11 @@ func _on_action_pressed() -> void: for bar in resource_bars: bar.building_id = current_building.building_id - var action: String = current_building.primary_action() - if action == "research": + if current_building.building_type == GameState.BUILDING_MOTHERSHIP: skill_menu.visible = true skill_tree.reset_scroll() _refresh_skill_menu() - elif action == "craft": + elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER: # 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 @@ -436,11 +478,11 @@ func _on_action_pressed() -> void: craft_building_id = current_building.building_id craft_menu.visible = true _refresh_craft_status() - elif action == "lab": + elif current_building.building_type == GameState.BUILDING_SHIP_LAB: lab_building_id = current_building.building_id lab_menu.visible = true _refresh_lab() - elif action == "formation": + elif current_building.building_type == GameState.BUILDING_COMMAND_CENTER: formation_menu.visible = true formation_editor.queue_redraw() @@ -509,7 +551,8 @@ 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". - _refresh_build_option_states() + build_extractor_button.enabled = _planet_under_fleet() != null \ + and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR) # Prices are shown under each option now, so this only needs to say # whether the fleet is somewhere an extractor could go. @@ -519,14 +562,16 @@ func _refresh_build_status() -> void: build_status.text = "" -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 _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 _close_build_menu() -> void: @@ -537,17 +582,40 @@ 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: - var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) - return int(definition.cost) if definition != null else 0 + 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 func _show_building_info(type: String) -> void: - var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) - if definition == null: - return - var cost: int = int(definition.cost) - var lines: Array = [definition.description] + var info: Dictionary = BUILDING_INFO[type] + var cost: int = cost_of(type) + var lines: Array = [info.body] var can_build: bool = true # Locked structures still list themselves, greyed, so the player can see @@ -558,7 +626,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 definition.placement_mode == "planet": + if can_build and type == GameState.BUILDING_EXTRACTOR: var planet: Node2D = _planet_under_fleet() if planet == null: lines.append("No planet in reach - fly over one.") @@ -567,15 +635,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." % [GameState.resource_name(planet.resource_type), planet.planet_name]) - elif can_build and definition.unique and GameState.has_building_of_type(type): + 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("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 = definition.display_name + info_title.text = info.title info_message.text = "\n\n".join(lines) info_cost.amount = cost info_build_button.visible = can_build @@ -600,31 +668,28 @@ 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 definition.placement_mode == "planet": + if type == GameState.BUILDING_EXTRACTOR: 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(definition.cost): - build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks] + if not GameState.spend(GameState.EXTRACTOR_COST): + build_status.text = "Not enough rocks (need %d, have %d)" % [GameState.EXTRACTOR_COST, GameState.rocks] return GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type) build_menu.visible = false return - if definition.unique and GameState.has_building_of_type(type): - if not GameState.can_afford(definition.cost): + if type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership(): + if not GameState.can_afford(GameState.MOTHERSHIP_COST): build_status.text = "Not enough rocks (need %d, have %d)" % [ - definition.cost, GameState.rocks + GameState.MOTHERSHIP_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(type) + GameState.remove_buildings_of_type(GameState.BUILDING_MOTHERSHIP) building_field.rebuild_all() _try_place(type, cost_of(type)) @@ -703,19 +768,26 @@ func _refresh_craft_status() -> void: # Prices live with the buttons they describe. The top of the screen is # intentionally reserved for the shared resource bar and a short action # message; repeating fleet, queue and transport counts here buried the UI. + craft_interceptor_label.text = "Interceptor\n%d ore" % GameState.SHIP_COST + craft_transporter_label.text = "Transporter\n%d ore" % GameState.TRANSPORTER_COST + craft_shield_label.text = "Shield Vessel\n%d ore" % GameState.SHIELD_COST craft_status.text = "" - var definitions: Array = VESSEL_DEFINITIONS.all() - for i in range(mini(definitions.size(), craft_vessel_buttons.size())): - var definition: Resource = definitions[i] - var button: IconButton = craft_vessel_buttons[i] - craft_vessel_labels[i].text = "%s\n%d ore" % [definition.display_name, definition.build_cost] - button.icon = definition.icon - button.glow_color = definition.glow_color - button.enabled = GameState.vessel_is_unlocked(definition.definition_id) - if definition.has_capability("transport"): - button.enabled = button.enabled and GameState.has_transport_hub() \ - and GameState.can_station_transporter() - button.queue_redraw() + # Nowhere to station one means the order would be refused, so say so + # rather than letting the button look available and do nothing. + craft_transporter_button.enabled = GameState.has_transport_hub() \ + and GameState.can_station_transporter() + craft_transporter_button.queue_redraw() + + craft_shield_button.enabled = GameState.shield_vessel_unlocked() + craft_shield_button.queue_redraw() + + # Fleet capacity no longer gates ORDERING (#23). A finished vessel waits + # at the mothership until the player crews it, so a full fleet is a + # reason not to collect one yet - not a reason the crafter cannot build. + # This is also what stopped the crafter being inert on a fresh save, + # where the fleet starts at exactly its cap. + craft_interceptor_button.enabled = true + craft_interceptor_button.queue_redraw() _refresh_queue() if craft_message != "": if Time.get_ticks_msec() < craft_message_until: @@ -725,40 +797,40 @@ func _refresh_craft_status() -> void: func _on_craft_interceptor() -> void: - _on_craft_vessel(GameState.VESSEL_INTERCEPTOR) - - -func _on_craft_vessel(vessel: String) -> void: if craft_building_id == 0: _say("No crafter selected - reopen this panel from the building") return - var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel) - if definition == null: - return - if not GameState.queue_order(craft_building_id, vessel): - if not GameState.vessel_is_unlocked(vessel): - _say("%s must be unlocked at a ship lab first" % definition.display_name) - elif definition.has_capability("transport") and not GameState.has_transport_hub(): - _say("Build a Transport Hub first - a transporter needs somewhere to be stationed") - elif definition.has_capability("transport"): - _say("All %d berths are taken - build another hub, or research Freight Yards" % - GameState.transport_capacity()) - return - _report_order(vessel) + # No affordability check: an order the crafter cannot pay for yet is + # QUEUED, and completes by itself once ore is delivered here (#16). But + # queueing silently looks identical to a dead button, so it says which + # happened and what the crafter is still short of. + GameState.queue_order(craft_building_id) + _report_order(GameState.PRODUCT_SHIP) func _report_order(product: String) -> void: var short: int = GameState.outstanding_cost(craft_building_id) if short > 0: _say("%s queued - %d more ore must be delivered here" % [ - GameState.vessel_name(product), short + GameState.VESSEL_NAMES.get(product, product), short ]) else: - _say("%s ordered - building now" % GameState.vessel_name(product)) + _say("%s ordered - building now" % GameState.VESSEL_NAMES.get(product, product)) func _on_craft_transporter() -> void: - _on_craft_vessel(GameState.VESSEL_TRANSPORTER) + if craft_building_id == 0: + _say("No crafter selected - reopen this panel from the building") + return + if not GameState.queue_order(craft_building_id, GameState.PRODUCT_TRANSPORTER): + # Two different refusals, and the player needs to know WHICH. + if not GameState.has_transport_hub(): + _say("Build a Transport Hub first - a transporter needs somewhere to be stationed") + else: + _say("All %d berths are taken - build another hub, or research Freight Yards" % + GameState.transport_capacity()) + return + _refresh_craft_status() # --- ship lab (#5) -------------------------------------------------------- @@ -768,13 +840,10 @@ func _lab_options() -> Array: # entry, because unlocking the type and upgrading it belong in the same # place - and its upgrades are pointless until it exists. var options: Array = [] - var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel) - if definition == null: - return options - if not GameState.vessel_is_unlocked(lab_vessel): - options.append({"unlock": true, "id": definition.unlock_id, - "name": definition.unlock_name, "desc": definition.unlock_description, - "cost": definition.unlock_cost, "icon": IconButton.Icon.UPGRADE_UNLOCK_SHIELD}) + if lab_vessel == GameState.VESSEL_SHIELD and not GameState.shield_vessel_unlocked(): + options.append({"unlock": true, "name": "Unlock Shield Vessel", + "desc": "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up.", + "cost": GameState.UNLOCK_SHIELD_COST}) return options for u in GameState.upgrades_for(lab_vessel): options.append({"unlock": false, "id": u.id, "name": u.name, "desc": u.desc}) @@ -782,10 +851,8 @@ func _lab_options() -> Array: func _on_lab_vessel(index: int) -> void: - var definitions: Array = VESSEL_DEFINITIONS.all() - if index < 0 or index >= definitions.size(): - return - lab_vessel = definitions[index].definition_id + lab_vessel = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, + GameState.VESSEL_TRANSPORTER][index] lab_choice = 0 _refresh_lab() @@ -803,7 +870,7 @@ func _on_lab_buy() -> void: if options.is_empty(): return var choice: Dictionary = options[lab_choice] - var id: String = String(choice.id) + var id: String = GameState.UNLOCK_SHIELD_VESSEL if choice.unlock else String(choice.id) if not GameState.queue_lab_research(lab_building_id, id): lab_status.text = "Research is already queued or complete" return @@ -812,17 +879,14 @@ func _on_lab_buy() -> void: func _refresh_lab() -> void: - var definitions: Array = VESSEL_DEFINITIONS.all() - for i in range(mini(lab_vessel_buttons.size(), definitions.size())): - var definition: Resource = definitions[i] - var this_vessel: String = definition.definition_id + for i in range(lab_vessel_buttons.size()): + var this_vessel: String = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, + GameState.VESSEL_TRANSPORTER][i] lab_vessel_buttons[i].enabled = true - lab_vessel_buttons[i].icon = definition.icon - lab_vessel_buttons[i].glow_color = definition.glow_color - lab_vessel_labels[i].text = definition.display_name + lab_vessel_buttons[i].glow_color = _vessel_colour(this_vessel) lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel lab_vessel_buttons[i].queue_redraw() - lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel) + lab_vessel_label.text = "%s research" % GameState.VESSEL_NAMES.get(lab_vessel, lab_vessel) var options: Array = _lab_options() for i in range(lab_upgrade_buttons.size()): @@ -832,7 +896,7 @@ func _refresh_lab() -> void: if not visible: continue var option: Dictionary = options[i] - var price_text: String = "%d ore" % (int(option.cost) if bool(option.unlock) else GameState.upgrade_cost(option.id)) + var price_text: String = "%d ore" % (GameState.UNLOCK_SHIELD_COST if bool(option.unlock) else GameState.upgrade_cost(option.id)) if not bool(option.unlock) and GameState.upgrade_maxed(option.id): price_text = "MAX" var level_note: String = "" if bool(option.unlock) else " %d/%d" % [ @@ -860,11 +924,29 @@ func _refresh_lab() -> void: func _upgrade_icon(option: Dictionary) -> IconButton.Icon: - return int(option.get("icon", IconButton.Icon.SHIP_LAB)) + if bool(option.unlock): + return IconButton.Icon.UPGRADE_UNLOCK_SHIELD + match String(option.id): + "int_damage": return IconButton.Icon.UPGRADE_DAMAGE + "int_rate": return IconButton.Icon.UPGRADE_RATE + "int_range": return IconButton.Icon.UPGRADE_RANGE + "int_speed", "shd_speed": return IconButton.Icon.UPGRADE_THRUSTERS + "shd_hits": return IconButton.Icon.UPGRADE_SHIELD_HITS + "shd_recharge": return IconButton.Icon.UPGRADE_SHIELD_RECHARGE + "trn_capacity": return IconButton.Icon.UPGRADE_CARGO + "trn_speed": return IconButton.Icon.UPGRADE_ENGINES + _: return IconButton.Icon.SHIP_LAB func _on_craft_shield() -> void: - _on_craft_vessel(GameState.VESSEL_SHIELD) + if craft_building_id == 0: + _say("No crafter selected - reopen this panel from the building") + return + if not GameState.shield_vessel_unlocked(): + _say("Shield vessels must be unlocked at a ship lab first") + return + GameState.queue_order(craft_building_id, GameState.VESSEL_SHIELD) + _report_order(GameState.VESSEL_SHIELD) # --- moving ore, in both directions, at any building (#24) ---------------- @@ -951,8 +1033,8 @@ func _refresh_queue() -> void: if not slot.visible: continue var order: Dictionary = orders[i] - var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product)) - slot.icon = definition.icon + slot.icon = IconButton.Icon.TRANSPORT_HUB if order.product == GameState.PRODUCT_TRANSPORTER \ + else IconButton.Icon.SHIP slot.glow_color = _vessel_colour(String(order.product)) # Only the one under construction reads as active; the rest are # waiting, and greyed says so without needing a caption each. @@ -965,24 +1047,29 @@ func _refresh_queue() -> void: var head: Dictionary = orders[0] if bool(head.started): queue_detail.text = "Building %s %d%%\nTap to cancel" % [ - GameState.vessel_name(String(head.product)), + GameState.VESSEL_NAMES.get(head.product, head.product), int(round(float(head.progress) * 100.0)), ] else: queue_detail.text = "%s waiting for ore\nTap to cancel" % \ - GameState.vessel_name(String(head.product)) + GameState.VESSEL_NAMES.get(head.product, head.product) func _vessel_colour(product: String) -> Color: - var definition: Resource = VESSEL_DEFINITIONS.get_definition(product) - return definition.glow_color if definition != null else Color(0.25, 0.85, 1.0) + match product: + GameState.VESSEL_SHIELD: + return Color(0.45, 0.8, 1.0) + GameState.PRODUCT_TRANSPORTER: + return Color(0.95, 0.55, 0.85) + _: + return Color(0.25, 0.85, 1.0) func _on_queue_slot(index: int) -> void: var orders: Array = GameState.order_list(craft_building_id) if index >= orders.size(): return - var vessel: String = GameState.vessel_name(String(orders[index].product)) + var vessel: String = GameState.VESSEL_NAMES.get(orders[index].product, orders[index].product) var refunded: bool = bool(orders[index].started) if not GameState.cancel_order(craft_building_id, index): return @@ -1007,12 +1094,12 @@ func _on_hangar_crew_type(index: int) -> void: return var stored_index: int = GameState.stored_vessels.find(type) if stored_index < 0: - hangar_status.text = "No %s available" % GameState.vessel_name(type) + hangar_status.text = "No %s available" % GameState.VESSEL_NAMES.get(type, type) return if not fleet.add_ship(GameState.take_stored_vessel(stored_index)): hangar_status.text = "Could not crew that vessel" return - hangar_status.text = "%s joined the fleet" % GameState.vessel_name(type) + hangar_status.text = "%s joined the fleet" % GameState.VESSEL_NAMES.get(type, type) _refresh_hangar() @@ -1024,40 +1111,32 @@ func _on_hangar_store_type(index: int) -> void: ship_index = i break if ship_index < 0: - hangar_status.text = "No %s in the fleet" % GameState.vessel_name(type) + hangar_status.text = "No %s in the fleet" % GameState.VESSEL_NAMES.get(type, type) return var stored: String = fleet.store_ship(ship_index) if stored == "": hangar_status.text = "Keep one ship deployed" return GameState.stored_vessels.append(stored) - hangar_status.text = "%s stored in the hangar" % GameState.vessel_name(stored) + hangar_status.text = "%s stored in the hangar" % GameState.VESSEL_NAMES.get(stored, stored) _refresh_hangar() func _hangar_types() -> Array: - var types: Array = [] - for definition in VESSEL_DEFINITIONS.all(): - types.append(definition.definition_id) - return types + return [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER] func _refresh_hangar() -> void: var types: Array = _hangar_types() for i in range(types.size()): var type: String = types[i] - var definition: Resource = VESSEL_DEFINITIONS.get_definition(type) var available: int = GameState.stored_vessels.count(type) var deployed: int = 0 for ship in fleet.ships: if is_instance_valid(ship) and String(ship.vessel_type) == type: deployed += 1 - hangar_available_labels[i].text = "%s\n%d available" % [definition.display_name, available] - hangar_fleet_labels[i].text = "%s\n%d in fleet" % [definition.display_name, deployed] - hangar_available_slots[i].icon = definition.icon - hangar_fleet_slots[i].icon = definition.icon - hangar_available_slots[i].glow_color = definition.glow_color - hangar_fleet_slots[i].glow_color = definition.glow_color + hangar_available_labels[i].text = "%s\n%d available" % [GameState.VESSEL_NAMES.get(type, type), available] + hangar_fleet_labels[i].text = "%s\n%d in fleet" % [GameState.VESSEL_NAMES.get(type, type), deployed] hangar_available_slots[i].enabled = available > 0 and fleet.ships.size() < GameState.max_fleet_size() hangar_fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1 hangar_available_slots[i].queue_redraw() diff --git a/Building.gd b/Building.gd index b9dea7e..d4387d7 100644 --- a/Building.gd +++ b/Building.gd @@ -1,42 +1,665 @@ extends Node2D -const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") +# 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. -@export var definition_id: String = "mothership" +@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 building_id: int = 0 @export var interact_radius: float = 150.0 -var definition: Resource -var visual: Node2D +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 building_type: String: - get: return definition_id - set(value): definition_id = value +# 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 _ready() -> void: add_to_group("buildings") z_index = -1 - 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) + # 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() -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() +func _process(delta: float) -> void: + anim_time = fmod(anim_time + delta, CYCLE) + phase_time += delta + apply_animation() -func primary_action() -> String: - for action in available_actions(): - if action != "transfer" and action != "hangar": - return action - return "" +# 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 get_display_name() -> String: - return String(definition.display_name) if definition != null else definition_id.capitalize() + 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)) diff --git a/Building.tscn b/Building.tscn index 3929101..174c302 100644 --- a/Building.tscn +++ b/Building.tscn @@ -4,5 +4,3 @@ [node name="Building" type="Node2D"] script = ExtResource("1") - -[node name="VisualHost" type="Node2D" parent="."] diff --git a/BuildingActionProvider.gd b/BuildingActionProvider.gd deleted file mode 100644 index 1a9dba8..0000000 --- a/BuildingActionProvider.gd +++ /dev/null @@ -1,3 +0,0 @@ -extends Node - -@export var actions: PackedStringArray = [] diff --git a/BuildingActionProvider.gd.uid b/BuildingActionProvider.gd.uid deleted file mode 100644 index e5e07f8..0000000 --- a/BuildingActionProvider.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c4dah6xluvenl diff --git a/BuildingDefinition.gd b/BuildingDefinition.gd deleted file mode 100644 index fdc990e..0000000 --- a/BuildingDefinition.gd +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 7473bd0..0000000 --- a/BuildingDefinition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d10s0frtxiw62 diff --git a/BuildingDefinitions.gd b/BuildingDefinitions.gd deleted file mode 100644 index f05bf8e..0000000 --- a/BuildingDefinitions.gd +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 0dead32..0000000 --- a/BuildingDefinitions.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cpm0u3gu0exke diff --git a/BuildingField.gd b/BuildingField.gd index 09b37bd..496c26a 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.definition_id = record.type + b.building_type = 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 deleted file mode 100644 index cd339b3..0000000 --- a/BuildingVisualBase.gd +++ /dev/null @@ -1,606 +0,0 @@ -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 deleted file mode 100644 index 8827724..0000000 --- a/BuildingVisualBase.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://p7vuv1xsksc diff --git a/CommandBuildingDefinition.tres b/CommandBuildingDefinition.tres deleted file mode 100644 index 727a6f4..0000000 --- a/CommandBuildingDefinition.tres +++ /dev/null @@ -1,20 +0,0 @@ -[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 deleted file mode 100644 index 713b407..0000000 --- a/CommandBuildingVisual.gd +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 9a270e2..0000000 --- a/CommandBuildingVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bpb5pq8g56ywn diff --git a/CommandBuildingVisual.tscn b/CommandBuildingVisual.tscn deleted file mode 100644 index 048b3f3..0000000 --- a/CommandBuildingVisual.tscn +++ /dev/null @@ -1,11 +0,0 @@ -[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 deleted file mode 100644 index 2a3f91f..0000000 --- a/CrafterBuildingDefinition.tres +++ /dev/null @@ -1,19 +0,0 @@ -[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 deleted file mode 100644 index ddd41a8..0000000 --- a/CrafterBuildingVisual.gd +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 05576b0..0000000 --- a/CrafterBuildingVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dbwebd1vf7biy diff --git a/CrafterBuildingVisual.tscn b/CrafterBuildingVisual.tscn deleted file mode 100644 index c3bb670..0000000 --- a/CrafterBuildingVisual.tscn +++ /dev/null @@ -1,11 +0,0 @@ -[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 deleted file mode 100644 index 0584758..0000000 --- a/ExtractorBuildingDefinition.tres +++ /dev/null @@ -1,16 +0,0 @@ -[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 37d110a..f855f2a 100644 --- a/GameState.gd +++ b/GameState.gd @@ -1,11 +1,5 @@ extends Node -const INVENTORY = preload("res://Inventory.gd") -const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") -const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") -const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") - - # Autoload singleton - the one thing that survives change_scene_to_file() # scene swaps between space (Main.tscn) and a planet's surface # (PlanetSurface.tscn). Everything else in either scene gets torn down @@ -18,7 +12,7 @@ var return_world_position: Vector2 = Vector2.ZERO var return_camera_zoom: float = 1.0 var planet_name: String = "" -var planet_definition_id: String = "rocky" +var planet_type: String = "" # Explored-chunk history for the world map's fog-of-war reveal. Lives # here (not on ExplorationTracker, which gets destroyed and recreated on @@ -28,101 +22,126 @@ var explored_chunks: Dictionary = {} # --- Resources ------------------------------------------------------------- -const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID -const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID -const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER +# Ore that has been BANKED at the mothership. This is the only pool anything +# can be bought with. +# Starts stocked for play-testing so buildings can be tried without mining +# first - drop this back to 0 for a real playthrough. +var rocks: int = 500 -# All resource-bearing locations use the same container. The old public -# names below are computed compatibility accessors, not separate storage. -var bank_inventory: RefCounted = INVENTORY.new({RESOURCE_ORE: 500}) -var fleet_inventory: RefCounted = INVENTORY.new() +# Gas is a second, banked resource. It is deliberately separate from ore: +# construction still spends ore, while future gas technologies can consume +# gas without making the two materials silently interchangeable. +var gas: int = 0 -var rocks: int: - get: return bank_inventory.amount(RESOURCE_ORE) - set(value): bank_inventory.set_amount(RESOURCE_ORE, value) +# Ore the fleet is physically carrying and has not delivered yet (#14). Mining +# fills this, not `rocks`: ore is cargo until the fleet brings it home, so a +# long mining run is a real trip with something to lose rather than instant +# income from anywhere on the map. +# +# Lives here, like everything else that has to survive Main.tscn being torn +# down and rebuilt on every trip to a planet's surface - and cargo mined ON a +# surface would otherwise be dropped the moment the player took off. +var fleet_cargo: int = 0 +var fleet_gas_cargo: int = 0 -var gas: int: - get: return bank_inventory.amount(RESOURCE_GAS) - set(value): bank_inventory.set_amount(RESOURCE_GAS, value) - -var fleet_cargo: int: - get: return fleet_inventory.amount(RESOURCE_ORE) - set(value): fleet_inventory.set_amount(RESOURCE_ORE, value) - -var fleet_gas_cargo: int: - get: return fleet_inventory.amount(RESOURCE_GAS) - set(value): fleet_inventory.set_amount(RESOURCE_GAS, value) +# Resource-facing UI and logistics iterate this list instead of growing a +# second set of controls and controller branches every time a material is +# introduced. Keep presentation data here with the state it describes so a +# resource has one name and colour everywhere it appears. +const RESOURCE_ORE: String = "ore" +const RESOURCE_GAS: String = "gas" +const RESOURCE_TYPES: PackedStringArray = [RESOURCE_ORE, RESOURCE_GAS] +const RESOURCE_NAMES: Dictionary = { + RESOURCE_ORE: "Ore", + RESOURCE_GAS: "Gas", +} +const RESOURCE_COLORS: Dictionary = { + RESOURCE_ORE: Color(0.0, 0.72, 0.82), + RESOURCE_GAS: Color(0.85, 0.52, 1.0), +} func collect_cargo(amount: int = 1) -> void: - fleet_inventory.deposit(RESOURCE_ORE, amount) + fleet_cargo += amount func collect_gas_cargo(amount: int = 1) -> void: - fleet_inventory.deposit(RESOURCE_GAS, amount) - - -func resource_types() -> PackedStringArray: - return RESOURCE_DEFINITIONS.ids() - - -func resource_definition(resource_type: String) -> Resource: - return RESOURCE_DEFINITIONS.get_definition(resource_type) + fleet_gas_cargo += amount func resource_name(resource_type: String) -> String: - var definition: Resource = resource_definition(resource_type) - return String(definition.display_name) if definition != null else resource_type.capitalize() + return String(RESOURCE_NAMES.get(resource_type, resource_type.capitalize())) func resource_color(resource_type: String) -> Color: - var definition: Resource = resource_definition(resource_type) - return definition.color if definition != null else Color.WHITE + return RESOURCE_COLORS.get(resource_type, Color.WHITE) func banked_resource(resource_type: String) -> int: - return bank_inventory.amount(resource_type) + match resource_type: + RESOURCE_ORE: return rocks + RESOURCE_GAS: return gas + _: return 0 func fleet_resource(resource_type: String) -> int: - return fleet_inventory.amount(resource_type) - - -func _inventory_for_record(record: Dictionary) -> RefCounted: - var inventory: RefCounted = record.get("inventory") as RefCounted - if inventory == null: - inventory = INVENTORY.new() - record.inventory = inventory - # Compatibility migration for state made before #33. Erase each legacy - # field once absorbed so it cannot drift out of step with the inventory. - if record.has("stock"): - inventory.set_amount(RESOURCE_ORE, int(record.stock)) - record.erase("stock") - if record.has("gas_stock"): - inventory.set_amount(RESOURCE_GAS, int(record.gas_stock)) - record.erase("gas_stock") - return inventory - - -func building_inventory(id: int) -> RefCounted: - var building: Dictionary = find_building(id) - if building.is_empty(): - return null - if building_has_capability(building, "bank"): - return bank_inventory - return _inventory_for_record(building) + match resource_type: + RESOURCE_ORE: return fleet_cargo + RESOURCE_GAS: return fleet_gas_cargo + _: return 0 func building_resource(id: int, resource_type: String) -> int: - var inventory: RefCounted = building_inventory(id) - return 0 if inventory == null else inventory.amount(resource_type) + var building: Dictionary = find_building(id) + if building.is_empty(): + return 0 + if building.type == BUILDING_MOTHERSHIP: + return banked_resource(resource_type) + match resource_type: + RESOURCE_ORE: return int(building.get("stock", 0)) + RESOURCE_GAS: return int(building.get("gas_stock", 0)) + _: return 0 func transfer_resource_to_building(id: int, resource_type: String, amount: int) -> int: - return fleet_inventory.transfer_to(building_inventory(id), resource_type, amount) + var building: Dictionary = find_building(id) + if building.is_empty(): + return 0 + var moved: int = mini(maxi(amount, 0), fleet_resource(resource_type)) + if moved <= 0: + return 0 + match resource_type: + RESOURCE_ORE: fleet_cargo -= moved + RESOURCE_GAS: fleet_gas_cargo -= moved + _: return 0 + if building.type == BUILDING_MOTHERSHIP: + match resource_type: + RESOURCE_ORE: rocks += moved + RESOURCE_GAS: gas += moved + else: + match resource_type: + RESOURCE_ORE: building.stock = int(building.get("stock", 0)) + moved + RESOURCE_GAS: building.gas_stock = int(building.get("gas_stock", 0)) + moved + return moved func transfer_resource_to_fleet(id: int, resource_type: String, amount: int) -> int: - var source: RefCounted = building_inventory(id) - return 0 if source == null else source.transfer_to(fleet_inventory, resource_type, amount) + var building: Dictionary = find_building(id) + if building.is_empty(): + return 0 + var moved: int = mini(maxi(amount, 0), building_resource(id, resource_type)) + if moved <= 0: + return 0 + if building.type == BUILDING_MOTHERSHIP: + match resource_type: + RESOURCE_ORE: rocks -= moved + RESOURCE_GAS: gas -= moved + else: + match resource_type: + RESOURCE_ORE: building.stock = int(building.get("stock", 0)) - moved + RESOURCE_GAS: building.gas_stock = int(building.get("gas_stock", 0)) - moved + match resource_type: + RESOURCE_ORE: fleet_cargo += moved + RESOURCE_GAS: fleet_gas_cargo += moved + return moved # The batch size the transfer menu offers, next to "All". A batch rather @@ -315,6 +334,11 @@ 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) @@ -376,8 +400,8 @@ func add_building(type: String, at_position: Vector2) -> int: var id: int = next_building_id next_building_id += 1 buildings.append({ - "id": id, "type": type, "position": at_position, - "inventory": INVENTORY.new(), "orders": [], + "id": id, "type": type, "position": at_position, "stock": 0, + "gas_stock": 0, "orders": [], }) return id @@ -389,13 +413,6 @@ 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 @@ -405,10 +422,21 @@ const VESSEL_INTERCEPTOR: String = "interceptor" const VESSEL_SHIELD: String = "shield" const VESSEL_TRANSPORTER: String = "transporter" -# Compatibility constants for callers and old tests. Runtime metadata is -# resolved from VesselDefinition below. +const VESSEL_NAMES: Dictionary = { + VESSEL_INTERCEPTOR: "Interceptor", + VESSEL_SHIELD: "Shield Vessel", + VESSEL_TRANSPORTER: "Transporter", +} + const SHIELD_COST: int = 14 -const TRANSPORTER_COST: int = 10 + +# How long one vessel takes to build, once its ore is actually in the +# building. A queue you can watch and cancel needs time to exist in. +const BUILD_TIMES: Dictionary = { + VESSEL_INTERCEPTOR: 10.0, + VESSEL_SHIELD: 16.0, + VESSEL_TRANSPORTER: 8.0, +} const LAB_RESEARCH_TIME: float = 8.0 @@ -419,14 +447,57 @@ var build_time_scale: float = 1.0 func build_time(product: String) -> float: - var definition: Resource = VESSEL_DEFINITIONS.get_definition(product) - return (definition.build_time if definition != null else 10.0) * build_time_scale + return float(BUILD_TIMES.get(product, 10.0)) * build_time_scale func lab_research_time() -> float: return LAB_RESEARCH_TIME * build_time_scale -# Compatibility constants. VesselDefinition owns the live base figures. +# Researched at the ship lab, per vessel type. Separate from SKILL_TREE on +# purpose: that gates what the player may build at all and is a strategic +# spine, while these are repeatable tuning on a vessel you already have. +# `cost` is the FIRST level; each level after costs cost * (level + 1), so +# the third point in something is not the same price as the first. +const VESSEL_UPGRADES: Array = [ + { + "id": "int_damage", "vessel": VESSEL_INTERCEPTOR, "name": "Heavy Slugs", + "cost": 30, "max_level": 3, "desc": "+1 damage per shot.", + }, + { + "id": "int_rate", "vessel": VESSEL_INTERCEPTOR, "name": "Autoloader", + "cost": 25, "max_level": 3, "desc": "Fires 15% faster per level.", + }, + { + "id": "int_range", "vessel": VESSEL_INTERCEPTOR, "name": "Targeting Array", + "cost": 20, "max_level": 3, "desc": "+60 firing range per level.", + }, + { + "id": "int_speed", "vessel": VESSEL_INTERCEPTOR, "name": "Thrusters", + "cost": 25, "max_level": 3, "desc": "+40 top speed, +60 acceleration per level.", + }, + { + "id": "shd_hits", "vessel": VESSEL_SHIELD, "name": "Layered Emitters", + "cost": 60, "max_level": 2, "desc": "+1 hit absorbed before the shield drops.", + }, + { + "id": "shd_recharge", "vessel": VESSEL_SHIELD, "name": "Fast Cycling", + "cost": 35, "max_level": 3, "desc": "Shield returns 1.2s sooner per level.", + }, + { + "id": "shd_speed", "vessel": VESSEL_SHIELD, "name": "Thrusters", + "cost": 25, "max_level": 3, "desc": "+40 top speed, +60 acceleration per level.", + }, + { + "id": "trn_capacity", "vessel": VESSEL_TRANSPORTER, "name": "Expanded Hold", + "cost": 30, "max_level": 3, "desc": "Carries 8 more ore per supply run.", + }, + { + "id": "trn_speed", "vessel": VESSEL_TRANSPORTER, "name": "Freight Engines", + "cost": 30, "max_level": 3, "desc": "+70 speed on supply runs.", + }, +] + +# Base figures every vessel starts from, before anything is researched. const SHIP_BASE_DAMAGE: int = 1 const SHIP_BASE_COOLDOWN: float = 0.5 const SHIP_BASE_RANGE: float = 320.0 @@ -444,13 +515,15 @@ signal vessel_upgraded(upgrade_id: String) func has_ship_lab() -> bool: for b in buildings: - if building_has_capability(b, "vessel_research"): + if b.type == BUILDING_SHIP_LAB: return true return false func shield_vessel_unlocked() -> bool: - return vessel_is_unlocked(VESSEL_SHIELD) + # Unlocked AT the lab, by paying for it there - a vessel type rather than + # a stat, so it is a one-off rather than a level. + return int(vessel_levels.get(UNLOCK_SHIELD_VESSEL, 0)) > 0 const UNLOCK_SHIELD_VESSEL: String = "unlock_shield" @@ -458,69 +531,27 @@ const UNLOCK_SHIELD_COST: int = 45 func unlock_shield_vessel() -> bool: - return unlock_vessel(VESSEL_SHIELD) - - -func vessel_definition(vessel: String) -> Resource: - return VESSEL_DEFINITIONS.get_definition(vessel) - - -func vessel_name(vessel: String) -> String: - var definition: Resource = vessel_definition(vessel) - return definition.display_name if definition != null else vessel.capitalize() - - -func vessel_has_capability(vessel: String, capability: String) -> bool: - var definition: Resource = vessel_definition(vessel) - return definition != null and definition.has_capability(capability) - - -func vessel_is_unlocked(vessel: String) -> bool: - var definition: Resource = vessel_definition(vessel) - if definition == null: + if shield_vessel_unlocked() or not has_ship_lab(): return false - return definition.unlock_id == "" or int(vessel_levels.get(definition.unlock_id, 0)) > 0 - - -func vessel_for_unlock(unlock_id: String) -> Resource: - for definition in VESSEL_DEFINITIONS.all(): - if definition.unlock_id == unlock_id: - return definition - return null - - -func unlock_vessel(vessel: String) -> bool: - var definition: Resource = vessel_definition(vessel) - if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab(): + if not spend(UNLOCK_SHIELD_COST): return false - if not spend(definition.unlock_cost): - return false - vessel_levels[definition.unlock_id] = 1 - vessel_upgraded.emit(definition.unlock_id) + vessel_levels[UNLOCK_SHIELD_VESSEL] = 1 + vessel_upgraded.emit(UNLOCK_SHIELD_VESSEL) return true func upgrade_def(id: String) -> Dictionary: - for definition in VESSEL_DEFINITIONS.all(): - for upgrade in definition.upgrades: - if upgrade.upgrade_id == id: - return {"id": upgrade.upgrade_id, "vessel": definition.definition_id, - "name": upgrade.display_name, "desc": upgrade.description, - "cost": upgrade.base_cost, "max_level": upgrade.max_level, - "icon": upgrade.icon, "resource": upgrade} + for u in VESSEL_UPGRADES: + if u.id == id: + return u return {} func upgrades_for(vessel: String) -> Array: var out: Array = [] - var definition: Resource = vessel_definition(vessel) - if definition == null: - return out - for upgrade in definition.upgrades: - out.append({"id": upgrade.upgrade_id, "vessel": vessel, - "name": upgrade.display_name, "desc": upgrade.description, - "cost": upgrade.base_cost, "max_level": upgrade.max_level, - "icon": upgrade.icon, "resource": upgrade}) + for u in VESSEL_UPGRADES: + if u.vessel == vessel: + out.append(u) return out @@ -554,60 +585,77 @@ func buy_upgrade(id: String) -> bool: return true -func vessel_stat(vessel: String, stat: String) -> float: - var definition: Resource = vessel_definition(vessel) - if definition == null: - return 0.0 - var value: float = float(definition.base_stats.get(stat, 0.0)) - for upgrade in definition.upgrades: - var level: int = upgrade_level(upgrade.upgrade_id) - value += float(upgrade.additive_modifiers.get(stat, 0.0)) * level - if upgrade.multiplicative_modifiers.has(stat): - value *= pow(float(upgrade.multiplicative_modifiers[stat]), level) - if definition.minimum_stats.has(stat): - value = maxf(value, float(definition.minimum_stats[stat])) - return value +# --- what those upgrades actually come to --------------------------------- +# Read live rather than baked into a vessel when it is built, so researching +# something improves the ships already flying instead of only the next ones. + +func ship_damage(vessel: String) -> int: + return SHIP_BASE_DAMAGE + (upgrade_level("int_damage") if vessel == VESSEL_INTERCEPTOR else 0) -func ship_damage(vessel: String) -> int: return int(vessel_stat(vessel, "damage")) -func ship_fire_cooldown(vessel: String) -> float: return vessel_stat(vessel, "fire_cooldown") -func ship_fire_range(vessel: String) -> float: return vessel_stat(vessel, "fire_range") -func ship_max_speed(vessel: String) -> float: return vessel_stat(vessel, "max_speed") -func ship_acceleration(vessel: String) -> float: return vessel_stat(vessel, "acceleration") +func ship_fire_cooldown(vessel: String) -> float: + if vessel != VESSEL_INTERCEPTOR: + return SHIP_BASE_COOLDOWN + return SHIP_BASE_COOLDOWN * pow(0.85, float(upgrade_level("int_rate"))) + + +func ship_fire_range(vessel: String) -> float: + return SHIP_BASE_RANGE + (60.0 * upgrade_level("int_range") if vessel == VESSEL_INTERCEPTOR else 0.0) + + +func _speed_levels(vessel: String) -> int: + if vessel == VESSEL_SHIELD: + return upgrade_level("shd_speed") + return upgrade_level("int_speed") + + +func ship_max_speed(vessel: String) -> float: + return SHIP_BASE_SPEED + 40.0 * _speed_levels(vessel) + + +func ship_acceleration(vessel: String) -> float: + return SHIP_BASE_ACCEL + 60.0 * _speed_levels(vessel) + func shield_max_charges() -> int: - var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield") - return int(vessel_stat(definition.definition_id, "shield_charges")) + return SHIELD_BASE_CHARGES + upgrade_level("shd_hits") + func shield_recharge_time() -> float: - var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield") - return vessel_stat(definition.definition_id, "shield_recharge") + return maxf(2.0, SHIELD_BASE_RECHARGE - 1.2 * upgrade_level("shd_recharge")) + func transporter_load_limit() -> int: - var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport") - return int(vessel_stat(definition.definition_id, "cargo_capacity")) + return TRANSPORTER_BASE_LOAD + 8 * upgrade_level("trn_capacity") + func transporter_speed() -> float: - var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport") - return vessel_stat(definition.definition_id, "transport_speed") + return TRANSPORTER_BASE_SPEED + 70.0 * upgrade_level("trn_speed") const PRODUCT_SHIP: String = "interceptor" const PRODUCT_TRANSPORTER: String = "transporter" +const TRANSPORTER_COST: int = 10 + func product_cost(product: String) -> int: - var definition: Resource = vessel_definition(product) - return definition.build_cost if definition != null else 0 + if product == PRODUCT_TRANSPORTER: + return TRANSPORTER_COST + if product == VESSEL_SHIELD: + return SHIELD_COST + return SHIP_COST func can_produce(type: String) -> bool: - var definition: Resource = BUILDING_DEFINITIONS.get_definition(type) - return definition != null and "production" in definition.capabilities + # Only the ship crafter builds anything. The hub STATIONS transporters, + # it does not make them (#12). + return type == BUILDING_SHIP_CRAFTER func building_stock(id: int) -> int: - return building_resource(id, RESOURCE_ORE) + var b: Dictionary = find_building(id) + return 0 if b.is_empty() else int(b.get("stock", 0)) func pending_orders(id: int) -> int: @@ -654,7 +702,7 @@ func cancel_order(id: int, index: int) -> bool: return false var order: Dictionary = orders[index] if bool(order.started): - _inventory_for_record(b).deposit(RESOURCE_ORE, product_cost(String(order.product))) + b.stock = int(b.get("stock", 0)) + product_cost(String(order.product)) orders.remove_at(index) return true @@ -669,7 +717,7 @@ func outstanding_cost(id: int) -> int: # A started build has already paid, so it is not still owed. if not bool(o.started): need += product_cost(String(o.product)) - return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 0) + return maxi(need - int(b.get("stock", 0)), 0) func deposit_to_building(id: int, amount: int) -> int: @@ -678,21 +726,21 @@ func deposit_to_building(id: int, amount: int) -> int: var b: Dictionary = find_building(id) if b.is_empty(): return 0 - return _inventory_for_record(b).deposit(RESOURCE_ORE, amount) + b.stock = int(b.get("stock", 0)) + amount + return amount func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool: var b: Dictionary = find_building(id) if b.is_empty() or not can_produce(b.type): return false - var definition: Resource = vessel_definition(product) - if definition == null or not vessel_is_unlocked(product): + if product == VESSEL_SHIELD and not shield_vessel_unlocked(): return false - if definition.has_capability("transport") and not has_transport_hub(): + if product == PRODUCT_TRANSPORTER and not has_transport_hub(): # Nowhere to station it. Refused rather than queued, so the player is # told now instead of watching an order sit forever. return false - if definition.has_capability("transport") and not can_station_transporter(): + if product == PRODUCT_TRANSPORTER and not can_station_transporter(): # Every berth is taken or spoken for (#20). return false (b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false}) @@ -714,19 +762,17 @@ func _advance_production(delta: float) -> void: var order: Dictionary = orders[0] var product: String = String(order.product) var cost: int = product_cost(product) - var inventory: RefCounted = _inventory_for_record(b) - var definition: Resource = vessel_definition(product) if not bool(order.started): - if inventory.amount(RESOURCE_ORE) < cost: + if int(b.get("stock", 0)) < cost: continue - if definition.has_capability("transport") and transporters.size() >= transport_capacity(): + if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity(): # A hub was demolished after this was ordered, so the berth it # was going to use is gone. Hold rather than overfill. continue - if definition.has_capability("transport") and nearest_hub_to(b.position).is_empty(): + if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty(): continue - inventory.withdraw(RESOURCE_ORE, cost) + b.stock = int(b.stock) - cost order.started = true order.elapsed = float(order.elapsed) + delta @@ -734,7 +780,7 @@ func _advance_production(delta: float) -> void: continue orders.pop_front() - if definition.has_capability("transport"): + if product == PRODUCT_TRANSPORTER: # Transporters are unchanged: they take themselves to the nearest # hub and wait for work (#19). _station_transporter(b.position) @@ -753,11 +799,10 @@ 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 not building_has_capability(lab, "vessel_research") or upgrade_id == "": + if lab.is_empty() or lab.type != BUILDING_SHIP_LAB or upgrade_id == "": return false var orders: Array = lab.get("research_orders", []) - var unlock_definition: Resource = vessel_for_unlock(upgrade_id) - if not orders.is_empty() or (unlock_definition == null and upgrade_maxed(upgrade_id)): + if not orders.is_empty() or (upgrade_id != UNLOCK_SHIELD_VESSEL and upgrade_maxed(upgrade_id)): return false orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false}) lab.research_orders = orders @@ -772,25 +817,23 @@ func lab_research_order(lab_id: int) -> Dictionary: func _advance_lab_research(delta: float) -> void: for lab in buildings: - if not building_has_capability(lab, "vessel_research"): + if lab.type != BUILDING_SHIP_LAB: continue var orders: Array = lab.get("research_orders", []) if orders.is_empty(): continue var order: Dictionary = orders[0] var id: String = String(order.id) - var unlock_definition: Resource = vessel_for_unlock(id) - var cost: int = unlock_definition.unlock_cost if unlock_definition != null else upgrade_cost(id) - var inventory: RefCounted = _inventory_for_record(lab) + var cost: int = UNLOCK_SHIELD_COST if id == UNLOCK_SHIELD_VESSEL else upgrade_cost(id) if not bool(order.started): - if inventory.amount(RESOURCE_ORE) < cost: + if int(lab.get("stock", 0)) < cost: continue - inventory.withdraw(RESOURCE_ORE, cost) + lab.stock = int(lab.stock) - cost order.started = true order.elapsed = float(order.elapsed) + delta if float(order.elapsed) < lab_research_time(): continue - if unlock_definition != null: + if id == UNLOCK_SHIELD_VESSEL: vessel_levels[id] = 1 else: vessel_levels[id] = upgrade_level(id) + 1 @@ -819,13 +862,6 @@ 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 @@ -842,7 +878,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 building_has_capability(b, "bank"): + if b.type == BUILDING_MOTHERSHIP: return b.position return null @@ -881,7 +917,7 @@ func rotate_formation(radians: float) -> void: # Extractors mine into their OWN local stock. They do not own a magical # hauler: ore becomes spendable only when a hub-stationed transporter picks # up a full load and physically delivers it to the mothership. Entries are -# {"position", "planet_name", "resource_type", "inventory", "mining_progress"}. +# {"position", "planet_name", "resource_type", "stock", "mining_progress"}. const EXTRACTOR_RATE: float = 2.0 # ore per second var extractors: Array = [] @@ -902,7 +938,7 @@ func add_extractor(planet_name: String, at_position: Vector2, resource_type: Str "position": at_position, "planet_name": planet_name, "resource_type": resource_type, - "inventory": INVENTORY.new(), + "stock": 0, "mining_progress": 0.0, }) # An extractor is built from SPACE, with the fleet parked on the planet - @@ -915,21 +951,7 @@ func add_extractor(planet_name: String, at_position: Vector2, resource_type: Str func extractor_stock(index: int) -> int: if index < 0 or index >= extractors.size(): return 0 - var extractor: Dictionary = extractors[index] - return _extractor_inventory(extractor).amount(String(extractor.get("resource_type", RESOURCE_ORE))) - - -func _extractor_inventory(extractor: Dictionary) -> RefCounted: - var inventory: RefCounted = extractor.get("inventory") as RefCounted - if inventory == null: - inventory = INVENTORY.new() - extractor.inventory = inventory - # Old extractor state had one stock field whose material was described by - # resource_type. Absorb it into that key on first access. - if extractor.has("stock"): - inventory.set_amount(String(extractor.get("resource_type", RESOURCE_ORE)), int(extractor.stock)) - extractor.erase("stock") - return inventory + return int(extractors[index].get("stock", 0)) func _process(delta: float) -> void: @@ -942,7 +964,7 @@ func _process(delta: float) -> void: e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE var whole: int = int(floor(float(e.mining_progress))) if whole > 0: - _extractor_inventory(e).deposit(String(e.get("resource_type", RESOURCE_ORE)), whole) + e.stock = int(e.get("stock", 0)) + whole e.mining_progress = float(e.mining_progress) - whole _advance_transporters(delta) @@ -1002,7 +1024,7 @@ func hub_capacity() -> int: func hub_count() -> int: var n: int = 0 for b in buildings: - if building_has_capability(b, "transport_hub"): + if b.type == BUILDING_TRANSPORT_HUB: n += 1 return n @@ -1017,7 +1039,7 @@ func pending_transporter_orders() -> int: var n: int = 0 for b in buildings: for o in b.get("orders", []): - if vessel_has_capability(String(o.product), "transport"): + if String(o.product) == PRODUCT_TRANSPORTER: n += 1 return n @@ -1041,7 +1063,7 @@ func nearest_hub_to(at: Vector2) -> Dictionary: var best: Dictionary = {} var best_d: float = INF for b in buildings: - if not building_has_capability(b, "transport_hub"): + if b.type != BUILDING_TRANSPORT_HUB: continue var d: float = at.distance_squared_to(b.position) if d < best_d: @@ -1104,8 +1126,7 @@ func _dispatch_transporters() -> void: var load: int = transporter_load_limit() for i in range(extractors.size()): var extractor: Dictionary = extractors[i] - var resource_id: String = String(extractor.get("resource_type", RESOURCE_ORE)) - if _extractor_inventory(extractor).amount(resource_id) < load or _transporter_inbound_to_extractor(i): + if int(extractor.get("stock", 0)) < load or _transporter_inbound_to_extractor(i): continue var chosen_extractor: Dictionary = {} var best_extractor_distance: float = INF @@ -1119,7 +1140,7 @@ func _dispatch_transporters() -> void: if chosen_extractor.is_empty(): return chosen_extractor.extractor_index = i - chosen_extractor.resource_type = resource_id + chosen_extractor.resource_type = String(extractor.get("resource_type", "ore")) chosen_extractor.target_id = 0 _begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position, TRANSPORTER_TO_EXTRACTOR) @@ -1170,13 +1191,13 @@ func _arrive(t: Dictionary) -> void: TRANSPORTER_TO_MOTHERSHIP: var target: Dictionary = find_building(int(t.target_id)) var want: int = outstanding_cost(int(t.target_id)) - var take: int = mini(mini(want, bank_inventory.amount(RESOURCE_ORE)), transporter_load_limit()) + var take: int = mini(mini(want, rocks), transporter_load_limit()) if take <= 0 or target.is_empty(): # Nothing to carry, or the building is gone. Go home empty. t.target_id = 0 _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) return - bank_inventory.withdraw(RESOURCE_ORE, take) + rocks -= take t.carrying = take _begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING) TRANSPORTER_TO_BUILDING: @@ -1184,7 +1205,7 @@ func _arrive(t: Dictionary) -> void: if deposit_to_building(int(t.target_id), int(t.carrying)) <= 0: # Building demolished in transit - put the ore back rather # than quietly destroying it. - bank_inventory.deposit(RESOURCE_ORE, int(t.carrying)) + rocks += int(t.carrying) t.carrying = 0 t.target_id = 0 _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) @@ -1195,19 +1216,20 @@ func _arrive(t: Dictionary) -> void: return var extractor: Dictionary = extractors[extractor_index] var load: int = transporter_load_limit() - var resource_id: String = String(extractor.get("resource_type", RESOURCE_ORE)) - var extractor_inventory: RefCounted = _extractor_inventory(extractor) - if extractor_inventory.amount(resource_id) < load: + if int(extractor.get("stock", 0)) < load: t.extractor_index = -1 _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) return - extractor_inventory.withdraw(resource_id, load) + extractor.stock = int(extractor.stock) - load t.carrying = load - t.resource_type = resource_id + t.resource_type = String(extractor.get("resource_type", "ore")) _begin_leg(t, here, get_mothership_position(), TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR) TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR: # Bank only on arrival; cargo retains the extractor's resource type. - bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ORE)), int(t.carrying)) + if String(t.get("resource_type", "ore")) == "gas": + gas += int(t.carrying) + else: + rocks += int(t.carrying) t.carrying = 0 t.extractor_index = -1 _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) @@ -1217,11 +1239,11 @@ func _arrive(t: Dictionary) -> void: func can_afford(cost: int) -> bool: - return bank_inventory.amount(RESOURCE_ORE) >= cost + return rocks >= cost func spend(cost: int) -> bool: if not can_afford(cost): return false - bank_inventory.withdraw(RESOURCE_ORE, cost) + rocks -= cost return true diff --git a/GasGiantDefinition.tres b/GasGiantDefinition.tres deleted file mode 100644 index 0a5109c..0000000 --- a/GasGiantDefinition.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] - -[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://GasGiantVisual.tscn" id="2"] - -[resource] -script = ExtResource("1") -definition_id = "gas_giant" -display_name = "Gas giant" -names = PackedStringArray("Aurelia", "Boreas", "Cerulean Deep", "Helios Major", "Vesper") -visual_scene = ExtResource("2") -surface_profile_id = "gas" -resource_id = "gas" -map_color = Color(0.65, 0.42, 0.9, 1) diff --git a/GasGiantVisual.gd b/GasGiantVisual.gd deleted file mode 100644 index e9172ca..0000000 --- a/GasGiantVisual.gd +++ /dev/null @@ -1,31 +0,0 @@ -extends Node2D - -var gas_phase: float = 0.0 - - -func configure(_shape_seed: int) -> void: - queue_redraw() - - -func extractor_anchor() -> Vector2: - return Vector2(0.0, -118.0) - - -func _process(delta: float) -> void: - gas_phase += delta - queue_redraw() - - -func _draw() -> void: - var rim := PackedVector2Array() - for i in range(32): - var angle := TAU * float(i) / 32.0 - rim.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0)) - NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9) - for row in [-62.0, -28.0, 8.0, 43.0, 73.0]: - var band := PackedVector2Array() - var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row)) - for i in range(15): - var x := lerpf(-half_width, half_width, float(i) / 14.0) - band.append(Vector2(x, row + sin(x * 0.045 + gas_phase * 1.6 + row) * 7.0)) - NeonDraw.draw_glowing_path(self, band, Color(0.85, 0.52, 1.0), 0.75, 0.65) diff --git a/GasGiantVisual.gd.uid b/GasGiantVisual.gd.uid deleted file mode 100644 index af718cf..0000000 --- a/GasGiantVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bxfw4ja2hw4pf diff --git a/GasGiantVisual.tscn b/GasGiantVisual.tscn deleted file mode 100644 index 513b87e..0000000 --- a/GasGiantVisual.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://GasGiantVisual.gd" id="1"] - -[node name="Visual" type="Node2D"] -script = ExtResource("1") diff --git a/GasResourceDefinition.tres b/GasResourceDefinition.tres deleted file mode 100644 index a36af07..0000000 --- a/GasResourceDefinition.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="ResourceDefinition" load_steps=3 format=3] - -[ext_resource type="Script" path="res://ResourceDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://GasResourceGlyph.tscn" id="2"] - -[resource] -script = ExtResource("1") -resource_id = "gas" -display_name = "Gas" -color = Color(0.85, 0.52, 1, 1) -glyph_scene = ExtResource("2") diff --git a/GasResourceGlyph.gd b/GasResourceGlyph.gd deleted file mode 100644 index 8bd9965..0000000 --- a/GasResourceGlyph.gd +++ /dev/null @@ -1,19 +0,0 @@ -extends Control - -@export var colour: Color = Color(0.85, 0.52, 1.0) - - -func _draw() -> void: - var centre: Vector2 = size * 0.5 - var radius: float = minf(size.x, size.y) * 0.34 - var bubbles: Array = [ - [centre + Vector2(-radius * 0.38, radius * 0.12), radius * 0.46], - [centre + Vector2(radius * 0.34, radius * 0.26), radius * 0.58], - [centre + Vector2(radius * 0.38, -radius * 0.48), radius * 0.32], - ] - for bubble in bubbles: - var ring := PackedVector2Array() - for i in range(12): - var angle: float = TAU * float(i) / 12.0 - ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1])) - NeonDraw.draw_glowing_polygon(self, ring, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) diff --git a/GasResourceGlyph.gd.uid b/GasResourceGlyph.gd.uid deleted file mode 100644 index 1dfbb0c..0000000 --- a/GasResourceGlyph.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d1ko6yhtjjavx diff --git a/GasResourceGlyph.tscn b/GasResourceGlyph.tscn deleted file mode 100644 index ee02276..0000000 --- a/GasResourceGlyph.tscn +++ /dev/null @@ -1,13 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://GasResourceGlyph.gd" id="1"] - -[node name="GlyphVisual" type="Control"] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 -script = ExtResource("1") diff --git a/GasSurfaceFeatures.tscn b/GasSurfaceFeatures.tscn deleted file mode 100644 index 576b66d..0000000 --- a/GasSurfaceFeatures.tscn +++ /dev/null @@ -1,7 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://GasVentField.gd" id="1"] - -[node name="ResourceProvider" type="Node2D"] -script = ExtResource("1") -fleet_path = NodePath("../../Fleet") diff --git a/GasSurfaceProfile.tres b/GasSurfaceProfile.tres deleted file mode 100644 index 0267c48..0000000 --- a/GasSurfaceProfile.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=3 format=3] - -[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] -[ext_resource type="PackedScene" path="res://GasSurfaceFeatures.tscn" id="2"] - -[resource] -script = ExtResource("1") -profile_id = "gas" -features_scene = ExtResource("2") -base_color = Color(0.13, 0.025, 0.22, 1) -crater_color = Color(0.055, 0.008, 0.11, 1) -highlight_color = Color(0.52, 0.17, 0.7, 1) -caption_color = Color(0.78, 0.88, 0.94, 1) -caption_outline_color = Color(0.02, 0.01, 0.05, 0.9) diff --git a/GasVentField.gd b/GasVentField.gd index 831ca4c..9cdd338 100644 --- a/GasVentField.gd +++ b/GasVentField.gd @@ -1,6 +1,5 @@ extends Node2D -@export var fleet_path: NodePath = NodePath("../Fleet") -@onready var fleet: Node2D = get_node(fleet_path) +@onready var fleet: Node2D = get_node("../Fleet") var vents: Array[Vector2] = [] var progress: float = 0.0 var emission_phase: float = 0.0 diff --git a/InterceptorDamageUpgrade.tres b/InterceptorDamageUpgrade.tres deleted file mode 100644 index 835851a..0000000 --- a/InterceptorDamageUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "int_damage" -display_name = "Heavy Slugs" -description = "+1 damage per shot." -base_cost = 30 -max_level = 3 -icon = 15 -additive_modifiers = {"damage": 1.0} diff --git a/InterceptorRangeUpgrade.tres b/InterceptorRangeUpgrade.tres deleted file mode 100644 index ce01efe..0000000 --- a/InterceptorRangeUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "int_range" -display_name = "Targeting Array" -description = "+60 firing range per level." -base_cost = 20 -max_level = 3 -icon = 17 -additive_modifiers = {"fire_range": 60.0} diff --git a/InterceptorRateUpgrade.tres b/InterceptorRateUpgrade.tres deleted file mode 100644 index 4add45e..0000000 --- a/InterceptorRateUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "int_rate" -display_name = "Autoloader" -description = "Fires 15% faster per level." -base_cost = 25 -max_level = 3 -icon = 16 -multiplicative_modifiers = {"fire_cooldown": 0.85} diff --git a/InterceptorSpeedUpgrade.tres b/InterceptorSpeedUpgrade.tres deleted file mode 100644 index 5549971..0000000 --- a/InterceptorSpeedUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "int_speed" -display_name = "Thrusters" -description = "+40 top speed, +60 acceleration per level." -base_cost = 25 -max_level = 3 -icon = 18 -additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0} diff --git a/InterceptorVesselDefinition.tres b/InterceptorVesselDefinition.tres deleted file mode 100644 index a18f6c4..0000000 --- a/InterceptorVesselDefinition.tres +++ /dev/null @@ -1,20 +0,0 @@ -[gd_resource type="Resource" script_class="VesselDefinition" load_steps=7 format=3] -[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://VesselWeaponComponent.tscn" id="2"] -[ext_resource type="Resource" path="res://InterceptorDamageUpgrade.tres" id="3"] -[ext_resource type="Resource" path="res://InterceptorRateUpgrade.tres" id="4"] -[ext_resource type="Resource" path="res://InterceptorRangeUpgrade.tres" id="5"] -[ext_resource type="Resource" path="res://InterceptorSpeedUpgrade.tres" id="6"] -[resource] -script = ExtResource("1") -definition_id = "interceptor" -display_name = "Interceptor" -description = "Fast combat vessel with a rapid-firing cannon." -build_cost = 6 -build_time = 10.0 -icon = 2 -glow_color = Color(0.25, 0.85, 1, 1) -capabilities = PackedStringArray("weapon") -component_scenes = Array[PackedScene]([ExtResource("2")]) -base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 450.0} -upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")]) diff --git a/Inventory.gd b/Inventory.gd deleted file mode 100644 index aab25fd..0000000 --- a/Inventory.gd +++ /dev/null @@ -1,79 +0,0 @@ -class_name Inventory -extends RefCounted - -var total_capacity: int = -1 -var resource_capacities: Dictionary = {} -var _amounts: Dictionary = {} - - -func _init(initial: Dictionary = {}, capacity: int = -1) -> void: - total_capacity = capacity - for resource_id in initial: - set_amount(String(resource_id), int(initial[resource_id])) - - -func amount(resource_id: String) -> int: - return int(_amounts.get(resource_id, 0)) - - -func total() -> int: - var result: int = 0 - for value in _amounts.values(): - result += int(value) - return result - - -func capacity_for(resource_id: String) -> int: - return int(resource_capacities.get(resource_id, -1)) - - -func space_for(resource_id: String) -> int: - var space: int = 1 << 30 - if total_capacity >= 0: - space = maxi(total_capacity - total(), 0) - var resource_capacity: int = capacity_for(resource_id) - if resource_capacity >= 0: - space = mini(space, maxi(resource_capacity - amount(resource_id), 0)) - return space - - -func set_amount(resource_id: String, value: int) -> int: - _amounts.erase(resource_id) - var accepted: int = mini(maxi(value, 0), space_for(resource_id)) - if accepted > 0: - _amounts[resource_id] = accepted - return amount(resource_id) - - -func deposit(resource_id: String, requested: int) -> int: - var moved: int = mini(maxi(requested, 0), space_for(resource_id)) - if moved > 0: - _amounts[resource_id] = amount(resource_id) + moved - return moved - - -func withdraw(resource_id: String, requested: int) -> int: - var moved: int = mini(maxi(requested, 0), amount(resource_id)) - if moved <= 0: - return 0 - var remaining: int = amount(resource_id) - moved - if remaining > 0: - _amounts[resource_id] = remaining - else: - _amounts.erase(resource_id) - return moved - - -func transfer_to(destination: Inventory, resource_id: String, requested: int) -> int: - if destination == null: - return 0 - var moved: int = mini(mini(maxi(requested, 0), amount(resource_id)), destination.space_for(resource_id)) - if moved <= 0: - return 0 - withdraw(resource_id, moved) - destination.deposit(resource_id, moved) - return moved - - -func snapshot() -> Dictionary: - return _amounts.duplicate() diff --git a/Inventory.gd.uid b/Inventory.gd.uid deleted file mode 100644 index 59722a2..0000000 --- a/Inventory.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://celx1yj6cp2rf diff --git a/MothershipBuildingDefinition.tres b/MothershipBuildingDefinition.tres deleted file mode 100644 index a032777..0000000 --- a/MothershipBuildingDefinition.tres +++ /dev/null @@ -1,21 +0,0 @@ -[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 deleted file mode 100644 index ba2f617..0000000 --- a/MothershipBuildingVisual.gd +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 7ac3669..0000000 --- a/MothershipBuildingVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://m43bwdwhqgy4 diff --git a/MothershipBuildingVisual.tscn b/MothershipBuildingVisual.tscn deleted file mode 100644 index 0cc3615..0000000 --- a/MothershipBuildingVisual.tscn +++ /dev/null @@ -1,11 +0,0 @@ -[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/OreResourceDefinition.tres b/OreResourceDefinition.tres deleted file mode 100644 index 6da27dc..0000000 --- a/OreResourceDefinition.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="ResourceDefinition" load_steps=3 format=3] - -[ext_resource type="Script" path="res://ResourceDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://OreResourceGlyph.tscn" id="2"] - -[resource] -script = ExtResource("1") -resource_id = "ore" -display_name = "Ore" -color = Color(0, 0.72, 0.82, 1) -glyph_scene = ExtResource("2") diff --git a/OreResourceGlyph.gd b/OreResourceGlyph.gd deleted file mode 100644 index e28f7a3..0000000 --- a/OreResourceGlyph.gd +++ /dev/null @@ -1,14 +0,0 @@ -extends Control - -@export var colour: Color = Color(0.0, 0.72, 0.82) - - -func _draw() -> void: - var centre: Vector2 = size * 0.5 - var radius: float = minf(size.x, size.y) * 0.34 - var points := PackedVector2Array() - for i in range(6): - var angle: float = TAU * float(i) / 6.0 - PI * 0.5 - points.append(centre + Vector2(cos(angle), sin(angle)) * radius) - NeonDraw.draw_glowing_polygon(self, points, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) - NeonDraw.draw_glowing_line(self, points[5], points[2], colour, 0.55, maxf(0.4, radius * 0.025), colour.darkened(0.78)) diff --git a/OreResourceGlyph.gd.uid b/OreResourceGlyph.gd.uid deleted file mode 100644 index 2ba6598..0000000 --- a/OreResourceGlyph.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cwgkhqo4ev5ao diff --git a/OreResourceGlyph.tscn b/OreResourceGlyph.tscn deleted file mode 100644 index 384a20e..0000000 --- a/OreResourceGlyph.tscn +++ /dev/null @@ -1,13 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://OreResourceGlyph.gd" id="1"] - -[node name="GlyphVisual" type="Control"] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 -script = ExtResource("1") diff --git a/Planet.gd b/Planet.gd index 4d17d84..c4eddbd 100644 --- a/Planet.gd +++ b/Planet.gd @@ -1,8 +1,5 @@ extends Node2D -const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") - - # A landable world-object - unlike regular Asteroid.gd, planets are # stationary ("Big Stable Asteroid" - stable means it doesn't drift or # destroy ships on touch). Whether the fleet counts as "landed" is decided @@ -14,33 +11,15 @@ const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") # while the fleet as a whole was clearly sitting on the planet. @export var planet_name: String = "" -@export var definition_id: String = PLANET_DEFINITIONS.DEFAULT_DEFINITION_ID +@export var planet_type: String = "rocky" +@export var resource_type: String = "ore" @export var landing_radius: float = 140.0 # Set by whoever spawns this planet (PlanetField, seeded from the chunk's # own coordinates) so the outline shape is stable across respawns of the # same chunk, instead of reading as a "different" planet every time. @export var shape_seed: int = 0 -var definition: Resource -var visual: Node2D - -var resource_type: String: - get: return String(definition.resource_id) if definition != null else "ore" - -var surface_profile_id: String: - get: return String(definition.surface_profile_id) if definition != null else "rocky" - -var shape_points: PackedVector2Array: - get: - if visual != null and "shape_points" in visual: - return visual.shape_points - return PackedVector2Array() - -var gas_phase: float: - get: - if visual != null and "gas_phase" in visual: - return float(visual.gas_phase) - return 0.0 +var shape_points: PackedVector2Array = PackedVector2Array() # An extractor is built down on this planet's SURFACE, in a different scene # the player is no longer looking at, so from space nothing distinguished a @@ -58,20 +37,31 @@ const DERRICK_HALF_WIDTH: float = 16.0 # only at _ready() left the planet looking untouched until it happened to # despawn and respawn. var shows_extractor: bool = false +var gas_phase: float = 0.0 func _ready() -> void: add_to_group("planets") z_index = -1 - definition = PLANET_DEFINITIONS.get_definition(definition_id) - visual = definition.visual_scene.instantiate() - visual.configure(shape_seed) - get_node("VisualHost").add_child(visual) + + var rng: RandomNumberGenerator = RandomNumberGenerator.new() + rng.seed = shape_seed + var point_count: int = 10 + for i in range(point_count): + var angle: float = (float(i) / point_count) * TAU + var radius: float = rng.randf_range(70.0, 100.0) + shape_points.append(Vector2(cos(angle), sin(angle)) * radius) shows_extractor = GameState.has_extractor_at(planet_name) GameState.extractor_added.connect(_on_extractor_added) queue_redraw() +func _process(delta: float) -> void: + if planet_type == "gas_giant": + gas_phase += delta + queue_redraw() + + func _on_extractor_added(built_on: String) -> void: # Only the planet it was actually built on, and only a redraw when the # answer really changed - every other planet in range gets this too. @@ -82,9 +72,14 @@ func _on_extractor_added(built_on: String) -> void: func extractor_derrick_anchor() -> Vector2: - if visual != null and visual.has_method("extractor_anchor"): - return visual.extractor_anchor() - return Vector2(0.0, -100.0) + # The rim vertex the derrick stands on: the shape's own topmost point, + # rather than a fixed radius, so it sits ON the outline whatever lumpy + # shape the seed drew instead of floating off it or sinking inside it. + var base: Vector2 = Vector2(0.0, -100.0) + for p in shape_points: + if p.y < base.y: + base = p + return base func extractor_derrick_tip() -> Vector2: @@ -116,5 +111,28 @@ func extractor_derrick_points() -> PackedVector2Array: func _draw() -> void: + if planet_type == "gas_giant": + _draw_gas_giant() + if shows_extractor: + NeonDraw.draw_glowing_polygon(self, extractor_derrick_points(), EXTRACTOR_MARKER_COLOR, 1.0) + return + NeonDraw.draw_glowing_polygon(self, shape_points, Color(0.6, 0.55, 0.5), 1.0) if shows_extractor: NeonDraw.draw_glowing_polygon(self, extractor_derrick_points(), EXTRACTOR_MARKER_COLOR, 1.0) + + +func _draw_gas_giant() -> void: + # A round, animated giant with drifting latitudinal bands. It is visibly + # unlike the rocky outline and keeps moving even while the fleet is idle. + var rim := PackedVector2Array() + for i in range(32): + var a := TAU * float(i) / 32.0 + rim.append(Vector2(cos(a) * 118.0, sin(a) * 118.0)) + NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9) + for row in [-62.0, -28.0, 8.0, 43.0, 73.0]: + var band := PackedVector2Array() + var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row)) + for i in range(15): + var x := lerpf(-half_width, half_width, float(i) / 14.0) + band.append(Vector2(x, row + sin(x * 0.045 + gas_phase * 1.6 + row) * 7.0)) + NeonDraw.draw_glowing_path(self, band, Color(0.85, 0.52, 1.0), 0.75, 0.65) diff --git a/Planet.tscn b/Planet.tscn index 0e7fe1f..844672b 100644 --- a/Planet.tscn +++ b/Planet.tscn @@ -4,5 +4,3 @@ [node name="Planet" type="Node2D"] script = ExtResource("1") - -[node name="VisualHost" type="Node2D" parent="."] diff --git a/PlanetAtmosphereController.gd b/PlanetAtmosphereController.gd index 3a66ea4..cc497d4 100644 --- a/PlanetAtmosphereController.gd +++ b/PlanetAtmosphereController.gd @@ -113,7 +113,7 @@ func _on_enter_pressed() -> void: GameState.return_world_position = current_planet.position GameState.return_camera_zoom = camera.zoom.x GameState.planet_name = current_planet.planet_name - GameState.planet_definition_id = current_planet.definition_id + GameState.planet_type = current_planet.planet_type var tween: Tween = create_tween() tween.tween_property(fade_rect, "color:a", 1.0, fade_duration) diff --git a/PlanetDefinition.gd b/PlanetDefinition.gd deleted file mode 100644 index e184241..0000000 --- a/PlanetDefinition.gd +++ /dev/null @@ -1,10 +0,0 @@ -class_name PlanetDefinition -extends Resource - -@export var definition_id: String = "rocky" -@export var display_name: String = "Rocky planet" -@export var names: PackedStringArray = [] -@export var visual_scene: PackedScene -@export var surface_profile_id: String = "rocky" -@export var resource_id: String = "ore" -@export var map_color: Color = Color(0.7, 0.65, 0.55) diff --git a/PlanetDefinition.gd.uid b/PlanetDefinition.gd.uid deleted file mode 100644 index ee09e6d..0000000 --- a/PlanetDefinition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dt6tsdmmlnfaf diff --git a/PlanetDefinitions.gd b/PlanetDefinitions.gd deleted file mode 100644 index fa9dd2d..0000000 --- a/PlanetDefinitions.gd +++ /dev/null @@ -1,14 +0,0 @@ -class_name PlanetDefinitions -extends RefCounted - -const ROCKY_ID: String = "rocky" -const GAS_GIANT_ID: String = "gas_giant" -const DEFAULT_DEFINITION_ID: String = ROCKY_ID -const DEFINITIONS: Dictionary = { - ROCKY_ID: preload("res://RockyPlanetDefinition.tres"), - GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"), -} - - -static func get_definition(definition_id: String) -> Resource: - return DEFINITIONS.get(definition_id, DEFINITIONS[DEFAULT_DEFINITION_ID]) diff --git a/PlanetDefinitions.gd.uid b/PlanetDefinitions.gd.uid deleted file mode 100644 index 188741c..0000000 --- a/PlanetDefinitions.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ipsld2ex2t8e diff --git a/PlanetField.gd b/PlanetField.gd index 6a8c3c6..4dbbdd8 100644 --- a/PlanetField.gd +++ b/PlanetField.gd @@ -1,6 +1,5 @@ extends Node2D - # Spawns "Big Stable Asteroid" planets in ASTEROID_FIELD biome chunks near # the fleet where WorldGen says one belongs, and despawns them once the # fleet has moved far enough away - mirrors AsteroidField's chunk-lifecycle @@ -84,7 +83,8 @@ func _spawn_chunk_planet(chunk: Vector2i) -> void: planet.position = origin + data.local_pos planet.planet_name = data.name planet.shape_seed = data.shape_seed - planet.definition_id = String(data.get("definition_id", "rocky")) + planet.planet_type = String(data.get("planet_type", "rocky")) + planet.resource_type = String(data.get("resource_type", "ore")) # Deferred: update_chunks() can run synchronously from inside a # parent's own _ready() chain (forced once right after the fleet is # positioned, to avoid a pop-in on scene load) - Godot refuses a diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index 81cea2d..507ec2b 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -7,7 +7,9 @@ [ext_resource type="Script" path="res://PlanetSurfaceController.gd" id="5"] [ext_resource type="Script" path="res://IconButton.gd" id="6"] [ext_resource type="Script" path="res://TouchJoystick.gd" id="7"] +[ext_resource type="Script" path="res://RockField.gd" id="8"] [ext_resource type="Script" path="res://ResourceCounter.gd" id="9"] +[ext_resource type="Script" path="res://GasVentField.gd" id="10"] [ext_resource type="Script" path="res://GasCounter.gd" id="11"] [ext_resource type="Script" path="res://CargoCounter.gd" id="12"] [ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"] @@ -47,7 +49,12 @@ ship_core_color = Color(0.02, 0.09, 0.15, 1) position = Vector2(0, 0) zoom = Vector2(1, 1) -[node name="SurfaceFeatures" type="Node2D" parent="."] +[node name="RockField" type="Node2D" parent="."] +script = ExtResource("8") + +[node name="GasVentField" type="Node2D" parent="."] +visible = false +script = ExtResource("10") [node name="SurfaceController" type="Node2D" parent="."] script = ExtResource("5") diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 7c683d4..502d8d4 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -1,8 +1,5 @@ extends Node2D -const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd") -const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") - # Bootstraps the surviving fleet on this scene's own startup (from # GameState, since this is a fresh scene tree - nothing here carries # over automatically) and handles the always-available exit prompt that @@ -16,15 +13,29 @@ const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") @onready var planet_name_label: Label = get_node("../UI/PlanetNameLabel") @onready var exit_button: IconButton = get_node("../UI/ExitButton") @onready var fade_rect: ColorRect = get_node("../FadeOverlay/FadeRect") -@onready var background: ColorRect = get_node("../BackgroundLayer/LunarBackground") -@onready var feature_host: Node2D = get_node("../SurfaceFeatures") var transitioning: bool = false func _ready() -> void: - var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id) - _compose_surface(SURFACE_PROFILES.get_profile(planet_definition.surface_profile_id)) + if GameState.planet_type == "gas_giant": + # A gas giant is not lunar terrain: its surface view uses a deep, + # coloured atmosphere instead of the rocky field. The background is + # shader-driven, so changing ColorRect.color does not tint it; update + # the actual palette uniforms. + get_node("../RockField").visible = false + get_node("../GasVentField").visible = true + var background: ColorRect = get_node("../BackgroundLayer/LunarBackground") + var atmosphere: ShaderMaterial = background.material + atmosphere.set_shader_parameter("base_color", Color(0.13, 0.025, 0.22, 1.0)) + atmosphere.set_shader_parameter("crater_color", Color(0.055, 0.008, 0.11, 1.0)) + atmosphere.set_shader_parameter("highlight_color", Color(0.52, 0.17, 0.7, 1.0)) + # The lunar surface uses dark captions on a bright ground. Flip only + # those captions for the night-dark gas atmosphere. + for caption_path in ["../UI/BankCaption", "../UI/FleetCaption"]: + var caption: Label = get_node(caption_path) + caption.add_theme_color_override("font_color", Color(0.78, 0.88, 0.94)) + caption.add_theme_color_override("font_outline_color", Color(0.02, 0.01, 0.05, 0.9)) fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO) camera.position = Vector2.ZERO camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom) @@ -36,21 +47,6 @@ func _ready() -> void: tween.tween_property(fade_rect, "color:a", 0.0, fade_duration) -func _compose_surface(profile: Resource) -> void: - var atmosphere: ShaderMaterial = background.material - atmosphere.set_shader_parameter("base_color", profile.base_color) - atmosphere.set_shader_parameter("crater_color", profile.crater_color) - atmosphere.set_shader_parameter("highlight_color", profile.highlight_color) - for caption_path in ["../UI/BankCaption", "../UI/FleetCaption"]: - var caption: Label = get_node(caption_path) - caption.add_theme_color_override("font_color", profile.caption_color) - caption.add_theme_color_override("font_outline_color", profile.caption_outline_color) - if profile.features_scene == null: - return - var features: Node = profile.features_scene.instantiate() - feature_host.add_child(features) - - func _on_exit_pressed() -> void: if transitioning: return diff --git a/PlanetSurfaceProfile.gd b/PlanetSurfaceProfile.gd deleted file mode 100644 index d2ea9be..0000000 --- a/PlanetSurfaceProfile.gd +++ /dev/null @@ -1,13 +0,0 @@ -class_name PlanetSurfaceProfile -extends Resource - -# Declarative recipe for a landed planet surface. The host scene only knows -# how to apply one of these; the feature scene owns the actual resource nodes. - -@export var profile_id: String = "rocky" -@export var features_scene: PackedScene -@export var base_color: Color = Color(0.75, 0.75, 0.76, 1.0) -@export var crater_color: Color = Color(0.44, 0.44, 0.45, 1.0) -@export var highlight_color: Color = Color(0.89, 0.89, 0.9, 1.0) -@export var caption_color: Color = Color(0.08, 0.16, 0.2, 1.0) -@export var caption_outline_color: Color = Color(0.9, 0.95, 0.96, 0.8) diff --git a/PlanetSurfaceProfile.gd.uid b/PlanetSurfaceProfile.gd.uid deleted file mode 100644 index 884fe16..0000000 --- a/PlanetSurfaceProfile.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b31vef6ge1tjh diff --git a/PlanetSurfaceProfiles.gd b/PlanetSurfaceProfiles.gd deleted file mode 100644 index 97da418..0000000 --- a/PlanetSurfaceProfiles.gd +++ /dev/null @@ -1,12 +0,0 @@ -class_name PlanetSurfaceProfiles -extends RefCounted - -const DEFAULT_PROFILE_ID: String = "rocky" -const PROFILES: Dictionary = { - "rocky": preload("res://RockySurfaceProfile.tres"), - "gas": preload("res://GasSurfaceProfile.tres"), -} - - -static func get_profile(profile_id: String) -> Resource: - return PROFILES.get(profile_id, PROFILES[DEFAULT_PROFILE_ID]) diff --git a/PlanetSurfaceProfiles.gd.uid b/PlanetSurfaceProfiles.gd.uid deleted file mode 100644 index c0f3f93..0000000 --- a/PlanetSurfaceProfiles.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bufr6mnwbobxb diff --git a/ResourceBar.gd b/ResourceBar.gd index 6594fc1..2bf11b4 100644 --- a/ResourceBar.gd +++ b/ResourceBar.gd @@ -3,7 +3,7 @@ extends Control # Compact resource table shared by crafting, research and the ship lab. # Rows are resources; columns are locations. Adding another material to -# The resource-definition catalog automatically gives each material a row. +# GameState.RESOURCE_TYPES automatically gives it a row here. var building_id: int = 0 @@ -24,7 +24,7 @@ func _ready() -> void: add_child(label) _header_labels.append(label) - for resource_type in GameState.resource_types(): + for resource_type in GameState.RESOURCE_TYPES: var glyph := ResourceGlyph.new() glyph.resource_type = resource_type add_child(glyph) @@ -78,7 +78,7 @@ func _layout() -> void: var row_height: float = (size.y - HEADER_HEIGHT - 4.0) / maxf(1.0, float(_rows.size())) var row_index: int = 0 - for resource_type in GameState.resource_types(): + for resource_type in GameState.RESOURCE_TYPES: var row: Dictionary = _rows[resource_type] var y: float = HEADER_HEIGHT + float(row_index) * row_height row.glyph.position = Vector2(2.0, y + 2.0) @@ -94,7 +94,7 @@ func _layout() -> void: func _process(_delta: float) -> void: - for resource_type in GameState.resource_types(): + for resource_type in GameState.RESOURCE_TYPES: var row: Dictionary = _rows.get(resource_type, {}) if row.is_empty(): continue diff --git a/ResourceDefinition.gd b/ResourceDefinition.gd deleted file mode 100644 index acb5acc..0000000 --- a/ResourceDefinition.gd +++ /dev/null @@ -1,7 +0,0 @@ -class_name ResourceDefinition -extends Resource - -@export var resource_id: String = "ore" -@export var display_name: String = "Ore" -@export var color: Color = Color.WHITE -@export var glyph_scene: PackedScene diff --git a/ResourceDefinition.gd.uid b/ResourceDefinition.gd.uid deleted file mode 100644 index 440e5e4..0000000 --- a/ResourceDefinition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://kvy02iu7pbyu diff --git a/ResourceDefinitions.gd b/ResourceDefinitions.gd deleted file mode 100644 index 7ddf4c0..0000000 --- a/ResourceDefinitions.gd +++ /dev/null @@ -1,18 +0,0 @@ -class_name ResourceDefinitions -extends RefCounted - -const ORE_ID: String = "ore" -const GAS_ID: String = "gas" -const ORDER: PackedStringArray = [ORE_ID, GAS_ID] -const DEFINITIONS: Dictionary = { - ORE_ID: preload("res://OreResourceDefinition.tres"), - GAS_ID: preload("res://GasResourceDefinition.tres"), -} - - -static func get_definition(resource_id: String) -> Resource: - return DEFINITIONS.get(resource_id) - - -static func ids() -> PackedStringArray: - return ORDER diff --git a/ResourceDefinitions.gd.uid b/ResourceDefinitions.gd.uid deleted file mode 100644 index 9ef7d79..0000000 --- a/ResourceDefinitions.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ci7yyyxyd3o67 diff --git a/ResourceGlyph.gd b/ResourceGlyph.gd index 14a90d0..aa80578 100644 --- a/ResourceGlyph.gd +++ b/ResourceGlyph.gd @@ -1,8 +1,6 @@ class_name ResourceGlyph extends Control -const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") - # A small, non-interactive resource mark shared by tables and transfer rows. # Resource buttons used to borrow IconButton, which made decorative icons # look tappable and encouraged each screen to invent its own version. @@ -14,21 +12,43 @@ func set_resource_type(value: String) -> void: if resource_type == value: return resource_type = value - if is_node_ready(): - _rebuild_visual() + queue_redraw() func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE - _rebuild_visual() + queue_redraw() -func _rebuild_visual() -> void: - for child in get_children(): - child.queue_free() - var definition: Resource = RESOURCE_DEFINITIONS.get_definition(resource_type) - if definition == null or definition.glyph_scene == null: - return - var visual: Control = definition.glyph_scene.instantiate() - visual.colour = definition.color - add_child(visual) +func _draw() -> void: + var colour: Color = GameState.resource_color(resource_type) + var centre: Vector2 = size * 0.5 + var radius: float = minf(size.x, size.y) * 0.34 + match resource_type: + GameState.RESOURCE_GAS: + _draw_gas(centre, radius, colour) + _: + _draw_ore(centre, radius, colour) + + +func _draw_ore(centre: Vector2, radius: float, colour: Color) -> void: + var points := PackedVector2Array() + for i in range(6): + var angle: float = TAU * float(i) / 6.0 - PI * 0.5 + points.append(centre + Vector2(cos(angle), sin(angle)) * radius) + NeonDraw.draw_glowing_polygon(self, points, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) + NeonDraw.draw_glowing_line(self, points[5], points[2], colour, 0.55, maxf(0.4, radius * 0.025), colour.darkened(0.78)) + + +func _draw_gas(centre: Vector2, radius: float, colour: Color) -> void: + var bubbles: Array = [ + [centre + Vector2(-radius * 0.38, radius * 0.12), radius * 0.46], + [centre + Vector2(radius * 0.34, radius * 0.26), radius * 0.58], + [centre + Vector2(radius * 0.38, -radius * 0.48), radius * 0.32], + ] + for bubble in bubbles: + var ring := PackedVector2Array() + for i in range(12): + var angle: float = TAU * float(i) / 12.0 + ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1])) + NeonDraw.draw_glowing_polygon(self, ring, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) diff --git a/ResourceTransferList.gd b/ResourceTransferList.gd index 859f0f1..1883119 100644 --- a/ResourceTransferList.gd +++ b/ResourceTransferList.gd @@ -27,7 +27,7 @@ func _ready() -> void: _destination_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER add_child(_destination_heading) - for resource_type in GameState.resource_types(): + for resource_type in GameState.RESOURCE_TYPES: var colour: Color = GameState.resource_color(resource_type) var glyph := ResourceGlyph.new() glyph.resource_type = resource_type @@ -140,7 +140,7 @@ func set_destination(id: int, heading: String) -> void: func refresh() -> void: - for resource_type in GameState.resource_types(): + for resource_type in GameState.RESOURCE_TYPES: var row: Dictionary = _rows.get(resource_type, {}) if row.is_empty(): continue @@ -169,7 +169,7 @@ func _layout() -> void: var row_height: float = (size.y - HEADER_HEIGHT) / maxf(1.0, float(_rows.size())) var row_index: int = 0 - for resource_type in GameState.resource_types(): + for resource_type in GameState.RESOURCE_TYPES: var row: Dictionary = _rows[resource_type] var y: float = HEADER_HEIGHT + float(row_index) * row_height var glyph_size: float = minf(66.0, row_height * 0.23) diff --git a/RockField.gd b/RockField.gd index dd839ed..a82389c 100644 --- a/RockField.gd +++ b/RockField.gd @@ -17,8 +17,7 @@ extends Node2D @export var despawn_radius: float = 2600.0 @export var check_interval: float = 0.5 -@export var fleet_path: NodePath = NodePath("../Fleet") -@onready var fleet: Node2D = get_node(fleet_path) +@onready var fleet: Node2D = get_node("../Fleet") var rock_scene: PackedScene = preload("res://Rock.tscn") var rocks: Array = [] @@ -27,6 +26,12 @@ var rng: RandomNumberGenerator = RandomNumberGenerator.new() func _ready() -> void: + # Gas giants have their own gas vents and must never receive ore pickups. + # This check happens before the initial population pass, unlike merely + # hiding the field later from PlanetSurfaceController. + if GameState.planet_type == "gas_giant": + set_process(false) + return rng.randomize() # Populate immediately rather than waiting out the first interval, so # the surface isn't visibly empty for half a second on arrival. diff --git a/RockyPlanetDefinition.tres b/RockyPlanetDefinition.tres deleted file mode 100644 index b0a3e9f..0000000 --- a/RockyPlanetDefinition.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] - -[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://RockyPlanetVisual.tscn" id="2"] - -[resource] -script = ExtResource("1") -definition_id = "rocky" -display_name = "Rocky planet" -names = PackedStringArray("Kepler's Rest", "Ceres Minor", "Vesta's Shadow", "Ironhold", "Drift Anchor", "Stonefall", "Grey Watch", "Silent Quarry", "Baros Rock", "Umbra Point") -visual_scene = ExtResource("2") -surface_profile_id = "rocky" -resource_id = "ore" -map_color = Color(0.7, 0.65, 0.55, 1) diff --git a/RockyPlanetVisual.gd b/RockyPlanetVisual.gd deleted file mode 100644 index dc2a889..0000000 --- a/RockyPlanetVisual.gd +++ /dev/null @@ -1,26 +0,0 @@ -extends Node2D - -var shape_points: PackedVector2Array = PackedVector2Array() - - -func configure(shape_seed: int) -> void: - shape_points.clear() - var rng := RandomNumberGenerator.new() - rng.seed = shape_seed - for i in range(10): - var angle: float = (float(i) / 10.0) * TAU - var radius: float = rng.randf_range(70.0, 100.0) - shape_points.append(Vector2(cos(angle), sin(angle)) * radius) - queue_redraw() - - -func extractor_anchor() -> Vector2: - var anchor := Vector2(0.0, -100.0) - for point in shape_points: - if point.y < anchor.y: - anchor = point - return anchor - - -func _draw() -> void: - NeonDraw.draw_glowing_polygon(self, shape_points, Color(0.6, 0.55, 0.5), 1.0) diff --git a/RockyPlanetVisual.gd.uid b/RockyPlanetVisual.gd.uid deleted file mode 100644 index b6ecadb..0000000 --- a/RockyPlanetVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b82xh8nosedlm diff --git a/RockyPlanetVisual.tscn b/RockyPlanetVisual.tscn deleted file mode 100644 index ee6a302..0000000 --- a/RockyPlanetVisual.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://RockyPlanetVisual.gd" id="1"] - -[node name="Visual" type="Node2D"] -script = ExtResource("1") diff --git a/RockySurfaceFeatures.tscn b/RockySurfaceFeatures.tscn deleted file mode 100644 index f31b5c8..0000000 --- a/RockySurfaceFeatures.tscn +++ /dev/null @@ -1,7 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://RockField.gd" id="1"] - -[node name="ResourceProvider" type="Node2D"] -script = ExtResource("1") -fleet_path = NodePath("../../Fleet") diff --git a/RockySurfaceProfile.tres b/RockySurfaceProfile.tres deleted file mode 100644 index ab220e0..0000000 --- a/RockySurfaceProfile.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=3 format=3] - -[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] -[ext_resource type="PackedScene" path="res://RockySurfaceFeatures.tscn" id="2"] - -[resource] -script = ExtResource("1") -profile_id = "rocky" -features_scene = ExtResource("2") -base_color = Color(0.75, 0.75, 0.76, 1) -crater_color = Color(0.44, 0.44, 0.45, 1) -highlight_color = Color(0.89, 0.89, 0.9, 1) -caption_color = Color(0.08, 0.16, 0.2, 1) -caption_outline_color = Color(0.9, 0.95, 0.96, 0.8) diff --git a/ShieldHitsUpgrade.tres b/ShieldHitsUpgrade.tres deleted file mode 100644 index 2530597..0000000 --- a/ShieldHitsUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "shd_hits" -display_name = "Layered Emitters" -description = "+1 hit absorbed before the shield drops." -base_cost = 60 -max_level = 2 -icon = 19 -additive_modifiers = {"shield_charges": 1.0} diff --git a/ShieldRechargeUpgrade.tres b/ShieldRechargeUpgrade.tres deleted file mode 100644 index d73cf09..0000000 --- a/ShieldRechargeUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "shd_recharge" -display_name = "Fast Cycling" -description = "Shield returns 1.2s sooner per level." -base_cost = 35 -max_level = 3 -icon = 20 -additive_modifiers = {"shield_recharge": -1.2} diff --git a/ShieldSpeedUpgrade.tres b/ShieldSpeedUpgrade.tres deleted file mode 100644 index fe5a4b6..0000000 --- a/ShieldSpeedUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "shd_speed" -display_name = "Thrusters" -description = "+40 top speed, +60 acceleration per level." -base_cost = 25 -max_level = 3 -icon = 18 -additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0} diff --git a/ShieldVesselDefinition.tres b/ShieldVesselDefinition.tres deleted file mode 100644 index 9df3bf8..0000000 --- a/ShieldVesselDefinition.tres +++ /dev/null @@ -1,25 +0,0 @@ -[gd_resource type="Resource" script_class="VesselDefinition" load_steps=7 format=3] -[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://VesselWeaponComponent.tscn" id="2"] -[ext_resource type="PackedScene" path="res://VesselShieldComponent.tscn" id="3"] -[ext_resource type="Resource" path="res://ShieldHitsUpgrade.tres" id="4"] -[ext_resource type="Resource" path="res://ShieldRechargeUpgrade.tres" id="5"] -[ext_resource type="Resource" path="res://ShieldSpeedUpgrade.tres" id="6"] -[resource] -script = ExtResource("1") -definition_id = "shield" -display_name = "Shield Vessel" -description = "Combat vessel whose rechargeable personal shield absorbs lethal impacts." -build_cost = 14 -build_time = 16.0 -icon = 2 -glow_color = Color(0.45, 0.8, 1, 1) -capabilities = PackedStringArray("weapon", "shield") -component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3")]) -base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 450.0, "shield_charges": 1.0, "shield_recharge": 6.0} -minimum_stats = {"shield_recharge": 2.0} -upgrades = Array[Resource]([ExtResource("4"), ExtResource("5"), ExtResource("6")]) -unlock_id = "unlock_shield" -unlock_name = "Unlock Shield Vessel" -unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up." -unlock_cost = 45 diff --git a/Ship.gd b/Ship.gd index 1c2727c..624c312 100644 --- a/Ship.gd +++ b/Ship.gd @@ -2,134 +2,224 @@ extends Area2D signal destroyed -const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") - +# Deliberately faster than Fleet.cruise_speed: the fleet as a whole travels +# at the cruise rate, and this headroom is what lets an individual ship +# close on its formation slot - so a pivot mid-flight resolves quickly +# instead of the ship having no speed budget left over for it. @export var max_speed: float = 460.0 @export var acceleration: float = 450.0 @export var rotation_speed: float = 8.0 @export var arrive_radius: float = 6.0 + +@export var fire_range: float = 320.0 +@export var fire_cooldown: float = 0.5 +# How long to wait before looking for targets again after finding none. +@export var idle_rescan_interval: float = 0.25 +@export var projectile_damage: int = 1 + +# Which kind of vessel this is (GameState.VESSEL_*). Drives both its +# researched statistics and whether it carries a shield (#5). @export var vessel_type: String = "interceptor" -var definition: Resource +# --- shield (#5) ---------------------------------------------------------- +const SHIELD_COLOR: Color = Color(0.45, 0.8, 1.0) +const SHIELD_RADIUS: float = 30.0 + +# A shield vessel survives a hit that would destroy any other ship, and is +# then defenceless until it cycles back up. Personal, not a bubble: it +# protects the vessel carrying it and nothing else. +var shield_charges: int = 0 +var shield_recharge_left: float = 0.0 +var _shield_ring: Node2D = null + +# Neon colours, assigned by Fleet on spawn so a scene can tint its fleet. +# The lunar surface is a bright grey, where the default near-white core all +# but vanishes - PlanetSurface.tscn passes a dark core there. See NeonDraw. var glow_color: Color = Color(0.25, 0.85, 1.0) -var core_color: Color = Color.WHITE +var core_color: Color = Color(1.0, 1.0, 1.0) + var velocity: Vector2 = Vector2.ZERO +# The fleet's own velocity, supplied by Fleet as CONTEXT rather than as a +# facing command - the ship works out where to point from it. +# +# It is needed because `velocity` below is not the ship's true motion while +# it is holding formation: the slot it chases sits inside arrive_radius, so +# the ship snaps onto it and reports zero velocity even though it is being +# carried along at cruise speed. Facing derived from `velocity` alone +# therefore froze at whatever heading the ship last happened to travel. +# Adding the two gives the ship's ACTUAL motion through the world, which is +# the honest thing to point along in every case: it equals the fleet's +# heading when station-keeping, the ship's own heading when manoeuvring +# alone, and the true blend of both while strafing into a new slot. var fleet_velocity: Vector2 = Vector2.ZERO var move_target: Vector2 - -# Compatibility accessors keep old saves/tests readable while shield state -# itself belongs exclusively to the optional ShieldComponent. -var shield_charges: int: - get: - var component: Node = get_component("shield") - return component.charges if component != null else 0 - set(value): - var component: Node = get_component("shield") - if component != null: - component.set_charges(value) - -var shield_recharge_left: float: - get: - var component: Node = get_component("shield") - return component.recharge_left if component != null else 0.0 - set(value): - var component: Node = get_component("shield") - if component != null: - component.recharge_left = value - -var projectile_damage: int: - get: - var component: Node = get_component("weapon") - return component.projectile_damage if component != null else 0 +var fire_timer: float = 0.0 +var projectile_scene: PackedScene = preload("res://Projectile.tscn") func _ready() -> void: move_target = position add_to_group("player_ships") area_entered.connect(_on_area_entered) - definition = VESSEL_DEFINITIONS.get_definition(vessel_type) - if definition == null: - return - glow_color = definition.glow_color - for component_scene in definition.component_scenes: - var component: Node = component_scene.instantiate() - add_child(component) - if component.has_method("setup"): - component.setup() + if vessel_type == GameState.VESSEL_SHIELD: + _build_shield_ring() + shield_charges = GameState.shield_max_charges() apply_vessel_stats() queue_redraw() -func get_component(capability: String) -> Node: - for child in get_children(): - if child.get("capability_id") == capability: - return child - return null - - -func has_capability(capability: String) -> bool: - return get_component(capability) != null - - func set_move_target(world_pos: Vector2) -> void: move_target = world_pos func apply_vessel_stats() -> void: - if definition == null: - definition = VESSEL_DEFINITIONS.get_definition(vessel_type) - if definition == null: - return - max_speed = GameState.vessel_stat(vessel_type, "max_speed") - acceleration = GameState.vessel_stat(vessel_type, "acceleration") - for child in get_children(): - if child.has_method("apply_stats"): - child.apply_stats() + # Read from GameState rather than baked in at spawn, so researching an + # upgrade improves the ships already flying and not just the next one + # built. Fleet calls this on every vessel when something is bought. + max_speed = GameState.ship_max_speed(vessel_type) + acceleration = GameState.ship_acceleration(vessel_type) + fire_range = GameState.ship_fire_range(vessel_type) + fire_cooldown = GameState.ship_fire_cooldown(vessel_type) + projectile_damage = GameState.ship_damage(vessel_type) + if vessel_type == GameState.VESSEL_SHIELD: + # Topping up a live shield with researched capacity, without + # refilling one that is currently down and cycling. + var was_up: bool = shield_charges > 0 + shield_charges = GameState.shield_max_charges() if was_up else shield_charges + _update_shield_visual() func has_shield() -> bool: - var component: Node = get_component("shield") - return component != null and component.is_active() + return vessel_type == GameState.VESSEL_SHIELD and shield_charges > 0 + + +func _update_shield_visual() -> void: + if _shield_ring == null: + return + # visible only - per #10 the ring is rasterised once and never redrawn to + # turn it on and off. + _shield_ring.visible = has_shield() + + +func _tick_shield(delta: float) -> void: + if vessel_type != GameState.VESSEL_SHIELD: + return + if shield_charges >= GameState.shield_max_charges(): + return + shield_recharge_left -= delta + if shield_recharge_left <= 0.0: + shield_charges += 1 + shield_recharge_left = GameState.shield_recharge_time() + _update_shield_visual() func _physics_process(delta: float) -> void: - for child in get_children(): - if child.has_method("tick"): - child.tick(delta) - + _tick_shield(delta) var to_target: Vector2 = move_target - position var distance: float = to_target.length() + if distance <= arrive_radius: + # Snap and stop dead - prevents residual float-level jitter that + # would otherwise keep nudging back and forth around the target. position = move_target velocity = Vector2.ZERO else: + # Braking-distance formula: the fastest speed we can be going right + # now and still be able to decelerate to 0 exactly at the target, + # given our acceleration. Without this the ship approaches at full + # speed, overshoots (stopping distance = max_speed^2 / (2*accel)), + # and oscillates back and forth forever instead of settling. var braking_speed: float = sqrt(2.0 * acceleration * distance) var desired_speed: float = min(max_speed, braking_speed) var desired_velocity: Vector2 = to_target.normalized() * desired_speed velocity = velocity.move_toward(desired_velocity, acceleration * delta) position += velocity * delta + # One owner, one rule: face the way this ship is actually travelling. var motion: Vector2 = velocity + fleet_velocity if motion.length() > 8.0: var target_angle: float = motion.angle() + PI / 2.0 rotation = lerp_angle(rotation, target_angle, rotation_speed * delta) + _update_weapon(delta) + + +func _update_weapon(delta: float) -> void: + fire_timer -= delta + if fire_timer > 0.0: + return + + var target: Node2D = _find_nearest_enemy() + if target == null: + # Back off before scanning again. Without this, fire_timer stays at + # or below zero and every ship queries the whole "enemies" group and + # loops it on EVERY physics frame whenever nothing is in range - + # which is most of the time. + fire_timer = idle_rescan_interval + return + + fire_timer = fire_cooldown + var proj: Node2D = projectile_scene.instantiate() + proj.position = position + proj.damage = projectile_damage + get_tree().root.add_child(proj) + proj.launch(target.position - position) + + +func _find_nearest_enemy() -> Node2D: + var enemies: Array = get_tree().get_nodes_in_group("enemies") + var nearest: Node2D = null + var best_dist: float = fire_range + + for e in enemies: + if not is_instance_valid(e): + continue + var d: float = position.distance_to(e.position) + if d <= best_dist: + best_dist = d + nearest = e + + return nearest + func _on_area_entered(area: Area2D) -> void: if not (area.is_in_group("enemies") or area.is_in_group("asteroids")): return - for child in get_children(): - if child.has_method("absorb_hit") and child.absorb_hit(): - if area.is_in_group("asteroids") and is_instance_valid(area): - area.queue_free() - return + if has_shield(): + # Absorbed. The thing that hit it is still destroyed - a shield stops + # the vessel dying, it does not make the rock harmless - but the + # vessel survives and is defenceless until the shield cycles back. + shield_charges -= 1 + shield_recharge_left = GameState.shield_recharge_time() + _update_shield_visual() + if area.is_in_group("asteroids") and is_instance_valid(area): + area.queue_free() + return ShipExplosionVFX.spawn(get_tree().root, position) destroyed.emit() queue_free() +func _build_shield_ring() -> void: + # Its own child, drawn once, toggled with `visible` (#10). A shield that + # came and went by redrawing the ship would re-tessellate the hull every + # time it cycled. + _shield_ring = NeonPiece.create(self, Vector2.ZERO, SHIELD_COLOR) + var points: PackedVector2Array = PackedVector2Array() + for i in range(14): + var a: float = (float(i) / 14.0) * TAU + points.append(Vector2(cos(a), sin(a)) * SHIELD_RADIUS) + _shield_ring.add_polygon(points, 0.7) + _shield_ring.queue_redraw() + _shield_ring.visible = false + + func _draw() -> void: - var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([ - Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16), - ]) + var nose: Vector2 = Vector2(0, -22) + var left: Vector2 = Vector2(-14, 16) + var right: Vector2 = Vector2(14, 16) + var points: PackedVector2Array = PackedVector2Array([nose, left, right]) + # Constant glow, drawn once. No pulse means nothing changes frame to + # frame, so the ship never needs redrawing after _ready() - which also + # removes four antialiased polyline rebuilds per ship per frame. NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color) diff --git a/ShipLabBuildingDefinition.tres b/ShipLabBuildingDefinition.tres deleted file mode 100644 index 0823f52..0000000 --- a/ShipLabBuildingDefinition.tres +++ /dev/null @@ -1,20 +0,0 @@ -[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 deleted file mode 100644 index 9655162..0000000 --- a/ShipLabBuildingVisual.gd +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index fbb5203..0000000 --- a/ShipLabBuildingVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b0p7p0fs0th0b diff --git a/ShipLabBuildingVisual.tscn b/ShipLabBuildingVisual.tscn deleted file mode 100644 index e71fdf2..0000000 --- a/ShipLabBuildingVisual.tscn +++ /dev/null @@ -1,11 +0,0 @@ -[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/TransportCapacityUpgrade.tres b/TransportCapacityUpgrade.tres deleted file mode 100644 index 2c2b920..0000000 --- a/TransportCapacityUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "trn_capacity" -display_name = "Expanded Hold" -description = "Carries 8 more resources per supply run." -base_cost = 30 -max_level = 3 -icon = 21 -additive_modifiers = {"cargo_capacity": 8.0} diff --git a/TransportHubBuildingDefinition.tres b/TransportHubBuildingDefinition.tres deleted file mode 100644 index 59ad15d..0000000 --- a/TransportHubBuildingDefinition.tres +++ /dev/null @@ -1,20 +0,0 @@ -[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 deleted file mode 100644 index a8a0d44..0000000 --- a/TransportHubBuildingVisual.gd +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index b1ca4cb..0000000 --- a/TransportHubBuildingVisual.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6qyl5qnhw0ho diff --git a/TransportHubBuildingVisual.tscn b/TransportHubBuildingVisual.tscn deleted file mode 100644 index 1426761..0000000 --- a/TransportHubBuildingVisual.tscn +++ /dev/null @@ -1,11 +0,0 @@ -[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/TransportSpeedUpgrade.tres b/TransportSpeedUpgrade.tres deleted file mode 100644 index 43c3c96..0000000 --- a/TransportSpeedUpgrade.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"] -[resource] -script = ExtResource("1") -upgrade_id = "trn_speed" -display_name = "Freight Engines" -description = "+70 speed on supply runs." -base_cost = 30 -max_level = 3 -icon = 22 -additive_modifiers = {"transport_speed": 70.0} diff --git a/TransporterVesselDefinition.tres b/TransporterVesselDefinition.tres deleted file mode 100644 index c654a48..0000000 --- a/TransporterVesselDefinition.tres +++ /dev/null @@ -1,19 +0,0 @@ -[gd_resource type="Resource" script_class="VesselDefinition" load_steps=6 format=3] -[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"] -[ext_resource type="PackedScene" path="res://VesselCargoComponent.tscn" id="2"] -[ext_resource type="PackedScene" path="res://VesselTransportComponent.tscn" id="3"] -[ext_resource type="Resource" path="res://TransportCapacityUpgrade.tres" id="4"] -[ext_resource type="Resource" path="res://TransportSpeedUpgrade.tres" id="5"] -[resource] -script = ExtResource("1") -definition_id = "transporter" -display_name = "Transporter" -description = "Unarmed cargo vessel stationed at a transport hub to move resources." -build_cost = 10 -build_time = 8.0 -icon = 12 -glow_color = Color(0.95, 0.55, 0.85, 1) -capabilities = PackedStringArray("cargo", "transport") -component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3")]) -base_stats = {"max_speed": 460.0, "acceleration": 450.0, "cargo_capacity": 20.0, "transport_speed": 340.0} -upgrades = Array[Resource]([ExtResource("4"), ExtResource("5")]) diff --git a/VesselCargoComponent.gd b/VesselCargoComponent.gd deleted file mode 100644 index 4373500..0000000 --- a/VesselCargoComponent.gd +++ /dev/null @@ -1,9 +0,0 @@ -class_name VesselCargoComponent -extends Node - -var capability_id: String = "cargo" -var capacity: int - -func apply_stats() -> void: - var state: Node = get_tree().root.get_node("GameState") - capacity = int(state.vessel_stat(get_parent().vessel_type, "cargo_capacity")) diff --git a/VesselCargoComponent.gd.uid b/VesselCargoComponent.gd.uid deleted file mode 100644 index 85443bc..0000000 --- a/VesselCargoComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bo16x4wq6hg5k diff --git a/VesselCargoComponent.tscn b/VesselCargoComponent.tscn deleted file mode 100644 index 34eafa6..0000000 --- a/VesselCargoComponent.tscn +++ /dev/null @@ -1,4 +0,0 @@ -[gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselCargoComponent.gd" id="1"] -[node name="CargoComponent" type="Node"] -script = ExtResource("1") diff --git a/VesselDefinition.gd b/VesselDefinition.gd deleted file mode 100644 index 0f6637f..0000000 --- a/VesselDefinition.gd +++ /dev/null @@ -1,26 +0,0 @@ -class_name VesselDefinition -extends Resource - -@export var definition_id: String -@export var display_name: String -@export_multiline var description: String -@export var build_cost: int -@export var build_time: float = 10.0 -@export var icon: int = 2 -@export var glow_color: Color = Color(0.25, 0.85, 1.0) -@export var hull_points: PackedVector2Array = PackedVector2Array([ - Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16), -]) -@export var capabilities: PackedStringArray = [] -@export var component_scenes: Array[PackedScene] = [] -@export var base_stats: Dictionary = {} -@export var minimum_stats: Dictionary = {} -@export var upgrades: Array[Resource] = [] -@export var unlock_id: String -@export var unlock_name: String -@export_multiline var unlock_description: String -@export var unlock_cost: int - - -func has_capability(capability: String) -> bool: - return capability in capabilities diff --git a/VesselDefinition.gd.uid b/VesselDefinition.gd.uid deleted file mode 100644 index 371e084..0000000 --- a/VesselDefinition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6txy3v61blm8 diff --git a/VesselDefinitions.gd b/VesselDefinitions.gd deleted file mode 100644 index 2b33845..0000000 --- a/VesselDefinitions.gd +++ /dev/null @@ -1,24 +0,0 @@ -class_name VesselDefinitions -extends RefCounted - -const ORDER: PackedStringArray = ["interceptor", "shield", "transporter"] -const DEFINITIONS: Dictionary = { - "interceptor": preload("res://InterceptorVesselDefinition.tres"), - "shield": preload("res://ShieldVesselDefinition.tres"), - "transporter": preload("res://TransporterVesselDefinition.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 - -static func with_capability(capability: String) -> Resource: - for definition in all(): - if definition.has_capability(capability): - return definition - return null diff --git a/VesselDefinitions.gd.uid b/VesselDefinitions.gd.uid deleted file mode 100644 index d907359..0000000 --- a/VesselDefinitions.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cgwj1q4nf3ar7 diff --git a/VesselDelivery.gd b/VesselDelivery.gd index 6392b62..4c07a6d 100644 --- a/VesselDelivery.gd +++ b/VesselDelivery.gd @@ -1,10 +1,8 @@ extends Node2D -const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") - # Visual-only craft-to-mothership flight. GameState owns position/progress; # this node just renders a finished hull headed toward the hangar. -var vessel_type: String = "interceptor" +var vessel_type: String = GameState.VESSEL_INTERCEPTOR func _ready() -> void: @@ -13,6 +11,6 @@ func _ready() -> void: func _draw() -> void: - var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type) - if definition != null: - NeonDraw.draw_glowing_polygon(self, definition.hull_points, definition.glow_color, 1.0) + var color: Color = Color(0.45, 0.8, 1.0) if vessel_type == GameState.VESSEL_SHIELD else Color(0.95, 0.55, 0.85) if vessel_type == GameState.VESSEL_TRANSPORTER else Color(0.25, 0.85, 1.0) + var hull := PackedVector2Array([Vector2(0, -20), Vector2(13, 15), Vector2(0, 9), Vector2(-13, 15)]) + NeonDraw.draw_glowing_polygon(self, hull, color, 1.0) diff --git a/VesselDeliveryField.gd b/VesselDeliveryField.gd index 9b47319..606b1cf 100644 --- a/VesselDeliveryField.gd +++ b/VesselDeliveryField.gd @@ -17,9 +17,9 @@ func _process(_delta: float) -> void: var node: Node2D = active.get(i) if node == null or not is_instance_valid(node): node = delivery_script.new() - node.vessel_type = String(delivery.type) get_parent().add_child(node) active[i] = node + node.vessel_type = String(delivery.type) node.position = pos var direction: Vector2 = (delivery.to as Vector2) - (delivery.from as Vector2) if direction.length() > 0.1: diff --git a/VesselShieldComponent.gd b/VesselShieldComponent.gd deleted file mode 100644 index 15a8edf..0000000 --- a/VesselShieldComponent.gd +++ /dev/null @@ -1,69 +0,0 @@ -class_name VesselShieldComponent -extends Node - -const SHIELD_COLOR: Color = Color(0.45, 0.8, 1.0) -const SHIELD_RADIUS: float = 30.0 - -var capability_id: String = "shield" -var charges: int = 0 -var recharge_left: float = 0.0 -var ring: Node2D - - -func setup() -> void: - var ship: Node2D = get_parent() - var state: Node = ship.get_tree().root.get_node("GameState") - ring = NeonPiece.create(ship, Vector2.ZERO, SHIELD_COLOR) - var points := PackedVector2Array() - for i in range(14): - var angle: float = float(i) / 14.0 * TAU - points.append(Vector2(cos(angle), sin(angle)) * SHIELD_RADIUS) - ring.add_polygon(points, 0.7) - ring.queue_redraw() - charges = int(state.vessel_stat(ship.vessel_type, "shield_charges")) - _update_visual() - - -func apply_stats() -> void: - var was_up: bool = charges > 0 - if was_up: - charges = int(_state().vessel_stat(get_parent().vessel_type, "shield_charges")) - _update_visual() - - -func tick(delta: float) -> void: - var maximum: int = int(_state().vessel_stat(get_parent().vessel_type, "shield_charges")) - if charges >= maximum: - return - recharge_left -= delta - if recharge_left <= 0.0: - charges += 1 - recharge_left = _state().vessel_stat(get_parent().vessel_type, "shield_recharge") - _update_visual() - - -func is_active() -> bool: - return charges > 0 - - -func absorb_hit() -> bool: - if not is_active(): - return false - charges -= 1 - recharge_left = _state().vessel_stat(get_parent().vessel_type, "shield_recharge") - _update_visual() - return true - - -func set_charges(value: int) -> void: - charges = value - _update_visual() - - -func _state() -> Node: - return get_tree().root.get_node("GameState") - - -func _update_visual() -> void: - if ring != null: - ring.visible = is_active() diff --git a/VesselShieldComponent.gd.uid b/VesselShieldComponent.gd.uid deleted file mode 100644 index 4ab2382..0000000 --- a/VesselShieldComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ghacatobxl11 diff --git a/VesselShieldComponent.tscn b/VesselShieldComponent.tscn deleted file mode 100644 index 50ba89e..0000000 --- a/VesselShieldComponent.tscn +++ /dev/null @@ -1,4 +0,0 @@ -[gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselShieldComponent.gd" id="1"] -[node name="ShieldComponent" type="Node"] -script = ExtResource("1") diff --git a/VesselTransportComponent.gd b/VesselTransportComponent.gd deleted file mode 100644 index 7aec619..0000000 --- a/VesselTransportComponent.gd +++ /dev/null @@ -1,9 +0,0 @@ -class_name VesselTransportComponent -extends Node - -var capability_id: String = "transport" -var travel_speed: float - -func apply_stats() -> void: - var state: Node = get_tree().root.get_node("GameState") - travel_speed = state.vessel_stat(get_parent().vessel_type, "transport_speed") diff --git a/VesselTransportComponent.gd.uid b/VesselTransportComponent.gd.uid deleted file mode 100644 index a2c81fd..0000000 --- a/VesselTransportComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://tsapg8ld1vl8 diff --git a/VesselTransportComponent.tscn b/VesselTransportComponent.tscn deleted file mode 100644 index a16d4e9..0000000 --- a/VesselTransportComponent.tscn +++ /dev/null @@ -1,4 +0,0 @@ -[gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselTransportComponent.gd" id="1"] -[node name="TransportComponent" type="Node"] -script = ExtResource("1") diff --git a/VesselUpgradeDefinition.gd b/VesselUpgradeDefinition.gd deleted file mode 100644 index 083e092..0000000 --- a/VesselUpgradeDefinition.gd +++ /dev/null @@ -1,11 +0,0 @@ -class_name VesselUpgradeDefinition -extends Resource - -@export var upgrade_id: String -@export var display_name: String -@export_multiline var description: String -@export var base_cost: int -@export var max_level: int = 1 -@export var icon: int -@export var additive_modifiers: Dictionary = {} -@export var multiplicative_modifiers: Dictionary = {} diff --git a/VesselUpgradeDefinition.gd.uid b/VesselUpgradeDefinition.gd.uid deleted file mode 100644 index c615214..0000000 --- a/VesselUpgradeDefinition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dw41jux1cn3sw diff --git a/VesselWeaponComponent.gd b/VesselWeaponComponent.gd deleted file mode 100644 index 1bc6855..0000000 --- a/VesselWeaponComponent.gd +++ /dev/null @@ -1,47 +0,0 @@ -class_name VesselWeaponComponent -extends Node - -var capability_id: String = "weapon" -var fire_range: float -var fire_cooldown: float -var projectile_damage: int -var fire_timer: float = 0.0 -var projectile_scene: PackedScene = preload("res://Projectile.tscn") - - -func apply_stats() -> void: - var ship: Node = get_parent() - var state: Node = ship.get_tree().root.get_node("GameState") - fire_range = state.vessel_stat(ship.vessel_type, "fire_range") - fire_cooldown = state.vessel_stat(ship.vessel_type, "fire_cooldown") - projectile_damage = int(state.vessel_stat(ship.vessel_type, "damage")) - - -func tick(delta: float) -> void: - fire_timer -= delta - if fire_timer > 0.0: - return - var ship: Node2D = get_parent() - var target: Node2D = _find_nearest_enemy(ship) - if target == null: - fire_timer = 0.25 - return - var projectile: Node2D = projectile_scene.instantiate() - projectile.position = ship.position - projectile.damage = projectile_damage - ship.get_tree().root.add_child(projectile) - projectile.launch(target.position - ship.position) - fire_timer = fire_cooldown - - -func _find_nearest_enemy(ship: Node2D) -> Node2D: - var nearest: Node2D = null - var best_distance: float = fire_range - for enemy in ship.get_tree().get_nodes_in_group("enemies"): - if not is_instance_valid(enemy): - continue - var distance: float = ship.position.distance_to(enemy.position) - if distance <= best_distance: - best_distance = distance - nearest = enemy - return nearest diff --git a/VesselWeaponComponent.gd.uid b/VesselWeaponComponent.gd.uid deleted file mode 100644 index 9fd5306..0000000 --- a/VesselWeaponComponent.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cwo0nkmr4e1sl diff --git a/VesselWeaponComponent.tscn b/VesselWeaponComponent.tscn deleted file mode 100644 index b3bd611..0000000 --- a/VesselWeaponComponent.tscn +++ /dev/null @@ -1,4 +0,0 @@ -[gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://VesselWeaponComponent.gd" id="1"] -[node name="WeaponComponent" type="Node"] -script = ExtResource("1") diff --git a/WorldGen.gd b/WorldGen.gd index 5e73ee5..e36a51f 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -1,8 +1,6 @@ class_name WorldGen extends RefCounted -const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") - # Deterministic procedural world generation: the world is divided into # square chunks, and each chunk's biome is derived from a seeded # low-frequency noise field sampled at the chunk's own coordinates - so @@ -24,6 +22,13 @@ const GAS_GIANT_THRESHOLD: float = 0.55 const NEARBY_GAS_GIANT_CHUNK: Vector2i = Vector2i.ZERO const NEARBY_GAS_GIANT_LOCAL_POS: Vector2 = Vector2(900.0, 600.0) +const PLANET_NAMES: Array = [ + "Kepler's Rest", "Ceres Minor", "Vesta's Shadow", "Ironhold", + "Drift Anchor", "Stonefall", "Grey Watch", "Silent Quarry", + "Baros Rock", "Umbra Point", +] +const GAS_GIANT_NAMES: Array = ["Aurelia", "Boreas", "Cerulean Deep", "Helios Major", "Vesper"] + static var _noise: FastNoiseLite static var _planet_noise: FastNoiseLite @@ -83,16 +88,15 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: var guaranteed_nearby_giant: bool = chunk == NEARBY_GAS_GIANT_CHUNK var local_pos: Vector2 = NEARBY_GAS_GIANT_LOCAL_POS if guaranteed_nearby_giant \ else Vector2(rng.randf() * CHUNK_SIZE, rng.randf() * CHUNK_SIZE) + var planet_name: String = PLANET_NAMES[rng.randi() % PLANET_NAMES.size()] var shape_seed: int = rng.randi() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE - var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID - var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) - var names: PackedStringArray = definition.names - var planet_name: String = names[rng.randi() % names.size()] + if gas_giant: + planet_name = GAS_GIANT_NAMES[rng.randi() % GAS_GIANT_NAMES.size()] return {"local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, - "definition_id": definition_id} + "planet_type": "gas_giant" if gas_giant else "rocky", "resource_type": "gas" if gas_giant else "ore"} static func biome_color(biome: int) -> Color: diff --git a/WorldMapContent.gd b/WorldMapContent.gd index 5d4667b..e8ba8df 100644 --- a/WorldMapContent.gd +++ b/WorldMapContent.gd @@ -1,10 +1,18 @@ 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 @@ -30,20 +38,28 @@ 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": definition.map_color, - "sides": definition.map_sides, - "radius": definition.map_radius, - "phase": definition.map_phase, + "color": _building_marker_color(b.type), }) 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(). @@ -89,8 +105,7 @@ func _draw() -> void: # spawn_radius_chunks of the fleet). var data: Dictionary = WorldGen.planet_data_at_chunk(chunk) var planet_local: Vector2 = origin_local + data.local_pos * WORLD_TO_LOCAL - var definition: Resource = PLANET_DEFINITIONS.get_definition(String(data.definition_id)) - draw_circle(planet_local, 5.0, definition.map_color) + draw_circle(planet_local, 5.0, Color(0.7, 0.65, 0.55)) draw_arc(planet_local, 5.0, 0.0, TAU, 16, Color(1.0, 1.0, 1.0, 0.9), 1.0, true) if chunk_has_extractor_badge(chunk): @@ -101,7 +116,17 @@ 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(): - _draw_marker_polygon(_regular_polygon(marker.position, marker.radius, marker.sides, marker.phase), marker.color) + 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) 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 d3c6b1d..3bb938e 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._show_building_info(GameState.BUILDING_MOTHERSHIP) + controller._on_place_mothership() diff --git a/tests/_crafter_preview.gd b/tests/_crafter_preview.gd index 52888aa..dc89fe4 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.visual.set_process(false) - b.visual.anim_time = float(cell.phase) * b.visual.CYCLE - b.visual.apply_animation() + b.set_process(false) + b.anim_time = float(cell.phase) * b.CYCLE + b.apply_animation() var cam: Camera2D = Camera2D.new() cam.zoom = Vector2(1.65, 1.65) diff --git a/tests/_gas_surface_preview.gd b/tests/_gas_surface_preview.gd index 5dafbe5..ba68e1c 100644 --- a/tests/_gas_surface_preview.gd +++ b/tests/_gas_surface_preview.gd @@ -3,7 +3,7 @@ extends Node2D func _ready() -> void: GameState.planet_name = "Violet Tempest" - GameState.planet_definition_id = "gas_giant" + GameState.planet_type = "gas_giant" GameState.surviving_ship_count = 3 GameState.rocks = 128 GameState.gas = 46 diff --git a/tests/test_animation_continuity.gd b/tests/test_animation_continuity.gd index 3919aa6..d373a25 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.visual + ship = b 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 c67d45c..ef1d708 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 = [ - {"type": "mothership", "title": "Mothership"}, - {"type": "ship_crafter", "title": "Ship Crafter"}, - {"type": "command_center", "title": "Command Center"}, + {"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"}, ] @@ -84,7 +84,7 @@ func _process(delta: float) -> bool: var built_before: int = _count(type) controller._on_build_pressed() - controller._show_building_info(type) + controller.call(option.press) 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._show_building_info(type) + controller.call(option.press) 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 deleted file mode 100644 index 7cadf5d..0000000 --- a/tests/test_building_composition.gd +++ /dev/null @@ -1,35 +0,0 @@ -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_composition.gd.uid b/tests/test_building_composition.gd.uid deleted file mode 100644 index f74b39a..0000000 --- a/tests/test_building_composition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bljnijxm3n20e diff --git a/tests/test_building_redraw_cost.gd b/tests/test_building_redraw_cost.gd index 1c81451..c083754 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.visual + crafter = b GameState.BUILDING_MOTHERSHIP: - mothership = b.visual + mothership = b GameState.BUILDING_COMMAND_CENTER: - command = b.visual + command = b 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.name, b.draw_count + b.building_type, b.draw_count ]) quit(1) return true diff --git a/tests/test_crafter_animation.gd b/tests/test_crafter_animation.gd index 9b5d57e..7ddab14 100644 --- a/tests/test_crafter_animation.gd +++ b/tests/test_crafter_animation.gd @@ -57,19 +57,16 @@ func _process(delta: float) -> bool: # Spawning is deferred, so give the tree a frame to attach them. if elapsed < 0.2: return false - var crafter_building: Node2D = _find(GameState.BUILDING_SHIP_CRAFTER) - if crafter_building == null: + crafter = _find(GameState.BUILDING_SHIP_CRAFTER) + if crafter == 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_building: Node2D = _find(GameState.BUILDING_MOTHERSHIP) - var mothership: Node2D = mothership_building.visual if mothership_building != null else null + var mothership: Node2D = _find(GameState.BUILDING_MOTHERSHIP) var mothership_animates: bool = mothership != null and mothership.is_processing() - var command_building: Node2D = _find(GameState.BUILDING_COMMAND_CENTER) - var command: Node2D = command_building.visual if command_building != null else null + var command: Node2D = _find(GameState.BUILDING_COMMAND_CENTER) 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 0a4b769..13b2567 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._show_building_info(GameState.BUILDING_EXTRACTOR) + controller._on_place_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._show_building_info(GameState.BUILDING_EXTRACTOR) + controller._on_place_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._show_building_info(GameState.BUILDING_EXTRACTOR) + controller._on_place_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_gas_system.gd b/tests/test_gas_system.gd index 94edc1d..898e43d 100644 --- a/tests/test_gas_system.gd +++ b/tests/test_gas_system.gd @@ -27,7 +27,8 @@ func _process(delta: float) -> bool: return _fail("nearby gas giant chunk does not contain a planet") if WorldGen.biome_at_chunk(nearby_chunk) != WorldGen.Biome.SPACE: return _fail("nearby gas giant is not in the normal space biome") - if String(nearby_data.get("definition_id", "")) != "gas_giant": + if String(nearby_data.get("planet_type", "")) != "gas_giant" \ + or String(nearby_data.get("resource_type", "")) != "gas": return _fail("nearby guaranteed planet is not a gas source") if nearby_position.length() > WorldGen.CHUNK_SIZE * 2.0: return _fail("nearby gas giant spawned too far away: %s" % nearby_position) @@ -35,49 +36,34 @@ func _process(delta: float) -> bool: # Gas surfaces have vents only: no deferred ore pickups, and the vent # emission advances continuously so the source visibly looks active. game_state.planet_name = "Gas Test" - game_state.planet_definition_id = "gas_giant" + game_state.planet_type = "gas_giant" var surface: Node = load("res://PlanetSurface.tscn").instantiate() root.add_child(surface) - var gas_field: Node = surface.get_node("SurfaceFeatures/ResourceProvider") - if gas_field.get_script() != load("res://GasVentField.gd"): + var rock_field: Node = surface.get_node("RockField") + var gas_field: Node = surface.get_node("GasVentField") + if not rock_field.rocks.is_empty() or rock_field.is_processing(): surface.free() - return _fail("gas surface did not compose the gas feature scene") - if surface.get_node_or_null("RockField") != null: - surface.free() - return _fail("gas surface tree contains an ore producer") + return _fail("gas surface spawned or continued processing ore rocks") gas_field._process(0.5) if gas_field.emission_phase <= 0.0: surface.free() return _fail("gas-source emission animation did not advance") surface.free() - game_state.planet_definition_id = "rocky" - var rocky_surface: Node = load("res://PlanetSurface.tscn").instantiate() - root.add_child(rocky_surface) - var rocky_features: Node = rocky_surface.get_node("SurfaceFeatures/ResourceProvider") - if rocky_features.get_script() != load("res://RockField.gd"): - rocky_surface.free() - return _fail("rocky surface did not compose the ore feature scene") - if rocky_surface.get_node_or_null("GasVentField") != null: - rocky_surface.free() - return _fail("rocky surface tree contains a gas producer") - rocky_surface.free() - var found_giant: bool = false for x in range(-12, 13): for y in range(-12, 13): var data: Dictionary = WorldGen.planet_data_at_chunk(Vector2i(x, y)) - if String(data.get("definition_id", "")) == "gas_giant": - found_giant = true + if String(data.get("planet_type", "")) == "gas_giant": + found_giant = String(data.get("resource_type", "")) == "gas" break if found_giant: break if not found_giant: return _fail("world generation produced no deterministic gas giant") var giant: Node2D = load("res://Planet.tscn").instantiate() - giant.definition_id = "gas_giant" - root.add_child(giant) - giant.visual._process(0.5) + giant.planet_type = "gas_giant" + giant._process(0.5) if giant.gas_phase <= 0.0: return _fail("gas giant animation did not advance") giant.free() diff --git a/tests/test_generic_inventory.gd b/tests/test_generic_inventory.gd deleted file mode 100644 index 9e9418c..0000000 --- a/tests/test_generic_inventory.gd +++ /dev/null @@ -1,48 +0,0 @@ -extends SceneTree - -const INVENTORY = preload("res://Inventory.gd") -const TEST_RESOURCE: String = "crystal" - - -func _initialize() -> void: - var game_state: Node = root.get_node("GameState") - game_state.buildings.clear() - game_state.bank_inventory.set_amount(TEST_RESOURCE, 40) - game_state.fleet_inventory.set_amount(TEST_RESOURCE, 0) - - var mothership_id: int = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) - var crafter_id: int = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(100, 0)) - if game_state.transfer_resource_to_fleet(mothership_id, TEST_RESOURCE, 15) != 15: - return _fail("third resource could not move from bank to fleet") - if game_state.transfer_resource_to_building(crafter_id, TEST_RESOURCE, 9) != 9: - return _fail("third resource could not move from fleet to building") - if game_state.transfer_resource_to_fleet(crafter_id, TEST_RESOURCE, 4) != 4: - return _fail("third resource could not move back out of building") - if game_state.banked_resource(TEST_RESOURCE) != 25 \ - or game_state.fleet_resource(TEST_RESOURCE) != 10 \ - or game_state.building_resource(crafter_id, TEST_RESOURCE) != 5: - return _fail("third-resource amounts were not conserved") - - var limited: RefCounted = INVENTORY.new({}, 10) - limited.resource_capacities[TEST_RESOURCE] = 6 - if limited.deposit(TEST_RESOURCE, 99) != 6 or limited.deposit("water", 99) != 4: - return _fail("total or per-resource capacity was not enforced") - - var legacy_id: int = game_state.next_building_id - game_state.next_building_id += 1 - game_state.buildings.append({"id": legacy_id, "type": GameState.BUILDING_SHIP_CRAFTER, - "position": Vector2.ZERO, "stock": 7, "gas_stock": 3, "orders": []}) - if game_state.building_resource(legacy_id, GameState.RESOURCE_ORE) != 7 \ - or game_state.building_resource(legacy_id, GameState.RESOURCE_GAS) != 3: - return _fail("legacy building stock was not migrated") - var migrated: Dictionary = game_state.find_building(legacy_id) - if migrated.has("stock") or migrated.has("gas_stock") or not migrated.has("inventory"): - return _fail("legacy fields remained after migration") - - print("TEST_GENERIC_INVENTORY: PASS") - quit(0) - - -func _fail(message: String) -> void: - print("TEST_GENERIC_INVENTORY: FAIL (%s)" % message) - quit(1) diff --git a/tests/test_generic_inventory.gd.uid b/tests/test_generic_inventory.gd.uid deleted file mode 100644 index d8972c6..0000000 --- a/tests/test_generic_inventory.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://53pexhmp3ns0 diff --git a/tests/test_hangar_management.gd b/tests/test_hangar_management.gd index 4de3cac..eb6d399 100644 --- a/tests/test_hangar_management.gd +++ b/tests/test_hangar_management.gd @@ -68,7 +68,6 @@ func _process(delta: float) -> bool: # own non-overlapping berths to the right only after expansion. var mothership: Node2D = load("res://Building.tscn").instantiate() mothership.set("building_type", GameState.BUILDING_MOTHERSHIP) - mothership._ready() controller.current_building = mothership controller.context_actions_expanded = true controller._layout_context_buttons() diff --git a/tests/test_manual_supply_route.gd b/tests/test_manual_supply_route.gd index bfb1f86..f8479d1 100644 --- a/tests/test_manual_supply_route.gd +++ b/tests/test_manual_supply_route.gd @@ -42,8 +42,7 @@ const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0) func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: - if b.type != GameState.BUILDING_MOTHERSHIP: - sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE) + sum += int(b.get("stock", 0)) return sum diff --git a/tests/test_mothership_replacement.gd b/tests/test_mothership_replacement.gd index 576f039..6ebe817 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._show_building_info(GameState.BUILDING_MOTHERSHIP) + controller._on_place_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._show_building_info(GameState.BUILDING_MOTHERSHIP) + controller._on_place_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._show_building_info(GameState.BUILDING_MOTHERSHIP) + controller._on_place_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._show_building_info(GameState.BUILDING_MOTHERSHIP) + controller._on_place_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_resource_transfer.gd b/tests/test_resource_transfer.gd index f5035bc..7b6a00c 100644 --- a/tests/test_resource_transfer.gd +++ b/tests/test_resource_transfer.gd @@ -39,7 +39,7 @@ func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: if b.type != GameState.BUILDING_MOTHERSHIP: - sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE) + sum += int(b.get("stock", 0)) return sum diff --git a/tests/test_resources_and_buildings.gd b/tests/test_resources_and_buildings.gd index 17c6780..6069a1b 100644 --- a/tests/test_resources_and_buildings.gd +++ b/tests/test_resources_and_buildings.gd @@ -38,7 +38,7 @@ func _process(delta: float) -> bool: match stage: 0: # --- gather a rock ------------------------------------------- - scene.get_node("SurfaceFeatures/ResourceProvider").set_process(false) + scene.get_node("RockField").set_process(false) rock = load("res://Rock.tscn").instantiate() rock.build_shape(1) # Placed OUTSIDE the pickup shape but inside attract_radius, so diff --git a/tests/test_skill_tree.gd b/tests/test_skill_tree.gd index 5c9d21c..7b2a887 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 - # the generic definition entry point by another route. + # _on_place_crafter() by another route. var hidden: bool = not scene.get_node("UI/BuildMenu/CrafterButton").visible - controller._show_building_info(GameState.BUILDING_SHIP_CRAFTER) + controller._on_place_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 dcb03a6..b2cc4eb 100644 --- a/tests/test_transport_hub_building.gd +++ b/tests/test_transport_hub_building.gd @@ -1,7 +1,5 @@ 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 @@ -69,7 +67,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 = BUILDING_DEFINITIONS.get_definition(type).action_icon + var icon: int = controller._action_icon_for(type) if icons.has(icon): print("TEST_TRANSPORT_HUB_BUILDING: FAIL (%s and %s share an icon)" % [type, icons[icon]]) quit(1) @@ -101,7 +99,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.visual + hub = b if hub == null: print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)") quit(1) @@ -122,9 +120,9 @@ func _process(delta: float) -> bool: for m in markers: if m.type == GameState.BUILDING_TRANSPORT_HUB: found = true - 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: + if m.color == map_content.CRAFTER_MARKER_COLOR \ + or m.color == map_content.MOTHERSHIP_MARKER_COLOR \ + or m.color == map_content.COMMAND_MARKER_COLOR: print("TEST_TRANSPORT_HUB_BUILDING: FAIL (map marker reuses another building's colour)") quit(1) return true diff --git a/tests/test_transporter_supply.gd b/tests/test_transporter_supply.gd index ca9dcbb..8130e16 100644 --- a/tests/test_transporter_supply.gd +++ b/tests/test_transporter_supply.gd @@ -43,8 +43,7 @@ const CRAFTER_AT: Vector2 = Vector2(900.0, -300.0) func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: - if b.type != GameState.BUILDING_MOTHERSHIP: - sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE) + sum += int(b.get("stock", 0)) for t in game_state.transporters: sum += int(t.get("carrying", 0)) # Ore inside a build that has STARTED but not finished (#23). It has left diff --git a/tests/test_vessel_composition.gd b/tests/test_vessel_composition.gd deleted file mode 100644 index f25f447..0000000 --- a/tests/test_vessel_composition.gd +++ /dev/null @@ -1,50 +0,0 @@ -extends SceneTree - -const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") - -var ships: Array = [] -var elapsed: float = 0.0 -var game_state: Node - - -func _initialize() -> void: - game_state = root.get_node("GameState") - var scene: PackedScene = load("res://Ship.tscn") - for definition in VESSEL_DEFINITIONS.all(): - var ship: Area2D = scene.instantiate() - ship.vessel_type = definition.definition_id - root.add_child(ship) - ships.append(ship) - - -func _process(delta: float) -> bool: - elapsed += delta - if elapsed < 0.1: - return false - var definitions: Array = VESSEL_DEFINITIONS.all() - if ships.size() != definitions.size(): - return _fail("catalog and composed fleet differ in size") - for i in range(definitions.size()): - var definition: Resource = definitions[i] - var ship: Node = ships[i] - if ship.definition == null or ship.definition.definition_id != definition.definition_id: - return _fail("%s did not resolve its definition" % definition.definition_id) - for capability in definition.capabilities: - if not ship.has_capability(capability): - return _fail("%s did not compose %s" % [definition.definition_id, capability]) - if game_state.product_cost(definition.definition_id) != definition.build_cost \ - or game_state.vessel_name(definition.definition_id) != definition.display_name: - return _fail("%s metadata did not come from its definition" % definition.definition_id) - if ships[0].has_capability("shield") or not ships[1].has_capability("shield"): - return _fail("shield behavior is not controlled by component presence") - if ships[2].has_capability("weapon") or not ships[2].has_capability("transport"): - return _fail("transporter capabilities were composed incorrectly") - print("TEST_VESSEL_COMPOSITION: PASS") - quit(0) - return true - - -func _fail(message: String) -> bool: - print("TEST_VESSEL_COMPOSITION: FAIL (%s)" % message) - quit(1) - return true diff --git a/tests/test_vessel_composition.gd.uid b/tests/test_vessel_composition.gd.uid deleted file mode 100644 index d490e27..0000000 --- a/tests/test_vessel_composition.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cq73poxld1e35