Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14081ad230 | ||
|
|
0504b24d8d | ||
|
|
1721edd123 | ||
|
|
02643c76c4 | ||
|
|
2f68515f33 |
137 changed files with 2476 additions and 1546 deletions
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
|
|
@ -22,20 +25,18 @@ extends Node2D
|
|||
@onready var building_action_button: IconButton = get_node("../UI/BuildingActionButton")
|
||||
|
||||
@onready var build_menu: Control = get_node("../UI/BuildMenu")
|
||||
@onready var build_mothership_button: IconButton = get_node("../UI/BuildMenu/MothershipButton")
|
||||
@onready var build_crafter_button: IconButton = get_node("../UI/BuildMenu/CrafterButton")
|
||||
@onready var build_command_button: IconButton = get_node("../UI/BuildMenu/CommandButton")
|
||||
@onready var build_close_button: IconButton = get_node("../UI/BuildMenu/CloseButton")
|
||||
@onready var build_status: Label = get_node("../UI/BuildMenu/StatusLabel")
|
||||
@onready var build_extractor_button: IconButton = get_node("../UI/BuildMenu/ExtractorButton")
|
||||
@onready var build_hub_button: IconButton = get_node("../UI/BuildMenu/TransportHubButton")
|
||||
@onready var build_lab_button: IconButton = get_node("../UI/BuildMenu/ShipLabButton")
|
||||
@onready var lab_menu: Control = get_node("../UI/LabMenu")
|
||||
@onready var lab_vessel_buttons: Array = [
|
||||
get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"),
|
||||
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")
|
||||
|
|
@ -78,20 +79,7 @@ extends Node2D
|
|||
get_node("../UI/HangarMenu/FleetShieldLabel"),
|
||||
get_node("../UI/HangarMenu/FleetTransporterLabel"),
|
||||
]
|
||||
@onready var build_menu_entries: Array = [
|
||||
[GameState.BUILDING_MOTHERSHIP, build_mothership_button,
|
||||
get_node("../UI/BuildMenu/MothershipLabel"), get_node("../UI/BuildMenu/MothershipCost")],
|
||||
[GameState.BUILDING_SHIP_CRAFTER, build_crafter_button,
|
||||
get_node("../UI/BuildMenu/CrafterLabel"), get_node("../UI/BuildMenu/CrafterCost")],
|
||||
[GameState.BUILDING_COMMAND_CENTER, build_command_button,
|
||||
get_node("../UI/BuildMenu/CommandLabel"), get_node("../UI/BuildMenu/CommandCost")],
|
||||
[GameState.BUILDING_EXTRACTOR, build_extractor_button,
|
||||
get_node("../UI/BuildMenu/ExtractorLabel"), get_node("../UI/BuildMenu/ExtractorCost")],
|
||||
[GameState.BUILDING_TRANSPORT_HUB, build_hub_button,
|
||||
get_node("../UI/BuildMenu/TransportHubLabel"), get_node("../UI/BuildMenu/TransportHubCost")],
|
||||
[GameState.BUILDING_SHIP_LAB, build_lab_button,
|
||||
get_node("../UI/BuildMenu/ShipLabLabel"), get_node("../UI/BuildMenu/ShipLabCost")],
|
||||
]
|
||||
var build_menu_entries: Array = []
|
||||
|
||||
# Which vessel the lab screen is showing, and which visible upgrade card is
|
||||
# selected. Each vessel gets its own small set of direct cards; the detail and
|
||||
|
|
@ -115,6 +103,12 @@ 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
|
||||
|
|
@ -148,41 +142,6 @@ var lab_building_id: int = 0
|
|||
@onready var formation_close_button: IconButton = get_node("../UI/FormationMenu/CloseButton")
|
||||
@onready var formation_reset_button: IconButton = get_node("../UI/FormationMenu/ResetButton")
|
||||
|
||||
# What each option is and does, shown before anything is spent. Picking a
|
||||
# structure is a real commitment - it costs ore, and a mothership placement
|
||||
# can destroy the one you already have - so no option builds straight from
|
||||
# the menu; each one explains itself first and waits for a confirmation.
|
||||
#
|
||||
# The bodies are kept short on purpose: at the UI's font size the popup holds
|
||||
# roughly eight lines, and a placement condition or a price warning is
|
||||
# appended below them and needs some of those lines.
|
||||
const BUILDING_INFO: Dictionary = {
|
||||
GameState.BUILDING_MOTHERSHIP: {
|
||||
"title": "Mothership",
|
||||
"body": "Fleet home port. A wiped-out fleet rebuilds here, and extractor haulers deliver ore here.",
|
||||
},
|
||||
GameState.BUILDING_SHIP_CRAFTER: {
|
||||
"title": "Ship Crafter",
|
||||
"body": "Fly the fleet onto it, then use the action button to build extra vessels.",
|
||||
},
|
||||
GameState.BUILDING_COMMAND_CENTER: {
|
||||
"title": "Command Center",
|
||||
"body": "Fly the fleet onto it to drag your fleet into a new formation. Rebuilt fleets keep it.",
|
||||
},
|
||||
GameState.BUILDING_SHIP_LAB: {
|
||||
"title": "Ship Lab",
|
||||
"body": "Fly the fleet onto it to upgrade each vessel type, and to unlock the shield vessel.",
|
||||
},
|
||||
GameState.BUILDING_TRANSPORT_HUB: {
|
||||
"title": "Transport Hub",
|
||||
"body": "Stations transport vessels. They fetch ore from your mothership and deliver it to whichever building is waiting on it.",
|
||||
},
|
||||
GameState.BUILDING_EXTRACTOR: {
|
||||
"title": "Extractor",
|
||||
"body": "Mines a planet and runs a hauler to your mothership. It keeps earning while you are away.",
|
||||
},
|
||||
}
|
||||
|
||||
# Which structure the popup is currently offering to build, or "" when it is
|
||||
# only explaining something that cannot be built right now.
|
||||
var pending_build_type: String = ""
|
||||
|
|
@ -210,13 +169,7 @@ var context_actions_expanded: bool = false
|
|||
func _ready() -> void:
|
||||
build_button.pressed.connect(_on_build_pressed)
|
||||
build_close_button.pressed.connect(_close_build_menu)
|
||||
build_mothership_button.pressed.connect(_on_place_mothership)
|
||||
build_crafter_button.pressed.connect(_on_place_crafter)
|
||||
build_command_button.pressed.connect(_on_place_command_center)
|
||||
build_extractor_button.pressed.connect(_on_place_extractor)
|
||||
build_hub_button.pressed.connect(_on_place_transport_hub)
|
||||
build_lab_button.pressed.connect(_on_place_ship_lab)
|
||||
craft_shield_button.pressed.connect(_on_craft_shield)
|
||||
_configure_build_menu_entries()
|
||||
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()):
|
||||
|
|
@ -229,8 +182,9 @@ func _ready() -> void:
|
|||
action_button.pressed.connect(_on_context_action_pressed)
|
||||
building_action_button.pressed.connect(_on_action_pressed)
|
||||
|
||||
craft_interceptor_button.pressed.connect(_on_craft_interceptor)
|
||||
craft_transporter_button.pressed.connect(_on_craft_transporter)
|
||||
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_close_button.pressed.connect(func(): craft_menu.visible = false)
|
||||
|
||||
transfer_button.pressed.connect(_on_transfer_pressed)
|
||||
|
|
@ -267,6 +221,21 @@ func _ready() -> void:
|
|||
_layout_context_buttons()
|
||||
|
||||
|
||||
func _configure_build_menu_entries() -> void:
|
||||
build_menu_entries.clear()
|
||||
for definition in BUILDING_DEFINITIONS.all():
|
||||
if definition.menu_button_name == "":
|
||||
continue
|
||||
var button: IconButton = get_node("../UI/BuildMenu/%s" % definition.menu_button_name)
|
||||
var label: Label = get_node("../UI/BuildMenu/%s" % definition.menu_label_name)
|
||||
var cost_number: NeonNumber = get_node("../UI/BuildMenu/%s" % definition.menu_cost_name)
|
||||
button.icon = definition.menu_icon
|
||||
button.pressed.connect(_show_building_info.bind(String(definition.definition_id)))
|
||||
label.text = definition.display_name
|
||||
cost_number.amount = definition.cost
|
||||
build_menu_entries.append([definition.definition_id, button, label, cost_number, definition])
|
||||
|
||||
|
||||
# True while a building prompt or panel owns the shared action slot, so the
|
||||
# enter-atmosphere prompt knows to stay out of the way.
|
||||
func is_showing_building_prompt() -> bool:
|
||||
|
|
@ -318,9 +287,7 @@ func _process(delta: float) -> void:
|
|||
if build_menu.visible:
|
||||
# Keep the greyed state live while the menu is open, so flying onto
|
||||
# a planet lights the option up without reopening it.
|
||||
build_extractor_button.enabled = _planet_under_fleet() != null \
|
||||
and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR)
|
||||
build_extractor_button.queue_redraw()
|
||||
_refresh_build_option_states()
|
||||
|
||||
if craft_menu.visible:
|
||||
# Live, so a delivery arriving while the menu is open visibly works
|
||||
|
|
@ -359,10 +326,7 @@ func _process(delta: float) -> void:
|
|||
|
||||
current_building = pending_building
|
||||
context_actions_expanded = false
|
||||
# The hangar is where finished vessels are delivered (#23), so it belongs
|
||||
# at the mothership and nowhere else.
|
||||
hangar_button.visible = current_building != null \
|
||||
and current_building.building_type == GameState.BUILDING_MOTHERSHIP
|
||||
hangar_button.visible = false
|
||||
# Every building can hold ore, so the transfer button rides alongside the
|
||||
# action button rather than being one of the things the action button
|
||||
# turns into - the two are independent choices at the same structure.
|
||||
|
|
@ -371,7 +335,7 @@ func _process(delta: float) -> void:
|
|||
action_button.visible = current_building != null
|
||||
if current_building != null:
|
||||
var actions: Array = _context_action_buttons()
|
||||
building_action_button.icon = _action_icon_for(current_building.building_type)
|
||||
building_action_button.icon = int(current_building.definition.action_icon)
|
||||
building_action_button.queue_redraw()
|
||||
action_button.icon = building_action_button.icon if actions.size() == 1 else IconButton.Icon.ACTION_MENU
|
||||
action_button.queue_redraw()
|
||||
|
|
@ -403,13 +367,20 @@ func _layout_context_buttons() -> void:
|
|||
func _context_action_buttons() -> Array:
|
||||
if current_building == null:
|
||||
return []
|
||||
var actions: Array = [transfer_button]
|
||||
if current_building.building_type in [GameState.BUILDING_MOTHERSHIP, GameState.BUILDING_SHIP_CRAFTER,
|
||||
GameState.BUILDING_SHIP_LAB, GameState.BUILDING_COMMAND_CENTER]:
|
||||
actions.push_front(building_action_button)
|
||||
if current_building.building_type == GameState.BUILDING_MOTHERSHIP:
|
||||
actions.append(hangar_button)
|
||||
return actions
|
||||
var buttons: Array = []
|
||||
for action in current_building.available_actions():
|
||||
var button: IconButton = _button_for_building_action(action)
|
||||
if button != null and button not in buttons:
|
||||
buttons.append(button)
|
||||
return buttons
|
||||
|
||||
|
||||
func _button_for_building_action(action: String) -> IconButton:
|
||||
if action == "transfer":
|
||||
return transfer_button
|
||||
if action == "hangar":
|
||||
return hangar_button
|
||||
return building_action_button
|
||||
|
||||
|
||||
func _on_context_action_pressed() -> void:
|
||||
|
|
@ -433,20 +404,6 @@ func _collapse_context_actions() -> void:
|
|||
_layout_context_buttons()
|
||||
|
||||
|
||||
func _action_icon_for(type: String) -> int:
|
||||
match type:
|
||||
GameState.BUILDING_SHIP_CRAFTER:
|
||||
return IconButton.Icon.SHIP
|
||||
GameState.BUILDING_TRANSPORT_HUB:
|
||||
return IconButton.Icon.TRANSPORT_HUB
|
||||
GameState.BUILDING_SHIP_LAB:
|
||||
return IconButton.Icon.SHIP_LAB
|
||||
GameState.BUILDING_MOTHERSHIP:
|
||||
return IconButton.Icon.MOTHERSHIP
|
||||
_:
|
||||
return IconButton.Icon.COMMAND_CENTER
|
||||
|
||||
|
||||
func _check_fleet_wipe() -> void:
|
||||
if not fleet.ships.is_empty():
|
||||
return
|
||||
|
|
@ -465,11 +422,12 @@ func _on_action_pressed() -> void:
|
|||
for bar in resource_bars:
|
||||
bar.building_id = current_building.building_id
|
||||
|
||||
if current_building.building_type == GameState.BUILDING_MOTHERSHIP:
|
||||
var action: String = current_building.primary_action()
|
||||
if action == "research":
|
||||
skill_menu.visible = true
|
||||
skill_tree.reset_scroll()
|
||||
_refresh_skill_menu()
|
||||
elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER:
|
||||
elif action == "craft":
|
||||
# Remembered explicitly: _process() clears current_building while any
|
||||
# menu is open, so by the next frame the panel would no longer know
|
||||
# which crafter it belongs to. Losing this line makes every button in
|
||||
|
|
@ -478,11 +436,11 @@ func _on_action_pressed() -> void:
|
|||
craft_building_id = current_building.building_id
|
||||
craft_menu.visible = true
|
||||
_refresh_craft_status()
|
||||
elif current_building.building_type == GameState.BUILDING_SHIP_LAB:
|
||||
elif action == "lab":
|
||||
lab_building_id = current_building.building_id
|
||||
lab_menu.visible = true
|
||||
_refresh_lab()
|
||||
elif current_building.building_type == GameState.BUILDING_COMMAND_CENTER:
|
||||
elif action == "formation":
|
||||
formation_menu.visible = true
|
||||
formation_editor.queue_redraw()
|
||||
|
||||
|
|
@ -551,8 +509,7 @@ func _refresh_build_status() -> void:
|
|||
# Researched but not usable right here is still shown, greyed: an
|
||||
# extractor needs a planet under the fleet, which is a "not now" rather
|
||||
# than a "not yet".
|
||||
build_extractor_button.enabled = _planet_under_fleet() != null \
|
||||
and GameState.is_building_unlocked(GameState.BUILDING_EXTRACTOR)
|
||||
_refresh_build_option_states()
|
||||
|
||||
# Prices are shown under each option now, so this only needs to say
|
||||
# whether the fleet is somewhere an extractor could go.
|
||||
|
|
@ -562,16 +519,14 @@ func _refresh_build_status() -> void:
|
|||
build_status.text = ""
|
||||
|
||||
|
||||
func _on_place_mothership() -> void:
|
||||
_show_building_info(GameState.BUILDING_MOTHERSHIP)
|
||||
|
||||
|
||||
func _on_place_crafter() -> void:
|
||||
_show_building_info(GameState.BUILDING_SHIP_CRAFTER)
|
||||
|
||||
|
||||
func _on_place_command_center() -> void:
|
||||
_show_building_info(GameState.BUILDING_COMMAND_CENTER)
|
||||
func _refresh_build_option_states() -> void:
|
||||
for entry in build_menu_entries:
|
||||
var definition: Resource = entry[4]
|
||||
var enabled: bool = GameState.is_building_unlocked(definition.definition_id)
|
||||
if definition.placement_mode == "planet":
|
||||
enabled = enabled and _planet_under_fleet() != null
|
||||
entry[1].enabled = enabled
|
||||
entry[1].queue_redraw()
|
||||
|
||||
|
||||
func _close_build_menu() -> void:
|
||||
|
|
@ -582,40 +537,17 @@ func _close_build_menu() -> void:
|
|||
build_menu.visible = false
|
||||
|
||||
|
||||
func _on_place_ship_lab() -> void:
|
||||
_show_building_info(GameState.BUILDING_SHIP_LAB)
|
||||
|
||||
|
||||
func _on_place_transport_hub() -> void:
|
||||
_show_building_info(GameState.BUILDING_TRANSPORT_HUB)
|
||||
|
||||
|
||||
func _on_place_extractor() -> void:
|
||||
# The option stays pressable while greyed, so this is also how a
|
||||
# not-over-a-planet extractor explains itself.
|
||||
_show_building_info(GameState.BUILDING_EXTRACTOR)
|
||||
|
||||
|
||||
func cost_of(type: String) -> int:
|
||||
match type:
|
||||
GameState.BUILDING_TRANSPORT_HUB:
|
||||
return GameState.TRANSPORT_HUB_COST
|
||||
GameState.BUILDING_SHIP_LAB:
|
||||
return GameState.SHIP_LAB_COST
|
||||
GameState.BUILDING_MOTHERSHIP:
|
||||
return GameState.MOTHERSHIP_COST
|
||||
GameState.BUILDING_SHIP_CRAFTER:
|
||||
return GameState.SHIP_CRAFTER_COST
|
||||
GameState.BUILDING_COMMAND_CENTER:
|
||||
return GameState.COMMAND_CENTER_COST
|
||||
_:
|
||||
return GameState.EXTRACTOR_COST
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||
return int(definition.cost) if definition != null else 0
|
||||
|
||||
|
||||
func _show_building_info(type: String) -> void:
|
||||
var info: Dictionary = BUILDING_INFO[type]
|
||||
var cost: int = cost_of(type)
|
||||
var lines: Array = [info.body]
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||
if definition == null:
|
||||
return
|
||||
var cost: int = int(definition.cost)
|
||||
var lines: Array = [definition.description]
|
||||
var can_build: bool = true
|
||||
|
||||
# Locked structures still list themselves, greyed, so the player can see
|
||||
|
|
@ -626,7 +558,7 @@ func _show_building_info(type: String) -> void:
|
|||
|
||||
# Anything that would stop this particular placement is spelled out in
|
||||
# the same popup, rather than silently failing after the press.
|
||||
if can_build and type == GameState.BUILDING_EXTRACTOR:
|
||||
if can_build and definition.placement_mode == "planet":
|
||||
var planet: Node2D = _planet_under_fleet()
|
||||
if planet == null:
|
||||
lines.append("No planet in reach - fly over one.")
|
||||
|
|
@ -635,15 +567,15 @@ func _show_building_info(type: String) -> void:
|
|||
lines.append("%s already has an extractor." % planet.planet_name)
|
||||
can_build = false
|
||||
else:
|
||||
lines.append("Will mine %s from %s." % ["gas" if planet.resource_type == "gas" else "ore", planet.planet_name])
|
||||
elif can_build and type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership():
|
||||
lines.append("Will mine %s from %s." % [GameState.resource_name(planet.resource_type), planet.planet_name])
|
||||
elif can_build and definition.unique and GameState.has_building_of_type(type):
|
||||
lines.append("WARNING: only one may exist - this destroys your current one.")
|
||||
|
||||
if can_build and not GameState.can_afford(cost):
|
||||
lines.append("Not enough rocks - needs %d, you have %d." % [cost, GameState.rocks])
|
||||
can_build = false
|
||||
|
||||
info_title.text = info.title
|
||||
info_title.text = definition.display_name
|
||||
info_message.text = "\n\n".join(lines)
|
||||
info_cost.amount = cost
|
||||
info_build_button.visible = can_build
|
||||
|
|
@ -668,28 +600,31 @@ func _confirm_build() -> void:
|
|||
# planet an extractor was going to mine.
|
||||
if not GameState.is_building_unlocked(type):
|
||||
return
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||
if definition == null:
|
||||
return
|
||||
|
||||
if type == GameState.BUILDING_EXTRACTOR:
|
||||
if definition.placement_mode == "planet":
|
||||
var planet: Node2D = _planet_under_fleet()
|
||||
if planet == null or GameState.has_extractor_at(planet.planet_name):
|
||||
build_status.text = "No planet in reach any more"
|
||||
return
|
||||
if not GameState.spend(GameState.EXTRACTOR_COST):
|
||||
build_status.text = "Not enough rocks (need %d, have %d)" % [GameState.EXTRACTOR_COST, GameState.rocks]
|
||||
if not GameState.spend(definition.cost):
|
||||
build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks]
|
||||
return
|
||||
GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type)
|
||||
build_menu.visible = false
|
||||
return
|
||||
|
||||
if type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership():
|
||||
if not GameState.can_afford(GameState.MOTHERSHIP_COST):
|
||||
if definition.unique and GameState.has_building_of_type(type):
|
||||
if not GameState.can_afford(definition.cost):
|
||||
build_status.text = "Not enough rocks (need %d, have %d)" % [
|
||||
GameState.MOTHERSHIP_COST, GameState.rocks
|
||||
definition.cost, GameState.rocks
|
||||
]
|
||||
return
|
||||
# Only now is the old one destroyed - never before the new one is
|
||||
# known to be affordable, or the player would be left with none.
|
||||
GameState.remove_buildings_of_type(GameState.BUILDING_MOTHERSHIP)
|
||||
GameState.remove_buildings_of_type(type)
|
||||
building_field.rebuild_all()
|
||||
|
||||
_try_place(type, cost_of(type))
|
||||
|
|
@ -768,26 +703,19 @@ 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 = ""
|
||||
# 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()
|
||||
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()
|
||||
_refresh_queue()
|
||||
if craft_message != "":
|
||||
if Time.get_ticks_msec() < craft_message_until:
|
||||
|
|
@ -797,40 +725,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
|
||||
# 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)
|
||||
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)
|
||||
|
||||
|
||||
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_NAMES.get(product, product), short
|
||||
GameState.vessel_name(product), short
|
||||
])
|
||||
else:
|
||||
_say("%s ordered - building now" % GameState.VESSEL_NAMES.get(product, product))
|
||||
_say("%s ordered - building now" % GameState.vessel_name(product))
|
||||
|
||||
|
||||
func _on_craft_transporter() -> void:
|
||||
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()
|
||||
_on_craft_vessel(GameState.VESSEL_TRANSPORTER)
|
||||
|
||||
|
||||
# --- ship lab (#5) --------------------------------------------------------
|
||||
|
|
@ -840,10 +768,13 @@ 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 = []
|
||||
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})
|
||||
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})
|
||||
return options
|
||||
for u in GameState.upgrades_for(lab_vessel):
|
||||
options.append({"unlock": false, "id": u.id, "name": u.name, "desc": u.desc})
|
||||
|
|
@ -851,8 +782,10 @@ func _lab_options() -> Array:
|
|||
|
||||
|
||||
func _on_lab_vessel(index: int) -> void:
|
||||
lab_vessel = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_TRANSPORTER][index]
|
||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||
if index < 0 or index >= definitions.size():
|
||||
return
|
||||
lab_vessel = definitions[index].definition_id
|
||||
lab_choice = 0
|
||||
_refresh_lab()
|
||||
|
||||
|
|
@ -870,7 +803,7 @@ func _on_lab_buy() -> void:
|
|||
if options.is_empty():
|
||||
return
|
||||
var choice: Dictionary = options[lab_choice]
|
||||
var id: String = GameState.UNLOCK_SHIELD_VESSEL if choice.unlock else String(choice.id)
|
||||
var id: String = String(choice.id)
|
||||
if not GameState.queue_lab_research(lab_building_id, id):
|
||||
lab_status.text = "Research is already queued or complete"
|
||||
return
|
||||
|
|
@ -879,14 +812,17 @@ func _on_lab_buy() -> void:
|
|||
|
||||
|
||||
func _refresh_lab() -> void:
|
||||
for i in range(lab_vessel_buttons.size()):
|
||||
var this_vessel: String = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_TRANSPORTER][i]
|
||||
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
|
||||
lab_vessel_buttons[i].enabled = true
|
||||
lab_vessel_buttons[i].glow_color = _vessel_colour(this_vessel)
|
||||
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].highlighted = this_vessel == lab_vessel
|
||||
lab_vessel_buttons[i].queue_redraw()
|
||||
lab_vessel_label.text = "%s research" % GameState.VESSEL_NAMES.get(lab_vessel, lab_vessel)
|
||||
lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel)
|
||||
|
||||
var options: Array = _lab_options()
|
||||
for i in range(lab_upgrade_buttons.size()):
|
||||
|
|
@ -896,7 +832,7 @@ func _refresh_lab() -> void:
|
|||
if not visible:
|
||||
continue
|
||||
var option: Dictionary = options[i]
|
||||
var price_text: String = "%d ore" % (GameState.UNLOCK_SHIELD_COST if bool(option.unlock) else GameState.upgrade_cost(option.id))
|
||||
var price_text: String = "%d ore" % (int(option.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" % [
|
||||
|
|
@ -924,29 +860,11 @@ func _refresh_lab() -> void:
|
|||
|
||||
|
||||
func _upgrade_icon(option: Dictionary) -> IconButton.Icon:
|
||||
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
|
||||
return int(option.get("icon", IconButton.Icon.SHIP_LAB))
|
||||
|
||||
|
||||
func _on_craft_shield() -> void:
|
||||
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)
|
||||
_on_craft_vessel(GameState.VESSEL_SHIELD)
|
||||
|
||||
|
||||
# --- moving ore, in both directions, at any building (#24) ----------------
|
||||
|
|
@ -1033,8 +951,8 @@ func _refresh_queue() -> void:
|
|||
if not slot.visible:
|
||||
continue
|
||||
var order: Dictionary = orders[i]
|
||||
slot.icon = IconButton.Icon.TRANSPORT_HUB if order.product == GameState.PRODUCT_TRANSPORTER \
|
||||
else IconButton.Icon.SHIP
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product))
|
||||
slot.icon = definition.icon
|
||||
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.
|
||||
|
|
@ -1047,29 +965,24 @@ func _refresh_queue() -> void:
|
|||
var head: Dictionary = orders[0]
|
||||
if bool(head.started):
|
||||
queue_detail.text = "Building %s %d%%\nTap to cancel" % [
|
||||
GameState.VESSEL_NAMES.get(head.product, head.product),
|
||||
GameState.vessel_name(String(head.product)),
|
||||
int(round(float(head.progress) * 100.0)),
|
||||
]
|
||||
else:
|
||||
queue_detail.text = "%s waiting for ore\nTap to cancel" % \
|
||||
GameState.VESSEL_NAMES.get(head.product, head.product)
|
||||
GameState.vessel_name(String(head.product))
|
||||
|
||||
|
||||
func _vessel_colour(product: String) -> Color:
|
||||
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)
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
|
||||
return definition.glow_color if definition != null else 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_NAMES.get(orders[index].product, orders[index].product)
|
||||
var vessel: String = GameState.vessel_name(String(orders[index].product))
|
||||
var refunded: bool = bool(orders[index].started)
|
||||
if not GameState.cancel_order(craft_building_id, index):
|
||||
return
|
||||
|
|
@ -1094,12 +1007,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_NAMES.get(type, type)
|
||||
hangar_status.text = "No %s available" % GameState.vessel_name(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_NAMES.get(type, type)
|
||||
hangar_status.text = "%s joined the fleet" % GameState.vessel_name(type)
|
||||
_refresh_hangar()
|
||||
|
||||
|
||||
|
|
@ -1111,32 +1024,40 @@ 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_NAMES.get(type, type)
|
||||
hangar_status.text = "No %s in the fleet" % GameState.vessel_name(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_NAMES.get(stored, stored)
|
||||
hangar_status.text = "%s stored in the hangar" % GameState.vessel_name(stored)
|
||||
_refresh_hangar()
|
||||
|
||||
|
||||
func _hangar_types() -> Array:
|
||||
return [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]
|
||||
var types: Array = []
|
||||
for definition in VESSEL_DEFINITIONS.all():
|
||||
types.append(definition.definition_id)
|
||||
return types
|
||||
|
||||
|
||||
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" % [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_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_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()
|
||||
|
|
|
|||
669
Building.gd
669
Building.gd
|
|
@ -1,665 +1,42 @@
|
|||
extends Node2D
|
||||
|
||||
# A player-placed structure in space. Three kinds for now:
|
||||
# mothership - where a wiped-out fleet is rebuilt
|
||||
# ship_crafter - fly the fleet onto it to buy additional ships
|
||||
# command_center - reconfigure the fleet formation
|
||||
#
|
||||
# Buildings are stationary and harmless; whether the fleet is "on" one is
|
||||
# decided by BuildingField comparing the fleet CENTROID against
|
||||
# interact_radius, the same stable single-number test Planet uses (see
|
||||
# Planet.gd for why per-ship overlap was a poor fit).
|
||||
#
|
||||
# Nothing here rests on ground or vents into an atmosphere - these are
|
||||
# orbital structures, so they are built out of trusses, radiator fins,
|
||||
# reactor cores and open cradles rather than walls and roofs.
|
||||
#
|
||||
# The crafter and the mothership animate, so they run _process. The command
|
||||
# centre is drawn once and never redrawn - per-frame redraws of art that
|
||||
# never changes were a real framerate cost earlier on.
|
||||
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
||||
|
||||
@export var building_type: String = "mothership"
|
||||
# Which GameState.buildings record this node is showing. Set by
|
||||
# BuildingField when it spawns, so a node can be traced back to the stock
|
||||
# and production queue that survive the scene swap (#16).
|
||||
@export var definition_id: String = "mothership"
|
||||
@export var building_id: int = 0
|
||||
@export var interact_radius: float = 150.0
|
||||
|
||||
const MOTHERSHIP_COLOR: Color = Color(0.45, 0.9, 0.55)
|
||||
const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25)
|
||||
const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0)
|
||||
const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85)
|
||||
const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9)
|
||||
var definition: Resource
|
||||
var visual: Node2D
|
||||
|
||||
# Ship lab: a test frame with a vessel held in it and instruments sweeping
|
||||
# over it. Reads as a workshop rather than a factory - the crafter already
|
||||
# owns "assembly line", so this is measurement, not production.
|
||||
const LAB_FRAME_W: float = 78.0
|
||||
const LAB_FRAME_H: float = 58.0
|
||||
const LAB_MOUNT_RADIUS: float = 15.0
|
||||
const LAB_SCANNER_ORBIT: float = 96.0
|
||||
const LAB_SCAN_SPEED: float = 0.9
|
||||
|
||||
# Transport hub: an octagonal dock ring with berths around it and a freight
|
||||
# core in the middle. Deliberately radial-but-flat, so it does not read as a
|
||||
# small mothership - the mothership is the only thing with swept arms.
|
||||
const HUB_RING_RADIUS: float = 74.0
|
||||
const HUB_DECK_RADIUS: float = 54.0
|
||||
const HUB_CORE_RADIUS: float = 22.0
|
||||
const HUB_BERTH_COUNT: int = 8
|
||||
const HUB_BERTH_LENGTH: float = 20.0
|
||||
const HUB_DOCKED_COUNT: int = 3
|
||||
const HUB_DOCKED_ORBIT: float = 96.0
|
||||
const HUB_SPIN_SPEED: float = 0.22
|
||||
# The vessel on the assembly line is drawn in the fleet's own colour, so it
|
||||
# reads as "one of yours being built" rather than as shipyard machinery.
|
||||
const HULL_COLOR: Color = Color(0.25, 0.85, 1.0)
|
||||
|
||||
# One assembly cycle: parts are welded on, the finished hull flashes, then
|
||||
# it clears the cradle and the line starts over.
|
||||
const CYCLE: float = 3.6
|
||||
const ASSEMBLE_END: float = 0.66 # fraction of CYCLE spent welding
|
||||
const FLASH_END: float = 0.76 # ... then the completion flash
|
||||
# ... then launch, to 1.0
|
||||
|
||||
const CORE_CENTRE: Vector2 = Vector2(-58.0, 12.0)
|
||||
const BAY_CENTRE: Vector2 = Vector2(46.0, 12.0)
|
||||
const RIB_RADIUS: float = 36.0
|
||||
const RAIL_Y: float = -32.0
|
||||
const FLASH_RING_RADIUS: float = 20.0
|
||||
|
||||
# The hull in four pieces, attached in this order. Each carries the point
|
||||
# the crane welds it on at, which is where the sparks fly from.
|
||||
const SHIP_PARTS: Array = [
|
||||
{"at": 0.04, "weld": Vector2(0, -12), "points": [Vector2(0, -26), Vector2(-8, 4), Vector2(8, 4)]},
|
||||
{"at": 0.28, "weld": Vector2(-13, 7), "points": [Vector2(-8, -4), Vector2(-22, 14), Vector2(-8, 10)]},
|
||||
{"at": 0.48, "weld": Vector2(13, 7), "points": [Vector2(8, -4), Vector2(22, 14), Vector2(8, 10)]},
|
||||
{"at": 0.70, "weld": Vector2(0, 10), "points": [Vector2(-7, 4), Vector2(7, 4), Vector2(5, 14), Vector2(-5, 14)]},
|
||||
]
|
||||
|
||||
# The assembly clock, wrapped at CYCLE because the whole build sequence is
|
||||
# expressed as a fraction of it.
|
||||
var anim_time: float = 0.0
|
||||
|
||||
# Free-running seconds for everything that is merely cyclic - reactor pulses,
|
||||
# orbiting satellites, blinking beacons. These must NOT share anim_time:
|
||||
# CYCLE is 3.6s, which is not a whole number of periods for any of them, so
|
||||
# wrapping made every one of them leap at the wrap. The satellites were the
|
||||
# obvious casualty, snapping back 66 degrees every 3.6 seconds.
|
||||
#
|
||||
# Deliberately never wrapped. GDScript floats are 64-bit, so even after hours
|
||||
# of play the argument to sin() keeps far more precision than a frame needs,
|
||||
# and any wrap point would reintroduce exactly the discontinuity above.
|
||||
var phase_time: float = 0.0
|
||||
|
||||
var _hull_parts: Array[PackedVector2Array] = []
|
||||
|
||||
# Every moving element is its own NeonPiece, rasterised once when the
|
||||
# building is created and thereafter only moved, scaled, faded or hidden.
|
||||
# Nothing here ever calls queue_redraw() again: the building's own _draw()
|
||||
# lays down the fixed structure a single time, and the animation is pure
|
||||
# node-property work from then on.
|
||||
var _core_cell: NeonPiece = null
|
||||
var _satellites: Node2D = null
|
||||
var _beacons: Array = []
|
||||
var _clamps: NeonPiece = null
|
||||
var _crane: Node2D = null
|
||||
var _hoist: NeonPiece = null
|
||||
var _sparks: Node2D = null
|
||||
var _hull_holder: Node2D = null
|
||||
var _hull_pieces: Array = []
|
||||
# Rotating holder for the transports stationed at a transport hub (#18).
|
||||
var _hub_traffic: Node2D = null
|
||||
# Sweeping instrument booms at a ship lab (#5).
|
||||
var _lab_scanners: Node2D = null
|
||||
var _flash_ring: NeonPiece = null
|
||||
var _wash: NeonPiece = null
|
||||
var building_type: String:
|
||||
get: return definition_id
|
||||
set(value): definition_id = value
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("buildings")
|
||||
z_index = -1
|
||||
# The crafter runs an assembly line and the mothership holds a live
|
||||
# reactor with satellites in orbit, so both animate. The command centre is
|
||||
# entirely static and needs no per-frame work at all.
|
||||
var animated: bool = building_type != GameState.BUILDING_COMMAND_CENTER
|
||||
set_process(animated)
|
||||
if building_type == GameState.BUILDING_SHIP_CRAFTER:
|
||||
for part in SHIP_PARTS:
|
||||
_hull_parts.append(PackedVector2Array(part.points))
|
||||
_build_crafter_pieces()
|
||||
elif building_type == GameState.BUILDING_MOTHERSHIP:
|
||||
_build_mothership_pieces()
|
||||
elif building_type == GameState.BUILDING_TRANSPORT_HUB:
|
||||
_build_hub_pieces()
|
||||
elif building_type == GameState.BUILDING_SHIP_LAB:
|
||||
_build_lab_pieces()
|
||||
queue_redraw()
|
||||
definition = BUILDING_DEFINITIONS.get_definition(definition_id)
|
||||
if definition == null or definition.visual_scene == null:
|
||||
return
|
||||
visual = definition.visual_scene.instantiate()
|
||||
get_node("VisualHost").add_child(visual)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
anim_time = fmod(anim_time + delta, CYCLE)
|
||||
phase_time += delta
|
||||
apply_animation()
|
||||
func available_actions() -> PackedStringArray:
|
||||
if visual == null:
|
||||
return PackedStringArray()
|
||||
var provider: Node = visual.get_node_or_null("ActionProvider")
|
||||
return provider.actions if provider != null else PackedStringArray()
|
||||
|
||||
|
||||
# Pushes the current clock onto the movable pieces. Normally driven from
|
||||
# _process; public so a preview or a test can pin the clock and see exactly
|
||||
# that frame without having to run the animation up to it in real time.
|
||||
func apply_animation() -> void:
|
||||
match building_type:
|
||||
GameState.BUILDING_MOTHERSHIP:
|
||||
_animate_mothership()
|
||||
GameState.BUILDING_SHIP_CRAFTER:
|
||||
_animate_crafter()
|
||||
GameState.BUILDING_TRANSPORT_HUB:
|
||||
_animate_transport_hub()
|
||||
GameState.BUILDING_SHIP_LAB:
|
||||
_animate_ship_lab()
|
||||
|
||||
|
||||
# Where the satellites are, and how open the reactor core is. Both are read
|
||||
# by _draw and by the test that checks they stay continuous across the
|
||||
# assembly clock's wrap.
|
||||
func orbit_angle() -> float:
|
||||
return phase_time * ORBIT_SPEED
|
||||
|
||||
|
||||
func core_pulse() -> float:
|
||||
return 0.5 + 0.5 * sin(phase_time * PULSE_SPEED)
|
||||
func primary_action() -> String:
|
||||
for action in available_actions():
|
||||
if action != "transfer" and action != "hangar":
|
||||
return action
|
||||
return ""
|
||||
|
||||
|
||||
func get_display_name() -> String:
|
||||
match building_type:
|
||||
GameState.BUILDING_MOTHERSHIP:
|
||||
return "Mothership"
|
||||
GameState.BUILDING_SHIP_CRAFTER:
|
||||
return "Ship Crafter"
|
||||
_:
|
||||
return "Command Center"
|
||||
|
||||
|
||||
# How many times this building's geometry has actually been rasterised. The
|
||||
# whole point of the NeonPiece decomposition is that this stays at 1 for the
|
||||
# building's lifetime however long it animates, so it is worth being able to
|
||||
# assert rather than assume.
|
||||
var draw_count: int = 0
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_count += 1
|
||||
# Runs exactly once per building: this is the fixed structure. Everything
|
||||
# that moves is a NeonPiece child, animated through node properties, so
|
||||
# nothing here ever asks for a redraw again.
|
||||
match building_type:
|
||||
GameState.BUILDING_COMMAND_CENTER:
|
||||
_draw_command_center()
|
||||
GameState.BUILDING_MOTHERSHIP:
|
||||
_draw_mothership_static(self)
|
||||
GameState.BUILDING_TRANSPORT_HUB:
|
||||
_draw_transport_hub_static(self)
|
||||
GameState.BUILDING_SHIP_LAB:
|
||||
_draw_ship_lab_static(self)
|
||||
_:
|
||||
_draw_crafter_static(self)
|
||||
|
||||
|
||||
# --- shared helpers -------------------------------------------------------
|
||||
|
||||
func _ring(centre: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array:
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(sides):
|
||||
var a: float = (float(i) / float(sides)) * TAU + phase
|
||||
pts.append(centre + Vector2(cos(a), sin(a)) * radius)
|
||||
return pts
|
||||
|
||||
|
||||
func _arc(centre: Vector2, radius: float, from_a: float, to_a: float, steps: int) -> PackedVector2Array:
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(steps + 1):
|
||||
var a: float = lerpf(from_a, to_a, float(i) / float(steps))
|
||||
pts.append(centre + Vector2(cos(a), sin(a)) * radius)
|
||||
return pts
|
||||
|
||||
|
||||
|
||||
|
||||
# --- command centre: a tactical plotting hub ------------------------------
|
||||
# The mothership is radial-and-swept, the crafter is an asymmetric assembly
|
||||
# line, so the command centre reads best as the odd one out: rigid, hexagonal,
|
||||
# a wheel rather than a claw. Six spokes tie a hex rim to a central diamond
|
||||
# readout, and every spoke ends in a formation port - half of them holding a
|
||||
# token, half left open - so the structure itself shows fleet slots waiting
|
||||
# to be filled, which is exactly what the drag-and-drop editor inside does.
|
||||
|
||||
const CMD_HEX_RADIUS: float = 88.0
|
||||
const CMD_DECK_RADIUS: float = 66.0
|
||||
const CMD_HUB_RADIUS: float = 30.0
|
||||
const CMD_SCREEN_RADIUS: float = 14.0
|
||||
const CMD_PORT_COUNT: int = 6
|
||||
const CMD_PORT_LENGTH: float = 16.0
|
||||
const CMD_TOKEN_RADIUS: float = 9.0
|
||||
const CMD_STUD_RADIUS: float = 7.0
|
||||
|
||||
|
||||
# --- ship lab -------------------------------------------------------------
|
||||
|
||||
func _draw_ship_lab_static(target: CanvasItem) -> void:
|
||||
var c: Color = SHIP_LAB_COLOR
|
||||
|
||||
# Outer test frame, open at the corners so it reads as a rig rather than
|
||||
# a solid hull.
|
||||
for sx in [-1.0, 1.0]:
|
||||
for sy in [-1.0, 1.0]:
|
||||
var corner: Vector2 = Vector2(sx * LAB_FRAME_W, sy * LAB_FRAME_H)
|
||||
NeonDraw.draw_glowing_line(target, corner,
|
||||
corner - Vector2(sx * LAB_FRAME_W * 0.45, 0.0), c, 0.9, 0.42)
|
||||
NeonDraw.draw_glowing_line(target, corner,
|
||||
corner - Vector2(0.0, sy * LAB_FRAME_H * 0.5), c, 0.9, 0.42)
|
||||
|
||||
# The vessel under test, held nose-up in the middle - deliberately the
|
||||
# fleet's own silhouette, so it is obvious what the lab is for.
|
||||
NeonDraw.draw_glowing_polygon(target, PackedVector2Array([
|
||||
Vector2(0, -26), Vector2(13, 12), Vector2(0, 5), Vector2(-13, 12),
|
||||
]), HULL_COLOR, 0.95)
|
||||
|
||||
# Mount ring and its bracing.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, LAB_MOUNT_RADIUS, 6, PI / 6.0), c, 0.5,
|
||||
NeonDraw.DEFAULT_CORE, 0.7)
|
||||
for i in range(4):
|
||||
var a: float = (float(i) / 4.0) * TAU + PI / 4.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
NeonDraw.draw_glowing_line(target, dir * LAB_MOUNT_RADIUS, dir * 40.0, c, 0.45, 0.3)
|
||||
|
||||
|
||||
func _build_lab_pieces() -> void:
|
||||
# Two instrument booms sweeping round the vessel. One holder, rasterised
|
||||
# once, rotated per frame (#10).
|
||||
_lab_scanners = Node2D.new()
|
||||
add_child(_lab_scanners)
|
||||
for i in range(2):
|
||||
var a: float = float(i) * PI
|
||||
var at: Vector2 = Vector2(cos(a), sin(a)) * LAB_SCANNER_ORBIT
|
||||
var piece: NeonPiece = NeonPiece.create(_lab_scanners, at, SHIP_LAB_COLOR)
|
||||
piece.add_polygon(PackedVector2Array([
|
||||
Vector2(-4, -12), Vector2(4, -12), Vector2(4, 12), Vector2(-4, 12),
|
||||
]))
|
||||
piece.add_line(Vector2(0, 0), Vector2(-cos(a), -sin(a)) * 26.0, 0.3, 0.5)
|
||||
piece.rotation = a
|
||||
piece.queue_redraw()
|
||||
|
||||
|
||||
func _animate_ship_lab() -> void:
|
||||
_lab_scanners.rotation = phase_time * LAB_SCAN_SPEED
|
||||
|
||||
|
||||
# --- transport hub --------------------------------------------------------
|
||||
|
||||
func _draw_transport_hub_static(target: CanvasItem) -> void:
|
||||
var c: Color = TRANSPORT_HUB_COLOR
|
||||
|
||||
# Dock ring and the deck nested inside it.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_RING_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_DECK_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 0.45, NeonDraw.DEFAULT_CORE, 0.7)
|
||||
|
||||
# Freight core: a squared-off hold rather than a reactor, so it reads as
|
||||
# storage and not as another power plant.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_CORE_RADIUS, 4, PI / 4.0), c, 0.95)
|
||||
|
||||
for i in range(HUB_BERTH_COUNT):
|
||||
var a: float = (float(i) / HUB_BERTH_COUNT) * TAU + PI / 8.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
NeonDraw.draw_glowing_line(target, dir * HUB_CORE_RADIUS, dir * HUB_DECK_RADIUS, c, 0.5, 0.32)
|
||||
# Berth arm standing off the ring, where a transporter parks.
|
||||
NeonDraw.draw_glowing_line(target, dir * HUB_RING_RADIUS,
|
||||
dir * (HUB_RING_RADIUS + HUB_BERTH_LENGTH), c, 0.75, 0.38)
|
||||
|
||||
|
||||
func _build_hub_pieces() -> void:
|
||||
# Stationed transports, as NeonPiece children of one holder that turns.
|
||||
# Per #10 the geometry is rasterised once and only the holder's rotation
|
||||
# changes, so the traffic costs no redraws at all.
|
||||
_hub_traffic = Node2D.new()
|
||||
add_child(_hub_traffic)
|
||||
for i in range(HUB_DOCKED_COUNT):
|
||||
var a: float = (float(i) / HUB_DOCKED_COUNT) * TAU
|
||||
var at: Vector2 = Vector2(cos(a), sin(a)) * HUB_DOCKED_ORBIT
|
||||
var piece: NeonPiece = NeonPiece.create(_hub_traffic, at, TRANSPORT_HUB_COLOR)
|
||||
# A stubby freighter: blunt, boxy, nothing like the fleet's darts.
|
||||
piece.add_polygon(PackedVector2Array([
|
||||
Vector2(-9, -5), Vector2(9, -5), Vector2(12, 0), Vector2(9, 5), Vector2(-9, 5),
|
||||
]))
|
||||
piece.rotation = a + PI / 2.0
|
||||
piece.queue_redraw()
|
||||
|
||||
|
||||
func _animate_transport_hub() -> void:
|
||||
_hub_traffic.rotation = phase_time * HUB_SPIN_SPEED
|
||||
|
||||
|
||||
func _diamond(centre: Vector2, r: float) -> PackedVector2Array:
|
||||
return PackedVector2Array([
|
||||
centre + Vector2(0, -r), centre + Vector2(r, 0),
|
||||
centre + Vector2(0, r), centre + Vector2(-r, 0),
|
||||
])
|
||||
|
||||
|
||||
func _draw_command_center() -> void:
|
||||
var c: Color = COMMAND_COLOR
|
||||
var readout: Color = Color(0.82, 0.86, 1.0)
|
||||
|
||||
# The rim: a hex table holding the whole layout together, with a fainter
|
||||
# deck ring nested just inside it - the same "plating has layers" read the
|
||||
# mothership gets from its nested reactor rings.
|
||||
NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_HEX_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_DECK_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 0.5, NeonDraw.DEFAULT_CORE, 0.7)
|
||||
|
||||
for i in range(CMD_PORT_COUNT):
|
||||
var a: float = (float(i) / CMD_PORT_COUNT) * TAU + PI / 6.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
# Spoke bracing the hub to the rim.
|
||||
NeonDraw.draw_glowing_line(self, dir * CMD_HUB_RADIUS, dir * CMD_HEX_RADIUS, c, 0.65, 0.36)
|
||||
# Socket where the mast plugs into the rim.
|
||||
NeonDraw.draw_glowing_polygon(self, _ring(dir * CMD_HEX_RADIUS, CMD_STUD_RADIUS, 6, a), c, 0.85)
|
||||
# Port mast standing proud of the rim; every other one is manned.
|
||||
var port_tip: Vector2 = dir * (CMD_HEX_RADIUS + CMD_PORT_LENGTH)
|
||||
NeonDraw.draw_glowing_line(self, dir * CMD_HEX_RADIUS, port_tip, c, 0.8, 0.4)
|
||||
if i % 2 == 0:
|
||||
NeonDraw.draw_glowing_polygon(self, _diamond(port_tip, CMD_TOKEN_RADIUS), c, 1.0)
|
||||
|
||||
# Command core: a diamond hub with a lit readout set inside it.
|
||||
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_HUB_RADIUS), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9)
|
||||
|
||||
|
||||
# --- mothership: a radial flagship ----------------------------------------
|
||||
# Not a big fighter. A mothership is the thing everything else orbits, so it
|
||||
# is built radially around a reactor core rather than pointed in a direction:
|
||||
# six swept talons, crystal panels set between them, and satellites circling
|
||||
# the whole assembly. A forward-facing hull, however large, just reads as
|
||||
# another ship.
|
||||
|
||||
const ARM_COUNT: int = 6
|
||||
const ARM_INNER: float = 42.0
|
||||
const ARM_OUTER: float = 106.0
|
||||
const ARM_SWEEP: float = 0.72
|
||||
const SATELLITE_COUNT: int = 4
|
||||
const SATELLITE_ORBIT: float = 126.0
|
||||
const ORBIT_SPEED: float = 0.32
|
||||
const PULSE_SPEED: float = 2.4
|
||||
|
||||
|
||||
func _talon(base_angle: float) -> PackedVector2Array:
|
||||
# One swept claw: the outer edge runs from the core to the tip, and the
|
||||
# inner edge comes back a little behind it, closing to a point so the arm
|
||||
# tapers instead of ending in a blunt stub.
|
||||
var steps: int = 7
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(steps + 1):
|
||||
var f: float = float(i) / float(steps)
|
||||
var a: float = base_angle + ARM_SWEEP * pow(f, 1.3)
|
||||
pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f))
|
||||
for i in range(steps, -1, -1):
|
||||
var f: float = float(i) / float(steps)
|
||||
var trail: float = 0.34 * (1.0 - f) + 0.04
|
||||
var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) - trail
|
||||
pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f))
|
||||
return pts
|
||||
|
||||
|
||||
func _draw_mothership_static(canvas: CanvasItem) -> void:
|
||||
var c: Color = MOTHERSHIP_COLOR
|
||||
for i in range(ARM_COUNT):
|
||||
var base: float = (float(i) / float(ARM_COUNT)) * TAU
|
||||
NeonDraw.draw_glowing_polygon(canvas, _talon(base), c, 0.95)
|
||||
# Crystal panel set into the gap behind each arm.
|
||||
var pa: float = base - 0.30
|
||||
NeonDraw.draw_glowing_polygon(canvas, PackedVector2Array([
|
||||
Vector2(cos(pa - 0.11), sin(pa - 0.11)) * 54.0,
|
||||
Vector2(cos(pa + 0.11), sin(pa + 0.11)) * 54.0,
|
||||
Vector2(cos(pa + 0.15), sin(pa + 0.15)) * 86.0,
|
||||
Vector2(cos(pa - 0.15), sin(pa - 0.15)) * 86.0,
|
||||
]), c, 0.7)
|
||||
|
||||
# Reactor housing: the nested rings around the cell that breathes.
|
||||
NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 38.0, 6, PI / 6.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 24.0, 6, 0.0), c, 0.85)
|
||||
|
||||
|
||||
const CORE_CELL_RADIUS: float = 12.0
|
||||
|
||||
|
||||
func _build_mothership_pieces() -> void:
|
||||
var core_color: Color = Color(0.75, 1.0, 0.85)
|
||||
|
||||
# The breathing cell: drawn at a reference size and then scaled, rather
|
||||
# than rebuilt at a new radius every frame.
|
||||
_core_cell = NeonPiece.create(self, Vector2.ZERO, core_color)
|
||||
_core_cell.add_polygon(_ring(Vector2.ZERO, CORE_CELL_RADIUS, 6, PI / 6.0))
|
||||
|
||||
# Satellites sit at fixed offsets inside a holder; the orbit is the
|
||||
# holder's rotation, so four hexagon pairs become one transform update.
|
||||
_satellites = Node2D.new()
|
||||
add_child(_satellites)
|
||||
for i in range(SATELLITE_COUNT):
|
||||
var a: float = (float(i) / float(SATELLITE_COUNT)) * TAU
|
||||
var at: Vector2 = Vector2(cos(a), sin(a)) * SATELLITE_ORBIT
|
||||
var shell: NeonPiece = NeonPiece.create(_satellites, at, MOTHERSHIP_COLOR)
|
||||
shell.add_polygon(_ring(Vector2.ZERO, 10.0, 6, PI / 6.0), 0.85)
|
||||
var cell: NeonPiece = NeonPiece.create(_satellites, at, core_color)
|
||||
cell.add_polygon(_ring(Vector2.ZERO, 4.5, 6, PI / 6.0), 0.8)
|
||||
|
||||
|
||||
func _animate_mothership() -> void:
|
||||
var breath: float = core_pulse()
|
||||
var radius: float = lerpf(9.0, 15.0, breath)
|
||||
_core_cell.scale = Vector2.ONE * (radius / CORE_CELL_RADIUS)
|
||||
_core_cell.modulate.a = lerpf(0.55, 1.0, breath)
|
||||
_satellites.rotation = orbit_angle()
|
||||
|
||||
|
||||
# --- ship crafter: an orbital yard with a hull in the cradle --------------
|
||||
|
||||
func _draw_crafter_static(canvas: CanvasItem) -> void:
|
||||
var c: Color = CRAFTER_COLOR
|
||||
|
||||
# Fabricator core housing. This replaces the chimney - nothing vents
|
||||
# smoke in vacuum.
|
||||
NeonDraw.draw_glowing_polygon(canvas, _ring(CORE_CENTRE, 26.0, 6, PI / 6.0), c, 1.0)
|
||||
|
||||
# Radiator fins above and below the core, slatted like real ones.
|
||||
for dir_y in [-1.0, 1.0]:
|
||||
var fin: PackedVector2Array = PackedVector2Array([
|
||||
CORE_CENTRE + Vector2(-16, 30 * dir_y), CORE_CENTRE + Vector2(18, 34 * dir_y),
|
||||
CORE_CENTRE + Vector2(24, 54 * dir_y), CORE_CENTRE + Vector2(-10, 50 * dir_y),
|
||||
])
|
||||
NeonDraw.draw_glowing_polygon(canvas, fin, c, 0.75)
|
||||
for f in [0.35, 0.68]:
|
||||
NeonDraw.draw_glowing_line(canvas,
|
||||
fin[0].lerp(fin[3], f), fin[1].lerp(fin[2], f), c, 0.5, 0.28)
|
||||
|
||||
# Truss spine linking the core to the cradle: two rails with X-bracing,
|
||||
# the standard "this was assembled in orbit" read.
|
||||
var spine_top: float = 4.0
|
||||
var spine_bottom: float = 22.0
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_top), Vector2(14, spine_top), c, 0.9, 0.4)
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_bottom), Vector2(14, spine_bottom), c, 0.9, 0.4)
|
||||
for i in range(3):
|
||||
var x0: float = -32.0 + float(i) * 15.0
|
||||
var x1: float = x0 + 15.0
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_top), Vector2(x1, spine_bottom), c, 0.55, 0.28)
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_bottom), Vector2(x1, spine_top), c, 0.55, 0.28)
|
||||
|
||||
# Construction cradle: two open ribs holding the hull, with the crane
|
||||
# rail overhead. Open at the top so the finished hull can clear it.
|
||||
NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, 2.10, 4.18, 7), c, 1.0, 0.7)
|
||||
NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, -1.04, 1.04, 7), c, 1.0, 0.7)
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(10, RAIL_Y), Vector2(82, RAIL_Y), c, 0.9, 0.55)
|
||||
for side in [-1.0, 1.0]:
|
||||
NeonDraw.draw_glowing_line(canvas,
|
||||
BAY_CENTRE + Vector2(RIB_RADIUS * side, -6.0), Vector2(BAY_CENTRE.x + 44.0 * side, RAIL_Y), c, 0.7, 0.3)
|
||||
|
||||
|
||||
|
||||
func _build_crafter_pieces() -> void:
|
||||
var c: Color = CRAFTER_COLOR
|
||||
|
||||
# The core's inner cell: fades with the reactor rather than being redrawn
|
||||
# at a new brightness each frame.
|
||||
_core_cell = NeonPiece.create(self, CORE_CENTRE, Color(1.0, 0.9, 0.6))
|
||||
_core_cell.add_polygon(_ring(Vector2.ZERO, 12.0, 6, PI / 6.0))
|
||||
|
||||
# Docking beacons, blinking out of step with each other.
|
||||
for i in range(2):
|
||||
var lamp: Vector2 = BAY_CENTRE + Vector2(-RIB_RADIUS + 2.0 + float(i) * (RIB_RADIUS * 2.0 - 4.0), 30.0)
|
||||
var beacon: NeonPiece = NeonPiece.create(self, lamp, Color(1.0, 0.95, 0.75))
|
||||
beacon.add_polygon(_ring(Vector2.ZERO, 3.0, 4))
|
||||
_beacons.append(beacon)
|
||||
|
||||
# Clamp arms reaching in from each rib. They let go as the hull launches,
|
||||
# which is a fade rather than a redraw.
|
||||
_clamps = NeonPiece.create(self, Vector2.ZERO, c)
|
||||
for side in [-1.0, 1.0]:
|
||||
var outer: Vector2 = BAY_CENTRE + Vector2(RIB_RADIUS * side, 6.0)
|
||||
var inner: Vector2 = BAY_CENTRE + Vector2(24.0 * side, 6.0)
|
||||
_clamps.add_line(outer, inner, 0.35)
|
||||
_clamps.add_line(inner + Vector2(0, -5), inner + Vector2(0, 5), 0.3)
|
||||
|
||||
# Crane: the head rides the rail, so only its x moves. The hoist is drawn
|
||||
# one unit long and stretched with scale.y to reach whatever seam is
|
||||
# being welded - stretching a line needs no redraw, redrawing it would.
|
||||
_crane = Node2D.new()
|
||||
add_child(_crane)
|
||||
var head: NeonPiece = NeonPiece.create(_crane, Vector2.ZERO, c)
|
||||
head.add_polygon(PackedVector2Array([
|
||||
Vector2(-10, -7), Vector2(10, -7), Vector2(6, 6), Vector2(-6, 6),
|
||||
]))
|
||||
head.add_line(Vector2(0, -7), Vector2(0, RAIL_Y - (RAIL_Y + 13.0)), 0.35, 0.8)
|
||||
_hoist = NeonPiece.create(_crane, Vector2(0, 6), c, 0.5)
|
||||
_hoist.add_line(Vector2.ZERO, Vector2(0, 1), 0.3)
|
||||
|
||||
# Weld sparks: four spokes on a holder that spins and stretches. The
|
||||
# jitter is genuinely procedural, but rotating and scaling a holder gives
|
||||
# the same flicker without re-tessellating the spokes every frame.
|
||||
_sparks = Node2D.new()
|
||||
add_child(_sparks)
|
||||
for i in range(4):
|
||||
var a: float = float(i) * TAU / 4.0
|
||||
var spark: NeonPiece = NeonPiece.create(_sparks, Vector2.ZERO, Color(1.0, 0.95, 0.7))
|
||||
spark.add_line(Vector2.ZERO, Vector2(cos(a), sin(a)) * 11.0, 0.3)
|
||||
|
||||
# The hull under construction. Each part is its own piece so attaching one
|
||||
# is a `visible` flip, and the launch is the holder's position and alpha.
|
||||
_hull_holder = Node2D.new()
|
||||
add_child(_hull_holder)
|
||||
for i in range(_hull_parts.size()):
|
||||
var part: NeonPiece = NeonPiece.create(_hull_holder, Vector2.ZERO, HULL_COLOR)
|
||||
part.add_polygon(_hull_parts[i])
|
||||
part.visible = false
|
||||
_hull_pieces.append(part)
|
||||
|
||||
# Engine wash under the hull, and the completion ring. The ring is drawn
|
||||
# at a reference radius and scaled outwards.
|
||||
_wash = NeonPiece.create(_hull_holder, Vector2.ZERO, Color(1.0, 0.85, 0.45), 0.0)
|
||||
for dx in [-4.0, 4.0]:
|
||||
_wash.add_line(Vector2(dx, 14), Vector2(dx * 1.6, 30.0), 0.45)
|
||||
|
||||
_flash_ring = NeonPiece.create(self, BAY_CENTRE, HULL_COLOR, 0.0)
|
||||
_flash_ring.add_polygon(_ring(Vector2.ZERO, FLASH_RING_RADIUS, 6, PI / 6.0))
|
||||
|
||||
|
||||
func _animate_crafter() -> void:
|
||||
var t: float = anim_time / CYCLE
|
||||
|
||||
_core_cell.modulate.a = 0.45 + 0.55 * (0.5 + 0.5 * sin(phase_time * 4.0))
|
||||
for i in range(_beacons.size()):
|
||||
_beacons[i].modulate.a = 0.15 + 0.45 * (0.5 + 0.5 * sin(phase_time * 6.0 + float(i) * PI))
|
||||
|
||||
if t < ASSEMBLE_END:
|
||||
_animate_assembly(t / ASSEMBLE_END)
|
||||
elif t < FLASH_END:
|
||||
_animate_completion_flash((t - ASSEMBLE_END) / (FLASH_END - ASSEMBLE_END))
|
||||
else:
|
||||
_animate_launch((t - FLASH_END) / (1.0 - FLASH_END))
|
||||
|
||||
|
||||
# How many pieces are on the hull at this point through the assembly phase.
|
||||
# Parts attach in order, so this is just how many thresholds have passed.
|
||||
func attached_part_count(p: float) -> int:
|
||||
var n: int = 0
|
||||
for part in SHIP_PARTS:
|
||||
if p >= float(part.at):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
func _place_hull(centre: Vector2, alpha: float) -> void:
|
||||
# The parts are defined around their own origin, so the holder goes
|
||||
# straight to the point they should be centred on.
|
||||
_hull_holder.position = centre
|
||||
_hull_holder.modulate.a = alpha
|
||||
|
||||
|
||||
func _animate_assembly(p: float) -> void:
|
||||
_clamps.visible = true
|
||||
_clamps.modulate.a = 0.8
|
||||
_flash_ring.modulate.a = 0.0
|
||||
_wash.modulate.a = 0.0
|
||||
_place_hull(BAY_CENTRE, 1.0)
|
||||
|
||||
var next_part: int = attached_part_count(p)
|
||||
for i in range(_hull_pieces.size()):
|
||||
var attached: bool = i < next_part
|
||||
_hull_pieces[i].visible = attached
|
||||
if attached:
|
||||
# Freshly attached parts flare, then settle to full brightness.
|
||||
var age: float = p - float(SHIP_PARTS[i].at)
|
||||
_hull_pieces[i].modulate.a = 1.0 if age > 0.08 else lerpf(0.3, 1.0, age / 0.08)
|
||||
|
||||
# The crane rides the rail over whichever seam is being worked, and
|
||||
# retracts once there is nothing left to attach.
|
||||
var weld: Vector2 = BAY_CENTRE + (SHIP_PARTS[mini(next_part, SHIP_PARTS.size() - 1)].weld as Vector2)
|
||||
if next_part >= _hull_pieces.size():
|
||||
weld.y = RAIL_Y + 16.0
|
||||
_crane.position = Vector2(weld.x, RAIL_Y + 13.0)
|
||||
_hoist.scale.y = maxf(weld.y - (RAIL_Y + 19.0), 1.0)
|
||||
|
||||
var welding: bool = next_part < _hull_pieces.size()
|
||||
_sparks.visible = welding
|
||||
if welding:
|
||||
var flicker: float = 0.45 + 0.55 * absf(sin(phase_time * 34.0))
|
||||
_sparks.position = weld
|
||||
_sparks.rotation = phase_time * 7.0
|
||||
_sparks.scale = Vector2.ONE * (0.55 + 0.45 * flicker)
|
||||
_sparks.modulate.a = flicker
|
||||
|
||||
|
||||
func _animate_completion_flash(p: float) -> void:
|
||||
# Whole hull, clamps springing open, and a ring pushing outwards to mark
|
||||
# it as finished.
|
||||
_clamps.modulate.a = 1.0 - p
|
||||
_sparks.visible = false
|
||||
_place_hull(BAY_CENTRE, 1.0)
|
||||
for piece in _hull_pieces:
|
||||
piece.visible = true
|
||||
piece.modulate.a = 1.0
|
||||
# Retracted, so it does not hang over the finished hull.
|
||||
_crane.position = Vector2(BAY_CENTRE.x, RAIL_Y + 13.0)
|
||||
_hoist.scale.y = 1.0
|
||||
_flash_ring.modulate.a = 1.0 - p
|
||||
_flash_ring.scale = Vector2.ONE * (lerpf(18.0, 56.0, p) / FLASH_RING_RADIUS)
|
||||
|
||||
|
||||
func _animate_launch(p: float) -> void:
|
||||
# Clears the cradle and fades out; the next cycle starts from bare ribs.
|
||||
_clamps.modulate.a = 0.0
|
||||
_sparks.visible = false
|
||||
_flash_ring.modulate.a = 0.0
|
||||
var eased: float = p * p
|
||||
var fade: float = clampf(1.0 - p * 1.15, 0.0, 1.0)
|
||||
_place_hull(BAY_CENTRE + Vector2(0.0, -124.0 * eased), fade)
|
||||
for piece in _hull_pieces:
|
||||
piece.visible = true
|
||||
piece.modulate.a = 1.0
|
||||
# Engine wash, strongest just after it breaks free of the clamps.
|
||||
_wash.modulate.a = 0.4 + 0.6 * sin(minf(p * 3.2, PI))
|
||||
return String(definition.display_name) if definition != null else definition_id.capitalize()
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@
|
|||
|
||||
[node name="Building" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="VisualHost" type="Node2D" parent="."]
|
||||
|
|
|
|||
3
BuildingActionProvider.gd
Normal file
3
BuildingActionProvider.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
extends Node
|
||||
|
||||
@export var actions: PackedStringArray = []
|
||||
1
BuildingActionProvider.gd.uid
Normal file
1
BuildingActionProvider.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c4dah6xluvenl
|
||||
20
BuildingDefinition.gd
Normal file
20
BuildingDefinition.gd
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
class_name BuildingDefinition
|
||||
extends Resource
|
||||
|
||||
@export var definition_id: String
|
||||
@export var display_name: String
|
||||
@export_multiline var description: String
|
||||
@export var cost: int
|
||||
@export var action_icon: int
|
||||
@export var menu_icon: int
|
||||
@export var visual_scene: PackedScene
|
||||
@export var capabilities: PackedStringArray = []
|
||||
@export var map_color: Color = Color.WHITE
|
||||
@export var map_sides: int = 4
|
||||
@export var map_radius: float = 7.0
|
||||
@export var map_phase: float = 0.0
|
||||
@export var menu_button_name: String
|
||||
@export var menu_label_name: String
|
||||
@export var menu_cost_name: String
|
||||
@export_enum("space", "planet") var placement_mode: String = "space"
|
||||
@export var unique: bool = false
|
||||
1
BuildingDefinition.gd.uid
Normal file
1
BuildingDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d10s0frtxiw62
|
||||
21
BuildingDefinitions.gd
Normal file
21
BuildingDefinitions.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
class_name BuildingDefinitions
|
||||
extends RefCounted
|
||||
|
||||
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "extractor", "transport_hub", "ship_lab"]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
"mothership": preload("res://MothershipBuildingDefinition.tres"),
|
||||
"ship_crafter": preload("res://CrafterBuildingDefinition.tres"),
|
||||
"command_center": preload("res://CommandBuildingDefinition.tres"),
|
||||
"extractor": preload("res://ExtractorBuildingDefinition.tres"),
|
||||
"transport_hub": preload("res://TransportHubBuildingDefinition.tres"),
|
||||
"ship_lab": preload("res://ShipLabBuildingDefinition.tres"),
|
||||
}
|
||||
|
||||
static func get_definition(definition_id: String) -> Resource:
|
||||
return DEFINITIONS.get(definition_id)
|
||||
|
||||
static func all() -> Array:
|
||||
var result: Array = []
|
||||
for definition_id in ORDER:
|
||||
result.append(DEFINITIONS[definition_id])
|
||||
return result
|
||||
1
BuildingDefinitions.gd.uid
Normal file
1
BuildingDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cpm0u3gu0exke
|
||||
|
|
@ -50,7 +50,7 @@ func get_building_fleet_is_on() -> Node2D:
|
|||
func _spawn(record: Dictionary) -> void:
|
||||
var b: Node2D = building_scene.instantiate()
|
||||
b.position = record.position
|
||||
b.building_type = record.type
|
||||
b.definition_id = record.type
|
||||
b.building_id = int(record.get("id", 0))
|
||||
# Deferred for the same reason as the other field spawners: this can run
|
||||
# from inside a parent's _ready() chain, where a direct add_child() is
|
||||
|
|
|
|||
606
BuildingVisualBase.gd
Normal file
606
BuildingVisualBase.gd
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
class_name BuildingVisualBase
|
||||
extends Node2D
|
||||
|
||||
# A player-placed structure in space. Three kinds for now:
|
||||
# mothership - where a wiped-out fleet is rebuilt
|
||||
# ship_crafter - fly the fleet onto it to buy additional ships
|
||||
# command_center - reconfigure the fleet formation
|
||||
#
|
||||
# Buildings are stationary and harmless; whether the fleet is "on" one is
|
||||
# decided by BuildingField comparing the fleet CENTROID against
|
||||
# interact_radius, the same stable single-number test Planet uses (see
|
||||
# Planet.gd for why per-ship overlap was a poor fit).
|
||||
#
|
||||
# Nothing here rests on ground or vents into an atmosphere - these are
|
||||
# orbital structures, so they are built out of trusses, radiator fins,
|
||||
# reactor cores and open cradles rather than walls and roofs.
|
||||
#
|
||||
# The crafter and the mothership animate, so they run _process. The command
|
||||
# centre is drawn once and never redrawn - per-frame redraws of art that
|
||||
# never changes were a real framerate cost earlier on.
|
||||
|
||||
const MOTHERSHIP_COLOR: Color = Color(0.45, 0.9, 0.55)
|
||||
const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25)
|
||||
const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0)
|
||||
const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85)
|
||||
const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9)
|
||||
|
||||
# Ship lab: a test frame with a vessel held in it and instruments sweeping
|
||||
# over it. Reads as a workshop rather than a factory - the crafter already
|
||||
# owns "assembly line", so this is measurement, not production.
|
||||
const LAB_FRAME_W: float = 78.0
|
||||
const LAB_FRAME_H: float = 58.0
|
||||
const LAB_MOUNT_RADIUS: float = 15.0
|
||||
const LAB_SCANNER_ORBIT: float = 96.0
|
||||
const LAB_SCAN_SPEED: float = 0.9
|
||||
|
||||
# Transport hub: an octagonal dock ring with berths around it and a freight
|
||||
# core in the middle. Deliberately radial-but-flat, so it does not read as a
|
||||
# small mothership - the mothership is the only thing with swept arms.
|
||||
const HUB_RING_RADIUS: float = 74.0
|
||||
const HUB_DECK_RADIUS: float = 54.0
|
||||
const HUB_CORE_RADIUS: float = 22.0
|
||||
const HUB_BERTH_COUNT: int = 8
|
||||
const HUB_BERTH_LENGTH: float = 20.0
|
||||
const HUB_DOCKED_COUNT: int = 3
|
||||
const HUB_DOCKED_ORBIT: float = 96.0
|
||||
const HUB_SPIN_SPEED: float = 0.22
|
||||
# The vessel on the assembly line is drawn in the fleet's own colour, so it
|
||||
# reads as "one of yours being built" rather than as shipyard machinery.
|
||||
const HULL_COLOR: Color = Color(0.25, 0.85, 1.0)
|
||||
|
||||
# One assembly cycle: parts are welded on, the finished hull flashes, then
|
||||
# it clears the cradle and the line starts over.
|
||||
const CYCLE: float = 3.6
|
||||
const ASSEMBLE_END: float = 0.66 # fraction of CYCLE spent welding
|
||||
const FLASH_END: float = 0.76 # ... then the completion flash
|
||||
# ... then launch, to 1.0
|
||||
|
||||
const CORE_CENTRE: Vector2 = Vector2(-58.0, 12.0)
|
||||
const BAY_CENTRE: Vector2 = Vector2(46.0, 12.0)
|
||||
const RIB_RADIUS: float = 36.0
|
||||
const RAIL_Y: float = -32.0
|
||||
const FLASH_RING_RADIUS: float = 20.0
|
||||
|
||||
# The hull in four pieces, attached in this order. Each carries the point
|
||||
# the crane welds it on at, which is where the sparks fly from.
|
||||
const SHIP_PARTS: Array = [
|
||||
{"at": 0.04, "weld": Vector2(0, -12), "points": [Vector2(0, -26), Vector2(-8, 4), Vector2(8, 4)]},
|
||||
{"at": 0.28, "weld": Vector2(-13, 7), "points": [Vector2(-8, -4), Vector2(-22, 14), Vector2(-8, 10)]},
|
||||
{"at": 0.48, "weld": Vector2(13, 7), "points": [Vector2(8, -4), Vector2(22, 14), Vector2(8, 10)]},
|
||||
{"at": 0.70, "weld": Vector2(0, 10), "points": [Vector2(-7, 4), Vector2(7, 4), Vector2(5, 14), Vector2(-5, 14)]},
|
||||
]
|
||||
|
||||
# The assembly clock, wrapped at CYCLE because the whole build sequence is
|
||||
# expressed as a fraction of it.
|
||||
var anim_time: float = 0.0
|
||||
|
||||
# Free-running seconds for everything that is merely cyclic - reactor pulses,
|
||||
# orbiting satellites, blinking beacons. These must NOT share anim_time:
|
||||
# CYCLE is 3.6s, which is not a whole number of periods for any of them, so
|
||||
# wrapping made every one of them leap at the wrap. The satellites were the
|
||||
# obvious casualty, snapping back 66 degrees every 3.6 seconds.
|
||||
#
|
||||
# Deliberately never wrapped. GDScript floats are 64-bit, so even after hours
|
||||
# of play the argument to sin() keeps far more precision than a frame needs,
|
||||
# and any wrap point would reintroduce exactly the discontinuity above.
|
||||
var phase_time: float = 0.0
|
||||
|
||||
var _hull_parts: Array[PackedVector2Array] = []
|
||||
|
||||
# Every moving element is its own NeonPiece, rasterised once when the
|
||||
# building is created and thereafter only moved, scaled, faded or hidden.
|
||||
# Nothing here ever calls queue_redraw() again: the building's own _draw()
|
||||
# lays down the fixed structure a single time, and the animation is pure
|
||||
# node-property work from then on.
|
||||
var _core_cell: NeonPiece = null
|
||||
var _satellites: Node2D = null
|
||||
var _beacons: Array = []
|
||||
var _clamps: NeonPiece = null
|
||||
var _crane: Node2D = null
|
||||
var _hoist: NeonPiece = null
|
||||
var _sparks: Node2D = null
|
||||
var _hull_holder: Node2D = null
|
||||
var _hull_pieces: Array = []
|
||||
# Rotating holder for the transports stationed at a transport hub (#18).
|
||||
var _hub_traffic: Node2D = null
|
||||
# Sweeping instrument booms at a ship lab (#5).
|
||||
var _lab_scanners: Node2D = null
|
||||
var _flash_ring: NeonPiece = null
|
||||
var _wash: NeonPiece = null
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
anim_time = fmod(anim_time + delta, CYCLE)
|
||||
phase_time += delta
|
||||
apply_animation()
|
||||
|
||||
|
||||
# Pushes the current clock onto the movable pieces. Normally driven from
|
||||
# _process; public so a preview or a test can pin the clock and see exactly
|
||||
# that frame without having to run the animation up to it in real time.
|
||||
func apply_animation() -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Where the satellites are, and how open the reactor core is. Both are read
|
||||
# by _draw and by the test that checks they stay continuous across the
|
||||
# assembly clock's wrap.
|
||||
func orbit_angle() -> float:
|
||||
return phase_time * ORBIT_SPEED
|
||||
|
||||
|
||||
func core_pulse() -> float:
|
||||
return 0.5 + 0.5 * sin(phase_time * PULSE_SPEED)
|
||||
|
||||
|
||||
# How many times this building's geometry has actually been rasterised. The
|
||||
# whole point of the NeonPiece decomposition is that this stays at 1 for the
|
||||
# building's lifetime however long it animates, so it is worth being able to
|
||||
# assert rather than assume.
|
||||
var draw_count: int = 0
|
||||
|
||||
|
||||
func register_draw() -> void:
|
||||
draw_count += 1
|
||||
|
||||
|
||||
# --- shared helpers -------------------------------------------------------
|
||||
|
||||
func _ring(centre: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array:
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(sides):
|
||||
var a: float = (float(i) / float(sides)) * TAU + phase
|
||||
pts.append(centre + Vector2(cos(a), sin(a)) * radius)
|
||||
return pts
|
||||
|
||||
|
||||
func _arc(centre: Vector2, radius: float, from_a: float, to_a: float, steps: int) -> PackedVector2Array:
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(steps + 1):
|
||||
var a: float = lerpf(from_a, to_a, float(i) / float(steps))
|
||||
pts.append(centre + Vector2(cos(a), sin(a)) * radius)
|
||||
return pts
|
||||
|
||||
|
||||
|
||||
|
||||
# --- command centre: a tactical plotting hub ------------------------------
|
||||
# The mothership is radial-and-swept, the crafter is an asymmetric assembly
|
||||
# line, so the command centre reads best as the odd one out: rigid, hexagonal,
|
||||
# a wheel rather than a claw. Six spokes tie a hex rim to a central diamond
|
||||
# readout, and every spoke ends in a formation port - half of them holding a
|
||||
# token, half left open - so the structure itself shows fleet slots waiting
|
||||
# to be filled, which is exactly what the drag-and-drop editor inside does.
|
||||
|
||||
const CMD_HEX_RADIUS: float = 88.0
|
||||
const CMD_DECK_RADIUS: float = 66.0
|
||||
const CMD_HUB_RADIUS: float = 30.0
|
||||
const CMD_SCREEN_RADIUS: float = 14.0
|
||||
const CMD_PORT_COUNT: int = 6
|
||||
const CMD_PORT_LENGTH: float = 16.0
|
||||
const CMD_TOKEN_RADIUS: float = 9.0
|
||||
const CMD_STUD_RADIUS: float = 7.0
|
||||
|
||||
|
||||
# --- ship lab -------------------------------------------------------------
|
||||
|
||||
func _draw_ship_lab_static(target: CanvasItem) -> void:
|
||||
var c: Color = SHIP_LAB_COLOR
|
||||
|
||||
# Outer test frame, open at the corners so it reads as a rig rather than
|
||||
# a solid hull.
|
||||
for sx in [-1.0, 1.0]:
|
||||
for sy in [-1.0, 1.0]:
|
||||
var corner: Vector2 = Vector2(sx * LAB_FRAME_W, sy * LAB_FRAME_H)
|
||||
NeonDraw.draw_glowing_line(target, corner,
|
||||
corner - Vector2(sx * LAB_FRAME_W * 0.45, 0.0), c, 0.9, 0.42)
|
||||
NeonDraw.draw_glowing_line(target, corner,
|
||||
corner - Vector2(0.0, sy * LAB_FRAME_H * 0.5), c, 0.9, 0.42)
|
||||
|
||||
# The vessel under test, held nose-up in the middle - deliberately the
|
||||
# fleet's own silhouette, so it is obvious what the lab is for.
|
||||
NeonDraw.draw_glowing_polygon(target, PackedVector2Array([
|
||||
Vector2(0, -26), Vector2(13, 12), Vector2(0, 5), Vector2(-13, 12),
|
||||
]), HULL_COLOR, 0.95)
|
||||
|
||||
# Mount ring and its bracing.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, LAB_MOUNT_RADIUS, 6, PI / 6.0), c, 0.5,
|
||||
NeonDraw.DEFAULT_CORE, 0.7)
|
||||
for i in range(4):
|
||||
var a: float = (float(i) / 4.0) * TAU + PI / 4.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
NeonDraw.draw_glowing_line(target, dir * LAB_MOUNT_RADIUS, dir * 40.0, c, 0.45, 0.3)
|
||||
|
||||
|
||||
func _build_lab_pieces() -> void:
|
||||
# Two instrument booms sweeping round the vessel. One holder, rasterised
|
||||
# once, rotated per frame (#10).
|
||||
_lab_scanners = Node2D.new()
|
||||
add_child(_lab_scanners)
|
||||
for i in range(2):
|
||||
var a: float = float(i) * PI
|
||||
var at: Vector2 = Vector2(cos(a), sin(a)) * LAB_SCANNER_ORBIT
|
||||
var piece: NeonPiece = NeonPiece.create(_lab_scanners, at, SHIP_LAB_COLOR)
|
||||
piece.add_polygon(PackedVector2Array([
|
||||
Vector2(-4, -12), Vector2(4, -12), Vector2(4, 12), Vector2(-4, 12),
|
||||
]))
|
||||
piece.add_line(Vector2(0, 0), Vector2(-cos(a), -sin(a)) * 26.0, 0.3, 0.5)
|
||||
piece.rotation = a
|
||||
piece.queue_redraw()
|
||||
|
||||
|
||||
func _animate_ship_lab() -> void:
|
||||
_lab_scanners.rotation = phase_time * LAB_SCAN_SPEED
|
||||
|
||||
|
||||
# --- transport hub --------------------------------------------------------
|
||||
|
||||
func _draw_transport_hub_static(target: CanvasItem) -> void:
|
||||
var c: Color = TRANSPORT_HUB_COLOR
|
||||
|
||||
# Dock ring and the deck nested inside it.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_RING_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_DECK_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 0.45, NeonDraw.DEFAULT_CORE, 0.7)
|
||||
|
||||
# Freight core: a squared-off hold rather than a reactor, so it reads as
|
||||
# storage and not as another power plant.
|
||||
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_CORE_RADIUS, 4, PI / 4.0), c, 0.95)
|
||||
|
||||
for i in range(HUB_BERTH_COUNT):
|
||||
var a: float = (float(i) / HUB_BERTH_COUNT) * TAU + PI / 8.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
NeonDraw.draw_glowing_line(target, dir * HUB_CORE_RADIUS, dir * HUB_DECK_RADIUS, c, 0.5, 0.32)
|
||||
# Berth arm standing off the ring, where a transporter parks.
|
||||
NeonDraw.draw_glowing_line(target, dir * HUB_RING_RADIUS,
|
||||
dir * (HUB_RING_RADIUS + HUB_BERTH_LENGTH), c, 0.75, 0.38)
|
||||
|
||||
|
||||
func _build_hub_pieces() -> void:
|
||||
# Stationed transports, as NeonPiece children of one holder that turns.
|
||||
# Per #10 the geometry is rasterised once and only the holder's rotation
|
||||
# changes, so the traffic costs no redraws at all.
|
||||
_hub_traffic = Node2D.new()
|
||||
add_child(_hub_traffic)
|
||||
for i in range(HUB_DOCKED_COUNT):
|
||||
var a: float = (float(i) / HUB_DOCKED_COUNT) * TAU
|
||||
var at: Vector2 = Vector2(cos(a), sin(a)) * HUB_DOCKED_ORBIT
|
||||
var piece: NeonPiece = NeonPiece.create(_hub_traffic, at, TRANSPORT_HUB_COLOR)
|
||||
# A stubby freighter: blunt, boxy, nothing like the fleet's darts.
|
||||
piece.add_polygon(PackedVector2Array([
|
||||
Vector2(-9, -5), Vector2(9, -5), Vector2(12, 0), Vector2(9, 5), Vector2(-9, 5),
|
||||
]))
|
||||
piece.rotation = a + PI / 2.0
|
||||
piece.queue_redraw()
|
||||
|
||||
|
||||
func _animate_transport_hub() -> void:
|
||||
_hub_traffic.rotation = phase_time * HUB_SPIN_SPEED
|
||||
|
||||
|
||||
func _diamond(centre: Vector2, r: float) -> PackedVector2Array:
|
||||
return PackedVector2Array([
|
||||
centre + Vector2(0, -r), centre + Vector2(r, 0),
|
||||
centre + Vector2(0, r), centre + Vector2(-r, 0),
|
||||
])
|
||||
|
||||
|
||||
func _draw_command_center() -> void:
|
||||
var c: Color = COMMAND_COLOR
|
||||
var readout: Color = Color(0.82, 0.86, 1.0)
|
||||
|
||||
# The rim: a hex table holding the whole layout together, with a fainter
|
||||
# deck ring nested just inside it - the same "plating has layers" read the
|
||||
# mothership gets from its nested reactor rings.
|
||||
NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_HEX_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_DECK_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 0.5, NeonDraw.DEFAULT_CORE, 0.7)
|
||||
|
||||
for i in range(CMD_PORT_COUNT):
|
||||
var a: float = (float(i) / CMD_PORT_COUNT) * TAU + PI / 6.0
|
||||
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||
# Spoke bracing the hub to the rim.
|
||||
NeonDraw.draw_glowing_line(self, dir * CMD_HUB_RADIUS, dir * CMD_HEX_RADIUS, c, 0.65, 0.36)
|
||||
# Socket where the mast plugs into the rim.
|
||||
NeonDraw.draw_glowing_polygon(self, _ring(dir * CMD_HEX_RADIUS, CMD_STUD_RADIUS, 6, a), c, 0.85)
|
||||
# Port mast standing proud of the rim; every other one is manned.
|
||||
var port_tip: Vector2 = dir * (CMD_HEX_RADIUS + CMD_PORT_LENGTH)
|
||||
NeonDraw.draw_glowing_line(self, dir * CMD_HEX_RADIUS, port_tip, c, 0.8, 0.4)
|
||||
if i % 2 == 0:
|
||||
NeonDraw.draw_glowing_polygon(self, _diamond(port_tip, CMD_TOKEN_RADIUS), c, 1.0)
|
||||
|
||||
# Command core: a diamond hub with a lit readout set inside it.
|
||||
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_HUB_RADIUS), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9)
|
||||
|
||||
|
||||
# --- mothership: a radial flagship ----------------------------------------
|
||||
# Not a big fighter. A mothership is the thing everything else orbits, so it
|
||||
# is built radially around a reactor core rather than pointed in a direction:
|
||||
# six swept talons, crystal panels set between them, and satellites circling
|
||||
# the whole assembly. A forward-facing hull, however large, just reads as
|
||||
# another ship.
|
||||
|
||||
const ARM_COUNT: int = 6
|
||||
const ARM_INNER: float = 42.0
|
||||
const ARM_OUTER: float = 106.0
|
||||
const ARM_SWEEP: float = 0.72
|
||||
const SATELLITE_COUNT: int = 4
|
||||
const SATELLITE_ORBIT: float = 126.0
|
||||
const ORBIT_SPEED: float = 0.32
|
||||
const PULSE_SPEED: float = 2.4
|
||||
|
||||
|
||||
func _talon(base_angle: float) -> PackedVector2Array:
|
||||
# One swept claw: the outer edge runs from the core to the tip, and the
|
||||
# inner edge comes back a little behind it, closing to a point so the arm
|
||||
# tapers instead of ending in a blunt stub.
|
||||
var steps: int = 7
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(steps + 1):
|
||||
var f: float = float(i) / float(steps)
|
||||
var a: float = base_angle + ARM_SWEEP * pow(f, 1.3)
|
||||
pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f))
|
||||
for i in range(steps, -1, -1):
|
||||
var f: float = float(i) / float(steps)
|
||||
var trail: float = 0.34 * (1.0 - f) + 0.04
|
||||
var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) - trail
|
||||
pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f))
|
||||
return pts
|
||||
|
||||
|
||||
func _draw_mothership_static(canvas: CanvasItem) -> void:
|
||||
var c: Color = MOTHERSHIP_COLOR
|
||||
for i in range(ARM_COUNT):
|
||||
var base: float = (float(i) / float(ARM_COUNT)) * TAU
|
||||
NeonDraw.draw_glowing_polygon(canvas, _talon(base), c, 0.95)
|
||||
# Crystal panel set into the gap behind each arm.
|
||||
var pa: float = base - 0.30
|
||||
NeonDraw.draw_glowing_polygon(canvas, PackedVector2Array([
|
||||
Vector2(cos(pa - 0.11), sin(pa - 0.11)) * 54.0,
|
||||
Vector2(cos(pa + 0.11), sin(pa + 0.11)) * 54.0,
|
||||
Vector2(cos(pa + 0.15), sin(pa + 0.15)) * 86.0,
|
||||
Vector2(cos(pa - 0.15), sin(pa - 0.15)) * 86.0,
|
||||
]), c, 0.7)
|
||||
|
||||
# Reactor housing: the nested rings around the cell that breathes.
|
||||
NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 38.0, 6, PI / 6.0), c, 1.0)
|
||||
NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 24.0, 6, 0.0), c, 0.85)
|
||||
|
||||
|
||||
const CORE_CELL_RADIUS: float = 12.0
|
||||
|
||||
|
||||
func _build_mothership_pieces() -> void:
|
||||
var core_color: Color = Color(0.75, 1.0, 0.85)
|
||||
|
||||
# The breathing cell: drawn at a reference size and then scaled, rather
|
||||
# than rebuilt at a new radius every frame.
|
||||
_core_cell = NeonPiece.create(self, Vector2.ZERO, core_color)
|
||||
_core_cell.add_polygon(_ring(Vector2.ZERO, CORE_CELL_RADIUS, 6, PI / 6.0))
|
||||
|
||||
# Satellites sit at fixed offsets inside a holder; the orbit is the
|
||||
# holder's rotation, so four hexagon pairs become one transform update.
|
||||
_satellites = Node2D.new()
|
||||
add_child(_satellites)
|
||||
for i in range(SATELLITE_COUNT):
|
||||
var a: float = (float(i) / float(SATELLITE_COUNT)) * TAU
|
||||
var at: Vector2 = Vector2(cos(a), sin(a)) * SATELLITE_ORBIT
|
||||
var shell: NeonPiece = NeonPiece.create(_satellites, at, MOTHERSHIP_COLOR)
|
||||
shell.add_polygon(_ring(Vector2.ZERO, 10.0, 6, PI / 6.0), 0.85)
|
||||
var cell: NeonPiece = NeonPiece.create(_satellites, at, core_color)
|
||||
cell.add_polygon(_ring(Vector2.ZERO, 4.5, 6, PI / 6.0), 0.8)
|
||||
|
||||
|
||||
func _animate_mothership() -> void:
|
||||
var breath: float = core_pulse()
|
||||
var radius: float = lerpf(9.0, 15.0, breath)
|
||||
_core_cell.scale = Vector2.ONE * (radius / CORE_CELL_RADIUS)
|
||||
_core_cell.modulate.a = lerpf(0.55, 1.0, breath)
|
||||
_satellites.rotation = orbit_angle()
|
||||
|
||||
|
||||
# --- ship crafter: an orbital yard with a hull in the cradle --------------
|
||||
|
||||
func _draw_crafter_static(canvas: CanvasItem) -> void:
|
||||
var c: Color = CRAFTER_COLOR
|
||||
|
||||
# Fabricator core housing. This replaces the chimney - nothing vents
|
||||
# smoke in vacuum.
|
||||
NeonDraw.draw_glowing_polygon(canvas, _ring(CORE_CENTRE, 26.0, 6, PI / 6.0), c, 1.0)
|
||||
|
||||
# Radiator fins above and below the core, slatted like real ones.
|
||||
for dir_y in [-1.0, 1.0]:
|
||||
var fin: PackedVector2Array = PackedVector2Array([
|
||||
CORE_CENTRE + Vector2(-16, 30 * dir_y), CORE_CENTRE + Vector2(18, 34 * dir_y),
|
||||
CORE_CENTRE + Vector2(24, 54 * dir_y), CORE_CENTRE + Vector2(-10, 50 * dir_y),
|
||||
])
|
||||
NeonDraw.draw_glowing_polygon(canvas, fin, c, 0.75)
|
||||
for f in [0.35, 0.68]:
|
||||
NeonDraw.draw_glowing_line(canvas,
|
||||
fin[0].lerp(fin[3], f), fin[1].lerp(fin[2], f), c, 0.5, 0.28)
|
||||
|
||||
# Truss spine linking the core to the cradle: two rails with X-bracing,
|
||||
# the standard "this was assembled in orbit" read.
|
||||
var spine_top: float = 4.0
|
||||
var spine_bottom: float = 22.0
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_top), Vector2(14, spine_top), c, 0.9, 0.4)
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_bottom), Vector2(14, spine_bottom), c, 0.9, 0.4)
|
||||
for i in range(3):
|
||||
var x0: float = -32.0 + float(i) * 15.0
|
||||
var x1: float = x0 + 15.0
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_top), Vector2(x1, spine_bottom), c, 0.55, 0.28)
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_bottom), Vector2(x1, spine_top), c, 0.55, 0.28)
|
||||
|
||||
# Construction cradle: two open ribs holding the hull, with the crane
|
||||
# rail overhead. Open at the top so the finished hull can clear it.
|
||||
NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, 2.10, 4.18, 7), c, 1.0, 0.7)
|
||||
NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, -1.04, 1.04, 7), c, 1.0, 0.7)
|
||||
NeonDraw.draw_glowing_line(canvas, Vector2(10, RAIL_Y), Vector2(82, RAIL_Y), c, 0.9, 0.55)
|
||||
for side in [-1.0, 1.0]:
|
||||
NeonDraw.draw_glowing_line(canvas,
|
||||
BAY_CENTRE + Vector2(RIB_RADIUS * side, -6.0), Vector2(BAY_CENTRE.x + 44.0 * side, RAIL_Y), c, 0.7, 0.3)
|
||||
|
||||
|
||||
|
||||
func _build_crafter_pieces() -> void:
|
||||
var c: Color = CRAFTER_COLOR
|
||||
|
||||
# The core's inner cell: fades with the reactor rather than being redrawn
|
||||
# at a new brightness each frame.
|
||||
_core_cell = NeonPiece.create(self, CORE_CENTRE, Color(1.0, 0.9, 0.6))
|
||||
_core_cell.add_polygon(_ring(Vector2.ZERO, 12.0, 6, PI / 6.0))
|
||||
|
||||
# Docking beacons, blinking out of step with each other.
|
||||
for i in range(2):
|
||||
var lamp: Vector2 = BAY_CENTRE + Vector2(-RIB_RADIUS + 2.0 + float(i) * (RIB_RADIUS * 2.0 - 4.0), 30.0)
|
||||
var beacon: NeonPiece = NeonPiece.create(self, lamp, Color(1.0, 0.95, 0.75))
|
||||
beacon.add_polygon(_ring(Vector2.ZERO, 3.0, 4))
|
||||
_beacons.append(beacon)
|
||||
|
||||
# Clamp arms reaching in from each rib. They let go as the hull launches,
|
||||
# which is a fade rather than a redraw.
|
||||
_clamps = NeonPiece.create(self, Vector2.ZERO, c)
|
||||
for side in [-1.0, 1.0]:
|
||||
var outer: Vector2 = BAY_CENTRE + Vector2(RIB_RADIUS * side, 6.0)
|
||||
var inner: Vector2 = BAY_CENTRE + Vector2(24.0 * side, 6.0)
|
||||
_clamps.add_line(outer, inner, 0.35)
|
||||
_clamps.add_line(inner + Vector2(0, -5), inner + Vector2(0, 5), 0.3)
|
||||
|
||||
# Crane: the head rides the rail, so only its x moves. The hoist is drawn
|
||||
# one unit long and stretched with scale.y to reach whatever seam is
|
||||
# being welded - stretching a line needs no redraw, redrawing it would.
|
||||
_crane = Node2D.new()
|
||||
add_child(_crane)
|
||||
var head: NeonPiece = NeonPiece.create(_crane, Vector2.ZERO, c)
|
||||
head.add_polygon(PackedVector2Array([
|
||||
Vector2(-10, -7), Vector2(10, -7), Vector2(6, 6), Vector2(-6, 6),
|
||||
]))
|
||||
head.add_line(Vector2(0, -7), Vector2(0, RAIL_Y - (RAIL_Y + 13.0)), 0.35, 0.8)
|
||||
_hoist = NeonPiece.create(_crane, Vector2(0, 6), c, 0.5)
|
||||
_hoist.add_line(Vector2.ZERO, Vector2(0, 1), 0.3)
|
||||
|
||||
# Weld sparks: four spokes on a holder that spins and stretches. The
|
||||
# jitter is genuinely procedural, but rotating and scaling a holder gives
|
||||
# the same flicker without re-tessellating the spokes every frame.
|
||||
_sparks = Node2D.new()
|
||||
add_child(_sparks)
|
||||
for i in range(4):
|
||||
var a: float = float(i) * TAU / 4.0
|
||||
var spark: NeonPiece = NeonPiece.create(_sparks, Vector2.ZERO, Color(1.0, 0.95, 0.7))
|
||||
spark.add_line(Vector2.ZERO, Vector2(cos(a), sin(a)) * 11.0, 0.3)
|
||||
|
||||
# The hull under construction. Each part is its own piece so attaching one
|
||||
# is a `visible` flip, and the launch is the holder's position and alpha.
|
||||
_hull_holder = Node2D.new()
|
||||
add_child(_hull_holder)
|
||||
for i in range(_hull_parts.size()):
|
||||
var part: NeonPiece = NeonPiece.create(_hull_holder, Vector2.ZERO, HULL_COLOR)
|
||||
part.add_polygon(_hull_parts[i])
|
||||
part.visible = false
|
||||
_hull_pieces.append(part)
|
||||
|
||||
# Engine wash under the hull, and the completion ring. The ring is drawn
|
||||
# at a reference radius and scaled outwards.
|
||||
_wash = NeonPiece.create(_hull_holder, Vector2.ZERO, Color(1.0, 0.85, 0.45), 0.0)
|
||||
for dx in [-4.0, 4.0]:
|
||||
_wash.add_line(Vector2(dx, 14), Vector2(dx * 1.6, 30.0), 0.45)
|
||||
|
||||
_flash_ring = NeonPiece.create(self, BAY_CENTRE, HULL_COLOR, 0.0)
|
||||
_flash_ring.add_polygon(_ring(Vector2.ZERO, FLASH_RING_RADIUS, 6, PI / 6.0))
|
||||
|
||||
|
||||
func _animate_crafter() -> void:
|
||||
var t: float = anim_time / CYCLE
|
||||
|
||||
_core_cell.modulate.a = 0.45 + 0.55 * (0.5 + 0.5 * sin(phase_time * 4.0))
|
||||
for i in range(_beacons.size()):
|
||||
_beacons[i].modulate.a = 0.15 + 0.45 * (0.5 + 0.5 * sin(phase_time * 6.0 + float(i) * PI))
|
||||
|
||||
if t < ASSEMBLE_END:
|
||||
_animate_assembly(t / ASSEMBLE_END)
|
||||
elif t < FLASH_END:
|
||||
_animate_completion_flash((t - ASSEMBLE_END) / (FLASH_END - ASSEMBLE_END))
|
||||
else:
|
||||
_animate_launch((t - FLASH_END) / (1.0 - FLASH_END))
|
||||
|
||||
|
||||
# How many pieces are on the hull at this point through the assembly phase.
|
||||
# Parts attach in order, so this is just how many thresholds have passed.
|
||||
func attached_part_count(p: float) -> int:
|
||||
var n: int = 0
|
||||
for part in SHIP_PARTS:
|
||||
if p >= float(part.at):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
func _place_hull(centre: Vector2, alpha: float) -> void:
|
||||
# The parts are defined around their own origin, so the holder goes
|
||||
# straight to the point they should be centred on.
|
||||
_hull_holder.position = centre
|
||||
_hull_holder.modulate.a = alpha
|
||||
|
||||
|
||||
func _animate_assembly(p: float) -> void:
|
||||
_clamps.visible = true
|
||||
_clamps.modulate.a = 0.8
|
||||
_flash_ring.modulate.a = 0.0
|
||||
_wash.modulate.a = 0.0
|
||||
_place_hull(BAY_CENTRE, 1.0)
|
||||
|
||||
var next_part: int = attached_part_count(p)
|
||||
for i in range(_hull_pieces.size()):
|
||||
var attached: bool = i < next_part
|
||||
_hull_pieces[i].visible = attached
|
||||
if attached:
|
||||
# Freshly attached parts flare, then settle to full brightness.
|
||||
var age: float = p - float(SHIP_PARTS[i].at)
|
||||
_hull_pieces[i].modulate.a = 1.0 if age > 0.08 else lerpf(0.3, 1.0, age / 0.08)
|
||||
|
||||
# The crane rides the rail over whichever seam is being worked, and
|
||||
# retracts once there is nothing left to attach.
|
||||
var weld: Vector2 = BAY_CENTRE + (SHIP_PARTS[mini(next_part, SHIP_PARTS.size() - 1)].weld as Vector2)
|
||||
if next_part >= _hull_pieces.size():
|
||||
weld.y = RAIL_Y + 16.0
|
||||
_crane.position = Vector2(weld.x, RAIL_Y + 13.0)
|
||||
_hoist.scale.y = maxf(weld.y - (RAIL_Y + 19.0), 1.0)
|
||||
|
||||
var welding: bool = next_part < _hull_pieces.size()
|
||||
_sparks.visible = welding
|
||||
if welding:
|
||||
var flicker: float = 0.45 + 0.55 * absf(sin(phase_time * 34.0))
|
||||
_sparks.position = weld
|
||||
_sparks.rotation = phase_time * 7.0
|
||||
_sparks.scale = Vector2.ONE * (0.55 + 0.45 * flicker)
|
||||
_sparks.modulate.a = flicker
|
||||
|
||||
|
||||
func _animate_completion_flash(p: float) -> void:
|
||||
# Whole hull, clamps springing open, and a ring pushing outwards to mark
|
||||
# it as finished.
|
||||
_clamps.modulate.a = 1.0 - p
|
||||
_sparks.visible = false
|
||||
_place_hull(BAY_CENTRE, 1.0)
|
||||
for piece in _hull_pieces:
|
||||
piece.visible = true
|
||||
piece.modulate.a = 1.0
|
||||
# Retracted, so it does not hang over the finished hull.
|
||||
_crane.position = Vector2(BAY_CENTRE.x, RAIL_Y + 13.0)
|
||||
_hoist.scale.y = 1.0
|
||||
_flash_ring.modulate.a = 1.0 - p
|
||||
_flash_ring.scale = Vector2.ONE * (lerpf(18.0, 56.0, p) / FLASH_RING_RADIUS)
|
||||
|
||||
|
||||
func _animate_launch(p: float) -> void:
|
||||
# Clears the cradle and fades out; the next cycle starts from bare ribs.
|
||||
_clamps.modulate.a = 0.0
|
||||
_sparks.visible = false
|
||||
_flash_ring.modulate.a = 0.0
|
||||
var eased: float = p * p
|
||||
var fade: float = clampf(1.0 - p * 1.15, 0.0, 1.0)
|
||||
_place_hull(BAY_CENTRE + Vector2(0.0, -124.0 * eased), fade)
|
||||
for piece in _hull_pieces:
|
||||
piece.visible = true
|
||||
piece.modulate.a = 1.0
|
||||
# Engine wash, strongest just after it breaks free of the clamps.
|
||||
_wash.modulate.a = 0.4 + 0.6 * sin(minf(p * 3.2, PI))
|
||||
1
BuildingVisualBase.gd.uid
Normal file
1
BuildingVisualBase.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://p7vuv1xsksc
|
||||
20
CommandBuildingDefinition.tres
Normal file
20
CommandBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://CommandBuildingVisual.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "command_center"
|
||||
display_name = "Command Center"
|
||||
description = "Fly the fleet onto it to drag your fleet into a new formation. Rebuilt fleets keep it."
|
||||
cost = 15
|
||||
action_icon = 10
|
||||
menu_icon = 10
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("formation")
|
||||
map_color = Color(0.55, 0.6, 1, 1)
|
||||
map_sides = 4
|
||||
map_radius = 6.0
|
||||
map_phase = 0.785398
|
||||
menu_button_name = "CommandButton"
|
||||
menu_label_name = "CommandLabel"
|
||||
menu_cost_name = "CommandCost"
|
||||
9
CommandBuildingVisual.gd
Normal file
9
CommandBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
extends "res://BuildingVisualBase.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(false)
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
register_draw()
|
||||
_draw_command_center()
|
||||
1
CommandBuildingVisual.gd.uid
Normal file
1
CommandBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bpb5pq8g56ywn
|
||||
11
CommandBuildingVisual.tscn
Normal file
11
CommandBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://CommandBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("formation", "transfer")
|
||||
19
CrafterBuildingDefinition.tres
Normal file
19
CrafterBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://CrafterBuildingVisual.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "ship_crafter"
|
||||
display_name = "Ship Crafter"
|
||||
description = "Fly the fleet onto it, then use the action button to build extra vessels."
|
||||
cost = 12
|
||||
action_icon = 2
|
||||
menu_icon = 9
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("production")
|
||||
map_color = Color(1, 0.72, 0.25, 1)
|
||||
map_sides = 4
|
||||
map_radius = 7.0
|
||||
menu_button_name = "CrafterButton"
|
||||
menu_label_name = "CrafterLabel"
|
||||
menu_cost_name = "CrafterCost"
|
||||
14
CrafterBuildingVisual.gd
Normal file
14
CrafterBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
extends "res://BuildingVisualBase.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
for part in SHIP_PARTS:
|
||||
_hull_parts.append(PackedVector2Array(part.points))
|
||||
_build_crafter_pieces()
|
||||
queue_redraw()
|
||||
|
||||
func apply_animation() -> void:
|
||||
_animate_crafter()
|
||||
|
||||
func _draw() -> void:
|
||||
register_draw()
|
||||
_draw_crafter_static(self)
|
||||
1
CrafterBuildingVisual.gd.uid
Normal file
1
CrafterBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dbwebd1vf7biy
|
||||
11
CrafterBuildingVisual.tscn
Normal file
11
CrafterBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://CrafterBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("craft", "transfer")
|
||||
16
ExtractorBuildingDefinition.tres
Normal file
16
ExtractorBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "extractor"
|
||||
display_name = "Extractor"
|
||||
description = "Mines resources into local storage for transport vessels to collect. It keeps extracting while you are away."
|
||||
cost = 25
|
||||
action_icon = 11
|
||||
menu_icon = 11
|
||||
capabilities = PackedStringArray("extraction")
|
||||
map_color = Color(1, 0.85, 0.3, 1)
|
||||
menu_button_name = "ExtractorButton"
|
||||
menu_label_name = "ExtractorLabel"
|
||||
menu_cost_name = "ExtractorCost"
|
||||
placement_mode = "planet"
|
||||
504
GameState.gd
504
GameState.gd
|
|
@ -1,5 +1,11 @@
|
|||
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
|
||||
|
|
@ -12,7 +18,7 @@ var return_world_position: Vector2 = Vector2.ZERO
|
|||
var return_camera_zoom: float = 1.0
|
||||
|
||||
var planet_name: String = ""
|
||||
var planet_type: String = ""
|
||||
var planet_definition_id: String = "rocky"
|
||||
|
||||
# Explored-chunk history for the world map's fog-of-war reveal. Lives
|
||||
# here (not on ExplorationTracker, which gets destroyed and recreated on
|
||||
|
|
@ -22,126 +28,101 @@ var explored_chunks: Dictionary = {}
|
|||
|
||||
# --- Resources -------------------------------------------------------------
|
||||
|
||||
# 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
|
||||
const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID
|
||||
const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID
|
||||
const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER
|
||||
|
||||
# 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
|
||||
# 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()
|
||||
|
||||
# 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 rocks: int:
|
||||
get: return bank_inventory.amount(RESOURCE_ORE)
|
||||
set(value): bank_inventory.set_amount(RESOURCE_ORE, 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),
|
||||
}
|
||||
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)
|
||||
|
||||
func collect_cargo(amount: int = 1) -> void:
|
||||
fleet_cargo += amount
|
||||
fleet_inventory.deposit(RESOURCE_ORE, amount)
|
||||
|
||||
func collect_gas_cargo(amount: int = 1) -> void:
|
||||
fleet_gas_cargo += amount
|
||||
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)
|
||||
|
||||
|
||||
func resource_name(resource_type: String) -> String:
|
||||
return String(RESOURCE_NAMES.get(resource_type, resource_type.capitalize()))
|
||||
var definition: Resource = resource_definition(resource_type)
|
||||
return String(definition.display_name) if definition != null else resource_type.capitalize()
|
||||
|
||||
|
||||
func resource_color(resource_type: String) -> Color:
|
||||
return RESOURCE_COLORS.get(resource_type, Color.WHITE)
|
||||
var definition: Resource = resource_definition(resource_type)
|
||||
return definition.color if definition != null else Color.WHITE
|
||||
|
||||
|
||||
func banked_resource(resource_type: String) -> int:
|
||||
match resource_type:
|
||||
RESOURCE_ORE: return rocks
|
||||
RESOURCE_GAS: return gas
|
||||
_: return 0
|
||||
return bank_inventory.amount(resource_type)
|
||||
|
||||
|
||||
func fleet_resource(resource_type: String) -> int:
|
||||
match resource_type:
|
||||
RESOURCE_ORE: return fleet_cargo
|
||||
RESOURCE_GAS: return fleet_gas_cargo
|
||||
_: return 0
|
||||
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)
|
||||
|
||||
|
||||
func building_resource(id: int, resource_type: String) -> int:
|
||||
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
|
||||
var inventory: RefCounted = building_inventory(id)
|
||||
return 0 if inventory == null else inventory.amount(resource_type)
|
||||
|
||||
|
||||
func transfer_resource_to_building(id: int, resource_type: String, amount: int) -> int:
|
||||
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
|
||||
return fleet_inventory.transfer_to(building_inventory(id), resource_type, amount)
|
||||
|
||||
|
||||
func transfer_resource_to_fleet(id: int, resource_type: String, amount: int) -> int:
|
||||
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
|
||||
var source: RefCounted = building_inventory(id)
|
||||
return 0 if source == null else source.transfer_to(fleet_inventory, resource_type, amount)
|
||||
|
||||
|
||||
# The batch size the transfer menu offers, next to "All". A batch rather
|
||||
|
|
@ -334,11 +315,6 @@ func unlock(id: String) -> bool:
|
|||
|
||||
|
||||
func is_building_unlocked(type: String) -> bool:
|
||||
# The mothership is always buildable: it is where research happens, so
|
||||
# locking it would leave a player with no mothership unable to ever get
|
||||
# one.
|
||||
if type == BUILDING_MOTHERSHIP:
|
||||
return true
|
||||
for node in SKILL_TREE:
|
||||
if node.get("unlocks", "") == type:
|
||||
return is_unlocked(node.id)
|
||||
|
|
@ -400,8 +376,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, "stock": 0,
|
||||
"gas_stock": 0, "orders": [],
|
||||
"id": id, "type": type, "position": at_position,
|
||||
"inventory": INVENTORY.new(), "orders": [],
|
||||
})
|
||||
return id
|
||||
|
||||
|
|
@ -413,6 +389,13 @@ func find_building(id: int) -> Dictionary:
|
|||
return {}
|
||||
|
||||
|
||||
func building_has_capability(building: Dictionary, capability: String) -> bool:
|
||||
if building.is_empty():
|
||||
return false
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(building.type))
|
||||
return definition != null and capability in definition.capabilities
|
||||
|
||||
|
||||
# --- vessel types and their upgrades (#5) ---------------------------------
|
||||
|
||||
# The three things that can be flying around. The first and last double as
|
||||
|
|
@ -422,21 +405,10 @@ const VESSEL_INTERCEPTOR: String = "interceptor"
|
|||
const VESSEL_SHIELD: String = "shield"
|
||||
const VESSEL_TRANSPORTER: String = "transporter"
|
||||
|
||||
const VESSEL_NAMES: Dictionary = {
|
||||
VESSEL_INTERCEPTOR: "Interceptor",
|
||||
VESSEL_SHIELD: "Shield Vessel",
|
||||
VESSEL_TRANSPORTER: "Transporter",
|
||||
}
|
||||
|
||||
# Compatibility constants for callers and old tests. Runtime metadata is
|
||||
# resolved from VesselDefinition below.
|
||||
const SHIELD_COST: int = 14
|
||||
|
||||
# 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 TRANSPORTER_COST: int = 10
|
||||
const LAB_RESEARCH_TIME: float = 8.0
|
||||
|
||||
|
||||
|
|
@ -447,57 +419,14 @@ var build_time_scale: float = 1.0
|
|||
|
||||
|
||||
func build_time(product: String) -> float:
|
||||
return float(BUILD_TIMES.get(product, 10.0)) * build_time_scale
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
|
||||
return (definition.build_time if definition != null else 10.0) * build_time_scale
|
||||
|
||||
|
||||
func lab_research_time() -> float:
|
||||
return LAB_RESEARCH_TIME * build_time_scale
|
||||
|
||||
# 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.
|
||||
# Compatibility constants. VesselDefinition owns the live base figures.
|
||||
const SHIP_BASE_DAMAGE: int = 1
|
||||
const SHIP_BASE_COOLDOWN: float = 0.5
|
||||
const SHIP_BASE_RANGE: float = 320.0
|
||||
|
|
@ -515,15 +444,13 @@ signal vessel_upgraded(upgrade_id: String)
|
|||
|
||||
func has_ship_lab() -> bool:
|
||||
for b in buildings:
|
||||
if b.type == BUILDING_SHIP_LAB:
|
||||
if building_has_capability(b, "vessel_research"):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func shield_vessel_unlocked() -> bool:
|
||||
# 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
|
||||
return vessel_is_unlocked(VESSEL_SHIELD)
|
||||
|
||||
|
||||
const UNLOCK_SHIELD_VESSEL: String = "unlock_shield"
|
||||
|
|
@ -531,27 +458,69 @@ const UNLOCK_SHIELD_COST: int = 45
|
|||
|
||||
|
||||
func unlock_shield_vessel() -> bool:
|
||||
if shield_vessel_unlocked() or not has_ship_lab():
|
||||
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:
|
||||
return false
|
||||
if not spend(UNLOCK_SHIELD_COST):
|
||||
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():
|
||||
return false
|
||||
vessel_levels[UNLOCK_SHIELD_VESSEL] = 1
|
||||
vessel_upgraded.emit(UNLOCK_SHIELD_VESSEL)
|
||||
if not spend(definition.unlock_cost):
|
||||
return false
|
||||
vessel_levels[definition.unlock_id] = 1
|
||||
vessel_upgraded.emit(definition.unlock_id)
|
||||
return true
|
||||
|
||||
|
||||
func upgrade_def(id: String) -> Dictionary:
|
||||
for u in VESSEL_UPGRADES:
|
||||
if u.id == id:
|
||||
return u
|
||||
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}
|
||||
return {}
|
||||
|
||||
|
||||
func upgrades_for(vessel: String) -> Array:
|
||||
var out: Array = []
|
||||
for u in VESSEL_UPGRADES:
|
||||
if u.vessel == vessel:
|
||||
out.append(u)
|
||||
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})
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -585,77 +554,60 @@ func buy_upgrade(id: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
# --- 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 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
|
||||
|
||||
|
||||
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 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 shield_max_charges() -> int:
|
||||
return SHIELD_BASE_CHARGES + upgrade_level("shd_hits")
|
||||
|
||||
var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield")
|
||||
return int(vessel_stat(definition.definition_id, "shield_charges"))
|
||||
|
||||
func shield_recharge_time() -> float:
|
||||
return maxf(2.0, SHIELD_BASE_RECHARGE - 1.2 * upgrade_level("shd_recharge"))
|
||||
|
||||
var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield")
|
||||
return vessel_stat(definition.definition_id, "shield_recharge")
|
||||
|
||||
func transporter_load_limit() -> int:
|
||||
return TRANSPORTER_BASE_LOAD + 8 * upgrade_level("trn_capacity")
|
||||
|
||||
var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport")
|
||||
return int(vessel_stat(definition.definition_id, "cargo_capacity"))
|
||||
|
||||
func transporter_speed() -> float:
|
||||
return TRANSPORTER_BASE_SPEED + 70.0 * upgrade_level("trn_speed")
|
||||
var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport")
|
||||
return vessel_stat(definition.definition_id, "transport_speed")
|
||||
|
||||
|
||||
const PRODUCT_SHIP: String = "interceptor"
|
||||
const PRODUCT_TRANSPORTER: String = "transporter"
|
||||
|
||||
const TRANSPORTER_COST: int = 10
|
||||
|
||||
|
||||
func product_cost(product: String) -> int:
|
||||
if product == PRODUCT_TRANSPORTER:
|
||||
return TRANSPORTER_COST
|
||||
if product == VESSEL_SHIELD:
|
||||
return SHIELD_COST
|
||||
return SHIP_COST
|
||||
var definition: Resource = vessel_definition(product)
|
||||
return definition.build_cost if definition != null else 0
|
||||
|
||||
|
||||
func can_produce(type: String) -> bool:
|
||||
# Only the ship crafter builds anything. The hub STATIONS transporters,
|
||||
# it does not make them (#12).
|
||||
return type == BUILDING_SHIP_CRAFTER
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||
return definition != null and "production" in definition.capabilities
|
||||
|
||||
|
||||
func building_stock(id: int) -> int:
|
||||
var b: Dictionary = find_building(id)
|
||||
return 0 if b.is_empty() else int(b.get("stock", 0))
|
||||
return building_resource(id, RESOURCE_ORE)
|
||||
|
||||
|
||||
func pending_orders(id: int) -> int:
|
||||
|
|
@ -702,7 +654,7 @@ func cancel_order(id: int, index: int) -> bool:
|
|||
return false
|
||||
var order: Dictionary = orders[index]
|
||||
if bool(order.started):
|
||||
b.stock = int(b.get("stock", 0)) + product_cost(String(order.product))
|
||||
_inventory_for_record(b).deposit(RESOURCE_ORE, product_cost(String(order.product)))
|
||||
orders.remove_at(index)
|
||||
return true
|
||||
|
||||
|
|
@ -717,7 +669,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 - int(b.get("stock", 0)), 0)
|
||||
return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 0)
|
||||
|
||||
|
||||
func deposit_to_building(id: int, amount: int) -> int:
|
||||
|
|
@ -726,21 +678,21 @@ func deposit_to_building(id: int, amount: int) -> int:
|
|||
var b: Dictionary = find_building(id)
|
||||
if b.is_empty():
|
||||
return 0
|
||||
b.stock = int(b.get("stock", 0)) + amount
|
||||
return amount
|
||||
return _inventory_for_record(b).deposit(RESOURCE_ORE, 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
|
||||
if product == VESSEL_SHIELD and not shield_vessel_unlocked():
|
||||
var definition: Resource = vessel_definition(product)
|
||||
if definition == null or not vessel_is_unlocked(product):
|
||||
return false
|
||||
if product == PRODUCT_TRANSPORTER and not has_transport_hub():
|
||||
if definition.has_capability("transport") 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 product == PRODUCT_TRANSPORTER and not can_station_transporter():
|
||||
if definition.has_capability("transport") 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})
|
||||
|
|
@ -762,17 +714,19 @@ 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 int(b.get("stock", 0)) < cost:
|
||||
if inventory.amount(RESOURCE_ORE) < cost:
|
||||
continue
|
||||
if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity():
|
||||
if definition.has_capability("transport") 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 product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
|
||||
if definition.has_capability("transport") and nearest_hub_to(b.position).is_empty():
|
||||
continue
|
||||
b.stock = int(b.stock) - cost
|
||||
inventory.withdraw(RESOURCE_ORE, cost)
|
||||
order.started = true
|
||||
|
||||
order.elapsed = float(order.elapsed) + delta
|
||||
|
|
@ -780,7 +734,7 @@ func _advance_production(delta: float) -> void:
|
|||
continue
|
||||
|
||||
orders.pop_front()
|
||||
if product == PRODUCT_TRANSPORTER:
|
||||
if definition.has_capability("transport"):
|
||||
# Transporters are unchanged: they take themselves to the nearest
|
||||
# hub and wait for work (#19).
|
||||
_station_transporter(b.position)
|
||||
|
|
@ -799,10 +753,11 @@ func _advance_production(delta: float) -> void:
|
|||
|
||||
func queue_lab_research(lab_id: int, upgrade_id: String) -> bool:
|
||||
var lab: Dictionary = find_building(lab_id)
|
||||
if lab.is_empty() or lab.type != BUILDING_SHIP_LAB or upgrade_id == "":
|
||||
if not building_has_capability(lab, "vessel_research") or upgrade_id == "":
|
||||
return false
|
||||
var orders: Array = lab.get("research_orders", [])
|
||||
if not orders.is_empty() or (upgrade_id != UNLOCK_SHIELD_VESSEL and upgrade_maxed(upgrade_id)):
|
||||
var unlock_definition: Resource = vessel_for_unlock(upgrade_id)
|
||||
if not orders.is_empty() or (unlock_definition == null and upgrade_maxed(upgrade_id)):
|
||||
return false
|
||||
orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false})
|
||||
lab.research_orders = orders
|
||||
|
|
@ -817,23 +772,25 @@ func lab_research_order(lab_id: int) -> Dictionary:
|
|||
|
||||
func _advance_lab_research(delta: float) -> void:
|
||||
for lab in buildings:
|
||||
if lab.type != BUILDING_SHIP_LAB:
|
||||
if not building_has_capability(lab, "vessel_research"):
|
||||
continue
|
||||
var orders: Array = lab.get("research_orders", [])
|
||||
if orders.is_empty():
|
||||
continue
|
||||
var order: Dictionary = orders[0]
|
||||
var id: String = String(order.id)
|
||||
var cost: int = UNLOCK_SHIELD_COST if id == UNLOCK_SHIELD_VESSEL else upgrade_cost(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)
|
||||
if not bool(order.started):
|
||||
if int(lab.get("stock", 0)) < cost:
|
||||
if inventory.amount(RESOURCE_ORE) < cost:
|
||||
continue
|
||||
lab.stock = int(lab.stock) - cost
|
||||
inventory.withdraw(RESOURCE_ORE, cost)
|
||||
order.started = true
|
||||
order.elapsed = float(order.elapsed) + delta
|
||||
if float(order.elapsed) < lab_research_time():
|
||||
continue
|
||||
if id == UNLOCK_SHIELD_VESSEL:
|
||||
if unlock_definition != null:
|
||||
vessel_levels[id] = 1
|
||||
else:
|
||||
vessel_levels[id] = upgrade_level(id) + 1
|
||||
|
|
@ -862,6 +819,13 @@ func has_mothership() -> bool:
|
|||
return get_mothership_position() != null
|
||||
|
||||
|
||||
func has_building_of_type(type: String) -> bool:
|
||||
for building in buildings:
|
||||
if building.type == type:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func remove_buildings_of_type(type: String) -> int:
|
||||
var kept: Array = []
|
||||
var removed: int = 0
|
||||
|
|
@ -878,7 +842,7 @@ func get_mothership_position() -> Variant:
|
|||
# Where a wiped-out fleet gets rebuilt. Returns null when no mothership
|
||||
# has been built yet, so the caller can fall back to the world origin.
|
||||
for b in buildings:
|
||||
if b.type == BUILDING_MOTHERSHIP:
|
||||
if building_has_capability(b, "bank"):
|
||||
return b.position
|
||||
return null
|
||||
|
||||
|
|
@ -917,7 +881,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", "stock", "mining_progress"}.
|
||||
# {"position", "planet_name", "resource_type", "inventory", "mining_progress"}.
|
||||
const EXTRACTOR_RATE: float = 2.0 # ore per second
|
||||
|
||||
var extractors: Array = []
|
||||
|
|
@ -938,7 +902,7 @@ func add_extractor(planet_name: String, at_position: Vector2, resource_type: Str
|
|||
"position": at_position,
|
||||
"planet_name": planet_name,
|
||||
"resource_type": resource_type,
|
||||
"stock": 0,
|
||||
"inventory": INVENTORY.new(),
|
||||
"mining_progress": 0.0,
|
||||
})
|
||||
# An extractor is built from SPACE, with the fleet parked on the planet -
|
||||
|
|
@ -951,7 +915,21 @@ 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
|
||||
return int(extractors[index].get("stock", 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
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
|
@ -964,7 +942,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:
|
||||
e.stock = int(e.get("stock", 0)) + whole
|
||||
_extractor_inventory(e).deposit(String(e.get("resource_type", RESOURCE_ORE)), whole)
|
||||
e.mining_progress = float(e.mining_progress) - whole
|
||||
_advance_transporters(delta)
|
||||
|
||||
|
|
@ -1024,7 +1002,7 @@ func hub_capacity() -> int:
|
|||
func hub_count() -> int:
|
||||
var n: int = 0
|
||||
for b in buildings:
|
||||
if b.type == BUILDING_TRANSPORT_HUB:
|
||||
if building_has_capability(b, "transport_hub"):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
|
@ -1039,7 +1017,7 @@ func pending_transporter_orders() -> int:
|
|||
var n: int = 0
|
||||
for b in buildings:
|
||||
for o in b.get("orders", []):
|
||||
if String(o.product) == PRODUCT_TRANSPORTER:
|
||||
if vessel_has_capability(String(o.product), "transport"):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
|
@ -1063,7 +1041,7 @@ func nearest_hub_to(at: Vector2) -> Dictionary:
|
|||
var best: Dictionary = {}
|
||||
var best_d: float = INF
|
||||
for b in buildings:
|
||||
if b.type != BUILDING_TRANSPORT_HUB:
|
||||
if not building_has_capability(b, "transport_hub"):
|
||||
continue
|
||||
var d: float = at.distance_squared_to(b.position)
|
||||
if d < best_d:
|
||||
|
|
@ -1126,7 +1104,8 @@ func _dispatch_transporters() -> void:
|
|||
var load: int = transporter_load_limit()
|
||||
for i in range(extractors.size()):
|
||||
var extractor: Dictionary = extractors[i]
|
||||
if int(extractor.get("stock", 0)) < load or _transporter_inbound_to_extractor(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):
|
||||
continue
|
||||
var chosen_extractor: Dictionary = {}
|
||||
var best_extractor_distance: float = INF
|
||||
|
|
@ -1140,7 +1119,7 @@ func _dispatch_transporters() -> void:
|
|||
if chosen_extractor.is_empty():
|
||||
return
|
||||
chosen_extractor.extractor_index = i
|
||||
chosen_extractor.resource_type = String(extractor.get("resource_type", "ore"))
|
||||
chosen_extractor.resource_type = resource_id
|
||||
chosen_extractor.target_id = 0
|
||||
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
||||
TRANSPORTER_TO_EXTRACTOR)
|
||||
|
|
@ -1191,13 +1170,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, rocks), transporter_load_limit())
|
||||
var take: int = mini(mini(want, bank_inventory.amount(RESOURCE_ORE)), 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
|
||||
rocks -= take
|
||||
bank_inventory.withdraw(RESOURCE_ORE, take)
|
||||
t.carrying = take
|
||||
_begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING)
|
||||
TRANSPORTER_TO_BUILDING:
|
||||
|
|
@ -1205,7 +1184,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.
|
||||
rocks += int(t.carrying)
|
||||
bank_inventory.deposit(RESOURCE_ORE, int(t.carrying))
|
||||
t.carrying = 0
|
||||
t.target_id = 0
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
|
|
@ -1216,20 +1195,19 @@ func _arrive(t: Dictionary) -> void:
|
|||
return
|
||||
var extractor: Dictionary = extractors[extractor_index]
|
||||
var load: int = transporter_load_limit()
|
||||
if int(extractor.get("stock", 0)) < load:
|
||||
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:
|
||||
t.extractor_index = -1
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
return
|
||||
extractor.stock = int(extractor.stock) - load
|
||||
extractor_inventory.withdraw(resource_id, load)
|
||||
t.carrying = load
|
||||
t.resource_type = String(extractor.get("resource_type", "ore"))
|
||||
t.resource_type = resource_id
|
||||
_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.
|
||||
if String(t.get("resource_type", "ore")) == "gas":
|
||||
gas += int(t.carrying)
|
||||
else:
|
||||
rocks += int(t.carrying)
|
||||
bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ORE)), int(t.carrying))
|
||||
t.carrying = 0
|
||||
t.extractor_index = -1
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
|
|
@ -1239,11 +1217,11 @@ func _arrive(t: Dictionary) -> void:
|
|||
|
||||
|
||||
func can_afford(cost: int) -> bool:
|
||||
return rocks >= cost
|
||||
return bank_inventory.amount(RESOURCE_ORE) >= cost
|
||||
|
||||
|
||||
func spend(cost: int) -> bool:
|
||||
if not can_afford(cost):
|
||||
return false
|
||||
rocks -= cost
|
||||
bank_inventory.withdraw(RESOURCE_ORE, cost)
|
||||
return true
|
||||
|
|
|
|||
14
GasGiantDefinition.tres
Normal file
14
GasGiantDefinition.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[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)
|
||||
31
GasGiantVisual.gd
Normal file
31
GasGiantVisual.gd
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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)
|
||||
1
GasGiantVisual.gd.uid
Normal file
1
GasGiantVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bxfw4ja2hw4pf
|
||||
6
GasGiantVisual.tscn
Normal file
6
GasGiantVisual.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[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")
|
||||
11
GasResourceDefinition.tres
Normal file
11
GasResourceDefinition.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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")
|
||||
19
GasResourceGlyph.gd
Normal file
19
GasResourceGlyph.gd
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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))
|
||||
1
GasResourceGlyph.gd.uid
Normal file
1
GasResourceGlyph.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d1ko6yhtjjavx
|
||||
13
GasResourceGlyph.tscn
Normal file
13
GasResourceGlyph.tscn
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[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")
|
||||
7
GasSurfaceFeatures.tscn
Normal file
7
GasSurfaceFeatures.tscn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[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")
|
||||
14
GasSurfaceProfile.tres
Normal file
14
GasSurfaceProfile.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[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)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
extends Node2D
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
@export var fleet_path: NodePath = NodePath("../Fleet")
|
||||
@onready var fleet: Node2D = get_node(fleet_path)
|
||||
var vents: Array[Vector2] = []
|
||||
var progress: float = 0.0
|
||||
var emission_phase: float = 0.0
|
||||
|
|
|
|||
11
InterceptorDamageUpgrade.tres
Normal file
11
InterceptorDamageUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
11
InterceptorRangeUpgrade.tres
Normal file
11
InterceptorRangeUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
11
InterceptorRateUpgrade.tres
Normal file
11
InterceptorRateUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
11
InterceptorSpeedUpgrade.tres
Normal file
11
InterceptorSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
20
InterceptorVesselDefinition.tres
Normal file
20
InterceptorVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[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")])
|
||||
79
Inventory.gd
Normal file
79
Inventory.gd
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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()
|
||||
1
Inventory.gd.uid
Normal file
1
Inventory.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://celx1yj6cp2rf
|
||||
21
MothershipBuildingDefinition.tres
Normal file
21
MothershipBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://MothershipBuildingVisual.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "mothership"
|
||||
display_name = "Mothership"
|
||||
description = "Fleet home port. A wiped-out fleet rebuilds here, and extractor transports deliver resources here."
|
||||
cost = 20
|
||||
action_icon = 8
|
||||
menu_icon = 8
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("bank", "research", "hangar")
|
||||
map_color = Color(0.45, 0.9, 0.55, 1)
|
||||
map_sides = 6
|
||||
map_radius = 8.0
|
||||
map_phase = 0.523599
|
||||
menu_button_name = "MothershipButton"
|
||||
menu_label_name = "MothershipLabel"
|
||||
menu_cost_name = "MothershipCost"
|
||||
unique = true
|
||||
12
MothershipBuildingVisual.gd
Normal file
12
MothershipBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
extends "res://BuildingVisualBase.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
_build_mothership_pieces()
|
||||
queue_redraw()
|
||||
|
||||
func apply_animation() -> void:
|
||||
_animate_mothership()
|
||||
|
||||
func _draw() -> void:
|
||||
register_draw()
|
||||
_draw_mothership_static(self)
|
||||
1
MothershipBuildingVisual.gd.uid
Normal file
1
MothershipBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://m43bwdwhqgy4
|
||||
11
MothershipBuildingVisual.tscn
Normal file
11
MothershipBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://MothershipBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("research", "transfer", "hangar")
|
||||
11
OreResourceDefinition.tres
Normal file
11
OreResourceDefinition.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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")
|
||||
14
OreResourceGlyph.gd
Normal file
14
OreResourceGlyph.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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))
|
||||
1
OreResourceGlyph.gd.uid
Normal file
1
OreResourceGlyph.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwgkhqo4ev5ao
|
||||
13
OreResourceGlyph.tscn
Normal file
13
OreResourceGlyph.tscn
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[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")
|
||||
80
Planet.gd
80
Planet.gd
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
|
|
@ -11,15 +14,33 @@ extends Node2D
|
|||
# while the fleet as a whole was clearly sitting on the planet.
|
||||
|
||||
@export var planet_name: String = ""
|
||||
@export var planet_type: String = "rocky"
|
||||
@export var resource_type: String = "ore"
|
||||
@export var definition_id: String = PLANET_DEFINITIONS.DEFAULT_DEFINITION_ID
|
||||
@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 shape_points: PackedVector2Array = PackedVector2Array()
|
||||
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
|
||||
|
||||
# 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
|
||||
|
|
@ -37,31 +58,20 @@ 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
|
||||
|
||||
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)
|
||||
definition = PLANET_DEFINITIONS.get_definition(definition_id)
|
||||
visual = definition.visual_scene.instantiate()
|
||||
visual.configure(shape_seed)
|
||||
get_node("VisualHost").add_child(visual)
|
||||
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.
|
||||
|
|
@ -72,14 +82,9 @@ func _on_extractor_added(built_on: String) -> void:
|
|||
|
||||
|
||||
func extractor_derrick_anchor() -> Vector2:
|
||||
# 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
|
||||
if visual != null and visual.has_method("extractor_anchor"):
|
||||
return visual.extractor_anchor()
|
||||
return Vector2(0.0, -100.0)
|
||||
|
||||
|
||||
func extractor_derrick_tip() -> Vector2:
|
||||
|
|
@ -111,28 +116,5 @@ 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)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@
|
|||
|
||||
[node name="Planet" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="VisualHost" type="Node2D" parent="."]
|
||||
|
|
|
|||
|
|
@ -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_type = current_planet.planet_type
|
||||
GameState.planet_definition_id = current_planet.definition_id
|
||||
|
||||
var tween: Tween = create_tween()
|
||||
tween.tween_property(fade_rect, "color:a", 1.0, fade_duration)
|
||||
|
|
|
|||
10
PlanetDefinition.gd
Normal file
10
PlanetDefinition.gd
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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)
|
||||
1
PlanetDefinition.gd.uid
Normal file
1
PlanetDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dt6tsdmmlnfaf
|
||||
14
PlanetDefinitions.gd
Normal file
14
PlanetDefinitions.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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])
|
||||
1
PlanetDefinitions.gd.uid
Normal file
1
PlanetDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ipsld2ex2t8e
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
|
@ -83,8 +84,7 @@ 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.planet_type = String(data.get("planet_type", "rocky"))
|
||||
planet.resource_type = String(data.get("resource_type", "ore"))
|
||||
planet.definition_id = String(data.get("definition_id", "rocky"))
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@
|
|||
[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"]
|
||||
|
|
@ -49,12 +47,7 @@ ship_core_color = Color(0.02, 0.09, 0.15, 1)
|
|||
position = Vector2(0, 0)
|
||||
zoom = Vector2(1, 1)
|
||||
|
||||
[node name="RockField" type="Node2D" parent="."]
|
||||
script = ExtResource("8")
|
||||
|
||||
[node name="GasVentField" type="Node2D" parent="."]
|
||||
visible = false
|
||||
script = ExtResource("10")
|
||||
[node name="SurfaceFeatures" type="Node2D" parent="."]
|
||||
|
||||
[node name="SurfaceController" type="Node2D" parent="."]
|
||||
script = ExtResource("5")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
|
|
@ -13,29 +16,15 @@ extends Node2D
|
|||
@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:
|
||||
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))
|
||||
var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id)
|
||||
_compose_surface(SURFACE_PROFILES.get_profile(planet_definition.surface_profile_id))
|
||||
fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO)
|
||||
camera.position = Vector2.ZERO
|
||||
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
||||
|
|
@ -47,6 +36,21 @@ 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
|
||||
|
|
|
|||
13
PlanetSurfaceProfile.gd
Normal file
13
PlanetSurfaceProfile.gd
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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)
|
||||
1
PlanetSurfaceProfile.gd.uid
Normal file
1
PlanetSurfaceProfile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b31vef6ge1tjh
|
||||
12
PlanetSurfaceProfiles.gd
Normal file
12
PlanetSurfaceProfiles.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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])
|
||||
1
PlanetSurfaceProfiles.gd.uid
Normal file
1
PlanetSurfaceProfiles.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bufr6mnwbobxb
|
||||
|
|
@ -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
|
||||
# GameState.RESOURCE_TYPES automatically gives it a row here.
|
||||
# The resource-definition catalog automatically gives each material a row.
|
||||
|
||||
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
|
||||
|
|
|
|||
7
ResourceDefinition.gd
Normal file
7
ResourceDefinition.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
1
ResourceDefinition.gd.uid
Normal file
1
ResourceDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://kvy02iu7pbyu
|
||||
18
ResourceDefinitions.gd
Normal file
18
ResourceDefinitions.gd
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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
|
||||
1
ResourceDefinitions.gd.uid
Normal file
1
ResourceDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ci7yyyxyd3o67
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
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.
|
||||
|
|
@ -12,43 +14,21 @@ func set_resource_type(value: String) -> void:
|
|||
if resource_type == value:
|
||||
return
|
||||
resource_type = value
|
||||
queue_redraw()
|
||||
if is_node_ready():
|
||||
_rebuild_visual()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
queue_redraw()
|
||||
_rebuild_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))
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ extends Node2D
|
|||
@export var despawn_radius: float = 2600.0
|
||||
@export var check_interval: float = 0.5
|
||||
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
@export var fleet_path: NodePath = NodePath("../Fleet")
|
||||
@onready var fleet: Node2D = get_node(fleet_path)
|
||||
|
||||
var rock_scene: PackedScene = preload("res://Rock.tscn")
|
||||
var rocks: Array = []
|
||||
|
|
@ -26,12 +27,6 @@ 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.
|
||||
|
|
|
|||
14
RockyPlanetDefinition.tres
Normal file
14
RockyPlanetDefinition.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[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)
|
||||
26
RockyPlanetVisual.gd
Normal file
26
RockyPlanetVisual.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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)
|
||||
1
RockyPlanetVisual.gd.uid
Normal file
1
RockyPlanetVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b82xh8nosedlm
|
||||
6
RockyPlanetVisual.tscn
Normal file
6
RockyPlanetVisual.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[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")
|
||||
7
RockySurfaceFeatures.tscn
Normal file
7
RockySurfaceFeatures.tscn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[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")
|
||||
14
RockySurfaceProfile.tres
Normal file
14
RockySurfaceProfile.tres
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[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)
|
||||
11
ShieldHitsUpgrade.tres
Normal file
11
ShieldHitsUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
11
ShieldRechargeUpgrade.tres
Normal file
11
ShieldRechargeUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
11
ShieldSpeedUpgrade.tres
Normal file
11
ShieldSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
25
ShieldVesselDefinition.tres
Normal file
25
ShieldVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[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
|
||||
234
Ship.gd
234
Ship.gd
|
|
@ -2,224 +2,134 @@ extends Area2D
|
|||
|
||||
signal destroyed
|
||||
|
||||
# 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.
|
||||
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||
|
||||
@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"
|
||||
|
||||
# --- 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 definition: Resource
|
||||
var glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||
var core_color: Color = Color(1.0, 1.0, 1.0)
|
||||
|
||||
var core_color: Color = Color.WHITE
|
||||
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
|
||||
var fire_timer: float = 0.0
|
||||
var projectile_scene: PackedScene = preload("res://Projectile.tscn")
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
move_target = position
|
||||
add_to_group("player_ships")
|
||||
area_entered.connect(_on_area_entered)
|
||||
if vessel_type == GameState.VESSEL_SHIELD:
|
||||
_build_shield_ring()
|
||||
shield_charges = GameState.shield_max_charges()
|
||||
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()
|
||||
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:
|
||||
# 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()
|
||||
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()
|
||||
|
||||
|
||||
func has_shield() -> bool:
|
||||
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()
|
||||
var component: Node = get_component("shield")
|
||||
return component != null and component.is_active()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_tick_shield(delta)
|
||||
for child in get_children():
|
||||
if child.has_method("tick"):
|
||||
child.tick(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
|
||||
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
|
||||
for child in get_children():
|
||||
if child.has_method("absorb_hit") and child.absorb_hit():
|
||||
if area.is_in_group("asteroids") and is_instance_valid(area):
|
||||
area.queue_free()
|
||||
return
|
||||
ShipExplosionVFX.spawn(get_tree().root, position)
|
||||
destroyed.emit()
|
||||
queue_free()
|
||||
|
||||
|
||||
func _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 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.
|
||||
var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([
|
||||
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||
])
|
||||
NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color)
|
||||
|
|
|
|||
20
ShipLabBuildingDefinition.tres
Normal file
20
ShipLabBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://ShipLabBuildingVisual.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "ship_lab"
|
||||
display_name = "Ship Lab"
|
||||
description = "Fly the fleet onto it to upgrade each vessel type and unlock new vessel designs."
|
||||
cost = 35
|
||||
action_icon = 13
|
||||
menu_icon = 13
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("vessel_research")
|
||||
map_color = Color(0.55, 0.95, 0.9, 1)
|
||||
map_sides = 3
|
||||
map_radius = 7.5
|
||||
map_phase = -1.570796
|
||||
menu_button_name = "ShipLabButton"
|
||||
menu_label_name = "ShipLabLabel"
|
||||
menu_cost_name = "ShipLabCost"
|
||||
12
ShipLabBuildingVisual.gd
Normal file
12
ShipLabBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
extends "res://BuildingVisualBase.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
_build_lab_pieces()
|
||||
queue_redraw()
|
||||
|
||||
func apply_animation() -> void:
|
||||
_animate_ship_lab()
|
||||
|
||||
func _draw() -> void:
|
||||
register_draw()
|
||||
_draw_ship_lab_static(self)
|
||||
1
ShipLabBuildingVisual.gd.uid
Normal file
1
ShipLabBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b0p7p0fs0th0b
|
||||
11
ShipLabBuildingVisual.tscn
Normal file
11
ShipLabBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ShipLabBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("lab", "transfer")
|
||||
11
TransportCapacityUpgrade.tres
Normal file
11
TransportCapacityUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
20
TransportHubBuildingDefinition.tres
Normal file
20
TransportHubBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://TransportHubBuildingVisual.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "transport_hub"
|
||||
display_name = "Transport Hub"
|
||||
description = "Stations transport vessels. They collect extracted resources and supply buildings that are waiting on materials."
|
||||
cost = 30
|
||||
action_icon = 12
|
||||
menu_icon = 12
|
||||
visual_scene = ExtResource("2")
|
||||
capabilities = PackedStringArray("transport_hub")
|
||||
map_color = Color(0.95, 0.55, 0.85, 1)
|
||||
map_sides = 8
|
||||
map_radius = 7.5
|
||||
map_phase = 0.392699
|
||||
menu_button_name = "TransportHubButton"
|
||||
menu_label_name = "TransportHubLabel"
|
||||
menu_cost_name = "TransportHubCost"
|
||||
12
TransportHubBuildingVisual.gd
Normal file
12
TransportHubBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
extends "res://BuildingVisualBase.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
_build_hub_pieces()
|
||||
queue_redraw()
|
||||
|
||||
func apply_animation() -> void:
|
||||
_animate_transport_hub()
|
||||
|
||||
func _draw() -> void:
|
||||
register_draw()
|
||||
_draw_transport_hub_static(self)
|
||||
1
TransportHubBuildingVisual.gd.uid
Normal file
1
TransportHubBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://6qyl5qnhw0ho
|
||||
11
TransportHubBuildingVisual.tscn
Normal file
11
TransportHubBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://TransportHubBuildingVisual.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://BuildingActionProvider.gd" id="2"]
|
||||
|
||||
[node name="Visual" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ActionProvider" type="Node" parent="."]
|
||||
script = ExtResource("2")
|
||||
actions = PackedStringArray("transfer")
|
||||
11
TransportSpeedUpgrade.tres
Normal file
11
TransportSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[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}
|
||||
19
TransporterVesselDefinition.tres
Normal file
19
TransporterVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[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")])
|
||||
9
VesselCargoComponent.gd
Normal file
9
VesselCargoComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
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"))
|
||||
1
VesselCargoComponent.gd.uid
Normal file
1
VesselCargoComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bo16x4wq6hg5k
|
||||
4
VesselCargoComponent.tscn
Normal file
4
VesselCargoComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[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")
|
||||
26
VesselDefinition.gd
Normal file
26
VesselDefinition.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
1
VesselDefinition.gd.uid
Normal file
1
VesselDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c6txy3v61blm8
|
||||
24
VesselDefinitions.gd
Normal file
24
VesselDefinitions.gd
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
1
VesselDefinitions.gd.uid
Normal file
1
VesselDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cgwj1q4nf3ar7
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
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 = GameState.VESSEL_INTERCEPTOR
|
||||
var vessel_type: String = "interceptor"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -11,6 +13,6 @@ func _ready() -> void:
|
|||
|
||||
|
||||
func _draw() -> void:
|
||||
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)
|
||||
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)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue