Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0a9f5b971 | ||
|
|
663648d949 |
35 changed files with 503 additions and 688 deletions
|
|
@ -6,10 +6,8 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
# Space-side building UI. Owns:
|
# Space-side building UI. Owns:
|
||||||
# - the build menu (place a mothership, ship crafter or command centre)
|
# - the build menu (place a mothership, ship crafter or command centre)
|
||||||
# - the shared ACTION button, which adapts to whatever the fleet is
|
# - the shared ACTION button, which adapts to whatever the fleet is
|
||||||
# standing on (craft ships at a crafter, configure formation at a
|
# standing on (craft ships, configure formation, or land). One action
|
||||||
# command centre). The enter-atmosphere prompt uses the same slot, and
|
# fires directly; multiple actions unfold horizontally.
|
||||||
# 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
|
# - rebuilding a wiped-out fleet at the mothership
|
||||||
#
|
#
|
||||||
# Buildings themselves live in GameState (they must outlive the trip to a
|
# Buildings themselves live in GameState (they must outlive the trip to a
|
||||||
|
|
@ -23,6 +21,8 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
@onready var build_button: IconButton = get_node("../UI/BuildButton")
|
@onready var build_button: IconButton = get_node("../UI/BuildButton")
|
||||||
@onready var action_button: IconButton = get_node("../UI/BuildActionButton")
|
@onready var action_button: IconButton = get_node("../UI/BuildActionButton")
|
||||||
@onready var building_action_button: IconButton = get_node("../UI/BuildingActionButton")
|
@onready var building_action_button: IconButton = get_node("../UI/BuildingActionButton")
|
||||||
|
@onready var landing_button: IconButton = get_node("../UI/EnterAtmosphereButton")
|
||||||
|
@onready var landing_controller: Node2D = get_node("../LandingController")
|
||||||
|
|
||||||
@onready var build_menu: Control = get_node("../UI/BuildMenu")
|
@onready var build_menu: Control = get_node("../UI/BuildMenu")
|
||||||
@onready var build_close_button: IconButton = get_node("../UI/BuildMenu/CloseButton")
|
@onready var build_close_button: IconButton = get_node("../UI/BuildMenu/CloseButton")
|
||||||
|
|
@ -62,22 +62,18 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
@onready var hangar_available_slots: Array = [
|
@onready var hangar_available_slots: Array = [
|
||||||
get_node("../UI/HangarMenu/AvailableInterceptor"),
|
get_node("../UI/HangarMenu/AvailableInterceptor"),
|
||||||
get_node("../UI/HangarMenu/AvailableShield"),
|
get_node("../UI/HangarMenu/AvailableShield"),
|
||||||
get_node("../UI/HangarMenu/AvailableTransporter"),
|
|
||||||
]
|
]
|
||||||
@onready var hangar_available_labels: Array = [
|
@onready var hangar_available_labels: Array = [
|
||||||
get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
|
get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
|
||||||
get_node("../UI/HangarMenu/AvailableShieldLabel"),
|
get_node("../UI/HangarMenu/AvailableShieldLabel"),
|
||||||
get_node("../UI/HangarMenu/AvailableTransporterLabel"),
|
|
||||||
]
|
]
|
||||||
@onready var hangar_fleet_slots: Array = [
|
@onready var hangar_fleet_slots: Array = [
|
||||||
get_node("../UI/HangarMenu/FleetInterceptor"),
|
get_node("../UI/HangarMenu/FleetInterceptor"),
|
||||||
get_node("../UI/HangarMenu/FleetShield"),
|
get_node("../UI/HangarMenu/FleetShield"),
|
||||||
get_node("../UI/HangarMenu/FleetTransporter"),
|
|
||||||
]
|
]
|
||||||
@onready var hangar_fleet_labels: Array = [
|
@onready var hangar_fleet_labels: Array = [
|
||||||
get_node("../UI/HangarMenu/FleetInterceptorLabel"),
|
get_node("../UI/HangarMenu/FleetInterceptorLabel"),
|
||||||
get_node("../UI/HangarMenu/FleetShieldLabel"),
|
get_node("../UI/HangarMenu/FleetShieldLabel"),
|
||||||
get_node("../UI/HangarMenu/FleetTransporterLabel"),
|
|
||||||
]
|
]
|
||||||
var build_menu_entries: Array = []
|
var build_menu_entries: Array = []
|
||||||
|
|
||||||
|
|
@ -97,24 +93,23 @@ var lab_building_id: int = 0
|
||||||
|
|
||||||
@onready var craft_menu: Control = get_node("../UI/CraftMenu")
|
@onready var craft_menu: Control = get_node("../UI/CraftMenu")
|
||||||
@onready var craft_interceptor_button: IconButton = get_node("../UI/CraftMenu/InterceptorButton")
|
@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_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton")
|
@onready var craft_close_button: IconButton = get_node("../UI/CraftMenu/CloseButton")
|
||||||
@onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel")
|
@onready var craft_status: Label = get_node("../UI/CraftMenu/StatusLabel")
|
||||||
@onready var craft_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel")
|
@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_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel")
|
||||||
@onready var craft_vessel_buttons: Array = [
|
@onready var craft_vessel_buttons: Array = [
|
||||||
craft_interceptor_button, craft_shield_button, craft_transporter_button,
|
craft_interceptor_button, craft_shield_button,
|
||||||
]
|
]
|
||||||
@onready var craft_vessel_labels: Array = [
|
@onready var craft_vessel_labels: Array = [
|
||||||
craft_interceptor_label, craft_shield_label, craft_transporter_label,
|
craft_interceptor_label, craft_shield_label,
|
||||||
]
|
]
|
||||||
|
|
||||||
# --- the transfer panel (#24) ---------------------------------------------
|
# --- the transfer panel (#24) ---------------------------------------------
|
||||||
# Ore used to be moved from inside whichever panel happened to be open: "load
|
# Ore used to be moved from inside whichever panel happened to be open: "load
|
||||||
# ore" lived in the research screen, "store ore here" in the ship crafter.
|
# ore" lived in the research screen, "store ore here" in the ship crafter.
|
||||||
# Both are now one top-level button that works at every building and in both
|
# Both are now one top-level shortcut beside Build at resource-bearing
|
||||||
# directions, so moving ore is never buried under an unrelated menu.
|
# locations and work in both directions, so moving cargo is never buried
|
||||||
|
# under an unrelated action foldout.
|
||||||
@onready var transfer_button: IconButton = get_node("../UI/TransferButton")
|
@onready var transfer_button: IconButton = get_node("../UI/TransferButton")
|
||||||
@onready var transfer_menu: Control = get_node("../UI/TransferMenu")
|
@onready var transfer_menu: Control = get_node("../UI/TransferMenu")
|
||||||
@onready var transfer_status: Label = get_node("../UI/TransferMenu/StatusLabel")
|
@onready var transfer_status: Label = get_node("../UI/TransferMenu/StatusLabel")
|
||||||
|
|
@ -165,6 +160,7 @@ var current_building: Node2D = null
|
||||||
var pending_building: Node2D = null
|
var pending_building: Node2D = null
|
||||||
var current_extractor_index: int = -1
|
var current_extractor_index: int = -1
|
||||||
var pending_extractor_index: int = -1
|
var pending_extractor_index: int = -1
|
||||||
|
var current_landing_planet: Node2D = null
|
||||||
var pending_time: float = 0.0
|
var pending_time: float = 0.0
|
||||||
var context_actions_expanded: bool = false
|
var context_actions_expanded: bool = false
|
||||||
|
|
||||||
|
|
@ -185,8 +181,8 @@ func _ready() -> void:
|
||||||
action_button.pressed.connect(_on_context_action_pressed)
|
action_button.pressed.connect(_on_context_action_pressed)
|
||||||
building_action_button.pressed.connect(_on_action_pressed)
|
building_action_button.pressed.connect(_on_action_pressed)
|
||||||
|
|
||||||
for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.all().size())):
|
for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.fleet().size())):
|
||||||
var definition: Resource = VESSEL_DEFINITIONS.all()[i]
|
var definition: Resource = VESSEL_DEFINITIONS.fleet()[i]
|
||||||
craft_vessel_buttons[i].pressed.connect(_on_craft_vessel.bind(definition.definition_id))
|
craft_vessel_buttons[i].pressed.connect(_on_craft_vessel.bind(definition.definition_id))
|
||||||
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
||||||
|
|
||||||
|
|
@ -218,6 +214,7 @@ func _ready() -> void:
|
||||||
transfer_menu.visible = false
|
transfer_menu.visible = false
|
||||||
action_button.visible = false
|
action_button.visible = false
|
||||||
building_action_button.visible = false
|
building_action_button.visible = false
|
||||||
|
landing_button.visible = false
|
||||||
transfer_button.visible = false
|
transfer_button.visible = false
|
||||||
hangar_button.visible = false
|
hangar_button.visible = false
|
||||||
hangar_menu.visible = false
|
hangar_menu.visible = false
|
||||||
|
|
@ -239,10 +236,10 @@ func _configure_build_menu_entries() -> void:
|
||||||
build_menu_entries.append([definition.definition_id, button, label, cost_number, definition])
|
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
|
# True while a modal panel owns the shared context controls. A plain building
|
||||||
# enter-atmosphere prompt knows to stay out of the way.
|
# prompt is not modal: landing can coexist with its actions in the same foldout.
|
||||||
func is_showing_building_prompt() -> bool:
|
func is_showing_building_prompt() -> bool:
|
||||||
return current_building != null or build_menu.visible or craft_menu.visible \
|
return build_menu.visible or craft_menu.visible \
|
||||||
or formation_menu.visible or skill_menu.visible or transfer_menu.visible \
|
or formation_menu.visible or skill_menu.visible or transfer_menu.visible \
|
||||||
or hangar_menu.visible
|
or hangar_menu.visible
|
||||||
|
|
||||||
|
|
@ -309,8 +306,10 @@ func _process(delta: float) -> void:
|
||||||
pending_building = null
|
pending_building = null
|
||||||
current_extractor_index = -1
|
current_extractor_index = -1
|
||||||
pending_extractor_index = -1
|
pending_extractor_index = -1
|
||||||
|
current_landing_planet = null
|
||||||
action_button.visible = false
|
action_button.visible = false
|
||||||
building_action_button.visible = false
|
building_action_button.visible = false
|
||||||
|
landing_button.visible = false
|
||||||
transfer_button.visible = false
|
transfer_button.visible = false
|
||||||
hangar_button.visible = false
|
hangar_button.visible = false
|
||||||
context_actions_expanded = false
|
context_actions_expanded = false
|
||||||
|
|
@ -332,28 +331,27 @@ func _process(delta: float) -> void:
|
||||||
else:
|
else:
|
||||||
pending_time += delta
|
pending_time += delta
|
||||||
|
|
||||||
|
var detected_landing_planet: Node2D = landing_controller.current_planet
|
||||||
if pending_time < prompt_debounce_time or (pending_building == current_building \
|
if pending_time < prompt_debounce_time or (pending_building == current_building \
|
||||||
and pending_extractor_index == current_extractor_index):
|
and pending_extractor_index == current_extractor_index \
|
||||||
|
and detected_landing_planet == current_landing_planet):
|
||||||
return
|
return
|
||||||
|
|
||||||
current_building = pending_building
|
current_building = pending_building
|
||||||
current_extractor_index = pending_extractor_index
|
current_extractor_index = pending_extractor_index
|
||||||
|
current_landing_planet = detected_landing_planet
|
||||||
context_actions_expanded = false
|
context_actions_expanded = false
|
||||||
hangar_button.visible = false
|
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.
|
|
||||||
transfer_button.visible = false
|
|
||||||
building_action_button.visible = false
|
building_action_button.visible = false
|
||||||
action_button.visible = current_building != null or current_extractor_index >= 0
|
landing_button.visible = false
|
||||||
|
transfer_button.visible = _has_transfer_context()
|
||||||
|
var actions: Array = _context_action_buttons()
|
||||||
|
action_button.visible = not actions.is_empty()
|
||||||
if current_building != null:
|
if current_building != null:
|
||||||
var actions: Array = _context_action_buttons()
|
|
||||||
building_action_button.icon = int(current_building.definition.action_icon)
|
building_action_button.icon = int(current_building.definition.action_icon)
|
||||||
building_action_button.queue_redraw()
|
building_action_button.queue_redraw()
|
||||||
action_button.icon = building_action_button.icon if actions.size() == 1 else IconButton.Icon.ACTION_MENU
|
if not actions.is_empty():
|
||||||
action_button.queue_redraw()
|
action_button.icon = int(actions[0].icon) if actions.size() == 1 else IconButton.Icon.ACTION_MENU
|
||||||
elif current_extractor_index >= 0:
|
|
||||||
action_button.icon = IconButton.Icon.TRANSFER
|
|
||||||
action_button.queue_redraw()
|
action_button.queue_redraw()
|
||||||
_layout_context_buttons()
|
_layout_context_buttons()
|
||||||
|
|
||||||
|
|
@ -381,33 +379,41 @@ func _layout_context_buttons() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _context_action_buttons() -> Array:
|
func _context_action_buttons() -> Array:
|
||||||
if current_extractor_index >= 0:
|
|
||||||
return [transfer_button]
|
|
||||||
if current_building == null:
|
|
||||||
return []
|
|
||||||
var buttons: Array = []
|
var buttons: Array = []
|
||||||
for action in current_building.available_actions():
|
if current_building != null:
|
||||||
var button: IconButton = _button_for_building_action(action)
|
for action in current_building.available_actions():
|
||||||
if button != null and button not in buttons:
|
var button: IconButton = _button_for_building_action(action)
|
||||||
buttons.append(button)
|
if button != null and button not in buttons:
|
||||||
|
buttons.append(button)
|
||||||
|
if current_landing_planet != null:
|
||||||
|
buttons.append(landing_button)
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
|
func _has_transfer_context() -> bool:
|
||||||
|
if current_extractor_index >= 0:
|
||||||
|
return true
|
||||||
|
return current_building != null and current_building.available_actions().has("transfer")
|
||||||
|
|
||||||
|
|
||||||
func _button_for_building_action(action: String) -> IconButton:
|
func _button_for_building_action(action: String) -> IconButton:
|
||||||
if action == "transfer":
|
if action == "transfer":
|
||||||
return transfer_button
|
return null
|
||||||
if action == "hangar":
|
if action == "hangar":
|
||||||
return hangar_button
|
return hangar_button
|
||||||
return building_action_button
|
return building_action_button
|
||||||
|
|
||||||
|
|
||||||
func _on_context_action_pressed() -> void:
|
func _on_context_action_pressed() -> void:
|
||||||
if current_building == null and current_extractor_index < 0:
|
if current_building == null and current_extractor_index < 0 \
|
||||||
|
and current_landing_planet == null:
|
||||||
return
|
return
|
||||||
var actions: Array = _context_action_buttons()
|
var actions: Array = _context_action_buttons()
|
||||||
if actions.size() == 1:
|
if actions.size() == 1:
|
||||||
if actions[0] == transfer_button:
|
if actions[0] == landing_button:
|
||||||
_on_transfer_pressed()
|
landing_controller._on_enter_pressed()
|
||||||
|
elif actions[0] == hangar_button:
|
||||||
|
_on_hangar_pressed()
|
||||||
else:
|
else:
|
||||||
_on_action_pressed()
|
_on_action_pressed()
|
||||||
return
|
return
|
||||||
|
|
@ -722,7 +728,7 @@ func _refresh_craft_status() -> void:
|
||||||
# intentionally reserved for the shared resource bar and a short action
|
# intentionally reserved for the shared resource bar and a short action
|
||||||
# message; repeating fleet, queue and transport counts here buried the UI.
|
# message; repeating fleet, queue and transport counts here buried the UI.
|
||||||
craft_status.text = ""
|
craft_status.text = ""
|
||||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
var definitions: Array = VESSEL_DEFINITIONS.fleet()
|
||||||
for i in range(mini(definitions.size(), craft_vessel_buttons.size())):
|
for i in range(mini(definitions.size(), craft_vessel_buttons.size())):
|
||||||
var definition: Resource = definitions[i]
|
var definition: Resource = definitions[i]
|
||||||
var button: IconButton = craft_vessel_buttons[i]
|
var button: IconButton = craft_vessel_buttons[i]
|
||||||
|
|
@ -730,9 +736,6 @@ func _refresh_craft_status() -> void:
|
||||||
button.icon = definition.icon
|
button.icon = definition.icon
|
||||||
button.glow_color = definition.glow_color
|
button.glow_color = definition.glow_color
|
||||||
button.enabled = GameState.vessel_is_unlocked(definition.definition_id)
|
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()
|
button.queue_redraw()
|
||||||
_refresh_queue()
|
_refresh_queue()
|
||||||
if craft_message != "":
|
if craft_message != "":
|
||||||
|
|
@ -756,11 +759,6 @@ func _on_craft_vessel(vessel: String) -> void:
|
||||||
if not GameState.queue_order(craft_building_id, vessel):
|
if not GameState.queue_order(craft_building_id, vessel):
|
||||||
if not GameState.vessel_is_unlocked(vessel):
|
if not GameState.vessel_is_unlocked(vessel):
|
||||||
_say("%s must be unlocked at a ship lab first" % definition.display_name)
|
_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
|
return
|
||||||
_report_order(vessel)
|
_report_order(vessel)
|
||||||
|
|
||||||
|
|
@ -775,10 +773,6 @@ func _report_order(product: String) -> void:
|
||||||
_say("%s ordered - building now" % GameState.vessel_name(product))
|
_say("%s ordered - building now" % GameState.vessel_name(product))
|
||||||
|
|
||||||
|
|
||||||
func _on_craft_transporter() -> void:
|
|
||||||
_on_craft_vessel(GameState.VESSEL_TRANSPORTER)
|
|
||||||
|
|
||||||
|
|
||||||
# --- ship lab (#5) --------------------------------------------------------
|
# --- ship lab (#5) --------------------------------------------------------
|
||||||
|
|
||||||
func _lab_options() -> Array:
|
func _lab_options() -> Array:
|
||||||
|
|
@ -1063,7 +1057,7 @@ func _on_hangar_store_type(index: int) -> void:
|
||||||
|
|
||||||
func _hangar_types() -> Array:
|
func _hangar_types() -> Array:
|
||||||
var types: Array = []
|
var types: Array = []
|
||||||
for definition in VESSEL_DEFINITIONS.all():
|
for definition in VESSEL_DEFINITIONS.fleet():
|
||||||
types.append(definition.definition_id)
|
types.append(definition.definition_id)
|
||||||
return types
|
return types
|
||||||
|
|
||||||
|
|
|
||||||
34
Fleet.gd
34
Fleet.gd
|
|
@ -4,10 +4,9 @@ extends Node2D
|
||||||
# centre) rather than being fixed here, so a layout the player arranged
|
# centre) rather than being fixed here, so a layout the player arranged
|
||||||
# survives scene swaps and fleet rebuilds. Ships are always
|
# survives scene swaps and fleet rebuilds. Ships are always
|
||||||
# spawned programmatically (spawn_ships), using however many of these
|
# spawned programmatically (spawn_ships), using however many of these
|
||||||
# offsets are needed - both for a fresh start (5 ships) and for restoring
|
# offsets are needed. The optional vessel-type list preserves the exact
|
||||||
# a possibly-smaller surviving fleet after returning from a planet's
|
# mixed fleet through scene swaps; a fresh start still defaults each slot
|
||||||
# surface, so both cases share one code path instead of two that could
|
# to an interceptor.
|
||||||
# drift out of sync.
|
|
||||||
# The fleet's cruise speed. This is the speed of the FLEET as a whole: the
|
# The fleet's cruise speed. This is the speed of the FLEET as a whole: the
|
||||||
# anchor below advances at exactly this rate, and individual ships are
|
# anchor below advances at exactly this rate, and individual ships are
|
||||||
# allowed to fly faster than it (Ship.max_speed) so they can close on their
|
# allowed to fly faster than it (Ship.max_speed) so they can close on their
|
||||||
|
|
@ -44,17 +43,29 @@ func _on_vessel_upgraded(_id: String) -> void:
|
||||||
ship.apply_vessel_stats()
|
ship.apply_vessel_stats()
|
||||||
|
|
||||||
|
|
||||||
func spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void:
|
func spawn_ships(count: int, center: Vector2 = Vector2.ZERO,
|
||||||
|
vessel_types: PackedStringArray = PackedStringArray()) -> void:
|
||||||
for ship in ships:
|
for ship in ships:
|
||||||
if is_instance_valid(ship):
|
if is_instance_valid(ship):
|
||||||
ship.queue_free()
|
ship.queue_free()
|
||||||
ships.clear()
|
ships.clear()
|
||||||
formation_offsets.clear()
|
formation_offsets.clear()
|
||||||
|
|
||||||
var n: int = clamp(count, 1, GameState.max_fleet_size())
|
var spawn_types: PackedStringArray = PackedStringArray()
|
||||||
|
if vessel_types.is_empty():
|
||||||
|
for i in range(count):
|
||||||
|
spawn_types.append(GameState.VESSEL_INTERCEPTOR)
|
||||||
|
else:
|
||||||
|
for vessel_type in vessel_types:
|
||||||
|
if GameState.vessel_is_fleet_eligible(vessel_type):
|
||||||
|
spawn_types.append(vessel_type)
|
||||||
|
if spawn_types.is_empty():
|
||||||
|
spawn_types.append(GameState.VESSEL_INTERCEPTOR)
|
||||||
|
var n: int = clamp(spawn_types.size(), 1, GameState.max_fleet_size())
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
var offset: Vector2 = GameState.formation[i]
|
var offset: Vector2 = GameState.formation[i]
|
||||||
var ship: Area2D = ship_scene.instantiate()
|
var ship: Area2D = ship_scene.instantiate()
|
||||||
|
ship.vessel_type = spawn_types[i]
|
||||||
ship.position = center + offset
|
ship.position = center + offset
|
||||||
ship.glow_color = ship_glow_color
|
ship.glow_color = ship_glow_color
|
||||||
ship.core_color = ship_core_color
|
ship.core_color = ship_core_color
|
||||||
|
|
@ -67,10 +78,19 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void:
|
||||||
fleet_target = center
|
fleet_target = center
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_types() -> PackedStringArray:
|
||||||
|
var result: PackedStringArray = PackedStringArray()
|
||||||
|
for ship in ships:
|
||||||
|
if is_instance_valid(ship):
|
||||||
|
result.append(String(ship.vessel_type))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
|
func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
|
||||||
# Adds one vessel in the next free formation slot (bought from a ship
|
# Adds one vessel in the next free formation slot (bought from a ship
|
||||||
# crafter). Returns false when the wedge is already full.
|
# crafter). Returns false when the wedge is already full.
|
||||||
if ships.size() >= GameState.max_fleet_size():
|
if not GameState.vessel_is_fleet_eligible(vessel_type) \
|
||||||
|
or ships.size() >= GameState.max_fleet_size():
|
||||||
return false
|
return false
|
||||||
var offset: Vector2 = GameState.formation[ships.size()]
|
var offset: Vector2 = GameState.formation[ships.size()]
|
||||||
var ship: Area2D = ship_scene.instantiate()
|
var ship: Area2D = ship_scene.instantiate()
|
||||||
|
|
|
||||||
148
GameState.gd
148
GameState.gd
|
|
@ -14,6 +14,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
|
|
||||||
var has_return_position: bool = false
|
var has_return_position: bool = false
|
||||||
var surviving_ship_count: int = 5
|
var surviving_ship_count: int = 5
|
||||||
|
var surviving_vessel_types: PackedStringArray = PackedStringArray()
|
||||||
var return_world_position: Vector2 = Vector2.ZERO
|
var return_world_position: Vector2 = Vector2.ZERO
|
||||||
var return_camera_zoom: float = 1.0
|
var return_camera_zoom: float = 1.0
|
||||||
|
|
||||||
|
|
@ -229,7 +230,7 @@ const SKILL_TREE: Array = [
|
||||||
{
|
{
|
||||||
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
||||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
||||||
"desc": "Build transport hubs, which supply your other buildings with ore.",
|
"desc": "Build transport hubs. Each hub includes its own automatic transporters.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
|
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
|
||||||
|
|
@ -312,6 +313,8 @@ func unlock(id: String) -> bool:
|
||||||
if not spend(int(skill_node(id).cost)):
|
if not spend(int(skill_node(id).cost)):
|
||||||
return false
|
return false
|
||||||
unlocked.append(id)
|
unlocked.append(id)
|
||||||
|
if int(skill_node(id).get("hub_bonus", 0)) > 0:
|
||||||
|
_sync_hub_transporters()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -380,6 +383,9 @@ func add_building(type: String, at_position: Vector2) -> int:
|
||||||
"id": id, "type": type, "position": at_position,
|
"id": id, "type": type, "position": at_position,
|
||||||
"inventory": INVENTORY.new(), "orders": [],
|
"inventory": INVENTORY.new(), "orders": [],
|
||||||
})
|
})
|
||||||
|
var building: Dictionary = buildings[-1]
|
||||||
|
if building_has_capability(building, "transport_hub"):
|
||||||
|
_provision_hub_transporters(building)
|
||||||
return id
|
return id
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -399,9 +405,9 @@ func building_has_capability(building: Dictionary, capability: String) -> bool:
|
||||||
|
|
||||||
# --- vessel types and their upgrades (#5) ---------------------------------
|
# --- vessel types and their upgrades (#5) ---------------------------------
|
||||||
|
|
||||||
# The three things that can be flying around. The first and last double as
|
# Combat vessels can join the player fleet. Transporter metadata lives in the
|
||||||
# production order ids, so a vessel type and the order that builds it are the
|
# same definition catalog so the lab can upgrade its cargo and travel stats,
|
||||||
# same string rather than two that have to be kept in step.
|
# but transporters themselves belong to hubs and are never production orders.
|
||||||
const VESSEL_INTERCEPTOR: String = "interceptor"
|
const VESSEL_INTERCEPTOR: String = "interceptor"
|
||||||
const VESSEL_SHIELD: String = "shield"
|
const VESSEL_SHIELD: String = "shield"
|
||||||
const VESSEL_TRANSPORTER: String = "transporter"
|
const VESSEL_TRANSPORTER: String = "transporter"
|
||||||
|
|
@ -409,7 +415,6 @@ const VESSEL_TRANSPORTER: String = "transporter"
|
||||||
# Compatibility constants for callers and old tests. Runtime metadata is
|
# Compatibility constants for callers and old tests. Runtime metadata is
|
||||||
# resolved from VesselDefinition below.
|
# resolved from VesselDefinition below.
|
||||||
const SHIELD_COST: int = 14
|
const SHIELD_COST: int = 14
|
||||||
const TRANSPORTER_COST: int = 10
|
|
||||||
const LAB_RESEARCH_TIME: float = 8.0
|
const LAB_RESEARCH_TIME: float = 8.0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -435,9 +440,6 @@ const SHIP_BASE_SPEED: float = 460.0
|
||||||
const SHIP_BASE_ACCEL: float = 450.0
|
const SHIP_BASE_ACCEL: float = 450.0
|
||||||
const SHIELD_BASE_CHARGES: int = 1
|
const SHIELD_BASE_CHARGES: int = 1
|
||||||
const SHIELD_BASE_RECHARGE: float = 6.0
|
const SHIELD_BASE_RECHARGE: float = 6.0
|
||||||
const TRANSPORTER_BASE_LOAD: int = 20
|
|
||||||
const TRANSPORTER_BASE_SPEED: float = 150.0
|
|
||||||
|
|
||||||
var vessel_levels: Dictionary = {}
|
var vessel_levels: Dictionary = {}
|
||||||
|
|
||||||
signal vessel_upgraded(upgrade_id: String)
|
signal vessel_upgraded(upgrade_id: String)
|
||||||
|
|
@ -476,6 +478,11 @@ func vessel_has_capability(vessel: String, capability: String) -> bool:
|
||||||
return definition != null and definition.has_capability(capability)
|
return definition != null and definition.has_capability(capability)
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_is_fleet_eligible(vessel: String) -> bool:
|
||||||
|
var definition: Resource = vessel_definition(vessel)
|
||||||
|
return definition != null and definition.fleet_eligible
|
||||||
|
|
||||||
|
|
||||||
func vessel_is_unlocked(vessel: String) -> bool:
|
func vessel_is_unlocked(vessel: String) -> bool:
|
||||||
var definition: Resource = vessel_definition(vessel)
|
var definition: Resource = vessel_definition(vessel)
|
||||||
if definition == null:
|
if definition == null:
|
||||||
|
|
@ -594,7 +601,6 @@ func transporter_speed() -> float:
|
||||||
|
|
||||||
|
|
||||||
const PRODUCT_SHIP: String = "interceptor"
|
const PRODUCT_SHIP: String = "interceptor"
|
||||||
const PRODUCT_TRANSPORTER: String = "transporter"
|
|
||||||
|
|
||||||
|
|
||||||
func product_cost(product: String) -> int:
|
func product_cost(product: String) -> int:
|
||||||
|
|
@ -690,14 +696,8 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
|
||||||
if b.is_empty() or not can_produce(b.type):
|
if b.is_empty() or not can_produce(b.type):
|
||||||
return false
|
return false
|
||||||
var definition: Resource = vessel_definition(product)
|
var definition: Resource = vessel_definition(product)
|
||||||
if definition == null or not vessel_is_unlocked(product):
|
if definition == null or not definition.fleet_eligible \
|
||||||
return false
|
or not vessel_is_unlocked(product):
|
||||||
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 definition.has_capability("transport") and not can_station_transporter():
|
|
||||||
# Every berth is taken or spoken for (#20).
|
|
||||||
return false
|
return false
|
||||||
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
|
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
|
||||||
return true
|
return true
|
||||||
|
|
@ -724,12 +724,6 @@ func _advance_production(delta: float) -> void:
|
||||||
if not bool(order.started):
|
if not bool(order.started):
|
||||||
if inventory.amount(RESOURCE_ORE) < cost:
|
if inventory.amount(RESOURCE_ORE) < cost:
|
||||||
continue
|
continue
|
||||||
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 definition.has_capability("transport") and nearest_hub_to(b.position).is_empty():
|
|
||||||
continue
|
|
||||||
inventory.withdraw(RESOURCE_ORE, cost)
|
inventory.withdraw(RESOURCE_ORE, cost)
|
||||||
order.started = true
|
order.started = true
|
||||||
|
|
||||||
|
|
@ -738,20 +732,15 @@ func _advance_production(delta: float) -> void:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
orders.pop_front()
|
orders.pop_front()
|
||||||
if definition.has_capability("transport"):
|
# A finished combat hull launches from the crafter and physically flies
|
||||||
# Transporters are unchanged: they take themselves to the nearest
|
# to the mothership hangar. Hub transporters are provisioned by hubs and
|
||||||
# hub and wait for work (#19).
|
# never enter this production path.
|
||||||
_station_transporter(b.position)
|
var mothership: Variant = get_mothership_position()
|
||||||
|
if mothership == null:
|
||||||
|
stored_vessels.append(product)
|
||||||
else:
|
else:
|
||||||
# A finished hull launches from the crafter and physically flies to
|
vessel_deliveries.append({"type": product, "from": b.position,
|
||||||
# the mothership hangar. With no mothership there is nowhere to fly,
|
"to": mothership, "progress": 0.0})
|
||||||
# so preserve the old safe fallback of holding it immediately.
|
|
||||||
var mothership: Variant = get_mothership_position()
|
|
||||||
if mothership == null:
|
|
||||||
stored_vessels.append(product)
|
|
||||||
else:
|
|
||||||
vessel_deliveries.append({"type": product, "from": b.position,
|
|
||||||
"to": mothership, "progress": 0.0})
|
|
||||||
order_completed.emit(int(b.id), product)
|
order_completed.emit(int(b.id), product)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -837,13 +826,21 @@ func has_building_of_type(type: String) -> bool:
|
||||||
|
|
||||||
func remove_buildings_of_type(type: String) -> int:
|
func remove_buildings_of_type(type: String) -> int:
|
||||||
var kept: Array = []
|
var kept: Array = []
|
||||||
|
var removed_ids: Array = []
|
||||||
var removed: int = 0
|
var removed: int = 0
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
if b.type == type:
|
if b.type == type:
|
||||||
removed += 1
|
removed += 1
|
||||||
|
removed_ids.append(int(b.id))
|
||||||
else:
|
else:
|
||||||
kept.append(b)
|
kept.append(b)
|
||||||
buildings = kept
|
buildings = kept
|
||||||
|
if type == BUILDING_TRANSPORT_HUB and not removed_ids.is_empty():
|
||||||
|
var kept_transporters: Array = []
|
||||||
|
for transporter in transporters:
|
||||||
|
if int(transporter.get("hub_id", 0)) not in removed_ids:
|
||||||
|
kept_transporters.append(transporter)
|
||||||
|
transporters = kept_transporters
|
||||||
return removed
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1016,11 +1013,8 @@ func _advance_vessel_deliveries(delta: float) -> void:
|
||||||
#
|
#
|
||||||
# Entries are {"hub_id", "state", "target_id", "carrying", "progress",
|
# Entries are {"hub_id", "state", "target_id", "carrying", "progress",
|
||||||
# "from", "to"}.
|
# "from", "to"}.
|
||||||
const TRANSPORTER_SPEED: float = 340.0
|
# How many transporters each hub owns. Raised for EVERY hub - built already
|
||||||
|
# or not - by research, and provisioned automatically rather than crafted.
|
||||||
# How many transporters one hub can station. Raised for EVERY hub - built
|
|
||||||
# already or not - by research, which is the same shape as max_fleet_size()
|
|
||||||
# rather than a second mechanism doing the same job.
|
|
||||||
const BASE_HUB_CAPACITY: int = 2
|
const BASE_HUB_CAPACITY: int = 2
|
||||||
|
|
||||||
const TRANSPORTER_IDLE: String = "idle"
|
const TRANSPORTER_IDLE: String = "idle"
|
||||||
|
|
@ -1049,61 +1043,30 @@ func hub_count() -> int:
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
func transport_capacity() -> int:
|
func _add_hub_transporter(hub: Dictionary) -> void:
|
||||||
# Total berths: more hubs, or more capacity per hub. Either raises it,
|
|
||||||
# which is exactly the choice #12 asks the player to make.
|
|
||||||
return hub_count() * hub_capacity()
|
|
||||||
|
|
||||||
|
|
||||||
func pending_transporter_orders() -> int:
|
|
||||||
var n: int = 0
|
|
||||||
for b in buildings:
|
|
||||||
for o in b.get("orders", []):
|
|
||||||
if vessel_has_capability(String(o.product), "transport"):
|
|
||||||
n += 1
|
|
||||||
return n
|
|
||||||
|
|
||||||
|
|
||||||
func transporters_claimed() -> int:
|
|
||||||
# Built plus already ordered: an order in the queue has a berth reserved,
|
|
||||||
# or a player could queue ten of them against two berths and only find out
|
|
||||||
# when they refused to appear.
|
|
||||||
return transporters.size() + pending_transporter_orders()
|
|
||||||
|
|
||||||
|
|
||||||
func can_station_transporter() -> bool:
|
|
||||||
return transporters_claimed() < transport_capacity()
|
|
||||||
|
|
||||||
|
|
||||||
func has_transport_hub() -> bool:
|
|
||||||
return not nearest_hub_to(Vector2.ZERO).is_empty()
|
|
||||||
|
|
||||||
|
|
||||||
func nearest_hub_to(at: Vector2) -> Dictionary:
|
|
||||||
var best: Dictionary = {}
|
|
||||||
var best_d: float = INF
|
|
||||||
for b in buildings:
|
|
||||||
if not building_has_capability(b, "transport_hub"):
|
|
||||||
continue
|
|
||||||
var d: float = at.distance_squared_to(b.position)
|
|
||||||
if d < best_d:
|
|
||||||
best_d = d
|
|
||||||
best = b
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
func _station_transporter(built_at: Vector2) -> void:
|
|
||||||
# Built at the crafter, then it flies itself to the nearest hub and waits
|
|
||||||
# there for orders (#12).
|
|
||||||
var hub: Dictionary = nearest_hub_to(built_at)
|
|
||||||
if hub.is_empty():
|
|
||||||
return
|
|
||||||
transporters.append({
|
transporters.append({
|
||||||
"hub_id": int(hub.id), "state": TRANSPORTER_RETURNING, "target_id": 0,
|
"hub_id": int(hub.id), "state": TRANSPORTER_IDLE, "target_id": 0,
|
||||||
"carrying": 0, "resource_type": "ore", "progress": 0.0, "from": built_at, "to": hub.position,
|
"extractor_index": -1, "carrying": 0, "resource_type": RESOURCE_ORE,
|
||||||
|
"progress": 1.0, "from": hub.position, "to": hub.position,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _provision_hub_transporters(hub: Dictionary) -> void:
|
||||||
|
var stationed: int = 0
|
||||||
|
for transporter in transporters:
|
||||||
|
if int(transporter.get("hub_id", 0)) == int(hub.id):
|
||||||
|
stationed += 1
|
||||||
|
while stationed < hub_capacity():
|
||||||
|
_add_hub_transporter(hub)
|
||||||
|
stationed += 1
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_hub_transporters() -> void:
|
||||||
|
for hub in buildings:
|
||||||
|
if building_has_capability(hub, "transport_hub"):
|
||||||
|
_provision_hub_transporters(hub)
|
||||||
|
|
||||||
|
|
||||||
func transporter_position(t: Dictionary) -> Vector2:
|
func transporter_position(t: Dictionary) -> Vector2:
|
||||||
return (t.from as Vector2).lerp(t.to as Vector2, clampf(t.progress, 0.0, 1.0))
|
return (t.from as Vector2).lerp(t.to as Vector2, clampf(t.progress, 0.0, 1.0))
|
||||||
|
|
||||||
|
|
@ -1190,6 +1153,7 @@ func _dispatch_transporters() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _advance_transporters(delta: float) -> void:
|
func _advance_transporters(delta: float) -> void:
|
||||||
|
_sync_hub_transporters()
|
||||||
if transporters.is_empty():
|
if transporters.is_empty():
|
||||||
return
|
return
|
||||||
_dispatch_transporters()
|
_dispatch_transporters()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ build_cost = 6
|
||||||
build_time = 10.0
|
build_time = 10.0
|
||||||
icon = 2
|
icon = 2
|
||||||
glow_color = Color(0.25, 0.85, 1, 1)
|
glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
|
fleet_eligible = true
|
||||||
capabilities = PackedStringArray("weapon")
|
capabilities = PackedStringArray("weapon")
|
||||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
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, "delivery_speed": 180.0}
|
base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 450.0, "delivery_speed": 180.0}
|
||||||
|
|
|
||||||
155
Main.tscn
155
Main.tscn
|
|
@ -148,20 +148,19 @@ digit_height = 38.0
|
||||||
[node name="PlanetNameLabel" type="Label" parent="UI"]
|
[node name="PlanetNameLabel" type="Label" parent="UI"]
|
||||||
visible = false
|
visible = false
|
||||||
offset_left = 24
|
offset_left = 24
|
||||||
offset_top = 156
|
offset_top = 480
|
||||||
offset_right = 696
|
offset_right = 696
|
||||||
offset_bottom = 282
|
offset_bottom = 532
|
||||||
text = ""
|
text = ""
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 32
|
||||||
|
|
||||||
[node name="EnterAtmosphereButton" type="Control" parent="UI"]
|
[node name="EnterAtmosphereButton" type="Control" parent="UI"]
|
||||||
visible = false
|
visible = false
|
||||||
anchor_top = 1.0
|
offset_left = 188
|
||||||
anchor_bottom = 1.0
|
offset_top = 324
|
||||||
offset_left = 24
|
offset_right = 336
|
||||||
offset_right = 172
|
offset_bottom = 472
|
||||||
offset_top = -720
|
|
||||||
offset_bottom = -572
|
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 3
|
icon = 3
|
||||||
|
|
||||||
|
|
@ -203,10 +202,10 @@ glow_color = Color(0.45, 0.9, 0.55, 1)
|
||||||
|
|
||||||
[node name="TransferButton" type="Control" parent="UI"]
|
[node name="TransferButton" type="Control" parent="UI"]
|
||||||
visible = false
|
visible = false
|
||||||
offset_left = 24
|
offset_left = 188
|
||||||
offset_right = 172
|
offset_top = 160
|
||||||
offset_top = 488
|
offset_right = 336
|
||||||
offset_bottom = 636
|
offset_bottom = 308
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 14
|
icon = 14
|
||||||
glow_color = Color(1, 0.78, 0.25, 1)
|
glow_color = Color(1, 0.78, 0.25, 1)
|
||||||
|
|
@ -789,8 +788,8 @@ horizontal_alignment = 1
|
||||||
[node name="AvailableInterceptor" type="Control" parent="UI/HangarMenu"]
|
[node name="AvailableInterceptor" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -250
|
offset_left = -180
|
||||||
offset_right = -130
|
offset_right = -60
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -800,8 +799,8 @@ icon = 2
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -300
|
offset_left = -250
|
||||||
offset_right = -80
|
offset_right = 10
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -810,8 +809,8 @@ horizontal_alignment = 1
|
||||||
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
|
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -60
|
offset_left = 60
|
||||||
offset_right = 60
|
offset_right = 180
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -822,30 +821,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -110
|
offset_left = -10
|
||||||
offset_right = 110
|
|
||||||
offset_top = 380
|
|
||||||
offset_bottom = 460
|
|
||||||
theme_override_font_sizes/font_size = 22
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="AvailableTransporter" type="Control" parent="UI/HangarMenu"]
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
offset_left = 130
|
|
||||||
offset_right = 250
|
offset_right = 250
|
||||||
offset_top = 255
|
|
||||||
offset_bottom = 375
|
|
||||||
script = ExtResource("10")
|
|
||||||
icon = 12
|
|
||||||
glow_color = Color(0.95, 0.55, 0.85, 1)
|
|
||||||
|
|
||||||
[node name="AvailableTransporterLabel" type="Label" parent="UI/HangarMenu"]
|
|
||||||
mouse_filter = 2
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
offset_left = 80
|
|
||||||
offset_right = 300
|
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -863,8 +840,8 @@ horizontal_alignment = 1
|
||||||
[node name="FleetInterceptor" type="Control" parent="UI/HangarMenu"]
|
[node name="FleetInterceptor" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -250
|
offset_left = -180
|
||||||
offset_right = -130
|
offset_right = -60
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -874,8 +851,8 @@ icon = 2
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -300
|
offset_left = -250
|
||||||
offset_right = -80
|
offset_right = 10
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -884,8 +861,8 @@ horizontal_alignment = 1
|
||||||
[node name="FleetShield" type="Control" parent="UI/HangarMenu"]
|
[node name="FleetShield" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -60
|
offset_left = 60
|
||||||
offset_right = 60
|
offset_right = 180
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -896,30 +873,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -110
|
offset_left = -10
|
||||||
offset_right = 110
|
|
||||||
offset_top = 690
|
|
||||||
offset_bottom = 770
|
|
||||||
theme_override_font_sizes/font_size = 22
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="FleetTransporter" type="Control" parent="UI/HangarMenu"]
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
offset_left = 130
|
|
||||||
offset_right = 250
|
offset_right = 250
|
||||||
offset_top = 565
|
|
||||||
offset_bottom = 685
|
|
||||||
script = ExtResource("10")
|
|
||||||
icon = 12
|
|
||||||
glow_color = Color(0.95, 0.55, 0.85, 1)
|
|
||||||
|
|
||||||
[node name="FleetTransporterLabel" type="Label" parent="UI/HangarMenu"]
|
|
||||||
mouse_filter = 2
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
offset_left = 80
|
|
||||||
offset_right = 300
|
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1291,10 +1246,10 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -234
|
offset_left = -220
|
||||||
offset_right = -86
|
offset_right = -72
|
||||||
offset_top = 60
|
offset_top = -100
|
||||||
offset_bottom = 208
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 2
|
icon = 2
|
||||||
|
|
||||||
|
|
@ -1304,51 +1259,23 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -300
|
offset_left = -290
|
||||||
offset_right = -20
|
offset_right = -2
|
||||||
offset_top = 216
|
offset_top = 56
|
||||||
offset_bottom = 296
|
offset_bottom = 136
|
||||||
theme_override_font_sizes/font_size = 32
|
theme_override_font_sizes/font_size = 32
|
||||||
text = "Interceptor"
|
text = "Interceptor"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="TransporterButton" type="Control" parent="UI/CraftMenu"]
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
anchor_top = 0.5
|
|
||||||
anchor_bottom = 0.5
|
|
||||||
offset_left = 86
|
|
||||||
offset_right = 234
|
|
||||||
offset_top = 60
|
|
||||||
offset_bottom = 208
|
|
||||||
script = ExtResource("10")
|
|
||||||
icon = 12
|
|
||||||
glow_color = Color(0.95, 0.55, 0.85, 1)
|
|
||||||
|
|
||||||
[node name="TransporterLabel" type="Label" parent="UI/CraftMenu"]
|
|
||||||
mouse_filter = 2
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
anchor_top = 0.5
|
|
||||||
anchor_bottom = 0.5
|
|
||||||
offset_left = 20
|
|
||||||
offset_right = 300
|
|
||||||
offset_top = 216
|
|
||||||
offset_bottom = 296
|
|
||||||
theme_override_font_sizes/font_size = 32
|
|
||||||
autowrap_mode = 2
|
|
||||||
text = "Transporter"
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="ShieldButton" type="Control" parent="UI/CraftMenu"]
|
[node name="ShieldButton" type="Control" parent="UI/CraftMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -74
|
offset_left = 72
|
||||||
offset_right = 74
|
offset_right = 220
|
||||||
offset_top = -200
|
offset_top = -100
|
||||||
offset_bottom = -52
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 2
|
icon = 2
|
||||||
glow_color = Color(0.45, 0.8, 1, 1)
|
glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
|
|
@ -1359,10 +1286,10 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -160
|
offset_left = 2
|
||||||
offset_right = 160
|
offset_right = 290
|
||||||
offset_top = -44
|
offset_top = 56
|
||||||
offset_bottom = 16
|
offset_bottom = 136
|
||||||
theme_override_font_sizes/font_size = 32
|
theme_override_font_sizes/font_size = 32
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
text = "Shield Vessel"
|
text = "Shield Vessel"
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ func _ready() -> void:
|
||||||
enter_button.pressed.connect(_on_enter_pressed)
|
enter_button.pressed.connect(_on_enter_pressed)
|
||||||
|
|
||||||
if GameState.has_return_position:
|
if GameState.has_return_position:
|
||||||
fleet.spawn_ships(GameState.surviving_ship_count, GameState.return_world_position)
|
fleet.spawn_ships(GameState.surviving_ship_count, GameState.return_world_position,
|
||||||
|
GameState.surviving_vessel_types)
|
||||||
camera.position = GameState.return_world_position
|
camera.position = GameState.return_world_position
|
||||||
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
||||||
GameState.has_return_position = false
|
GameState.has_return_position = false
|
||||||
|
|
@ -57,16 +58,12 @@ func _process(delta: float) -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
if world_map.visible or build_controller.is_showing_building_prompt():
|
if world_map.visible or build_controller.is_showing_building_prompt():
|
||||||
# EnterAtmosphereButton shares one "action" slot with the map's
|
# Context actions belong to BuildController. This controller owns the
|
||||||
# RecenterButton and with BuildController's building prompt - it doesn't make sense to enter
|
# debounced planet selection and label, and suppresses both while a modal
|
||||||
# a planet's atmosphere while looking at the star map anyway, so
|
# screen is open.
|
||||||
# suppress the prompt entirely rather than let it sit hidden
|
|
||||||
# behind the map's backdrop only to pop back in the instant the
|
|
||||||
# map closes.
|
|
||||||
current_planet = null
|
current_planet = null
|
||||||
pending_planet = null
|
pending_planet = null
|
||||||
planet_name_label.visible = false
|
planet_name_label.visible = false
|
||||||
enter_button.visible = false
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Debounced: only commit a visible/hidden change once the raw
|
# Debounced: only commit a visible/hidden change once the raw
|
||||||
|
|
@ -90,10 +87,8 @@ func _process(delta: float) -> void:
|
||||||
if current_planet != null:
|
if current_planet != null:
|
||||||
planet_name_label.text = current_planet.planet_name
|
planet_name_label.text = current_planet.planet_name
|
||||||
planet_name_label.visible = true
|
planet_name_label.visible = true
|
||||||
enter_button.visible = true
|
|
||||||
else:
|
else:
|
||||||
planet_name_label.visible = false
|
planet_name_label.visible = false
|
||||||
enter_button.visible = false
|
|
||||||
|
|
||||||
|
|
||||||
func _on_enter_pressed() -> void:
|
func _on_enter_pressed() -> void:
|
||||||
|
|
@ -105,6 +100,7 @@ func _on_enter_pressed() -> void:
|
||||||
|
|
||||||
GameState.has_return_position = true
|
GameState.has_return_position = true
|
||||||
GameState.surviving_ship_count = fleet.ships.size()
|
GameState.surviving_ship_count = fleet.ships.size()
|
||||||
|
GameState.surviving_vessel_types = fleet.vessel_types()
|
||||||
# The planet's own position, not fleet.get_centroid() - the fleet only
|
# The planet's own position, not fleet.get_centroid() - the fleet only
|
||||||
# needs to be somewhere within the planet's landing radius (not
|
# needs to be somewhere within the planet's landing radius (not
|
||||||
# centered exactly on it) for the prompt to show, so the centroid can
|
# centered exactly on it) for the prompt to show, so the centroid can
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ extends Node2D
|
||||||
const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd")
|
const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd")
|
||||||
const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd")
|
const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd")
|
||||||
|
|
||||||
# Bootstraps the surviving fleet on this scene's own startup (from
|
# Bootstraps the surviving fleet, including each vessel type, on this scene's
|
||||||
# GameState, since this is a fresh scene tree - nothing here carries
|
# own startup (from GameState, since this is a fresh scene tree - nothing here
|
||||||
# over automatically) and handles the always-available exit prompt that
|
# carries over automatically) and handles the always-available exit prompt that
|
||||||
# fades back out to Main.tscn, restoring the fleet at wherever it left
|
# fades back out to Main.tscn, restoring the fleet at wherever it left
|
||||||
# space from.
|
# space from.
|
||||||
|
|
||||||
|
|
@ -25,7 +25,8 @@ var transitioning: bool = false
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id)
|
var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id)
|
||||||
_compose_surface(SURFACE_PROFILES.get_profile(planet_definition.surface_profile_id))
|
_compose_surface(SURFACE_PROFILES.get_profile(planet_definition.surface_profile_id))
|
||||||
fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO)
|
fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO,
|
||||||
|
GameState.surviving_vessel_types)
|
||||||
camera.position = Vector2.ZERO
|
camera.position = Vector2.ZERO
|
||||||
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
||||||
planet_name_label.text = GameState.planet_name
|
planet_name_label.text = GameState.planet_name
|
||||||
|
|
@ -57,6 +58,7 @@ func _on_exit_pressed() -> void:
|
||||||
transitioning = true
|
transitioning = true
|
||||||
|
|
||||||
GameState.surviving_ship_count = fleet.ships.size()
|
GameState.surviving_ship_count = fleet.ships.size()
|
||||||
|
GameState.surviving_vessel_types = fleet.vessel_types()
|
||||||
|
|
||||||
var tween: Tween = create_tween()
|
var tween: Tween = create_tween()
|
||||||
tween.tween_property(fade_rect, "color:a", 1.0, fade_duration)
|
tween.tween_property(fade_rect, "color:a", 1.0, fade_duration)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ build_cost = 14
|
||||||
build_time = 16.0
|
build_time = 16.0
|
||||||
icon = 30
|
icon = 30
|
||||||
glow_color = Color(0.45, 0.8, 1, 1)
|
glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
|
fleet_eligible = true
|
||||||
capabilities = PackedStringArray("shield")
|
capabilities = PackedStringArray("shield")
|
||||||
component_scenes = Array[PackedScene]([ExtResource("3")])
|
component_scenes = Array[PackedScene]([ExtResource("3")])
|
||||||
hull_points = PackedVector2Array(0, -24, 20.7846, -12, 20.7846, 12, 0, 24, -20.7846, 12, -20.7846, -12)
|
hull_points = PackedVector2Array(0, -24, 20.7846, -12, 20.7846, 12, 0, 24, -20.7846, 12, -20.7846, -12)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
[gd_scene load_steps=3 format=3]
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://TransportHubBuildingVisual.gd" id="1"]
|
[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"]
|
[node name="Visual" type="Node2D"]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
[node name="ActionProvider" type="Node" parent="."]
|
|
||||||
script = ExtResource("2")
|
|
||||||
actions = PackedStringArray("transfer")
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
definition_id = "transporter"
|
definition_id = "transporter"
|
||||||
display_name = "Transporter"
|
display_name = "Transporter"
|
||||||
description = "Unarmed cargo vessel stationed at a transport hub to move resources."
|
description = "Hub-owned logistics vessel that moves resources automatically."
|
||||||
build_cost = 10
|
build_cost = 0
|
||||||
build_time = 8.0
|
build_time = 0.0
|
||||||
icon = 12
|
icon = 12
|
||||||
glow_color = Color(0.95, 0.55, 0.85, 1)
|
glow_color = Color(0.95, 0.55, 0.85, 1)
|
||||||
capabilities = PackedStringArray("cargo", "transport")
|
capabilities = PackedStringArray("cargo", "transport")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ extends Resource
|
||||||
@export var hull_points: PackedVector2Array = PackedVector2Array([
|
@export var hull_points: PackedVector2Array = PackedVector2Array([
|
||||||
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||||
])
|
])
|
||||||
|
@export var fleet_eligible: bool = false
|
||||||
@export var capabilities: PackedStringArray = []
|
@export var capabilities: PackedStringArray = []
|
||||||
@export var component_scenes: Array[PackedScene] = []
|
@export var component_scenes: Array[PackedScene] = []
|
||||||
@export var base_stats: Dictionary = {}
|
@export var base_stats: Dictionary = {}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@ static func all() -> Array:
|
||||||
result.append(DEFINITIONS[definition_id])
|
result.append(DEFINITIONS[definition_id])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func fleet() -> Array:
|
||||||
|
var result: Array = []
|
||||||
|
for definition in all():
|
||||||
|
if definition.fleet_eligible:
|
||||||
|
result.append(definition)
|
||||||
|
return result
|
||||||
|
|
||||||
static func with_capability(capability: String) -> Resource:
|
static func with_capability(capability: String) -> Resource:
|
||||||
for definition in all():
|
for definition in all():
|
||||||
if definition.has_capability(capability):
|
if definition.has_capability(capability):
|
||||||
|
|
|
||||||
37
tests/_context_actions_preview.gd
Normal file
37
tests/_context_actions_preview.gd
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Visual-only phone preview for the top-level context controls. It composes a
|
||||||
|
# mothership action set with a landing action so the horizontal foldout and
|
||||||
|
# the independent Transfer shortcut can be inspected in one screenshot.
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.buildings.clear()
|
||||||
|
GameState.extractors.clear()
|
||||||
|
var main: Node2D = load("res://Main.tscn").instantiate()
|
||||||
|
add_child(main)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("LandingController").set_process(false)
|
||||||
|
|
||||||
|
var building: Node2D = load("res://Building.tscn").instantiate()
|
||||||
|
building.definition_id = GameState.BUILDING_MOTHERSHIP
|
||||||
|
building.position = Vector2(0, 420)
|
||||||
|
main.add_child(building)
|
||||||
|
|
||||||
|
var controller: Node2D = main.get_node("BuildController")
|
||||||
|
controller.set_process(false)
|
||||||
|
controller.current_building = building
|
||||||
|
var planet: Node2D = Node2D.new()
|
||||||
|
main.add_child(planet)
|
||||||
|
controller.current_landing_planet = planet
|
||||||
|
controller.building_action_button.icon = int(building.definition.action_icon)
|
||||||
|
controller.transfer_button.visible = controller._has_transfer_context()
|
||||||
|
controller.action_button.visible = true
|
||||||
|
controller.action_button.icon = IconButton.Icon.ACTION_MENU
|
||||||
|
controller.context_actions_expanded = true
|
||||||
|
controller._layout_context_buttons()
|
||||||
|
|
||||||
|
var label: Label = main.get_node("UI/PlanetNameLabel")
|
||||||
|
label.text = "PLANET ACTIONS"
|
||||||
|
label.visible = true
|
||||||
1
tests/_context_actions_preview.gd.uid
Normal file
1
tests/_context_actions_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://r4kyx6d4s60i
|
||||||
6
tests/_context_actions_preview.tscn
Normal file
6
tests/_context_actions_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/_context_actions_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="ContextActionsPreview" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
23
tests/_hangar_menu_preview.gd
Normal file
23
tests/_hangar_menu_preview.gd
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Visual-only phone preview for the combat-fleet hangar. Logistics
|
||||||
|
# transporters deliberately have no card here.
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.buildings.clear()
|
||||||
|
GameState.extractors.clear()
|
||||||
|
GameState.transporters.clear()
|
||||||
|
GameState.stored_vessels = [
|
||||||
|
GameState.VESSEL_INTERCEPTOR,
|
||||||
|
GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD,
|
||||||
|
]
|
||||||
|
var main: Node = load("res://Main.tscn").instantiate()
|
||||||
|
add_child(main)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("Fleet").spawn_ships(3, Vector2.ZERO, PackedStringArray([
|
||||||
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
|
||||||
|
GameState.VESSEL_SHIELD,
|
||||||
|
]))
|
||||||
|
main.get_node("BuildController")._on_hangar_pressed()
|
||||||
1
tests/_hangar_menu_preview.gd.uid
Normal file
1
tests/_hangar_menu_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://grp4nm3xvngn
|
||||||
6
tests/_hangar_menu_preview.tscn
Normal file
6
tests/_hangar_menu_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/_hangar_menu_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="HangarMenuPreview" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
@ -15,17 +15,20 @@ func _ready() -> void:
|
||||||
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-420.0, 0.0))
|
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-420.0, 0.0))
|
||||||
GameState.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(420.0, 0.0))
|
GameState.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(420.0, 0.0))
|
||||||
|
|
||||||
# One laden, mid-leg, and one running home empty, so both states show.
|
# The hub already provisioned these two. Put one laden mid-leg and one on
|
||||||
GameState.transporters.append({
|
# its way home empty so both visual states show.
|
||||||
"hub_id": 2, "state": GameState.TRANSPORTER_TO_BUILDING, "target_id": 2,
|
var laden: Dictionary = GameState.transporters[0]
|
||||||
"carrying": 8, "progress": 0.45,
|
laden.state = GameState.TRANSPORTER_TO_BUILDING
|
||||||
"from": Vector2(-420.0, 0.0), "to": Vector2(420.0, 0.0),
|
laden.target_id = 2
|
||||||
})
|
laden.carrying = 8
|
||||||
GameState.transporters.append({
|
laden.progress = 0.45
|
||||||
"hub_id": 2, "state": GameState.TRANSPORTER_RETURNING, "target_id": 0,
|
laden.from = Vector2(-420.0, 0.0)
|
||||||
"carrying": 0, "progress": 0.5,
|
laden.to = Vector2(420.0, 0.0)
|
||||||
"from": Vector2(-380.0, 150.0), "to": Vector2(420.0, 150.0),
|
var returning: Dictionary = GameState.transporters[1]
|
||||||
})
|
returning.state = GameState.TRANSPORTER_RETURNING
|
||||||
|
returning.progress = 0.5
|
||||||
|
returning.from = Vector2(-380.0, 150.0)
|
||||||
|
returning.to = Vector2(420.0, 150.0)
|
||||||
|
|
||||||
var main: Node = load("res://Main.tscn").instantiate()
|
var main: Node = load("res://Main.tscn").instantiate()
|
||||||
add_child(main)
|
add_child(main)
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,12 @@ func _initialize() -> void:
|
||||||
])
|
])
|
||||||
return
|
return
|
||||||
var provider: Node = building.visual.get_node_or_null("ActionProvider")
|
var provider: Node = building.visual.get_node_or_null("ActionProvider")
|
||||||
if provider == null or not provider.actions.has("transfer"):
|
if definition.definition_id == GameState.BUILDING_TRANSPORT_HUB:
|
||||||
_fail("%s has no composed action provider" % definition.definition_id)
|
if provider != null or not building.available_actions().is_empty():
|
||||||
|
_fail("transport hub composed a manual action")
|
||||||
|
return
|
||||||
|
elif provider == null or not provider.actions.has("transfer"):
|
||||||
|
_fail("%s has no composed transfer action" % definition.definition_id)
|
||||||
return
|
return
|
||||||
building.free()
|
building.free()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,9 @@ func _process(delta: float) -> bool:
|
||||||
print("TEST_CRAFTER_FEEDBACK: FAIL (fleet parked on the crafter never registered)")
|
print("TEST_CRAFTER_FEEDBACK: FAIL (fleet parked on the crafter never registered)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
# A crafter now has both Craft and Transfer, so the shared action
|
# Transfer has its own shortcut beside Build, leaving Craft as the
|
||||||
# button unfolds those choices before the craft action is selected.
|
# single contextual action that fires directly.
|
||||||
_tap(controller.action_button)
|
_tap(controller.action_button)
|
||||||
_tap(controller.building_action_button)
|
|
||||||
if not controller.craft_menu.visible:
|
if not controller.craft_menu.visible:
|
||||||
print("TEST_CRAFTER_FEEDBACK: FAIL (the action button did not open the crafter panel)")
|
print("TEST_CRAFTER_FEEDBACK: FAIL (the action button did not open the crafter panel)")
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,10 @@ func _process(delta: float) -> bool:
|
||||||
print("extractor mined locally: stock=%d bank=%d" % [
|
print("extractor mined locally: stock=%d bank=%d" % [
|
||||||
game_state.extractor_stock(extractor_index), game_state.rocks])
|
game_state.extractor_stock(extractor_index), game_state.rocks])
|
||||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||||
var hub_id: int = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||||
# A manually seeded idle transporter is enough to exercise the same
|
if game_state.transporters.size() != game_state.hub_capacity():
|
||||||
# simulation used by a produced one.
|
_fail("hub did not provision its transporters")
|
||||||
game_state.transporters.append({"hub_id": hub_id, "state": GameState.TRANSPORTER_IDLE,
|
return true
|
||||||
"target_id": 0, "extractor_index": -1, "carrying": 0, "progress": 1.0,
|
|
||||||
"from": HUB_AT, "to": HUB_AT})
|
|
||||||
base_load = game_state.transporter_load_limit()
|
base_load = game_state.transporter_load_limit()
|
||||||
game_state.extractors[extractor_index].stock = base_load - 1
|
game_state.extractors[extractor_index].stock = base_load - 1
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,13 @@ func _process(delta: float) -> bool:
|
||||||
1:
|
1:
|
||||||
if elapsed < 1.2:
|
if elapsed < 1.2:
|
||||||
return false
|
return false
|
||||||
if controller.current_extractor_index != extractor_index or not controller.action_button.visible:
|
if controller.current_extractor_index != extractor_index or not controller.transfer_button.visible:
|
||||||
return _fail("extractor did not expose a contextual action")
|
return _fail("extractor did not expose the transfer shortcut")
|
||||||
if not scene.get_node("UI/EnterAtmosphereButton").visible:
|
if not controller.action_button.visible \
|
||||||
return _fail("extractor action hid the separate landing action")
|
or controller.action_button.icon != controller.landing_button.icon \
|
||||||
controller._on_context_action_pressed()
|
or controller.landing_button.visible:
|
||||||
|
return _fail("landing was not folded into the shared action control")
|
||||||
|
controller._on_transfer_pressed()
|
||||||
if not controller.transfer_menu.visible or controller.transfer_list.extractor_index != extractor_index:
|
if not controller.transfer_menu.visible or controller.transfer_list.extractor_index != extractor_index:
|
||||||
return _fail("transfer panel did not bind to the extractor")
|
return _fail("transfer panel did not bind to the extractor")
|
||||||
var before: int = state.fleet_resource(resource_type)
|
var before: int = state.fleet_resource(resource_type)
|
||||||
|
|
@ -61,7 +63,7 @@ func _process(delta: float) -> bool:
|
||||||
return _fail("fleet did not fetch five resources from extractor")
|
return _fail("fleet did not fetch five resources from extractor")
|
||||||
if state.extractor_resource(extractor_index, resource_type) < 7:
|
if state.extractor_resource(extractor_index, resource_type) < 7:
|
||||||
return _fail("extractor transfer removed too much stock")
|
return _fail("extractor transfer removed too much stock")
|
||||||
print("fleet fetched %s from extractor while landing remained available" % resource_type)
|
print("fleet fetched %s while landing remained the contextual action" % resource_type)
|
||||||
print("TEST_EXTRACTOR_TRANSFER: PASS")
|
print("TEST_EXTRACTOR_TRANSFER: PASS")
|
||||||
quit(0)
|
quit(0)
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,19 @@ func _process(delta: float) -> bool:
|
||||||
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(100, 0))
|
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(100, 0))
|
||||||
game_state.add_extractor("Aurelia", Vector2(400, 0), "gas")
|
game_state.add_extractor("Aurelia", Vector2(400, 0), "gas")
|
||||||
game_state.extractors[0].stock = game_state.transporter_load_limit()
|
game_state.extractors[0].stock = game_state.transporter_load_limit()
|
||||||
game_state.transporters.append({"hub_id": 2, "state": GameState.TRANSPORTER_TO_EXTRACTOR,
|
var transporter: Dictionary = game_state.transporters[0]
|
||||||
"target_id": 0, "extractor_index": 0, "carrying": 0, "resource_type": "gas",
|
transporter.state = GameState.TRANSPORTER_TO_EXTRACTOR
|
||||||
"progress": 1.0, "from": Vector2(100, 0), "to": Vector2(400, 0)})
|
transporter.target_id = 0
|
||||||
game_state._arrive(game_state.transporters[0])
|
transporter.extractor_index = 0
|
||||||
|
transporter.carrying = 0
|
||||||
|
transporter.resource_type = "gas"
|
||||||
|
transporter.progress = 1.0
|
||||||
|
transporter.from = Vector2(100, 0)
|
||||||
|
transporter.to = Vector2(400, 0)
|
||||||
|
game_state._arrive(transporter)
|
||||||
if game_state.transporters[0].state != GameState.TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR:
|
if game_state.transporters[0].state != GameState.TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR:
|
||||||
return _fail("transporter did not collect gas from the extractor")
|
return _fail("transporter did not collect gas from the extractor")
|
||||||
game_state._arrive(game_state.transporters[0])
|
game_state._arrive(transporter)
|
||||||
if game_state.gas != game_state.transporter_load_limit() or game_state.rocks != 0:
|
if game_state.gas != game_state.transporter_load_limit() or game_state.rocks != 0:
|
||||||
return _fail("gas delivery banked gas=%d ore=%d" % [game_state.gas, game_state.rocks])
|
return _fail("gas delivery banked gas=%d ore=%d" % [game_state.gas, game_state.rocks])
|
||||||
var mothership_id: int = int(game_state.buildings[0].id)
|
var mothership_id: int = int(game_state.buildings[0].id)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ func _process(delta: float) -> bool:
|
||||||
a.queue_free()
|
a.queue_free()
|
||||||
fleet.spawn_ships(1, Vector2.ZERO)
|
fleet.spawn_ships(1, Vector2.ZERO)
|
||||||
fleet.add_ship(GameState.VESSEL_SHIELD)
|
fleet.add_ship(GameState.VESSEL_SHIELD)
|
||||||
game_state.stored_vessels = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD]
|
game_state.stored_vessels = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||||
|
GameState.VESSEL_TRANSPORTER]
|
||||||
controller._on_hangar_pressed()
|
controller._on_hangar_pressed()
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
stage = 1
|
stage = 1
|
||||||
|
|
@ -48,6 +49,10 @@ func _process(delta: float) -> bool:
|
||||||
if "Interceptor" not in available_label.text or "1 available" not in available_label.text:
|
if "Interceptor" not in available_label.text or "1 available" not in available_label.text:
|
||||||
_fail("available interceptor card says '%s'" % available_label.text)
|
_fail("available interceptor card says '%s'" % available_label.text)
|
||||||
return true
|
return true
|
||||||
|
if controller._hangar_types().has(GameState.VESSEL_TRANSPORTER) \
|
||||||
|
or fleet.add_ship(GameState.VESSEL_TRANSPORTER):
|
||||||
|
_fail("a logistics transporter was exposed as a fleet vessel")
|
||||||
|
return true
|
||||||
controller._on_hangar_crew_type(0)
|
controller._on_hangar_crew_type(0)
|
||||||
if fleet.ships.size() != 3 or game_state.stored_vessels.has(GameState.VESSEL_INTERCEPTOR):
|
if fleet.ships.size() != 3 or game_state.stored_vessels.has(GameState.VESSEL_INTERCEPTOR):
|
||||||
_fail("tapping an available interceptor did not crew it")
|
_fail("tapping an available interceptor did not crew it")
|
||||||
|
|
@ -63,24 +68,30 @@ func _process(delta: float) -> bool:
|
||||||
_fail("hangar allowed the last deployed ship to be stored")
|
_fail("hangar allowed the last deployed ship to be stored")
|
||||||
return true
|
return true
|
||||||
|
|
||||||
# Context actions are compact until expanded. A mothership has three
|
# Context actions are compact until expanded. A mothership unfolds
|
||||||
# choices: research, transfer cargo and hangar, which unfold in their
|
# research and hangar; transfer stays as a persistent shortcut beside
|
||||||
# own non-overlapping berths to the right only after expansion.
|
# Build and is not duplicated in the foldout.
|
||||||
var mothership: Node2D = load("res://Building.tscn").instantiate()
|
var mothership: Node2D = load("res://Building.tscn").instantiate()
|
||||||
mothership.set("building_type", GameState.BUILDING_MOTHERSHIP)
|
mothership.set("building_type", GameState.BUILDING_MOTHERSHIP)
|
||||||
mothership._ready()
|
mothership._ready()
|
||||||
controller.current_building = mothership
|
controller.current_building = mothership
|
||||||
controller.context_actions_expanded = true
|
controller.context_actions_expanded = true
|
||||||
|
controller.transfer_button.visible = controller._has_transfer_context()
|
||||||
controller._layout_context_buttons()
|
controller._layout_context_buttons()
|
||||||
var buttons: Array = [controller.building_action_button, controller.transfer_button, controller.hangar_button]
|
var buttons: Array = [controller.building_action_button, controller.hangar_button]
|
||||||
for i in range(buttons.size() - 1):
|
for i in range(buttons.size() - 1):
|
||||||
if buttons[i].offset_right > buttons[i + 1].offset_left:
|
if buttons[i].offset_right > buttons[i + 1].offset_left:
|
||||||
_fail("context buttons %d and %d overlap" % [i, i + 1])
|
_fail("context buttons %d and %d overlap" % [i, i + 1])
|
||||||
return true
|
return true
|
||||||
controller._collapse_context_actions()
|
controller._collapse_context_actions()
|
||||||
if controller.building_action_button.visible or controller.transfer_button.visible or controller.hangar_button.visible:
|
if controller.building_action_button.visible or controller.hangar_button.visible:
|
||||||
_fail("collapsing the context button left an action visible")
|
_fail("collapsing the context button left an action visible")
|
||||||
return true
|
return true
|
||||||
|
if not controller.transfer_button.visible \
|
||||||
|
or controller.transfer_button.offset_top != controller.build_button.offset_top \
|
||||||
|
or controller.transfer_button.offset_left <= controller.build_button.offset_left:
|
||||||
|
_fail("transfer shortcut is not beside Build")
|
||||||
|
return true
|
||||||
mothership.free()
|
mothership.free()
|
||||||
print("hangar crews and stores typed vessels; contextual actions expand and collapse")
|
print("hangar crews and stores typed vessels; contextual actions expand and collapse")
|
||||||
print("TEST_HANGAR_MANAGEMENT: PASS")
|
print("TEST_HANGAR_MANAGEMENT: PASS")
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,56 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
# Headless gameplay test: run with
|
# A transport hub owns its logistics vessels directly. Building a hub
|
||||||
# godot --headless -s res://tests/test_hub_capacity.gd
|
# provisions every researched slot immediately; Freight Yards provisions its
|
||||||
# Forgejo #20 (part 7 of #12): a hub stations a limited number of
|
# extra slot on hubs that already exist.
|
||||||
# transporters. More hubs, or research, raise it.
|
|
||||||
#
|
|
||||||
# Verifies:
|
|
||||||
# 1. capacity is per hub, so a second hub raises the total
|
|
||||||
# 2. ordering past the cap is REFUSED, not silently swallowed - the ticket
|
|
||||||
# is explicit that the player has to be told
|
|
||||||
# 3. a queued-but-unbuilt transporter still holds its berth, or ten could
|
|
||||||
# be ordered against two and only fail on arrival
|
|
||||||
# 4. researching Freight Yards raises capacity for hubs ALREADY BUILT, not
|
|
||||||
# just ones placed afterwards
|
|
||||||
# 5. the extra berth is immediately usable
|
|
||||||
|
|
||||||
var game_state: Node
|
|
||||||
var scene: Node
|
|
||||||
var field: Node
|
|
||||||
var crafter_id: int = 0
|
|
||||||
var stage: int = 0
|
|
||||||
var elapsed: float = 0.0
|
|
||||||
|
|
||||||
const CRAFTER_AT: Vector2 = Vector2(400.0, 0.0)
|
|
||||||
const HUB_A: Vector2 = Vector2(-500.0, 0.0)
|
|
||||||
const HUB_B: Vector2 = Vector2(-500.0, 600.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
game_state = root.get_node("GameState")
|
var state: Node = root.get_node("GameState")
|
||||||
game_state.buildings.clear()
|
state.buildings.clear()
|
||||||
game_state.extractors.clear()
|
state.extractors.clear()
|
||||||
game_state.transporters.clear()
|
state.transporters.clear()
|
||||||
game_state.rocks = 900
|
state.reset_progression()
|
||||||
game_state.fleet_cargo = 0
|
state.rocks = 900
|
||||||
game_state.stored_vessels.clear()
|
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||||
game_state.build_time_scale = 0.02
|
|
||||||
game_state.unlocked = ["shipyards", "logistics", "freight"]
|
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
|
||||||
scene = load("res://Main.tscn").instantiate()
|
var per_hub: int = state.hub_capacity()
|
||||||
root.add_child(scene)
|
if state.transporters.size() != per_hub or _at_hub(state, hub_a) != per_hub:
|
||||||
field = scene.get_node("BuildingField")
|
return _fail("first hub did not immediately provision %d transporters" % per_hub)
|
||||||
|
print("first hub provisioned %d transporters" % per_hub)
|
||||||
|
|
||||||
|
var hub_b: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 600))
|
||||||
|
if state.transporters.size() != per_hub * 2 or _at_hub(state, hub_b) != per_hub:
|
||||||
|
return _fail("second hub did not bring its own transporters")
|
||||||
|
print("second hub raised logistics fleet to %d" % state.transporters.size())
|
||||||
|
|
||||||
|
var before: int = state.transporters.size()
|
||||||
|
if not state.unlock("freight_yards"):
|
||||||
|
return _fail("could not research Freight Yards")
|
||||||
|
var expected: int = state.hub_capacity() * state.hub_count()
|
||||||
|
if state.transporters.size() != expected or state.transporters.size() != before + 2:
|
||||||
|
return _fail("Freight Yards did not provision one transporter per existing hub")
|
||||||
|
if _at_hub(state, hub_a) != state.hub_capacity() \
|
||||||
|
or _at_hub(state, hub_b) != state.hub_capacity():
|
||||||
|
return _fail("researched transporters were assigned to the wrong hubs")
|
||||||
|
|
||||||
|
print("Freight Yards provisioned existing hubs: %d -> %d" % [before, expected])
|
||||||
|
if state.remove_buildings_of_type(state.BUILDING_TRANSPORT_HUB) != 2 \
|
||||||
|
or not state.transporters.is_empty():
|
||||||
|
return _fail("removing hubs left orphaned logistics transporters")
|
||||||
|
print("TEST_HUB_CAPACITY: PASS")
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
func _fill_berths(n: int) -> void:
|
func _at_hub(state: Node, hub_id: int) -> int:
|
||||||
# Queue and immediately fund n transporters.
|
var count: int = 0
|
||||||
for i in range(n):
|
for transporter in state.transporters:
|
||||||
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
if int(transporter.hub_id) == hub_id:
|
||||||
game_state.deposit_to_building(crafter_id, GameState.TRANSPORTER_COST)
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _fail(message: String) -> void:
|
||||||
elapsed += delta
|
print("TEST_HUB_CAPACITY: FAIL (%s)" % message)
|
||||||
match stage:
|
quit(1)
|
||||||
0:
|
|
||||||
scene.get_node("AsteroidField").set_process(false)
|
|
||||||
scene.get_node("EnemySpawner").set_process(false)
|
|
||||||
field.place(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
|
||||||
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
|
||||||
crafter_id = int(game_state.buildings[-1].id)
|
|
||||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_A)
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 1
|
|
||||||
return false
|
|
||||||
1:
|
|
||||||
if elapsed < 0.3:
|
|
||||||
return false
|
|
||||||
var per_hub: int = game_state.hub_capacity()
|
|
||||||
if game_state.transport_capacity() != per_hub:
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (one hub gives %d berths, per-hub capacity is %d)" % [
|
|
||||||
game_state.transport_capacity(), per_hub
|
|
||||||
])
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("1 hub -> %d berths (%d per hub)" % [game_state.transport_capacity(), per_hub])
|
|
||||||
|
|
||||||
# --- 2 + 3. fill it, then try to exceed it ------------------
|
|
||||||
_fill_berths(per_hub)
|
|
||||||
if game_state.transporters_claimed() != per_hub:
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (claimed %d berths after ordering %d)" % [
|
|
||||||
game_state.transporters_claimed(), per_hub
|
|
||||||
])
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
# Still only QUEUED at this point - the berth must already count.
|
|
||||||
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (queued a transporter past the %d-berth cap)" % per_hub)
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("cap enforced at %d, including orders not yet built" % per_hub)
|
|
||||||
|
|
||||||
# --- 1. a second hub raises the total -----------------------
|
|
||||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_B)
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 2
|
|
||||||
return false
|
|
||||||
2:
|
|
||||||
if elapsed < 0.3:
|
|
||||||
return false
|
|
||||||
if game_state.transport_capacity() != game_state.hub_capacity() * 2:
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (2 hubs give %d berths, expected %d)" % [
|
|
||||||
game_state.transport_capacity(), game_state.hub_capacity() * 2
|
|
||||||
])
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("2 hubs -> %d berths" % game_state.transport_capacity())
|
|
||||||
if not game_state.can_station_transporter():
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (second hub did not free a berth)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
|
|
||||||
# Fill the second hub too, so capacity is exhausted again.
|
|
||||||
_fill_berths(game_state.transport_capacity() - game_state.transporters_claimed())
|
|
||||||
if game_state.can_station_transporter():
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (still room after filling every berth)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
|
|
||||||
# --- 4. research raises it for hubs ALREADY built -----------
|
|
||||||
var before: int = game_state.transport_capacity()
|
|
||||||
game_state.unlocked.append("freight_yards")
|
|
||||||
var after: int = game_state.transport_capacity()
|
|
||||||
print("researched Freight Yards: %d -> %d berths across %d existing hubs" % [
|
|
||||||
before, after, game_state.hub_count()
|
|
||||||
])
|
|
||||||
if after != before + game_state.hub_count():
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (research gave %d berths, expected +1 per existing hub)" % (after - before))
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
|
|
||||||
# --- 5. and the new berth is usable straight away -----------
|
|
||||||
if not game_state.can_station_transporter():
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (research raised the number but left no usable berth)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
if not game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
|
||||||
print("TEST_HUB_CAPACITY: FAIL (could not order into the berth research just added)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("ordered into the newly researched berth")
|
|
||||||
print("TEST_HUB_CAPACITY: PASS")
|
|
||||||
quit(0)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,8 @@ func _initialize() -> void:
|
||||||
state.rocks = 100
|
state.rocks = 100
|
||||||
state.build_time_scale = 0.02
|
state.build_time_scale = 0.02
|
||||||
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||||
var hub_id: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
|
state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
|
||||||
lab_id = state.add_building(state.BUILDING_SHIP_LAB, Vector2(100, 0))
|
lab_id = state.add_building(state.BUILDING_SHIP_LAB, Vector2(100, 0))
|
||||||
state.transporters.append({"hub_id": hub_id, "state": state.TRANSPORTER_IDLE,
|
|
||||||
"target_id": 0, "extractor_index": -1, "carrying": 0, "progress": 1.0,
|
|
||||||
"from": Vector2(50, 0), "to": Vector2(50, 0)})
|
|
||||||
if not state.queue_lab_research(lab_id, "int_damage"):
|
if not state.queue_lab_research(lab_id, "int_damage"):
|
||||||
return _fail("could not queue lab research")
|
return _fail("could not queue lab research")
|
||||||
if state.outstanding_cost(lab_id) != state.upgrade_cost("int_damage"):
|
if state.outstanding_cost(lab_id) != state.upgrade_cost("int_damage"):
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,13 @@ var target_chunk: Vector2i
|
||||||
var position_before_leaving: Vector2
|
var position_before_leaving: Vector2
|
||||||
var cached_fade_duration: float = 0.4
|
var cached_fade_duration: float = 0.4
|
||||||
var returned_ship_count: int = -1
|
var returned_ship_count: int = -1
|
||||||
|
var returned_vessel_types: PackedStringArray = PackedStringArray()
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
|
var expected_vessel_types: PackedStringArray = PackedStringArray([
|
||||||
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||||
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||||
|
GameState.VESSEL_INTERCEPTOR,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
|
@ -58,7 +64,7 @@ func _initialize() -> void:
|
||||||
landing_controller = main_scene.get_node("LandingController")
|
landing_controller = main_scene.get_node("LandingController")
|
||||||
asteroid_field_ref = main_scene.get_node("AsteroidField")
|
asteroid_field_ref = main_scene.get_node("AsteroidField")
|
||||||
planet_name_label = main_scene.get_node("UI/PlanetNameLabel")
|
planet_name_label = main_scene.get_node("UI/PlanetNameLabel")
|
||||||
enter_button = main_scene.get_node("UI/EnterAtmosphereButton")
|
enter_button = main_scene.get_node("UI/BuildActionButton")
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
|
|
@ -85,6 +91,10 @@ func _process(delta: float) -> bool:
|
||||||
# collide with a ship mid-test. Clear the existing ones too.
|
# collide with a ship mid-test. Clear the existing ones too.
|
||||||
for a in get_nodes_in_group("asteroids"):
|
for a in get_nodes_in_group("asteroids"):
|
||||||
a.queue_free()
|
a.queue_free()
|
||||||
|
# A mixed fleet proves the handoff preserves composition rather
|
||||||
|
# than merely reconstructing the same number of interceptors.
|
||||||
|
fleet.spawn_ships(expected_vessel_types.size(), fleet.get_centroid(),
|
||||||
|
expected_vessel_types)
|
||||||
|
|
||||||
# Get the fleet into the target chunk's vicinity first so
|
# Get the fleet into the target chunk's vicinity first so
|
||||||
# PlanetField's spawn radius covers it - the planet's own exact
|
# PlanetField's spawn radius covers it - the planet's own exact
|
||||||
|
|
@ -151,12 +161,13 @@ func _process(delta: float) -> bool:
|
||||||
|
|
||||||
var surface_fleet: Node = current_scene.get_node("Fleet")
|
var surface_fleet: Node = current_scene.get_node("Fleet")
|
||||||
var ship_count_ok: bool = surface_fleet.ships.size() == 5
|
var ship_count_ok: bool = surface_fleet.ships.size() == 5
|
||||||
|
var composition_ok: bool = surface_fleet.vessel_types() == expected_vessel_types
|
||||||
var no_asteroids: bool = get_nodes_in_group("asteroids").is_empty()
|
var no_asteroids: bool = get_nodes_in_group("asteroids").is_empty()
|
||||||
var no_enemies: bool = get_nodes_in_group("enemies").is_empty()
|
var no_enemies: bool = get_nodes_in_group("enemies").is_empty()
|
||||||
print("ship_count_ok=%s (count=%d) no_asteroids=%s no_enemies=%s" % [
|
print("ship_count_ok=%s (count=%d) no_asteroids=%s no_enemies=%s" % [
|
||||||
ship_count_ok, surface_fleet.ships.size(), no_asteroids, no_enemies
|
ship_count_ok, surface_fleet.ships.size(), no_asteroids, no_enemies
|
||||||
])
|
])
|
||||||
if not (ship_count_ok and no_asteroids and no_enemies):
|
if not (ship_count_ok and composition_ok and no_asteroids and no_enemies):
|
||||||
print("TEST_PLANET_ATMOSPHERE_TRANSITION: FAIL (surface scene contents wrong)")
|
print("TEST_PLANET_ATMOSPHERE_TRANSITION: FAIL (surface scene contents wrong)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
|
|
@ -217,6 +228,7 @@ func _process(delta: float) -> bool:
|
||||||
for a in get_nodes_in_group("asteroids"):
|
for a in get_nodes_in_group("asteroids"):
|
||||||
a.queue_free()
|
a.queue_free()
|
||||||
returned_ship_count = space_fleet.ships.size()
|
returned_ship_count = space_fleet.ships.size()
|
||||||
|
returned_vessel_types = space_fleet.vessel_types()
|
||||||
|
|
||||||
# Ships respawn in a wedge and then steer into formation, so the
|
# Ships respawn in a wedge and then steer into formation, so the
|
||||||
# centroid only means anything once they have actually stopped.
|
# centroid only means anything once they have actually stopped.
|
||||||
|
|
@ -236,12 +248,13 @@ func _process(delta: float) -> bool:
|
||||||
# across correctly and a fleet came back.
|
# across correctly and a fleet came back.
|
||||||
var handoff_ok: bool = game_state.surviving_ship_count == 5
|
var handoff_ok: bool = game_state.surviving_ship_count == 5
|
||||||
var ship_count_ok: bool = handoff_ok and returned_ship_count >= 1
|
var ship_count_ok: bool = handoff_ok and returned_ship_count >= 1
|
||||||
|
var composition_ok: bool = returned_vessel_types == expected_vessel_types
|
||||||
var position_ok: bool = space_fleet.get_centroid().distance_to(position_before_leaving) < 50.0
|
var position_ok: bool = space_fleet.get_centroid().distance_to(position_before_leaving) < 50.0
|
||||||
print("handoff=%d returned_ships=%d ship_count_ok=%s position_ok=%s centroid=%s expected=%s" % [
|
print("handoff=%d returned_ships=%d ship_count_ok=%s position_ok=%s centroid=%s expected=%s" % [
|
||||||
game_state.surviving_ship_count, returned_ship_count,
|
game_state.surviving_ship_count, returned_ship_count,
|
||||||
ship_count_ok, position_ok, space_fleet.get_centroid(), position_before_leaving
|
ship_count_ok, position_ok, space_fleet.get_centroid(), position_before_leaving
|
||||||
])
|
])
|
||||||
if ship_count_ok and position_ok:
|
if ship_count_ok and composition_ok and position_ok:
|
||||||
print("TEST_PLANET_ATMOSPHERE_TRANSITION: PASS")
|
print("TEST_PLANET_ATMOSPHERE_TRANSITION: PASS")
|
||||||
quit(0)
|
quit(0)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,8 @@ extends SceneTree
|
||||||
|
|
||||||
# Headless gameplay test: run with
|
# Headless gameplay test: run with
|
||||||
# godot --headless -s res://tests/test_planet_prompt_map_interaction.gd
|
# godot --headless -s res://tests/test_planet_prompt_map_interaction.gd
|
||||||
# Regression test for a real bug: EnterAtmosphereButton (shown while
|
# Opening the map while standing on a planet must suppress the shared
|
||||||
# standing on a planet, bottom-center) and the world map's own
|
# contextual action and planet label, then restore them after closing.
|
||||||
# RecenterButton (also bottom-center) occupy the exact same screen
|
|
||||||
# position. Opening the map while standing on a planet must suppress the
|
|
||||||
# enter-atmosphere prompt entirely - not just let it sit hidden behind
|
|
||||||
# the map's backdrop, since closing the map would then abruptly reveal it
|
|
||||||
# at the same instant RecenterButton disappears (which is what most
|
|
||||||
# plausibly explains "buttons blinking/disappearing").
|
|
||||||
|
|
||||||
var main_scene: Node
|
var main_scene: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
|
|
@ -48,7 +42,7 @@ func _initialize() -> void:
|
||||||
world_map = main_scene.get_node("UI/WorldMap")
|
world_map = main_scene.get_node("UI/WorldMap")
|
||||||
map_button = main_scene.get_node("UI/MapButton")
|
map_button = main_scene.get_node("UI/MapButton")
|
||||||
planet_name_label = main_scene.get_node("UI/PlanetNameLabel")
|
planet_name_label = main_scene.get_node("UI/PlanetNameLabel")
|
||||||
enter_button = main_scene.get_node("UI/EnterAtmosphereButton")
|
enter_button = main_scene.get_node("UI/BuildActionButton")
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ func _initialize() -> void:
|
||||||
root.add_child(main_scene)
|
root.add_child(main_scene)
|
||||||
fleet = main_scene.get_node("Fleet")
|
fleet = main_scene.get_node("Fleet")
|
||||||
planet_field = main_scene.get_node("PlanetField")
|
planet_field = main_scene.get_node("PlanetField")
|
||||||
enter_button = main_scene.get_node("UI/EnterAtmosphereButton")
|
enter_button = main_scene.get_node("UI/BuildActionButton")
|
||||||
main_scene.get_node("AsteroidField").set_process(false)
|
main_scene.get_node("AsteroidField").set_process(false)
|
||||||
main_scene.get_node("EnemySpawner").set_process(false)
|
main_scene.get_node("EnemySpawner").set_process(false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ extends SceneTree
|
||||||
# research screen.
|
# research screen.
|
||||||
#
|
#
|
||||||
# Verifies:
|
# Verifies:
|
||||||
# 1. the transfer button appears alongside the action button whenever the
|
# 1. the transfer shortcut appears beside Build whenever the fleet is
|
||||||
# fleet is standing on a building, and opens the transfer panel bound to
|
# standing on a transferable building, and opens the panel bound to
|
||||||
# THAT building - the `== 0` guard failure that made every crafter
|
# THAT building - the `== 0` guard failure that made every crafter
|
||||||
# button look dead is the same trap here
|
# button look dead is the same trap here
|
||||||
# 2. ore moves both ways at an ordinary building, capped by what is
|
# 2. ore moves both ways at an ordinary building, capped by what is
|
||||||
|
|
@ -73,8 +73,6 @@ func _tap(button: Control) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _open_transfer() -> void:
|
func _open_transfer() -> void:
|
||||||
# Contextual choices collapse into one button until it is opened.
|
|
||||||
_tap(controller.action_button)
|
|
||||||
_tap(controller.transfer_button)
|
_tap(controller.transfer_button)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -108,6 +106,8 @@ func _process(delta: float) -> bool:
|
||||||
return false
|
return false
|
||||||
_expect(controller.current_building != null,
|
_expect(controller.current_building != null,
|
||||||
"parking on the crafter never registered")
|
"parking on the crafter never registered")
|
||||||
|
_expect(controller.transfer_button.visible,
|
||||||
|
"transfer shortcut did not appear beside Build")
|
||||||
_open_transfer()
|
_open_transfer()
|
||||||
_expect(controller.transfer_menu.visible,
|
_expect(controller.transfer_menu.visible,
|
||||||
"tapping the transfer button did not open the panel")
|
"tapping the transfer button did not open the panel")
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func _initialize() -> void:
|
||||||
fleet = main_scene.get_node("Fleet")
|
fleet = main_scene.get_node("Fleet")
|
||||||
planet_field = main_scene.get_node("PlanetField")
|
planet_field = main_scene.get_node("PlanetField")
|
||||||
landing_controller = main_scene.get_node("LandingController")
|
landing_controller = main_scene.get_node("LandingController")
|
||||||
enter_button = main_scene.get_node("UI/EnterAtmosphereButton")
|
enter_button = main_scene.get_node("UI/BuildActionButton")
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ var scene: Node
|
||||||
var field: Node
|
var field: Node
|
||||||
var controller: Node
|
var controller: Node
|
||||||
var map_content: Node2D
|
var map_content: Node2D
|
||||||
|
var hub_building: Node2D
|
||||||
var hub: Node2D
|
var hub: Node2D
|
||||||
var stage: int = 0
|
var stage: int = 0
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
|
|
@ -101,11 +102,19 @@ func _process(delta: float) -> bool:
|
||||||
return false
|
return false
|
||||||
for b in field.spawned:
|
for b in field.spawned:
|
||||||
if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB:
|
if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB:
|
||||||
|
hub_building = b
|
||||||
hub = b.visual
|
hub = b.visual
|
||||||
if hub == null:
|
if hub == null:
|
||||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)")
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
|
if not hub_building.available_actions().is_empty():
|
||||||
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (offers a manual resource transfer)")
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
for ship in scene.get_node("Fleet").ships:
|
||||||
|
ship.position = HUB_AT
|
||||||
|
scene.get_node("Fleet").move_to(HUB_AT)
|
||||||
|
|
||||||
# --- 3. drawn as itself, not via the crafter catch-all -------
|
# --- 3. drawn as itself, not via the crafter catch-all -------
|
||||||
# _hub_traffic only exists if _ready() took the transport-hub
|
# _hub_traffic only exists if _ready() took the transport-hub
|
||||||
|
|
@ -142,6 +151,14 @@ func _process(delta: float) -> bool:
|
||||||
# --- 4. animates without ever redrawing (#10) ----------------
|
# --- 4. animates without ever redrawing (#10) ----------------
|
||||||
if elapsed < 2.0:
|
if elapsed < 2.0:
|
||||||
return false
|
return false
|
||||||
|
if controller.current_building != hub_building \
|
||||||
|
or controller.action_button.visible or controller.transfer_button.visible:
|
||||||
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (actionless hub context current=%s expected=%s action=%s transfer=%s)" % [
|
||||||
|
controller.current_building, hub_building, controller.action_button.visible,
|
||||||
|
controller.transfer_button.visible,
|
||||||
|
])
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
print("rasterisations after %.1fs: %d" % [elapsed, hub.draw_count])
|
print("rasterisations after %.1fs: %d" % [elapsed, hub.draw_count])
|
||||||
if hub.draw_count != 1:
|
if hub.draw_count != 1:
|
||||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (redrew its geometry %d times to animate)" % hub.draw_count)
|
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (redrew its geometry %d times to animate)" % hub.draw_count)
|
||||||
|
|
|
||||||
|
|
@ -1,217 +1,89 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
# Headless gameplay test: run with
|
# Hub-owned transporters supply an unfunded combat-vessel order without ever
|
||||||
# godot --headless -s res://tests/test_transporter_supply.gd
|
# being crafted, stored at the mothership, or added to the player fleet.
|
||||||
# Forgejo #19 (part 6 of #12): transporters stationed at a hub fetch ore from
|
|
||||||
# the mothership and deliver it to whichever building is waiting on it, with
|
|
||||||
# no player input at all.
|
|
||||||
#
|
|
||||||
# Verifies:
|
|
||||||
# 1. a transporter is built at the CRAFTER and takes itself to the nearest
|
|
||||||
# hub, rather than appearing at the hub out of nowhere
|
|
||||||
# 2. a queued order the crafter cannot pay for is fulfilled end to end
|
|
||||||
# automatically - the whole point of the ticket
|
|
||||||
# 3. ore is conserved across the entire route (bank + hold + every
|
|
||||||
# building's stock), at every step, not just at the end
|
|
||||||
# 4. ore leaves the bank when it is LOADED, not when the order is placed -
|
|
||||||
# the vessel is carrying something real
|
|
||||||
# 5. transporters are not ordered when there is no hub to station them at
|
|
||||||
# 6. no unbounded node accumulation: the fleet of transporters is exactly
|
|
||||||
# what was built, however many runs they make
|
|
||||||
|
|
||||||
var game_state: Node
|
var state: Node
|
||||||
var scene: Node
|
var crafter_id: int
|
||||||
var fleet: Node
|
var initial_transporters: int
|
||||||
var field: Node
|
var total_at_start: int
|
||||||
var crafter_id: int = 0
|
|
||||||
var hub_id: int = 0
|
|
||||||
var stage: int = 0
|
|
||||||
var elapsed: float = 0.0
|
|
||||||
var total_at_start: int = 0
|
|
||||||
var bank_when_ordered: int = 0
|
|
||||||
var conservation_broken: bool = false
|
|
||||||
# Ore legitimately consumed by completed production. Conservation means
|
|
||||||
# "nothing vanishes in TRANSIT", not "the total never falls" - building a
|
|
||||||
# vessel spends ore, and that is the one way it is allowed to leave.
|
|
||||||
var consumed: int = 0
|
var consumed: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0)
|
var conservation_broken: bool = false
|
||||||
const HUB_AT: Vector2 = Vector2(600.0, 200.0)
|
|
||||||
const CRAFTER_AT: Vector2 = Vector2(900.0, -300.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _total() -> int:
|
|
||||||
var sum: int = game_state.rocks + game_state.fleet_cargo
|
|
||||||
for b in game_state.buildings:
|
|
||||||
if b.type != GameState.BUILDING_MOTHERSHIP:
|
|
||||||
sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE)
|
|
||||||
for t in game_state.transporters:
|
|
||||||
sum += int(t.get("carrying", 0))
|
|
||||||
# Ore inside a build that has STARTED but not finished (#23). It has left
|
|
||||||
# the building's stock and has not yet been consumed by a completed
|
|
||||||
# vessel, so without counting it the total dips for the length of every
|
|
||||||
# build - which is a gap in this sum, not a leak in the game.
|
|
||||||
for b in game_state.buildings:
|
|
||||||
for o in b.get("orders", []):
|
|
||||||
if bool(o.started):
|
|
||||||
sum += game_state.product_cost(String(o.product))
|
|
||||||
return sum
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
game_state = root.get_node("GameState")
|
state = root.get_node("GameState")
|
||||||
game_state.buildings.clear()
|
state.buildings.clear()
|
||||||
game_state.extractors.clear()
|
state.extractors.clear()
|
||||||
game_state.transporters.clear()
|
state.transporters.clear()
|
||||||
game_state.rocks = 200
|
state.stored_vessels.clear()
|
||||||
game_state.fleet_cargo = 0
|
state.vessel_deliveries.clear()
|
||||||
game_state.stored_vessels.clear()
|
state.reset_progression()
|
||||||
game_state.build_time_scale = 0.02
|
state.rocks = 200
|
||||||
game_state.unlocked = ["shipyards", "logistics", "freight", "coordination", "wing", "squadron", "armada"]
|
state.build_time_scale = 0.02
|
||||||
scene = load("res://Main.tscn").instantiate()
|
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||||
root.add_child(scene)
|
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||||
fleet = scene.get_node("Fleet")
|
crafter_id = state.add_building(state.BUILDING_SHIP_CRAFTER, Vector2(900, -300))
|
||||||
field = scene.get_node("BuildingField")
|
|
||||||
game_state.order_completed.connect(_on_order_completed)
|
|
||||||
|
|
||||||
|
if state.queue_order(crafter_id, state.VESSEL_TRANSPORTER):
|
||||||
|
return _fail("the crafter accepted a logistics transporter order")
|
||||||
|
var hub_id: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(600, 200))
|
||||||
|
initial_transporters = state.hub_capacity()
|
||||||
|
if state.transporters.size() != initial_transporters:
|
||||||
|
return _fail("hub provisioned %d transporters, expected %d" % [
|
||||||
|
state.transporters.size(), initial_transporters])
|
||||||
|
for transporter in state.transporters:
|
||||||
|
if int(transporter.hub_id) != hub_id or transporter.state != state.TRANSPORTER_IDLE:
|
||||||
|
return _fail("a provisioned transporter was not idle at its hub")
|
||||||
|
|
||||||
func _on_order_completed(_building_id: int, product: String) -> void:
|
var bank_before: int = state.rocks
|
||||||
consumed += game_state.product_cost(product)
|
if not state.queue_order(crafter_id, state.PRODUCT_SHIP):
|
||||||
|
return _fail("crafter refused the combat-vessel order")
|
||||||
|
if state.rocks != bank_before:
|
||||||
|
return _fail("placing the order changed the bank before a transporter loaded")
|
||||||
|
state.order_completed.connect(_on_order_completed)
|
||||||
|
total_at_start = _total()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
elapsed += delta
|
elapsed += delta
|
||||||
|
if _total() + consumed != total_at_start:
|
||||||
# --- 3. conservation, checked EVERY tick ------------------------------
|
|
||||||
# Ore in flight is the easy thing to lose: a leg that loads without
|
|
||||||
# debiting, or delivers without crediting, would show up here and nowhere
|
|
||||||
# else. Checked continuously rather than at the end, so it cannot be
|
|
||||||
# masked by a later correction.
|
|
||||||
if stage >= 2 and _total() + consumed != total_at_start:
|
|
||||||
conservation_broken = true
|
conservation_broken = true
|
||||||
|
if state.pending_orders(crafter_id) > 0:
|
||||||
|
if elapsed > 25.0:
|
||||||
|
return _fail("hub transporters never supplied the queued order")
|
||||||
|
return false
|
||||||
|
if conservation_broken:
|
||||||
|
return _fail("ore was created or destroyed on the automatic route")
|
||||||
|
if state.transporters.size() != initial_transporters:
|
||||||
|
return _fail("transporters accumulated during delivery")
|
||||||
|
if state.stored_vessels.has(state.VESSEL_TRANSPORTER):
|
||||||
|
return _fail("a transporter appeared in the mothership hangar")
|
||||||
|
print("%d hub-owned transporters supplied the crafter automatically" % initial_transporters)
|
||||||
|
print("TEST_TRANSPORTER_SUPPLY: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
|
||||||
match stage:
|
|
||||||
0:
|
|
||||||
scene.get_node("AsteroidField").set_process(false)
|
|
||||||
scene.get_node("EnemySpawner").set_process(false)
|
|
||||||
for a in get_nodes_in_group("asteroids"):
|
|
||||||
if is_instance_valid(a):
|
|
||||||
a.queue_free()
|
|
||||||
|
|
||||||
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
func _on_order_completed(_building_id: int, product: String) -> void:
|
||||||
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
consumed += state.product_cost(product)
|
||||||
crafter_id = int(game_state.buildings[-1].id)
|
|
||||||
|
|
||||||
# --- 5. no hub yet: a transporter cannot be ordered ----------
|
|
||||||
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (ordered a transporter with no hub to station it at)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("no hub -> transporter order refused")
|
|
||||||
|
|
||||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
func _total() -> int:
|
||||||
hub_id = int(game_state.buildings[-1].id)
|
var sum: int = state.rocks + state.fleet_cargo
|
||||||
elapsed = 0.0
|
for building in state.buildings:
|
||||||
stage = 1
|
if building.type != state.BUILDING_MOTHERSHIP:
|
||||||
return false
|
sum += state.building_resource(int(building.id), state.RESOURCE_ORE)
|
||||||
1:
|
for order in building.get("orders", []):
|
||||||
if elapsed < 0.2:
|
if bool(order.started):
|
||||||
return false
|
sum += state.product_cost(String(order.product))
|
||||||
# --- 1. build a transporter at the crafter ------------------
|
for transporter in state.transporters:
|
||||||
if not game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
sum += int(transporter.get("carrying", 0))
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (could not order a transporter with a hub built)")
|
return sum
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
game_state.deposit_to_building(crafter_id, GameState.TRANSPORTER_COST)
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 2
|
|
||||||
total_at_start = _total()
|
|
||||||
return false
|
|
||||||
2:
|
|
||||||
if elapsed < game_state.build_time(GameState.PRODUCT_TRANSPORTER) * 3.0:
|
|
||||||
return false
|
|
||||||
if game_state.transporters.size() != 1:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (built %d transporters from one order)" % game_state.transporters.size())
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
var t: Dictionary = game_state.transporters[0]
|
|
||||||
if int(t.hub_id) != hub_id:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (transporter did not take the nearest hub)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
# It must fly FROM the crafter TO the hub, not appear at the hub.
|
|
||||||
# Checked on the leg's endpoints rather than its current position,
|
|
||||||
# which is already partway there by now - and being partway is
|
|
||||||
# the correct behaviour.
|
|
||||||
if (t.from as Vector2).distance_to(CRAFTER_AT) > 1.0:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (transporter's first leg starts at %s, not at the crafter that built it)" % t.from)
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
if (t.to as Vector2).distance_to(HUB_AT) > 1.0:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (transporter's first leg does not end at the hub: %s)" % t.to)
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("transporter built at the crafter, heading for hub %d" % hub_id)
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 3
|
|
||||||
return false
|
|
||||||
3:
|
|
||||||
# Give it time to reach the hub and go idle.
|
|
||||||
if elapsed < 4.0:
|
|
||||||
return false
|
|
||||||
if game_state.transporters[0].state != GameState.TRANSPORTER_IDLE:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (never settled at its hub: state=%s)" % game_state.transporters[0].state)
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("stationed at the hub, idle")
|
|
||||||
|
|
||||||
# --- 2 + 4. an order it cannot pay for ----------------------
|
|
||||||
game_state.queue_order(crafter_id, GameState.PRODUCT_SHIP)
|
|
||||||
bank_when_ordered = game_state.rocks
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 4
|
|
||||||
return false
|
|
||||||
4:
|
|
||||||
# One tick: placing the order must not touch the bank.
|
|
||||||
if elapsed < 0.1:
|
|
||||||
return false
|
|
||||||
if game_state.rocks != bank_when_ordered:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (bank moved on placing the order, before anything was loaded)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("order placed, bank untouched at %d" % game_state.rocks)
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 5
|
|
||||||
return false
|
|
||||||
5:
|
|
||||||
# --- 2. the whole run, with no player input -----------------
|
|
||||||
if game_state.pending_orders(crafter_id) > 0:
|
|
||||||
if elapsed > 25.0:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (order never fulfilled: transporter state=%s carrying=%d crafter stock=%d)" % [
|
|
||||||
game_state.transporters[0].state,
|
|
||||||
int(game_state.transporters[0].carrying),
|
|
||||||
game_state.building_stock(crafter_id),
|
|
||||||
])
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
if conservation_broken:
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (ore created or destroyed somewhere along the route)")
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("order fulfilled automatically in %.1fs, no player input" % elapsed)
|
|
||||||
|
|
||||||
# --- 6. no vessel accumulation ------------------------------
|
func _fail(message: String) -> bool:
|
||||||
if game_state.transporters.size() != 1:
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (%s)" % message)
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (%d transporters exist after the run, one was built)" % game_state.transporters.size())
|
quit(1)
|
||||||
quit(1)
|
return true
|
||||||
return true
|
|
||||||
print("still exactly 1 transporter after a full round trip")
|
|
||||||
print("bank=%d crafter stock=%d total=%d + %d consumed = %d (started %d)" % [
|
|
||||||
game_state.rocks, game_state.building_stock(crafter_id), _total(),
|
|
||||||
consumed, _total() + consumed, total_at_start
|
|
||||||
])
|
|
||||||
print("TEST_TRANSPORTER_SUPPLY: PASS")
|
|
||||||
quit(0)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ func _process(delta: float) -> bool:
|
||||||
if elapsed < 0.1:
|
if elapsed < 0.1:
|
||||||
return false
|
return false
|
||||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||||
|
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
|
||||||
|
if fleet_definitions.size() != 2 \
|
||||||
|
or fleet_definitions.any(func(definition): return definition.has_capability("transport")):
|
||||||
|
return _fail("logistics transporters leaked into the combat-fleet catalog")
|
||||||
if ships.size() != definitions.size():
|
if ships.size() != definitions.size():
|
||||||
return _fail("catalog and composed fleet differ in size")
|
return _fail("catalog and composed fleet differ in size")
|
||||||
for i in range(definitions.size()):
|
for i in range(definitions.size()):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue