Forgejo #13: releasing the stick paused, then moved again before settling. stop() aims each ship at a braking point ahead of it, but the ship's own velocity was ~0 - the anchor had been carrying it - so it started from REST, accelerated into its own stopping distance, and halted a second time. Measured: 0 -> 45 -> 90 -> 135 -> 180 -> brake. stop() now hands the fleet's motion to the ship, so the release is one continuous decelerating coast (222 -> 0) to the identical endpoint. test_joystick_control now asserts the speed one frame after release is a real fraction of cruise, and never rises afterwards. Forgejo #21: unresearched structures are no longer listed greyed out. They are hidden, and the menu repacks around what is left - a fixed grid with entries hidden would otherwise leave holes where they used to be. An entry alone on its row is centred rather than hanging in the left column. Researched-but-not-usable is still shown greyed: an extractor with no planet under the fleet is "not now", not "not yet". This reverses a documented decision (the old comment argued that always listing everything stops the player wondering whether the game has it at all), so test_skill_tree's matching assertion was updated from greyed-but-listed to hidden. The gate itself is unchanged and still refuses a build forced past the menu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
638 lines
25 KiB
GDScript
638 lines
25 KiB
GDScript
extends Node2D
|
|
|
|
# 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
|
|
# standing on (craft ships at a crafter, configure formation at a
|
|
# command centre). The enter-atmosphere prompt uses the same slot, and
|
|
# PlanetAtmosphereController suppresses itself while a building prompt
|
|
# is showing so the two can never fight over it.
|
|
# - rebuilding a wiped-out fleet at the mothership
|
|
#
|
|
# Buildings themselves live in GameState (they must outlive the trip to a
|
|
# planet's surface); this only drives the UI and spends resources.
|
|
|
|
@export var prompt_debounce_time: float = 0.15
|
|
|
|
@onready var fleet: Node2D = get_node("../Fleet")
|
|
@onready var building_field: Node2D = get_node("../BuildingField")
|
|
@onready var world_map: Control = get_node("../UI/WorldMap")
|
|
@onready var build_button: IconButton = get_node("../UI/BuildButton")
|
|
@onready var action_button: IconButton = get_node("../UI/BuildActionButton")
|
|
|
|
@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_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")],
|
|
]
|
|
@onready var planet_field: Node2D = get_node("../PlanetField")
|
|
@onready var info_popup: Control = get_node("../UI/BuildMenu/InfoPopup")
|
|
@onready var info_title: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/TitleLabel")
|
|
@onready var info_message: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/MessageLabel")
|
|
@onready var info_cost: NeonNumber = get_node("../UI/BuildMenu/InfoPopup/Panel/CostNumber")
|
|
@onready var info_build_button: IconButton = get_node("../UI/BuildMenu/InfoPopup/Panel/BuildButton")
|
|
@onready var info_close_button: IconButton = get_node("../UI/BuildMenu/InfoPopup/Panel/CloseButton")
|
|
|
|
@onready var craft_menu: Control = get_node("../UI/CraftMenu")
|
|
@onready var craft_interceptor_button: IconButton = get_node("../UI/CraftMenu/InterceptorButton")
|
|
@onready var craft_transporter_button: IconButton = get_node("../UI/CraftMenu/TransporterButton")
|
|
@onready var craft_store_button: IconButton = get_node("../UI/CraftMenu/StoreOreButton")
|
|
@onready var craft_store_label: Label = get_node("../UI/CraftMenu/StoreOreLabel")
|
|
@onready var craft_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton")
|
|
@onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel")
|
|
@onready var load_ore_button: IconButton = get_node("../UI/SkillMenu/LoadOreButton")
|
|
@onready var load_ore_label: Label = get_node("../UI/SkillMenu/LoadOreLabel")
|
|
|
|
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
|
|
@onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree")
|
|
@onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel")
|
|
@onready var skill_cost: NeonNumber = get_node("../UI/SkillMenu/CostNumber")
|
|
@onready var skill_status: Label = get_node("../UI/SkillMenu/StatusLabel")
|
|
@onready var skill_unlock_button: IconButton = get_node("../UI/SkillMenu/UnlockButton")
|
|
@onready var skill_close_button: IconButton = get_node("../UI/SkillMenu/CloseButton")
|
|
|
|
@onready var formation_menu: Control = get_node("../UI/FormationMenu")
|
|
@onready var formation_editor: Control = get_node("../UI/FormationMenu/Editor")
|
|
@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_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 = ""
|
|
# Which crafter the craft menu is showing (see _on_action_pressed).
|
|
var craft_building_id: int = 0
|
|
# The craft panel refreshes every frame while it is open (so a delivery
|
|
# arriving visibly works the queue down). That overwrote any message the
|
|
# moment after it was set, which made a working button look dead - the store
|
|
# button in particular. Messages therefore have to outlive a refresh.
|
|
var craft_message: String = ""
|
|
var craft_message_until: int = 0
|
|
|
|
var current_building: Node2D = null
|
|
var pending_building: Node2D = null
|
|
var pending_time: float = 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
build_button.pressed.connect(_on_build_pressed)
|
|
build_close_button.pressed.connect(func(): build_menu.visible = false)
|
|
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)
|
|
info_close_button.pressed.connect(_close_info)
|
|
info_build_button.pressed.connect(_confirm_build)
|
|
|
|
action_button.pressed.connect(_on_action_pressed)
|
|
|
|
craft_interceptor_button.pressed.connect(_on_craft_interceptor)
|
|
load_ore_button.pressed.connect(_on_load_ore)
|
|
craft_transporter_button.pressed.connect(_on_craft_transporter)
|
|
craft_store_button.pressed.connect(_on_store_ore)
|
|
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
|
|
|
formation_close_button.pressed.connect(func(): formation_menu.visible = false)
|
|
formation_reset_button.pressed.connect(_on_reset_formation)
|
|
formation_editor.formation_changed.connect(_on_formation_changed)
|
|
|
|
skill_close_button.pressed.connect(func(): skill_menu.visible = false)
|
|
skill_unlock_button.pressed.connect(_on_unlock_pressed)
|
|
skill_tree.node_picked.connect(_on_skill_node_picked)
|
|
|
|
build_menu.visible = false
|
|
info_popup.visible = false
|
|
craft_menu.visible = false
|
|
formation_menu.visible = false
|
|
skill_menu.visible = false
|
|
action_button.visible = false
|
|
|
|
|
|
# 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:
|
|
return current_building != null or build_menu.visible or craft_menu.visible \
|
|
or formation_menu.visible or skill_menu.visible
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
# The formation editor and the skill tree are driven from here rather than
|
|
# via _gui_input, for the same reason as the joystick: a Control that
|
|
# consumes touches through the GUI system swallows them before they get
|
|
# here, which is what made dragging a formation slot silently do nothing.
|
|
if skill_menu.visible:
|
|
if event is InputEventScreenTouch:
|
|
skill_tree.handle_touch(event)
|
|
# Handled either way. A touch that lands on the panel but misses a
|
|
# node is still a touch ON THE PANEL - letting it through moved the
|
|
# fleet and pinch-zoomed the world behind the menu, because
|
|
# GameController sits on the root node and sees whatever this leaves
|
|
# unhandled.
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if formation_menu.visible:
|
|
if event is InputEventScreenTouch:
|
|
formation_editor.handle_touch(event)
|
|
elif event is InputEventScreenDrag:
|
|
formation_editor.handle_drag(event)
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if build_menu.visible or craft_menu.visible or world_map.visible:
|
|
# These panels have no raw-input handler of their own, but the world
|
|
# behind them must still not react.
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_check_fleet_wipe()
|
|
|
|
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()
|
|
|
|
if craft_menu.visible:
|
|
# Live, so a delivery arriving while the menu is open visibly works
|
|
# the queue down instead of looking stuck.
|
|
_refresh_craft_status()
|
|
|
|
if world_map.visible or build_menu.visible or craft_menu.visible \
|
|
or formation_menu.visible or skill_menu.visible:
|
|
current_building = null
|
|
pending_building = null
|
|
action_button.visible = false
|
|
return
|
|
|
|
# Debounced exactly like the enter-atmosphere prompt, so the action
|
|
# button can't flicker while the fleet drifts on the interact boundary.
|
|
var detected: Node2D = building_field.get_building_fleet_is_on()
|
|
|
|
if detected != pending_building:
|
|
pending_building = detected
|
|
pending_time = 0.0
|
|
else:
|
|
pending_time += delta
|
|
|
|
if pending_time < prompt_debounce_time or pending_building == current_building:
|
|
return
|
|
|
|
current_building = pending_building
|
|
action_button.visible = current_building != null
|
|
if current_building != null:
|
|
action_button.icon = _action_icon_for(current_building.building_type)
|
|
action_button.queue_redraw()
|
|
|
|
|
|
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_MOTHERSHIP:
|
|
return IconButton.Icon.MOTHERSHIP
|
|
_:
|
|
return IconButton.Icon.COMMAND_CENTER
|
|
|
|
|
|
func _check_fleet_wipe() -> void:
|
|
if not fleet.ships.is_empty():
|
|
return
|
|
# Whole fleet destroyed: rebuild a single vessel at the mothership, or
|
|
# at the world origin when none has been built yet.
|
|
var spawn_pos: Variant = GameState.get_mothership_position()
|
|
fleet.spawn_ships(1, spawn_pos if spawn_pos != null else Vector2.ZERO)
|
|
|
|
|
|
func _on_action_pressed() -> void:
|
|
if current_building == null:
|
|
return
|
|
if current_building.building_type == GameState.BUILDING_MOTHERSHIP:
|
|
skill_menu.visible = true
|
|
_refresh_load_ore()
|
|
_refresh_skill_menu()
|
|
elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER:
|
|
craft_menu.visible = true
|
|
_refresh_craft_status()
|
|
elif current_building.building_type == GameState.BUILDING_COMMAND_CENTER:
|
|
formation_menu.visible = true
|
|
formation_editor.queue_redraw()
|
|
|
|
|
|
func _on_build_pressed() -> void:
|
|
build_menu.visible = true
|
|
info_popup.visible = false
|
|
pending_build_type = ""
|
|
_refresh_build_status()
|
|
|
|
|
|
func _planet_under_fleet() -> Node2D:
|
|
# An extractor mines a specific planet, so one has to be within reach.
|
|
return planet_field.get_planet_fleet_is_on()
|
|
|
|
|
|
# Grid geometry, shared with the layout below. Rows of two, and the menu is
|
|
# rebuilt from whatever is currently researched (#21) rather than being a
|
|
# fixed arrangement with holes punched in it.
|
|
const MENU_ROW_TOPS: Array = [-300.0, -10.0, 280.0]
|
|
const MENU_COL_X: float = 140.0
|
|
const MENU_BUTTON_HALF: float = 64.0
|
|
const MENU_BUTTON_H: float = 128.0
|
|
const MENU_LABEL_HALF: float = 150.0
|
|
const MENU_LABEL_H: float = 66.0
|
|
const MENU_COST_HALF: float = 60.0
|
|
const MENU_COST_H: float = 52.0
|
|
|
|
|
|
func _place_entry(node: Control, cx: float, top: float, half: float, height: float) -> void:
|
|
node.offset_left = cx - half
|
|
node.offset_right = cx + half
|
|
node.offset_top = top
|
|
node.offset_bottom = top + height
|
|
|
|
|
|
func _layout_build_menu() -> void:
|
|
# Structures the player has not researched are hidden outright (#21), and
|
|
# what remains is repacked so there are no gaps where they used to be.
|
|
# This reverses the earlier decision to always list everything greyed
|
|
# out; an option you cannot explain is apparently worse than one you
|
|
# cannot see yet.
|
|
var shown: Array = []
|
|
for entry in build_menu_entries:
|
|
var visible: bool = GameState.is_building_unlocked(entry[0])
|
|
entry[1].visible = visible
|
|
entry[2].visible = visible
|
|
entry[3].visible = visible
|
|
if visible:
|
|
shown.append(entry)
|
|
|
|
for i in range(shown.size()):
|
|
var row: int = i / 2
|
|
# Centred when it is the only one on its row, rather than hanging in
|
|
# the left column looking like something is missing beside it.
|
|
var alone: bool = (i == shown.size() - 1) and (i % 2 == 0)
|
|
var cx: float = 0.0 if alone else (-MENU_COL_X if i % 2 == 0 else MENU_COL_X)
|
|
var top: float = MENU_ROW_TOPS[mini(row, MENU_ROW_TOPS.size() - 1)]
|
|
_place_entry(shown[i][1], cx, top, MENU_BUTTON_HALF, MENU_BUTTON_H)
|
|
_place_entry(shown[i][2], cx, top + 134.0, MENU_LABEL_HALF, MENU_LABEL_H)
|
|
_place_entry(shown[i][3], cx, top + 204.0, MENU_COST_HALF, MENU_COST_H)
|
|
|
|
|
|
func _refresh_build_status() -> void:
|
|
_layout_build_menu()
|
|
# 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)
|
|
|
|
# Prices are shown under each option now, so this only needs to say
|
|
# whether the fleet is somewhere an extractor could go.
|
|
if _planet_under_fleet() != null:
|
|
build_status.text = "Over %s - an extractor can be built here" % _planet_under_fleet().planet_name
|
|
else:
|
|
build_status.text = ""
|
|
|
|
|
|
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 _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_MOTHERSHIP:
|
|
return GameState.MOTHERSHIP_COST
|
|
GameState.BUILDING_SHIP_CRAFTER:
|
|
return GameState.SHIP_CRAFTER_COST
|
|
GameState.BUILDING_COMMAND_CENTER:
|
|
return GameState.COMMAND_CENTER_COST
|
|
_:
|
|
return GameState.EXTRACTOR_COST
|
|
|
|
|
|
func _show_building_info(type: String) -> void:
|
|
var info: Dictionary = BUILDING_INFO[type]
|
|
var cost: int = cost_of(type)
|
|
var lines: Array = [info.body]
|
|
var can_build: bool = true
|
|
|
|
# Locked structures still list themselves, greyed, so the player can see
|
|
# what exists and what research would open it up.
|
|
if not GameState.is_building_unlocked(type):
|
|
lines.append("Locked. Research %s at your mothership." % GameState.research_for_building(type))
|
|
can_build = false
|
|
|
|
# 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:
|
|
var planet: Node2D = _planet_under_fleet()
|
|
if planet == null:
|
|
lines.append("No planet in reach - fly over one.")
|
|
can_build = false
|
|
elif GameState.has_extractor_at(planet.planet_name):
|
|
lines.append("%s already has an extractor." % planet.planet_name)
|
|
can_build = false
|
|
else:
|
|
lines.append("Will mine %s." % planet.planet_name)
|
|
elif can_build and type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership():
|
|
lines.append("WARNING: only one may exist - this destroys your current one.")
|
|
|
|
if can_build and not GameState.can_afford(cost):
|
|
lines.append("Not enough rocks - needs %d, you have %d." % [cost, GameState.rocks])
|
|
can_build = false
|
|
|
|
info_title.text = info.title
|
|
info_message.text = "\n\n".join(lines)
|
|
info_cost.amount = cost
|
|
info_build_button.visible = can_build
|
|
pending_build_type = type if can_build else ""
|
|
info_popup.visible = true
|
|
|
|
|
|
func _close_info() -> void:
|
|
info_popup.visible = false
|
|
pending_build_type = ""
|
|
|
|
|
|
func _confirm_build() -> void:
|
|
var type: String = pending_build_type
|
|
if type == "":
|
|
return
|
|
pending_build_type = ""
|
|
info_popup.visible = false
|
|
|
|
# Everything is re-checked here rather than trusted from when the popup
|
|
# opened: the fleet keeps flying while it is up, so it can drift off the
|
|
# planet an extractor was going to mine.
|
|
if not GameState.is_building_unlocked(type):
|
|
return
|
|
|
|
if type == GameState.BUILDING_EXTRACTOR:
|
|
var planet: Node2D = _planet_under_fleet()
|
|
if planet == null or GameState.has_extractor_at(planet.planet_name):
|
|
build_status.text = "No planet in reach any more"
|
|
return
|
|
if not GameState.spend(GameState.EXTRACTOR_COST):
|
|
build_status.text = "Not enough rocks (need %d, have %d)" % [GameState.EXTRACTOR_COST, GameState.rocks]
|
|
return
|
|
GameState.add_extractor(planet.planet_name, planet.position)
|
|
build_menu.visible = false
|
|
return
|
|
|
|
if type == GameState.BUILDING_MOTHERSHIP and GameState.has_mothership():
|
|
if not GameState.can_afford(GameState.MOTHERSHIP_COST):
|
|
build_status.text = "Not enough rocks (need %d, have %d)" % [
|
|
GameState.MOTHERSHIP_COST, GameState.rocks
|
|
]
|
|
return
|
|
# Only now is the old one destroyed - never before the new one is
|
|
# known to be affordable, or the player would be left with none.
|
|
GameState.remove_buildings_of_type(GameState.BUILDING_MOTHERSHIP)
|
|
building_field.rebuild_all()
|
|
|
|
_try_place(type, cost_of(type))
|
|
|
|
|
|
func _try_place(type: String, cost: int) -> void:
|
|
if not GameState.spend(cost):
|
|
build_status.text = "Not enough rocks (need %d, have %d)" % [cost, GameState.rocks]
|
|
return
|
|
building_field.place(type, fleet.get_centroid())
|
|
build_menu.visible = false
|
|
|
|
|
|
# --- research (the mothership's skill tree) --------------------------------
|
|
|
|
func _on_skill_node_picked(id: String) -> void:
|
|
_refresh_skill_menu(id)
|
|
|
|
|
|
func _refresh_skill_menu(id: String = "") -> void:
|
|
if id == "":
|
|
id = skill_tree.selected_id
|
|
if id == "":
|
|
# Nothing chosen yet: point the player at the tree rather than
|
|
# leaving the panel blank, which reads as broken.
|
|
skill_detail.text = "Select a node to see what it unlocks."
|
|
skill_cost.amount = 0
|
|
skill_status.text = "Fleet capacity %d" % GameState.max_fleet_size()
|
|
skill_unlock_button.visible = false
|
|
return
|
|
|
|
var node: Dictionary = GameState.skill_node(id)
|
|
if node.is_empty():
|
|
return
|
|
skill_detail.text = "%s\n%s" % [node.name, node.desc]
|
|
skill_cost.amount = int(node.cost)
|
|
|
|
if GameState.is_unlocked(id):
|
|
skill_status.text = "Researched"
|
|
skill_unlock_button.visible = false
|
|
elif not GameState.prerequisite_met(id):
|
|
skill_status.text = "Needs %s first" % GameState.skill_node(node.requires).name
|
|
skill_unlock_button.visible = false
|
|
elif not GameState.can_afford(int(node.cost)):
|
|
skill_status.text = "Not enough rocks - you have %d" % GameState.rocks
|
|
skill_unlock_button.visible = false
|
|
else:
|
|
skill_status.text = ""
|
|
skill_unlock_button.visible = true
|
|
|
|
|
|
func _on_unlock_pressed() -> void:
|
|
var id: String = skill_tree.selected_id
|
|
if id == "":
|
|
return
|
|
if not GameState.unlock(id):
|
|
# Re-check rather than trust the button: rocks can be spent elsewhere
|
|
# while this menu is open, since haulers keep delivering.
|
|
_refresh_skill_menu(id)
|
|
return
|
|
# A capacity upgrade has to reach the fleet that is already flying, not
|
|
# just ones rebuilt later.
|
|
if int(GameState.skill_node(id).get("fleet_bonus", 0)) > 0:
|
|
fleet.apply_formation()
|
|
skill_tree.queue_redraw()
|
|
_refresh_skill_menu(id)
|
|
|
|
|
|
func _say(message: String) -> void:
|
|
craft_message = message
|
|
craft_message_until = Time.get_ticks_msec() + 3000
|
|
_refresh_craft_status()
|
|
|
|
|
|
func _refresh_craft_status() -> void:
|
|
# "Here" is the ore delivered to THIS crafter, not the bank: since #16 a
|
|
# building builds out of its own stock, and showing the bank would say
|
|
# you can afford something the crafter cannot actually make.
|
|
var queued: int = GameState.pending_orders(craft_building_id)
|
|
var stock: int = GameState.building_stock(craft_building_id)
|
|
var queue_text: String = "" if queued == 0 else " Queued %d" % queued
|
|
craft_status.text = "Interceptor %d Transporter %d Here %d%s Transporters %d/%d" % [
|
|
GameState.SHIP_COST, GameState.TRANSPORTER_COST, stock, queue_text,
|
|
GameState.transporters_claimed(), GameState.transport_capacity()
|
|
]
|
|
# 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_store_label.text = "Store ore here\nHold %d" % GameState.fleet_cargo
|
|
# Deliberately left ENABLED with an empty hold: a greyed-out button that
|
|
# cannot say why is exactly the dead-button problem this panel already
|
|
# had. Pressing it explains instead.
|
|
craft_store_button.enabled = true
|
|
craft_store_button.queue_redraw()
|
|
|
|
# A full fleet cannot take another vessel, so the order would be refused.
|
|
# Showing that on the button is the difference between "not available yet"
|
|
# and a button that looks live and silently does nothing when pressed.
|
|
var room: bool = fleet.ships.size() + GameState.ships_owed \
|
|
+ GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP) \
|
|
< GameState.max_fleet_size()
|
|
craft_interceptor_button.enabled = room
|
|
craft_interceptor_button.queue_redraw()
|
|
# One statement of the fleet count, either way round: the capacity line
|
|
# replaces the plain readout rather than being bolted in front of it.
|
|
if craft_message != "":
|
|
if Time.get_ticks_msec() < craft_message_until:
|
|
craft_status.text = "%s\n%s" % [craft_message, craft_status.text]
|
|
else:
|
|
craft_message = ""
|
|
if room:
|
|
craft_status.text += " Fleet %d/%d" % [fleet.ships.size(), GameState.max_fleet_size()]
|
|
else:
|
|
craft_status.text = "Fleet full %d/%d - research capacity at the mothership\n%s" % [
|
|
fleet.ships.size(), GameState.max_fleet_size(), craft_status.text
|
|
]
|
|
|
|
|
|
func _on_craft_interceptor() -> void:
|
|
if craft_building_id == 0:
|
|
return
|
|
# Still refused when there is nowhere to put the vessel - queueing an
|
|
# order that can never be delivered would just bank ships_owed forever.
|
|
if fleet.ships.size() + GameState.ships_owed \
|
|
+ GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP) \
|
|
>= GameState.max_fleet_size():
|
|
_say("Fleet is full (%d slots)" % GameState.max_fleet_size())
|
|
return
|
|
# No affordability check: an order the crafter cannot pay for yet is
|
|
# QUEUED, and completes by itself once ore is delivered here (#16).
|
|
GameState.queue_order(craft_building_id)
|
|
_refresh_craft_status()
|
|
|
|
|
|
func _refresh_load_ore() -> void:
|
|
load_ore_label.text = "Load ore\nBank %d Hold %d" % [GameState.rocks, GameState.fleet_cargo]
|
|
load_ore_button.enabled = GameState.rocks > 0
|
|
load_ore_button.queue_redraw()
|
|
|
|
|
|
func _on_load_ore() -> void:
|
|
GameState.withdraw_to_fleet()
|
|
_refresh_load_ore()
|
|
|
|
|
|
func _on_store_ore() -> void:
|
|
if craft_building_id == 0:
|
|
return
|
|
if GameState.fleet_cargo <= 0:
|
|
_say("The fleet is carrying nothing - load ore at the mothership first")
|
|
return
|
|
var moved: int = GameState.unload_into_building(craft_building_id)
|
|
_say("Stored %d ore here" % moved)
|
|
|
|
|
|
func _on_craft_transporter() -> void:
|
|
if craft_building_id == 0:
|
|
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()
|
|
|
|
|
|
func _on_formation_changed() -> void:
|
|
fleet.apply_formation()
|
|
|
|
|
|
func _on_reset_formation() -> void:
|
|
GameState.reset_formation()
|
|
fleet.apply_formation()
|
|
formation_editor.queue_redraw()
|