Add timed craft queue and hangar
This commit is contained in:
parent
b6427b080d
commit
82552464d2
14 changed files with 660 additions and 191 deletions
|
|
@ -42,6 +42,21 @@ extends Node2D
|
||||||
@onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton")
|
@onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton")
|
||||||
@onready var lab_close_button: IconButton = get_node("../UI/LabMenu/CloseButton")
|
@onready var lab_close_button: IconButton = get_node("../UI/LabMenu/CloseButton")
|
||||||
@onready var craft_shield_button: IconButton = get_node("../UI/CraftMenu/ShieldButton")
|
@onready var craft_shield_button: IconButton = get_node("../UI/CraftMenu/ShieldButton")
|
||||||
|
@onready var queue_slots: Array = [
|
||||||
|
get_node("../UI/CraftMenu/QueueSlot0"), get_node("../UI/CraftMenu/QueueSlot1"),
|
||||||
|
get_node("../UI/CraftMenu/QueueSlot2"), get_node("../UI/CraftMenu/QueueSlot3"),
|
||||||
|
]
|
||||||
|
@onready var queue_detail: Label = get_node("../UI/CraftMenu/QueueDetail")
|
||||||
|
@onready var hangar_button: IconButton = get_node("../UI/HangarButton")
|
||||||
|
@onready var hangar_menu: Control = get_node("../UI/HangarMenu")
|
||||||
|
@onready var hangar_list: Label = get_node("../UI/HangarMenu/ListLabel")
|
||||||
|
@onready var hangar_status: Label = get_node("../UI/HangarMenu/StatusLabel")
|
||||||
|
@onready var hangar_next_button: IconButton = get_node("../UI/HangarMenu/NextButton")
|
||||||
|
@onready var hangar_crew_button: IconButton = get_node("../UI/HangarMenu/CrewButton")
|
||||||
|
@onready var hangar_close_button: IconButton = get_node("../UI/HangarMenu/CloseButton")
|
||||||
|
|
||||||
|
# Which stored vessel the hangar is offering to crew.
|
||||||
|
var hangar_choice: int = 0
|
||||||
@onready var build_menu_entries: Array = [
|
@onready var build_menu_entries: Array = [
|
||||||
[GameState.BUILDING_MOTHERSHIP, build_mothership_button,
|
[GameState.BUILDING_MOTHERSHIP, build_mothership_button,
|
||||||
get_node("../UI/BuildMenu/MothershipLabel"), get_node("../UI/BuildMenu/MothershipCost")],
|
get_node("../UI/BuildMenu/MothershipLabel"), get_node("../UI/BuildMenu/MothershipCost")],
|
||||||
|
|
@ -199,6 +214,12 @@ func _ready() -> void:
|
||||||
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
||||||
|
|
||||||
transfer_button.pressed.connect(_on_transfer_pressed)
|
transfer_button.pressed.connect(_on_transfer_pressed)
|
||||||
|
hangar_button.pressed.connect(_on_hangar_pressed)
|
||||||
|
hangar_close_button.pressed.connect(func(): hangar_menu.visible = false)
|
||||||
|
hangar_next_button.pressed.connect(_on_hangar_next)
|
||||||
|
hangar_crew_button.pressed.connect(_on_hangar_crew)
|
||||||
|
for i in range(queue_slots.size()):
|
||||||
|
queue_slots[i].pressed.connect(_on_queue_slot.bind(i))
|
||||||
transfer_close_button.pressed.connect(func(): transfer_menu.visible = false)
|
transfer_close_button.pressed.connect(func(): transfer_menu.visible = false)
|
||||||
to_building_batch_button.pressed.connect(_on_transfer.bind(true, GameState.CARGO_LOAD_BATCH))
|
to_building_batch_button.pressed.connect(_on_transfer.bind(true, GameState.CARGO_LOAD_BATCH))
|
||||||
to_building_all_button.pressed.connect(_on_transfer.bind(true, ALL))
|
to_building_all_button.pressed.connect(_on_transfer.bind(true, ALL))
|
||||||
|
|
@ -222,13 +243,16 @@ func _ready() -> void:
|
||||||
transfer_menu.visible = false
|
transfer_menu.visible = false
|
||||||
action_button.visible = false
|
action_button.visible = false
|
||||||
transfer_button.visible = false
|
transfer_button.visible = false
|
||||||
|
hangar_button.visible = false
|
||||||
|
hangar_menu.visible = false
|
||||||
|
|
||||||
|
|
||||||
# True while a building prompt or panel owns the shared action slot, so the
|
# True while a building prompt or panel owns the shared action slot, so the
|
||||||
# enter-atmosphere prompt knows to stay out of the way.
|
# enter-atmosphere prompt knows to stay out of the way.
|
||||||
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 current_building != null or 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
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
|
@ -254,7 +278,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible \
|
if build_menu.visible or craft_menu.visible or world_map.visible or lab_menu.visible \
|
||||||
or transfer_menu.visible:
|
or transfer_menu.visible or hangar_menu.visible:
|
||||||
# These panels have no raw-input handler of their own, but the world
|
# These panels have no raw-input handler of their own, but the world
|
||||||
# behind them must still not react.
|
# behind them must still not react.
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
@ -282,11 +306,12 @@ func _process(delta: float) -> void:
|
||||||
|
|
||||||
if world_map.visible or build_menu.visible or craft_menu.visible \
|
if world_map.visible or build_menu.visible or craft_menu.visible \
|
||||||
or formation_menu.visible or skill_menu.visible or lab_menu.visible \
|
or formation_menu.visible or skill_menu.visible or lab_menu.visible \
|
||||||
or transfer_menu.visible:
|
or transfer_menu.visible or hangar_menu.visible:
|
||||||
current_building = null
|
current_building = null
|
||||||
pending_building = null
|
pending_building = null
|
||||||
action_button.visible = false
|
action_button.visible = false
|
||||||
transfer_button.visible = false
|
transfer_button.visible = false
|
||||||
|
hangar_button.visible = false
|
||||||
return
|
return
|
||||||
|
|
||||||
# Debounced exactly like the enter-atmosphere prompt, so the action
|
# Debounced exactly like the enter-atmosphere prompt, so the action
|
||||||
|
|
@ -304,6 +329,10 @@ func _process(delta: float) -> void:
|
||||||
|
|
||||||
current_building = pending_building
|
current_building = pending_building
|
||||||
action_button.visible = current_building != null
|
action_button.visible = current_building != null
|
||||||
|
# The hangar is where finished vessels are delivered (#23), so it belongs
|
||||||
|
# at the mothership and nowhere else.
|
||||||
|
hangar_button.visible = current_building != null \
|
||||||
|
and current_building.building_type == GameState.BUILDING_MOTHERSHIP
|
||||||
# Every building can hold ore, so the transfer button rides alongside the
|
# Every building can hold ore, so the transfer button rides alongside the
|
||||||
# action button rather than being one of the things the action button
|
# action button rather than being one of the things the action button
|
||||||
# turns into - the two are independent choices at the same structure.
|
# turns into - the two are independent choices at the same structure.
|
||||||
|
|
@ -662,27 +691,19 @@ func _refresh_craft_status() -> void:
|
||||||
craft_shield_button.enabled = GameState.shield_vessel_unlocked()
|
craft_shield_button.enabled = GameState.shield_vessel_unlocked()
|
||||||
craft_shield_button.queue_redraw()
|
craft_shield_button.queue_redraw()
|
||||||
|
|
||||||
# A full fleet cannot take another vessel, so the order would be refused.
|
# Fleet capacity no longer gates ORDERING (#23). A finished vessel waits
|
||||||
# Showing that on the button is the difference between "not available yet"
|
# at the mothership until the player crews it, so a full fleet is a
|
||||||
# and a button that looks live and silently does nothing when pressed.
|
# reason not to collect one yet - not a reason the crafter cannot build.
|
||||||
var room: bool = fleet.ships.size() + GameState.vessels_owed.size() \
|
# This is also what stopped the crafter being inert on a fresh save,
|
||||||
+ GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP) \
|
# where the fleet starts at exactly its cap.
|
||||||
< GameState.max_fleet_size()
|
craft_interceptor_button.enabled = true
|
||||||
craft_interceptor_button.enabled = room
|
|
||||||
craft_interceptor_button.queue_redraw()
|
craft_interceptor_button.queue_redraw()
|
||||||
# One statement of the fleet count, either way round: the capacity line
|
craft_status.text += " Fleet %d/%d Ready %d" % [
|
||||||
# replaces the plain readout rather than being bolted in front of it.
|
fleet.ships.size(), GameState.max_fleet_size(), GameState.stored_vessels.size()
|
||||||
if room:
|
|
||||||
craft_status.text += " Fleet %d/%d" % [fleet.ships.size(), GameState.max_fleet_size()]
|
|
||||||
else:
|
|
||||||
var queued_ships: int = GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP)
|
|
||||||
var owed: int = GameState.vessels_owed.size() + queued_ships
|
|
||||||
var pending_note: String = "" if owed == 0 else " (%d already on order)" % owed
|
|
||||||
craft_status.text = "No room in the fleet: %d/%d%s - research capacity at the mothership\n%s" % [
|
|
||||||
fleet.ships.size(), GameState.max_fleet_size(), pending_note, craft_status.text
|
|
||||||
]
|
]
|
||||||
# LAST, so a message about what just happened is the first line the
|
# LAST, so a message about what just happened is the first line the
|
||||||
# player reads rather than being buried under the standing state.
|
# player reads rather than being buried under the standing state.
|
||||||
|
_refresh_queue()
|
||||||
if craft_message != "":
|
if craft_message != "":
|
||||||
if Time.get_ticks_msec() < craft_message_until:
|
if Time.get_ticks_msec() < craft_message_until:
|
||||||
craft_status.text = "%s\n%s" % [craft_message, craft_status.text]
|
craft_status.text = "%s\n%s" % [craft_message, craft_status.text]
|
||||||
|
|
@ -694,13 +715,6 @@ func _on_craft_interceptor() -> void:
|
||||||
if craft_building_id == 0:
|
if craft_building_id == 0:
|
||||||
_say("No crafter selected - reopen this panel from the building")
|
_say("No crafter selected - reopen this panel from the building")
|
||||||
return
|
return
|
||||||
# Still refused when there is nowhere to put the vessel - queueing an
|
|
||||||
# order that can never be delivered would just bank vessels_owed forever.
|
|
||||||
if fleet.ships.size() + GameState.vessels_owed.size() \
|
|
||||||
+ GameState.pending_orders_of(craft_building_id, GameState.PRODUCT_SHIP) \
|
|
||||||
>= GameState.max_fleet_size():
|
|
||||||
_say("Fleet is full (%d slots)" % GameState.max_fleet_size())
|
|
||||||
return
|
|
||||||
# No affordability check: an order the crafter cannot pay for yet is
|
# No affordability check: an order the crafter cannot pay for yet is
|
||||||
# QUEUED, and completes by itself once ore is delivered here (#16). But
|
# QUEUED, and completes by itself once ore is delivered here (#16). But
|
||||||
# queueing silently looks identical to a dead button, so it says which
|
# queueing silently looks identical to a dead button, so it says which
|
||||||
|
|
@ -839,10 +853,6 @@ func _on_craft_shield() -> void:
|
||||||
if not GameState.shield_vessel_unlocked():
|
if not GameState.shield_vessel_unlocked():
|
||||||
_say("Shield vessels must be unlocked at a ship lab first")
|
_say("Shield vessels must be unlocked at a ship lab first")
|
||||||
return
|
return
|
||||||
if fleet.ships.size() + GameState.vessels_owed.size() \
|
|
||||||
+ GameState.pending_orders(craft_building_id) >= GameState.max_fleet_size():
|
|
||||||
_say("Fleet is full (%d slots)" % GameState.max_fleet_size())
|
|
||||||
return
|
|
||||||
GameState.queue_order(craft_building_id, GameState.VESSEL_SHIELD)
|
GameState.queue_order(craft_building_id, GameState.VESSEL_SHIELD)
|
||||||
_report_order(GameState.VESSEL_SHIELD)
|
_report_order(GameState.VESSEL_SHIELD)
|
||||||
|
|
||||||
|
|
@ -927,6 +937,115 @@ func _refresh_transfer() -> void:
|
||||||
button.queue_redraw()
|
button.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_queue() -> void:
|
||||||
|
# The queue down the right of the craft panel: one slot per pending
|
||||||
|
# vessel, the first being the one actually under construction. Tapping a
|
||||||
|
# slot cancels it (#23).
|
||||||
|
var orders: Array = GameState.order_list(craft_building_id)
|
||||||
|
for i in range(queue_slots.size()):
|
||||||
|
var slot: IconButton = queue_slots[i]
|
||||||
|
slot.visible = i < orders.size()
|
||||||
|
if not slot.visible:
|
||||||
|
continue
|
||||||
|
var order: Dictionary = orders[i]
|
||||||
|
slot.icon = IconButton.Icon.TRANSPORT_HUB if order.product == GameState.PRODUCT_TRANSPORTER \
|
||||||
|
else IconButton.Icon.SHIP
|
||||||
|
slot.glow_color = _vessel_colour(String(order.product))
|
||||||
|
# Only the one under construction reads as active; the rest are
|
||||||
|
# waiting, and greyed says so without needing a caption each.
|
||||||
|
slot.enabled = bool(order.started)
|
||||||
|
slot.queue_redraw()
|
||||||
|
|
||||||
|
if orders.is_empty():
|
||||||
|
queue_detail.text = "Nothing queued"
|
||||||
|
return
|
||||||
|
var head: Dictionary = orders[0]
|
||||||
|
if bool(head.started):
|
||||||
|
queue_detail.text = "Building %s %d%%\nTap to cancel" % [
|
||||||
|
GameState.VESSEL_NAMES.get(head.product, head.product),
|
||||||
|
int(round(float(head.progress) * 100.0)),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
queue_detail.text = "%s waiting for ore\nTap to cancel" % \
|
||||||
|
GameState.VESSEL_NAMES.get(head.product, head.product)
|
||||||
|
|
||||||
|
|
||||||
|
func _vessel_colour(product: String) -> Color:
|
||||||
|
match product:
|
||||||
|
GameState.VESSEL_SHIELD:
|
||||||
|
return Color(0.45, 0.8, 1.0)
|
||||||
|
GameState.PRODUCT_TRANSPORTER:
|
||||||
|
return Color(0.95, 0.55, 0.85)
|
||||||
|
_:
|
||||||
|
return Color(0.25, 0.85, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_queue_slot(index: int) -> void:
|
||||||
|
var orders: Array = GameState.order_list(craft_building_id)
|
||||||
|
if index >= orders.size():
|
||||||
|
return
|
||||||
|
var vessel: String = GameState.VESSEL_NAMES.get(orders[index].product, orders[index].product)
|
||||||
|
var refunded: bool = bool(orders[index].started)
|
||||||
|
if not GameState.cancel_order(craft_building_id, index):
|
||||||
|
return
|
||||||
|
_say("%s cancelled%s" % [vessel, " - ore refunded here" if refunded else ""])
|
||||||
|
_refresh_queue()
|
||||||
|
|
||||||
|
|
||||||
|
# --- hangar (#23) ---------------------------------------------------------
|
||||||
|
|
||||||
|
func _on_hangar_pressed() -> void:
|
||||||
|
hangar_choice = 0
|
||||||
|
hangar_status.text = ""
|
||||||
|
hangar_menu.visible = true
|
||||||
|
_refresh_hangar()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hangar_next() -> void:
|
||||||
|
if GameState.stored_vessels.is_empty():
|
||||||
|
return
|
||||||
|
hangar_choice = (hangar_choice + 1) % GameState.stored_vessels.size()
|
||||||
|
_refresh_hangar()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hangar_crew() -> void:
|
||||||
|
if GameState.stored_vessels.is_empty():
|
||||||
|
hangar_status.text = "Nothing built yet - order vessels at a ship crafter"
|
||||||
|
return
|
||||||
|
if fleet.ships.size() >= GameState.max_fleet_size():
|
||||||
|
hangar_status.text = "Fleet is full (%d/%d) - research capacity at the mothership" % [
|
||||||
|
fleet.ships.size(), GameState.max_fleet_size()
|
||||||
|
]
|
||||||
|
return
|
||||||
|
var product: String = GameState.claim_stored_vessel(hangar_choice)
|
||||||
|
if product == "" or not fleet.add_ship(product):
|
||||||
|
hangar_status.text = "Could not crew that vessel"
|
||||||
|
return
|
||||||
|
GameState.take_stored_vessel(hangar_choice)
|
||||||
|
hangar_status.text = "%s joined the fleet" % GameState.VESSEL_NAMES.get(product, product)
|
||||||
|
hangar_choice = 0
|
||||||
|
_refresh_hangar()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_hangar() -> void:
|
||||||
|
var stored: Array = GameState.stored_vessels
|
||||||
|
if stored.is_empty():
|
||||||
|
hangar_list.text = "No vessels waiting.\n\nOrder them at a ship crafter - finished vessels are delivered here."
|
||||||
|
hangar_crew_button.enabled = false
|
||||||
|
hangar_next_button.enabled = false
|
||||||
|
else:
|
||||||
|
hangar_choice = clampi(hangar_choice, 0, stored.size() - 1)
|
||||||
|
var lines: Array = []
|
||||||
|
for i in range(stored.size()):
|
||||||
|
var marker: String = ">" if i == hangar_choice else " "
|
||||||
|
lines.append("%s %s" % [marker, GameState.VESSEL_NAMES.get(stored[i], stored[i])])
|
||||||
|
hangar_list.text = "\n".join(lines)
|
||||||
|
hangar_crew_button.enabled = fleet.ships.size() < GameState.max_fleet_size()
|
||||||
|
hangar_next_button.enabled = stored.size() > 1
|
||||||
|
hangar_crew_button.queue_redraw()
|
||||||
|
hangar_next_button.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _on_formation_changed() -> void:
|
func _on_formation_changed() -> void:
|
||||||
fleet.apply_formation()
|
fleet.apply_formation()
|
||||||
|
|
||||||
|
|
|
||||||
14
Fleet.gd
14
Fleet.gd
|
|
@ -128,9 +128,6 @@ func move_in_direction(direction: Vector2) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
# Before the steering early-out: a ship the crafter has already built and
|
|
||||||
# paid for is owed whether or not the fleet happens to be flying (#16).
|
|
||||||
_claim_owed_ships()
|
|
||||||
if steer_direction == Vector2.ZERO:
|
if steer_direction == Vector2.ZERO:
|
||||||
return
|
return
|
||||||
# The anchor - the fleet's formation centre - travels at cruise_speed.
|
# The anchor - the fleet's formation centre - travels at cruise_speed.
|
||||||
|
|
@ -140,17 +137,6 @@ func _process(delta: float) -> void:
|
||||||
_assign_slot_targets()
|
_assign_slot_targets()
|
||||||
|
|
||||||
|
|
||||||
func _claim_owed_ships() -> void:
|
|
||||||
# Production completes in the GameState autoload, which may well happen
|
|
||||||
# while the player is on a planet's surface with no Fleet node in
|
|
||||||
# existence to hand a vessel to. Those vessels wait in `vessels_owed`
|
|
||||||
# fleet with room can take them.
|
|
||||||
while not GameState.vessels_owed.is_empty() and ships.size() < GameState.max_fleet_size():
|
|
||||||
if not add_ship(String(GameState.vessels_owed[0])):
|
|
||||||
return
|
|
||||||
GameState.vessels_owed.pop_front()
|
|
||||||
|
|
||||||
|
|
||||||
func _assign_slot_targets() -> void:
|
func _assign_slot_targets() -> void:
|
||||||
for i in range(ships.size()):
|
for i in range(ships.size()):
|
||||||
ships[i].set_move_target(fleet_target + formation_offsets[i])
|
ships[i].set_move_target(fleet_target + formation_offsets[i])
|
||||||
|
|
|
||||||
137
GameState.gd
137
GameState.gd
|
|
@ -294,12 +294,15 @@ func reset_progression() -> void:
|
||||||
# on the same spot.
|
# on the same spot.
|
||||||
var next_building_id: int = 1
|
var next_building_id: int = 1
|
||||||
|
|
||||||
# Vessels that have been paid for and built but not yet handed to the fleet,
|
# Finished vessels waiting at the mothership for the player to collect
|
||||||
# as a list of VESSEL_* types - a shield vessel finishing while the player is
|
# (#23), as a list of VESSEL_* types - a shield vessel must not come back as
|
||||||
# on a planet must not come back as an interceptor. Production is simulated
|
# an interceptor. They are NOT handed to the fleet automatically: the player
|
||||||
# here, in the autoload, the same way extractor haulers are, so an order can
|
# picks what to crew from the hangar, so a full fleet no longer stops a
|
||||||
# complete where there is no Fleet node to hand a ship to.
|
# crafter from building.
|
||||||
var vessels_owed: Array = []
|
#
|
||||||
|
# Production is simulated here, in the autoload, the same way extractor
|
||||||
|
# haulers are, so a build finishes whether or not the player is in space.
|
||||||
|
var stored_vessels: Array = []
|
||||||
|
|
||||||
signal order_completed(building_id: int, type: String)
|
signal order_completed(building_id: int, type: String)
|
||||||
|
|
||||||
|
|
@ -337,6 +340,24 @@ const VESSEL_NAMES: Dictionary = {
|
||||||
|
|
||||||
const SHIELD_COST: int = 14
|
const SHIELD_COST: int = 14
|
||||||
|
|
||||||
|
# How long one vessel takes to build, once its ore is actually in the
|
||||||
|
# building. A queue you can watch and cancel needs time to exist in.
|
||||||
|
const BUILD_TIMES: Dictionary = {
|
||||||
|
VESSEL_INTERCEPTOR: 10.0,
|
||||||
|
VESSEL_SHIELD: 16.0,
|
||||||
|
VESSEL_TRANSPORTER: 8.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Test seam. Headless tests need the timed path - queued, started, finishing
|
||||||
|
# - without sitting through a real 10-second build each, and a suite that
|
||||||
|
# slow stops being run. Left at 1.0 in the game.
|
||||||
|
var build_time_scale: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
func build_time(product: String) -> float:
|
||||||
|
return float(BUILD_TIMES.get(product, 10.0)) * build_time_scale
|
||||||
|
|
||||||
# Researched at the ship lab, per vessel type. Separate from SKILL_TREE on
|
# Researched at the ship lab, per vessel type. Separate from SKILL_TREE on
|
||||||
# purpose: that gates what the player may build at all and is a strategic
|
# purpose: that gates what the player may build at all and is a strategic
|
||||||
# spine, while these are repeatable tuning on a vessel you already have.
|
# spine, while these are repeatable tuning on a vessel you already have.
|
||||||
|
|
@ -553,11 +574,44 @@ func pending_orders_of(id: int, product: String) -> int:
|
||||||
return 0
|
return 0
|
||||||
var n: int = 0
|
var n: int = 0
|
||||||
for o in b.get("orders", []):
|
for o in b.get("orders", []):
|
||||||
if o == product:
|
if String(o.product) == product:
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
func order_list(id: int) -> Array:
|
||||||
|
# What the queue panel draws: product, and how far along the one being
|
||||||
|
# built is. Everything behind it reads as simply waiting.
|
||||||
|
var out: Array = []
|
||||||
|
var b: Dictionary = find_building(id)
|
||||||
|
if b.is_empty():
|
||||||
|
return out
|
||||||
|
for o in b.get("orders", []):
|
||||||
|
var product: String = String(o.product)
|
||||||
|
out.append({
|
||||||
|
"product": product,
|
||||||
|
"started": bool(o.started),
|
||||||
|
"progress": clampf(float(o.elapsed) / maxf(build_time(product), 0.001), 0.0, 1.0),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func cancel_order(id: int, index: int) -> bool:
|
||||||
|
# Refunds whatever was actually taken: a build that has started has its
|
||||||
|
# ore in the building already, one still waiting never paid.
|
||||||
|
var b: Dictionary = find_building(id)
|
||||||
|
if b.is_empty():
|
||||||
|
return false
|
||||||
|
var orders: Array = b.get("orders", [])
|
||||||
|
if index < 0 or index >= orders.size():
|
||||||
|
return false
|
||||||
|
var order: Dictionary = orders[index]
|
||||||
|
if bool(order.started):
|
||||||
|
b.stock = int(b.get("stock", 0)) + product_cost(String(order.product))
|
||||||
|
orders.remove_at(index)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func outstanding_cost(id: int) -> int:
|
func outstanding_cost(id: int) -> int:
|
||||||
# What this building still needs delivered to clear everything queued.
|
# What this building still needs delivered to clear everything queued.
|
||||||
var b: Dictionary = find_building(id)
|
var b: Dictionary = find_building(id)
|
||||||
|
|
@ -565,7 +619,9 @@ func outstanding_cost(id: int) -> int:
|
||||||
return 0
|
return 0
|
||||||
var need: int = 0
|
var need: int = 0
|
||||||
for o in b.get("orders", []):
|
for o in b.get("orders", []):
|
||||||
need += product_cost(o)
|
# A started build has already paid, so it is not still owed.
|
||||||
|
if not bool(o.started):
|
||||||
|
need += product_cost(String(o.product))
|
||||||
return maxi(need - int(b.get("stock", 0)), 0)
|
return maxi(need - int(b.get("stock", 0)), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -592,42 +648,71 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
|
||||||
if product == PRODUCT_TRANSPORTER and not can_station_transporter():
|
if product == PRODUCT_TRANSPORTER and not can_station_transporter():
|
||||||
# Every berth is taken or spoken for (#20).
|
# Every berth is taken or spoken for (#20).
|
||||||
return false
|
return false
|
||||||
(b.orders as Array).append(product)
|
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _advance_production() -> void:
|
func _advance_production(delta: float) -> void:
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
if not can_produce(b.type):
|
if not can_produce(b.type):
|
||||||
continue
|
continue
|
||||||
# `while`, not `if`: a delivery big enough to cover several queued
|
|
||||||
# orders should clear them all rather than dribbling one out per
|
|
||||||
# frame.
|
|
||||||
var orders: Array = b.get("orders", [])
|
var orders: Array = b.get("orders", [])
|
||||||
while not orders.is_empty():
|
if orders.is_empty():
|
||||||
var product: String = String(orders[0])
|
continue
|
||||||
|
|
||||||
|
# ONE at a time, in order. A queue the player can watch and cancel
|
||||||
|
# has to have a thing being built and things waiting behind it -
|
||||||
|
# clearing the whole queue the instant the ore lands (which is what
|
||||||
|
# this did before #23) leaves nothing to see or cancel.
|
||||||
|
var order: Dictionary = orders[0]
|
||||||
|
var product: String = String(order.product)
|
||||||
var cost: int = product_cost(product)
|
var cost: int = product_cost(product)
|
||||||
|
|
||||||
|
if not bool(order.started):
|
||||||
if int(b.get("stock", 0)) < cost:
|
if int(b.get("stock", 0)) < cost:
|
||||||
break
|
continue
|
||||||
if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity():
|
if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity():
|
||||||
# A hub was demolished after this was ordered, so the berth it
|
# A hub was demolished after this was ordered, so the berth it
|
||||||
# was going to use is gone. Hold the order rather than
|
# was going to use is gone. Hold rather than overfill.
|
||||||
# overfilling what is left.
|
continue
|
||||||
break
|
|
||||||
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
|
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
|
||||||
# Every hub was demolished while this sat in the queue. Leave
|
continue
|
||||||
# it queued and the stock untouched rather than producing a
|
|
||||||
# vessel with nowhere to live.
|
|
||||||
break
|
|
||||||
b.stock = int(b.stock) - cost
|
b.stock = int(b.stock) - cost
|
||||||
|
order.started = true
|
||||||
|
|
||||||
|
order.elapsed = float(order.elapsed) + delta
|
||||||
|
if float(order.elapsed) < build_time(product):
|
||||||
|
continue
|
||||||
|
|
||||||
orders.pop_front()
|
orders.pop_front()
|
||||||
if product == PRODUCT_TRANSPORTER:
|
if product == PRODUCT_TRANSPORTER:
|
||||||
|
# Transporters are unchanged: they take themselves to the nearest
|
||||||
|
# hub and wait for work (#19).
|
||||||
_station_transporter(b.position)
|
_station_transporter(b.position)
|
||||||
else:
|
else:
|
||||||
vessels_owed.append(product)
|
# Everything else is delivered to the mothership and waits there
|
||||||
|
# until the player crews it (#23).
|
||||||
|
stored_vessels.append(product)
|
||||||
order_completed.emit(int(b.id), product)
|
order_completed.emit(int(b.id), product)
|
||||||
|
|
||||||
|
|
||||||
|
func claim_stored_vessel(index: int) -> String:
|
||||||
|
# Hands one finished vessel over to be crewed. Returns its type, or ""
|
||||||
|
# when there is nothing there - the caller decides whether the fleet
|
||||||
|
# actually has room.
|
||||||
|
if index < 0 or index >= stored_vessels.size():
|
||||||
|
return ""
|
||||||
|
return String(stored_vessels[index])
|
||||||
|
|
||||||
|
|
||||||
|
func take_stored_vessel(index: int) -> String:
|
||||||
|
var product: String = claim_stored_vessel(index)
|
||||||
|
if product == "":
|
||||||
|
return ""
|
||||||
|
stored_vessels.remove_at(index)
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
func has_mothership() -> bool:
|
func has_mothership() -> bool:
|
||||||
return get_mothership_position() != null
|
return get_mothership_position() != null
|
||||||
|
|
||||||
|
|
@ -756,7 +841,7 @@ func _process(delta: float) -> void:
|
||||||
# Runs in the autoload, so extraction keeps earning - and queued orders
|
# Runs in the autoload, so extraction keeps earning - and queued orders
|
||||||
# keep completing - while the player is on a planet's surface or nowhere
|
# keep completing - while the player is on a planet's surface or nowhere
|
||||||
# near either.
|
# near either.
|
||||||
_advance_production()
|
_advance_production(delta)
|
||||||
_advance_transporters(delta)
|
_advance_transporters(delta)
|
||||||
if extractors.is_empty():
|
if extractors.is_empty():
|
||||||
return
|
return
|
||||||
|
|
@ -827,7 +912,7 @@ func pending_transporter_orders() -> int:
|
||||||
var n: int = 0
|
var n: int = 0
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
for o in b.get("orders", []):
|
for o in b.get("orders", []):
|
||||||
if o == PRODUCT_TRANSPORTER:
|
if String(o.product) == PRODUCT_TRANSPORTER:
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
|
||||||
198
Main.tscn
198
Main.tscn
|
|
@ -145,6 +145,18 @@ offset_bottom = -572
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 2
|
icon = 2
|
||||||
|
|
||||||
|
[node name="HangarButton" type="Control" parent="UI"]
|
||||||
|
visible = false
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 24
|
||||||
|
offset_right = 172
|
||||||
|
offset_top = -776
|
||||||
|
offset_bottom = -628
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 2
|
||||||
|
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
|
||||||
anchor_top = 1.0
|
anchor_top = 1.0
|
||||||
|
|
@ -631,6 +643,105 @@ script = ExtResource("10")
|
||||||
icon = 1
|
icon = 1
|
||||||
glow_color = Color(0, 0.62, 0.7, 1)
|
glow_color = Color(0, 0.62, 0.7, 1)
|
||||||
|
|
||||||
|
[node name="HangarMenu" type="Control" parent="UI" groups=["modal_menus"]]
|
||||||
|
visible = false
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
|
||||||
|
[node name="Backdrop" type="ColorRect" parent="UI/HangarMenu"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0.03, 0.03, 0.08, 0.94)
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="UI/HangarMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = 24
|
||||||
|
offset_top = 60
|
||||||
|
offset_right = -210
|
||||||
|
offset_bottom = 170
|
||||||
|
text = "Hangar"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="ListLabel" type="Label" parent="UI/HangarMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32
|
||||||
|
offset_top = 200
|
||||||
|
offset_right = -32
|
||||||
|
offset_bottom = -420
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="UI/HangarMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32
|
||||||
|
offset_top = -410
|
||||||
|
offset_right = -32
|
||||||
|
offset_bottom = -300
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="NextButton" type="Control" parent="UI/HangarMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -230
|
||||||
|
offset_right = -110
|
||||||
|
offset_top = -280
|
||||||
|
offset_bottom = -160
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 6
|
||||||
|
glow_color = Color(0.55, 0.95, 0.9, 1)
|
||||||
|
|
||||||
|
[node name="CrewButton" type="Control" parent="UI/HangarMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 110
|
||||||
|
offset_right = 230
|
||||||
|
offset_top = -280
|
||||||
|
offset_bottom = -160
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.45, 0.9, 0.55, 1)
|
||||||
|
|
||||||
|
[node name="HintLabel" type="Label" parent="UI/HangarMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 24
|
||||||
|
offset_top = -150
|
||||||
|
offset_right = -24
|
||||||
|
offset_bottom = -60
|
||||||
|
theme_override_font_sizes/font_size = 28
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = "Next Crew"
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Control" parent="UI/HangarMenu"]
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -172
|
||||||
|
offset_top = 24
|
||||||
|
offset_right = -24
|
||||||
|
offset_bottom = 172
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 1
|
||||||
|
glow_color = Color(0.45, 0.9, 0.55, 1)
|
||||||
|
|
||||||
[node name="LabMenu" type="Control" parent="UI" groups=["modal_menus"]]
|
[node name="LabMenu" type="Control" parent="UI" groups=["modal_menus"]]
|
||||||
visible = false
|
visible = false
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
@ -828,7 +939,7 @@ mouse_filter = 2
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
offset_left = 24
|
offset_left = 24
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_right = -24
|
offset_right = -200
|
||||||
offset_bottom = 420
|
offset_bottom = 420
|
||||||
theme_override_font_sizes/font_size = 36
|
theme_override_font_sizes/font_size = 36
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
|
|
@ -917,6 +1028,91 @@ autowrap_mode = 2
|
||||||
text = "Shield Vessel"
|
text = "Shield Vessel"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -200
|
||||||
|
offset_right = -10
|
||||||
|
offset_top = -450
|
||||||
|
offset_bottom = -390
|
||||||
|
theme_override_font_sizes/font_size = 26
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = "Queue"
|
||||||
|
|
||||||
|
[node name="QueueDetail" type="Label" parent="UI/CraftMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 24
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = -150
|
||||||
|
offset_bottom = -30
|
||||||
|
theme_override_font_sizes/font_size = 26
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="QueueSlot0" type="Control" parent="UI/CraftMenu"]
|
||||||
|
visible = false
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -140
|
||||||
|
offset_right = -30
|
||||||
|
offset_top = -380
|
||||||
|
offset_bottom = -270
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
|
|
||||||
|
[node name="QueueSlot1" type="Control" parent="UI/CraftMenu"]
|
||||||
|
visible = false
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -140
|
||||||
|
offset_right = -30
|
||||||
|
offset_top = -262
|
||||||
|
offset_bottom = -152
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
|
|
||||||
|
[node name="QueueSlot2" type="Control" parent="UI/CraftMenu"]
|
||||||
|
visible = false
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -140
|
||||||
|
offset_right = -30
|
||||||
|
offset_top = -144
|
||||||
|
offset_bottom = -34
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
|
|
||||||
|
[node name="QueueSlot3" type="Control" parent="UI/CraftMenu"]
|
||||||
|
visible = false
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -140
|
||||||
|
offset_right = -30
|
||||||
|
offset_top = -26
|
||||||
|
offset_bottom = 84
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
|
|
||||||
[node name="CloseButton" type="Control" parent="UI/CraftMenu"]
|
[node name="CloseButton" type="Control" parent="UI/CraftMenu"]
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ set -uo pipefail
|
||||||
PROJECT_DIR="$1"; shift
|
PROJECT_DIR="$1"; shift
|
||||||
JOBS="$1"; shift
|
JOBS="$1"; shift
|
||||||
VERBOSE="$1"; shift
|
VERBOSE="$1"; shift
|
||||||
|
# A broken test can otherwise leave SceneTree alive after an initialization
|
||||||
|
# error, printing the same error every frame forever. Gameplay tests normally
|
||||||
|
# complete in seconds, even on a busy phone, so make that failure explicit
|
||||||
|
# rather than making the suite look hung. Override for deliberate long runs.
|
||||||
|
TEST_TIMEOUT_SECONDS="${TEST_TIMEOUT_SECONDS:-120}"
|
||||||
|
|
||||||
GODOT_BIN="/root/Godot_v4.7.1-stable_linux.arm64"
|
GODOT_BIN="/root/Godot_v4.7.1-stable_linux.arm64"
|
||||||
cd "$PROJECT_DIR" || exit 2
|
cd "$PROJECT_DIR" || exit 2
|
||||||
|
|
@ -26,6 +31,8 @@ fi
|
||||||
|
|
||||||
outdir="$(mktemp -d)"
|
outdir="$(mktemp -d)"
|
||||||
trap 'rm -rf "$outdir"' EXIT
|
trap 'rm -rf "$outdir"' EXIT
|
||||||
|
REPORT_DIR="${TEST_REPORT_DIR:-$PROJECT_DIR/.test-results}"
|
||||||
|
mkdir -p "$REPORT_DIR"
|
||||||
|
|
||||||
# Import once, serially, before anything runs in parallel. Godot writes to
|
# Import once, serially, before anything runs in parallel. Godot writes to
|
||||||
# .godot/ while importing; several instances discovering a cold cache at the
|
# .godot/ while importing; several instances discovering a cold cache at the
|
||||||
|
|
@ -46,7 +53,8 @@ run_one() {
|
||||||
# 3 parallel runs. Pinned, a test simulates the same 60fps whatever else
|
# 3 parallel runs. Pinned, a test simulates the same 60fps whatever else
|
||||||
# the phone is doing, and its result no longer depends on how busy the
|
# the phone is doing, and its result no longer depends on how busy the
|
||||||
# machine was.
|
# machine was.
|
||||||
"$GODOT_BIN" --headless --fixed-fps 60 -s "res://tests/$name" >"$log" 2>&1
|
timeout "$TEST_TIMEOUT_SECONDS" "$GODOT_BIN" --headless --fixed-fps 60 \
|
||||||
|
-s "res://tests/$name" >"$log" 2>&1
|
||||||
local rc=$?
|
local rc=$?
|
||||||
end=$(date +%s%N)
|
end=$(date +%s%N)
|
||||||
local ms=$(( (end - start) / 1000000 ))
|
local ms=$(( (end - start) / 1000000 ))
|
||||||
|
|
@ -57,6 +65,16 @@ run_one() {
|
||||||
# while the log says otherwise.
|
# while the log says otherwise.
|
||||||
if (( rc == 0 )) && grep -q ": PASS" "$log"; then
|
if (( rc == 0 )) && grep -q ": PASS" "$log"; then
|
||||||
printf 'PASS %6dms %s\n' "$ms" "$name" > "$outdir/$name.status"
|
printf 'PASS %6dms %s\n' "$ms" "$name" > "$outdir/$name.status"
|
||||||
|
elif (( rc == 124 )); then
|
||||||
|
printf 'TIMEOUT %4dms %s (limit %ss)\n' \
|
||||||
|
"$ms" "$name" "$TEST_TIMEOUT_SECONDS" > "$outdir/$name.status"
|
||||||
|
elif (( rc == 137 || rc == 139 )); then
|
||||||
|
# Killed, not failed - almost always the kernel reclaiming memory. Worth
|
||||||
|
# saying plainly: a killed worker and a failing one both simply produced
|
||||||
|
# no verdict before, so a whole suite dying to the OOM killer was
|
||||||
|
# indistinguishable from a hang.
|
||||||
|
printf 'KILLED %4dms %s (signal %d - out of memory? try JOBS=1)\n' \
|
||||||
|
"$ms" "$name" "$(( rc - 128 ))" > "$outdir/$name.status"
|
||||||
else
|
else
|
||||||
printf 'FAIL %6dms %s (exit %d)\n' "$ms" "$name" "$rc" > "$outdir/$name.status"
|
printf 'FAIL %6dms %s (exit %d)\n' "$ms" "$name" "$rc" > "$outdir/$name.status"
|
||||||
fi
|
fi
|
||||||
|
|
@ -90,4 +108,9 @@ done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "$(( total - failed ))/$total passed"
|
echo "$(( total - failed ))/$total passed"
|
||||||
|
# Keep the concise verdicts after the temporary logs are removed. This is
|
||||||
|
# especially useful from Termux, where a long-running child can outlive the
|
||||||
|
# terminal command that launched it and its final buffered output is lost.
|
||||||
|
rm -f "$REPORT_DIR"/*.status
|
||||||
|
cp "$outdir"/*.status "$REPORT_DIR"/
|
||||||
(( failed == 0 ))
|
(( failed == 0 ))
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,36 @@ set -uo pipefail
|
||||||
|
|
||||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
# Leave headroom: Godot is memory-hungry and this is a phone, not a build
|
# Worker count comes from FREE MEMORY, not core count. Each Godot instance is
|
||||||
# server. Override with JOBS= if you know better.
|
# ~400MB and this phone shares its RAM with everything else running on it.
|
||||||
JOBS="${JOBS:-6}"
|
# Getting it wrong is bad in both directions:
|
||||||
|
# - too many and the kernel SIGKILLs the runner mid-suite (exit 137), which
|
||||||
|
# looks exactly like a hang
|
||||||
|
# - too many under swap pressure and every worker thrashes: the same suite
|
||||||
|
# measured 100-190s with memory free, and 888s while 9GB sat in swap
|
||||||
|
# One worker per ~900MB available, capped at 6, and forced to 1 once swap is
|
||||||
|
# more than half consumed - past that, parallelism costs more than it buys.
|
||||||
|
# Override with JOBS= when you know better.
|
||||||
|
_pick_jobs() {
|
||||||
|
local avail swap_total swap_free n
|
||||||
|
avail=$(awk '/MemAvailable/ {print int($2 / 1024)}' /proc/meminfo 2>/dev/null)
|
||||||
|
swap_total=$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null)
|
||||||
|
swap_free=$(awk '/SwapFree/ {print $2}' /proc/meminfo 2>/dev/null)
|
||||||
|
if [[ -z "$avail" ]]; then
|
||||||
|
echo 2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -n "$swap_total" ]] && (( swap_total > 0 )) \
|
||||||
|
&& (( (swap_total - swap_free) * 2 > swap_total )); then
|
||||||
|
echo 1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
n=$(( avail / 900 ))
|
||||||
|
(( n < 1 )) && n=1
|
||||||
|
(( n > 6 )) && n=6
|
||||||
|
echo "$n"
|
||||||
|
}
|
||||||
|
JOBS="${JOBS:-$(_pick_jobs)}"
|
||||||
VERBOSE="${VERBOSE:-0}"
|
VERBOSE="${VERBOSE:-0}"
|
||||||
IN_PLACE="${IN_PLACE:-0}"
|
IN_PLACE="${IN_PLACE:-0}"
|
||||||
WORK_DIR="${WORK_DIR:-$HOME/.cache/spaceship-tests}"
|
WORK_DIR="${WORK_DIR:-$HOME/.cache/spaceship-tests}"
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,29 @@ extends SceneTree
|
||||||
|
|
||||||
# Headless gameplay test: run with
|
# Headless gameplay test: run with
|
||||||
# godot --headless -s res://tests/test_building_stock_queue.gd
|
# godot --headless -s res://tests/test_building_stock_queue.gd
|
||||||
# Forgejo #16 (part 3 of #12): a building produces out of its OWN stock, and
|
# Forgejo #16 (part 3 of #12), updated for #23: a building produces out of
|
||||||
# an order it cannot pay for yet is queued rather than refused.
|
# its OWN stock, an order it cannot pay for yet is queued rather than
|
||||||
|
# refused, and building now TAKES TIME.
|
||||||
#
|
#
|
||||||
# Before this, BuildController deducted from the bank at the moment of
|
# What #23 changed here: orders no longer complete the instant the ore
|
||||||
# purchase and refused outright with "Not enough rocks", so nothing ever had
|
# lands. The head of the queue consumes its ore, builds for build_time, and
|
||||||
# to be delivered anywhere.
|
# only then finishes - and it finishes into the mothership hangar rather
|
||||||
|
# than straight into the fleet. So a queue is something the player can watch
|
||||||
|
# and cancel, and a full fleet no longer stops a crafter working.
|
||||||
#
|
#
|
||||||
# Verifies:
|
# Verifies:
|
||||||
# 1. ordering with an empty stock queues instead of failing, and produces
|
# 1. ordering with an empty stock queues instead of failing, and produces
|
||||||
# nothing yet
|
# nothing yet
|
||||||
# 2. it does NOT quietly take the cost out of the bank - the whole point is
|
# 2. it does NOT quietly take the cost out of the bank - the whole point is
|
||||||
# that the building has to be supplied
|
# that the building has to be supplied
|
||||||
# 3. delivering enough stock completes the order on its own
|
# 3. delivering enough stock starts the build, and it completes on its own
|
||||||
# 4. one delivery covering several queued orders clears them all, rather
|
# 4. orders build ONE AT A TIME in order, not all at once the moment a big
|
||||||
# than dribbling out one per frame
|
# delivery covers them
|
||||||
# 5. a part-delivery is not enough: stock below the cost leaves the order
|
# 5. a part-delivery is not enough: stock below the cost leaves the order
|
||||||
# queued and the ore untouched
|
# queued and the ore untouched
|
||||||
# 6. stock and queue survive the space scene being torn down (the trip to
|
# 6. stock and queue survive the space scene being torn down
|
||||||
# a planet's surface)
|
# 7. a build completes with NO fleet in existence, into the hangar, and
|
||||||
# 7. production completes with NO fleet in existence, and the fleet picks
|
# waits there - it is not lost, and not silently crewed either
|
||||||
# the vessel up when it comes back - orders must not depend on which
|
|
||||||
# scene the player is standing in
|
|
||||||
|
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
var scene: Node
|
var scene: Node
|
||||||
|
|
@ -34,6 +35,7 @@ var stage: int = 0
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
var ships_at_start: int = 0
|
var ships_at_start: int = 0
|
||||||
var bank_at_start: int = 0
|
var bank_at_start: int = 0
|
||||||
|
var stored_at_start: int = 0
|
||||||
var ticks: int = 0
|
var ticks: int = 0
|
||||||
|
|
||||||
const CRAFTER_AT: Vector2 = Vector2(900.0, 0.0)
|
const CRAFTER_AT: Vector2 = Vector2(900.0, 0.0)
|
||||||
|
|
@ -43,13 +45,12 @@ func _initialize() -> void:
|
||||||
game_state = root.get_node("GameState")
|
game_state = root.get_node("GameState")
|
||||||
game_state.buildings.clear()
|
game_state.buildings.clear()
|
||||||
game_state.extractors.clear()
|
game_state.extractors.clear()
|
||||||
|
game_state.stored_vessels.clear()
|
||||||
game_state.rocks = 500
|
game_state.rocks = 500
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.vessels_owed.clear()
|
# Real build times are 10s+; the timed path is what matters here, not the
|
||||||
# Room to grow: the default cap is BASE_FLEET_SIZE (5) and the fleet
|
# wall clock. See GameState.build_time_scale.
|
||||||
# starts at exactly 5, so without this every completed order would have
|
game_state.build_time_scale = 0.02
|
||||||
# nowhere to put its vessel and the test would be measuring the cap
|
|
||||||
# rather than the queue.
|
|
||||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
|
|
@ -57,6 +58,10 @@ func _initialize() -> void:
|
||||||
field = scene.get_node("BuildingField")
|
field = scene.get_node("BuildingField")
|
||||||
|
|
||||||
|
|
||||||
|
func _build_time() -> float:
|
||||||
|
return game_state.build_time(GameState.PRODUCT_SHIP)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
elapsed += delta
|
elapsed += delta
|
||||||
match stage:
|
match stage:
|
||||||
|
|
@ -90,7 +95,7 @@ func _process(delta: float) -> bool:
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order did not stay queued: %d pending)" % game_state.pending_orders(crafter_id))
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order did not stay queued: %d pending)" % game_state.pending_orders(crafter_id))
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
if fleet.ships.size() != ships_at_start:
|
if not game_state.stored_vessels.is_empty():
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a vessel appeared with an empty stock)")
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a vessel appeared with an empty stock)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
|
|
@ -98,8 +103,8 @@ func _process(delta: float) -> bool:
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (queueing took %d out of the bank - the building is supposed to be supplied)" % (bank_at_start - game_state.rocks))
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (queueing took %d out of the bank - the building is supposed to be supplied)" % (bank_at_start - game_state.rocks))
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
print("empty stock -> queued=%d ships=%d bank=%d (untouched)" % [
|
print("empty stock -> queued=%d stored=%d bank=%d (untouched)" % [
|
||||||
game_state.pending_orders(crafter_id), fleet.ships.size(), game_state.rocks
|
game_state.pending_orders(crafter_id), game_state.stored_vessels.size(), game_state.rocks
|
||||||
])
|
])
|
||||||
|
|
||||||
# --- 5. a part-delivery is not enough -----------------------
|
# --- 5. a part-delivery is not enough -----------------------
|
||||||
|
|
@ -110,7 +115,7 @@ func _process(delta: float) -> bool:
|
||||||
3:
|
3:
|
||||||
if elapsed < 0.3:
|
if elapsed < 0.3:
|
||||||
return false
|
return false
|
||||||
if game_state.pending_orders(crafter_id) != 1 or fleet.ships.size() != ships_at_start:
|
if game_state.pending_orders(crafter_id) != 1 or not game_state.stored_vessels.is_empty():
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (produced on a part-delivery of %d against a cost of %d)" % [
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (produced on a part-delivery of %d against a cost of %d)" % [
|
||||||
GameState.SHIP_COST - 1, GameState.SHIP_COST
|
GameState.SHIP_COST - 1, GameState.SHIP_COST
|
||||||
])
|
])
|
||||||
|
|
@ -124,70 +129,95 @@ func _process(delta: float) -> bool:
|
||||||
game_state.building_stock(crafter_id), GameState.SHIP_COST
|
game_state.building_stock(crafter_id), GameState.SHIP_COST
|
||||||
])
|
])
|
||||||
|
|
||||||
# --- 3. topping it up completes the order -------------------
|
# --- 3. topping it up starts the build ----------------------
|
||||||
game_state.deposit_to_building(crafter_id, 1)
|
game_state.deposit_to_building(crafter_id, 1)
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
stage = 4
|
stage = 4
|
||||||
return false
|
return false
|
||||||
4:
|
4:
|
||||||
if elapsed < 0.4:
|
# One tick: the build must have STARTED (ore taken) but not yet
|
||||||
|
# finished, or "takes time" means nothing.
|
||||||
|
ticks += 1
|
||||||
|
if ticks < 2:
|
||||||
return false
|
return false
|
||||||
if game_state.pending_orders(crafter_id) != 0:
|
var queue: Array = game_state.order_list(crafter_id)
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order not completed once the stock arrived)")
|
if queue.is_empty() or not bool(queue[0].started):
|
||||||
quit(1)
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a fully funded order never started building)")
|
||||||
return true
|
|
||||||
if fleet.ships.size() != ships_at_start + 1:
|
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (no vessel delivered: %d -> %d)" % [
|
|
||||||
ships_at_start, fleet.ships.size()
|
|
||||||
])
|
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
if game_state.building_stock(crafter_id) != 0:
|
if game_state.building_stock(crafter_id) != 0:
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (stock not consumed: %d left)" % game_state.building_stock(crafter_id))
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (starting the build did not consume the ore)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
print("supplied -> ships %d->%d, stock spent" % [ships_at_start, fleet.ships.size()])
|
if not game_state.stored_vessels.is_empty():
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the vessel appeared instantly - builds are supposed to take time)")
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
print("funded -> building started, ore consumed, nothing delivered yet")
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 5
|
||||||
|
return false
|
||||||
|
5:
|
||||||
|
if elapsed < _build_time() * 2.0:
|
||||||
|
return false
|
||||||
|
if game_state.pending_orders(crafter_id) != 0:
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (build never finished)")
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
if game_state.stored_vessels.size() != 1:
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (finished build did not reach the hangar: %d stored)" % game_state.stored_vessels.size())
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
if fleet.ships.size() != ships_at_start:
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the vessel crewed itself - the player is supposed to pick it up)")
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
print("built -> 1 vessel waiting in the hangar, fleet untouched at %d" % fleet.ships.size())
|
||||||
|
|
||||||
# --- 4. one delivery clears ALL the orders it covers ---------
|
# --- 4. one at a time, in order -----------------------------
|
||||||
# Measured in TICKS, not seconds, and with more orders than the
|
stored_at_start = game_state.stored_vessels.size()
|
||||||
# ticks allowed: production that handled one order per frame
|
|
||||||
# would still clear them all inside any generous wall-clock wait,
|
|
||||||
# so a timed check cannot tell "cleared at once" from "dribbled
|
|
||||||
# out". Three orders, two ticks - only clearing them together
|
|
||||||
# passes.
|
|
||||||
ships_at_start = fleet.ships.size()
|
|
||||||
game_state.queue_order(crafter_id)
|
game_state.queue_order(crafter_id)
|
||||||
game_state.queue_order(crafter_id)
|
game_state.queue_order(crafter_id)
|
||||||
game_state.queue_order(crafter_id)
|
game_state.queue_order(crafter_id)
|
||||||
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST * 3)
|
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST * 3)
|
||||||
ticks = 0
|
|
||||||
stage = 5
|
|
||||||
return false
|
|
||||||
5:
|
|
||||||
ticks += 1
|
|
||||||
if ticks < 2:
|
|
||||||
return false
|
|
||||||
if game_state.pending_orders(crafter_id) != 0 or fleet.ships.size() != ships_at_start + 3:
|
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a delivery covering 3 orders left %d queued after %d ticks, ships %d->%d)" % [
|
|
||||||
game_state.pending_orders(crafter_id), ticks, ships_at_start, fleet.ships.size()
|
|
||||||
])
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("one delivery cleared 3 orders in %d ticks -> ships %d->%d" % [
|
|
||||||
ticks, ships_at_start, fleet.ships.size()
|
|
||||||
])
|
|
||||||
|
|
||||||
# --- 6 + 7. no scene, no fleet: production still completes --
|
|
||||||
# The scene has to be GONE before the order is placed. With a
|
|
||||||
# fleet still alive and under its cap it claims the vessel the
|
|
||||||
# same frame production finishes, which would leave vessels_owed
|
|
||||||
# back at 0 and prove nothing about orders surviving without it.
|
|
||||||
ships_at_start = fleet.ships.size()
|
|
||||||
scene.queue_free()
|
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
stage = 6
|
stage = 6
|
||||||
return false
|
return false
|
||||||
6:
|
6:
|
||||||
|
# Enough time for exactly one build, and no more. Funding all
|
||||||
|
# three at once must NOT finish all three at once - that is the
|
||||||
|
# behaviour #23 replaced.
|
||||||
|
if elapsed < _build_time() * 1.4:
|
||||||
|
return false
|
||||||
|
var done: int = game_state.stored_vessels.size() - stored_at_start
|
||||||
|
if done != 1:
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (after one build time, %d of 3 orders finished - they are not building one at a time)" % done)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
if game_state.pending_orders(crafter_id) != 2:
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (%d orders left, expected 2 still queued)" % game_state.pending_orders(crafter_id))
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
print("one build time -> 1 of 3 done, 2 still queued (sequential)")
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 7
|
||||||
|
return false
|
||||||
|
7:
|
||||||
|
if elapsed < _build_time() * 4.0:
|
||||||
|
return false
|
||||||
|
if game_state.stored_vessels.size() - stored_at_start != 3:
|
||||||
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the rest of the queue never drained: %d of 3)" % (game_state.stored_vessels.size() - stored_at_start))
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
print("queue drained in order -> 3 vessels waiting")
|
||||||
|
|
||||||
|
# --- 6 + 7. no scene, no fleet ------------------------------
|
||||||
|
stored_at_start = game_state.stored_vessels.size()
|
||||||
|
scene.queue_free()
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 8
|
||||||
|
return false
|
||||||
|
8:
|
||||||
if elapsed < 0.3:
|
if elapsed < 0.3:
|
||||||
return false
|
return false
|
||||||
if is_instance_valid(fleet):
|
if is_instance_valid(fleet):
|
||||||
|
|
@ -195,39 +225,22 @@ func _process(delta: float) -> bool:
|
||||||
game_state.queue_order(crafter_id)
|
game_state.queue_order(crafter_id)
|
||||||
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST)
|
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST)
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
stage = 61
|
stage = 9
|
||||||
return false
|
return false
|
||||||
61:
|
9:
|
||||||
if elapsed < 0.4:
|
if elapsed < _build_time() * 3.0:
|
||||||
return false
|
return false
|
||||||
# The queue and stock live in the autoload, so tearing the space
|
# The queue, the stock and the hangar live in the autoload, so
|
||||||
# scene down must not touch them - and the order must complete
|
# tearing the space scene down must not touch them.
|
||||||
# even with no Fleet node anywhere to hand the vessel to.
|
|
||||||
if game_state.pending_orders(crafter_id) != 0:
|
if game_state.pending_orders(crafter_id) != 0:
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order stalled once the space scene was gone)")
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (build stalled once the space scene was gone)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
if game_state.vessels_owed.is_empty():
|
if game_state.stored_vessels.size() != stored_at_start + 1:
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (vessel completed with no fleet present was lost, not owed)")
|
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a vessel built with no fleet present was lost)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
print("no scene at all -> order completed, %d vessel owed" % game_state.vessels_owed.size())
|
print("no scene at all -> build completed into the hangar (%d waiting)" % game_state.stored_vessels.size())
|
||||||
|
|
||||||
# Come back to space: the fleet should claim what it is owed.
|
|
||||||
scene = load("res://Main.tscn").instantiate()
|
|
||||||
root.add_child(scene)
|
|
||||||
fleet = scene.get_node("Fleet")
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 8
|
|
||||||
return false
|
|
||||||
8:
|
|
||||||
if elapsed < 0.5:
|
|
||||||
return false
|
|
||||||
if not game_state.vessels_owed.is_empty():
|
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: FAIL (returning fleet never claimed its %d owed vessel(s))" % game_state.vessels_owed.size())
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
print("fleet returned and claimed the owed vessel (ships=%d)" % fleet.ships.size())
|
|
||||||
print("TEST_BUILDING_STOCK_QUEUE: PASS")
|
print("TEST_BUILDING_STOCK_QUEUE: PASS")
|
||||||
quit(0)
|
quit(0)
|
||||||
return true
|
return true
|
||||||
|
|
@ -235,10 +248,6 @@ func _process(delta: float) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func _clear_hazards() -> void:
|
func _clear_hazards() -> void:
|
||||||
# set_process(false) on AsteroidField only stops NEW chunks spawning -
|
|
||||||
# rocks already in the world keep drifting and can destroy a parked ship,
|
|
||||||
# which silently changes the fleet size this test asserts on. This is a
|
|
||||||
# logistics test, not a survival one.
|
|
||||||
for a in get_nodes_in_group("asteroids"):
|
for a in get_nodes_in_group("asteroids"):
|
||||||
if is_instance_valid(a):
|
if is_instance_valid(a):
|
||||||
a.queue_free()
|
a.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ func _initialize() -> void:
|
||||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||||
game_state.rocks = 100
|
game_state.rocks = 100
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.build_time_scale = 0.02
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
controller = scene.get_node("BuildController")
|
controller = scene.get_node("BuildController")
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ func _initialize() -> void:
|
||||||
game_state.transporters.clear()
|
game_state.transporters.clear()
|
||||||
game_state.rocks = 900
|
game_state.rocks = 900
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.build_time_scale = 0.02
|
||||||
game_state.unlocked = ["shipyards", "logistics", "freight"]
|
game_state.unlocked = ["shipyards", "logistics", "freight"]
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ func _initialize() -> void:
|
||||||
game_state.extractors.clear()
|
game_state.extractors.clear()
|
||||||
game_state.rocks = 40
|
game_state.rocks = 40
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.build_time_scale = 0.02
|
||||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
|
|
@ -161,20 +162,21 @@ func _process(delta: float) -> bool:
|
||||||
stage = 6
|
stage = 6
|
||||||
return false
|
return false
|
||||||
6:
|
6:
|
||||||
if elapsed < 0.5:
|
# Long enough for the build itself, which #23 made take time.
|
||||||
|
if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0:
|
||||||
return false
|
return false
|
||||||
# --- 5. the queued order completed off the fleet's own ore ---
|
# --- 5. the queued order completed off the fleet's own ore ---
|
||||||
if game_state.pending_orders(crafter_id) != 0:
|
if game_state.pending_orders(crafter_id) != 0:
|
||||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (order still queued after a hand delivery)")
|
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (order still queued after a hand delivery)")
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
if fleet.ships.size() != ships_before + 1:
|
# Since #23 a finished vessel waits in the hangar rather than
|
||||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (no vessel: %d -> %d)" % [ships_before, fleet.ships.size()])
|
# crewing itself, so this is where it lands.
|
||||||
|
if game_state.stored_vessels.size() != 1:
|
||||||
|
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (no vessel built: %d in the hangar)" % game_state.stored_vessels.size())
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
print("queued order fulfilled by hand, no transport hub: ships %d->%d" % [
|
print("queued order fulfilled by hand, no transport hub: %d vessel in the hangar" % game_state.stored_vessels.size())
|
||||||
ships_before, fleet.ships.size()
|
|
||||||
])
|
|
||||||
|
|
||||||
# --- 6. conservation ----------------------------------------
|
# --- 6. conservation ----------------------------------------
|
||||||
var expected: int = total_before - GameState.SHIP_COST
|
var expected: int = total_before - GameState.SHIP_COST
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func _initialize() -> void:
|
||||||
game_state.transporters.clear()
|
game_state.transporters.clear()
|
||||||
game_state.rocks = 100
|
game_state.rocks = 100
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ func _initialize() -> void:
|
||||||
game_state.buildings.clear()
|
game_state.buildings.clear()
|
||||||
game_state.extractors.clear()
|
game_state.extractors.clear()
|
||||||
game_state.transporters.clear()
|
game_state.transporters.clear()
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.build_time_scale = 0.02
|
||||||
game_state.reset_progression()
|
game_state.reset_progression()
|
||||||
game_state.rocks = 2000
|
game_state.rocks = 2000
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ func _initialize() -> void:
|
||||||
game_state.buildings.clear()
|
game_state.buildings.clear()
|
||||||
game_state.extractors.clear()
|
game_state.extractors.clear()
|
||||||
game_state.transporters.clear()
|
game_state.transporters.clear()
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.build_time_scale = 0.02
|
||||||
game_state.reset_progression()
|
game_state.reset_progression()
|
||||||
game_state.rocks = 1200
|
game_state.rocks = 1200
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
|
|
@ -154,16 +155,25 @@ func _process(delta: float) -> bool:
|
||||||
stage = 2
|
stage = 2
|
||||||
return false
|
return false
|
||||||
2:
|
2:
|
||||||
if elapsed < 0.6:
|
if elapsed < game_state.build_time(GameState.VESSEL_SHIELD) * 3.0:
|
||||||
return false
|
return false
|
||||||
|
# Finished vessels wait at the mothership (#23); crew it here so
|
||||||
|
# the rest of the checks can look at a real ship.
|
||||||
|
if game_state.stored_vessels.size() != 1:
|
||||||
|
print("TEST_SHIP_LAB: FAIL (the shield order produced nothing: %s)" % [game_state.stored_vessels])
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
if String(game_state.stored_vessels[0]) != GameState.VESSEL_SHIELD:
|
||||||
|
print("TEST_SHIP_LAB: FAIL (a shield order came back as %s)" % game_state.stored_vessels[0])
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
fleet.add_ship(game_state.take_stored_vessel(0))
|
||||||
var shield_ship: Node = null
|
var shield_ship: Node = null
|
||||||
for ship in fleet.ships:
|
for ship in fleet.ships:
|
||||||
if is_instance_valid(ship) and ship.vessel_type == GameState.VESSEL_SHIELD:
|
if is_instance_valid(ship) and ship.vessel_type == GameState.VESSEL_SHIELD:
|
||||||
shield_ship = ship
|
shield_ship = ship
|
||||||
if shield_ship == null:
|
if shield_ship == null:
|
||||||
print("TEST_SHIP_LAB: FAIL (the shield order produced no shield vessel: %d ships, owed %s)" % [
|
print("TEST_SHIP_LAB: FAIL (crewing the stored shield vessel produced none: %d ships)" % fleet.ships.size())
|
||||||
fleet.ships.size(), game_state.vessels_owed
|
|
||||||
])
|
|
||||||
quit(1)
|
quit(1)
|
||||||
return true
|
return true
|
||||||
if not shield_ship.has_shield():
|
if not shield_ship.has_shield():
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@ func _total() -> int:
|
||||||
sum += int(b.get("stock", 0))
|
sum += int(b.get("stock", 0))
|
||||||
for t in game_state.transporters:
|
for t in game_state.transporters:
|
||||||
sum += int(t.get("carrying", 0))
|
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
|
return sum
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -56,7 +64,8 @@ func _initialize() -> void:
|
||||||
game_state.transporters.clear()
|
game_state.transporters.clear()
|
||||||
game_state.rocks = 200
|
game_state.rocks = 200
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.vessels_owed.clear()
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.build_time_scale = 0.02
|
||||||
game_state.unlocked = ["shipyards", "logistics", "freight", "coordination", "wing", "squadron", "armada"]
|
game_state.unlocked = ["shipyards", "logistics", "freight", "coordination", "wing", "squadron", "armada"]
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
|
|
@ -118,7 +127,7 @@ func _process(delta: float) -> bool:
|
||||||
total_at_start = _total()
|
total_at_start = _total()
|
||||||
return false
|
return false
|
||||||
2:
|
2:
|
||||||
if elapsed < 0.3:
|
if elapsed < game_state.build_time(GameState.PRODUCT_TRANSPORTER) * 3.0:
|
||||||
return false
|
return false
|
||||||
if game_state.transporters.size() != 1:
|
if game_state.transporters.size() != 1:
|
||||||
print("TEST_TRANSPORTER_SUPPLY: FAIL (built %d transporters from one order)" % game_state.transporters.size())
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (built %d transporters from one order)" % game_state.transporters.size())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue