Add gas bomber skill-shot vessel

This commit is contained in:
Loïc Wisniewski 2026-08-15 04:45:15 +02:00
commit 1d9afcbc2e
28 changed files with 882 additions and 85 deletions

View file

@ -30,12 +30,12 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
@onready var lab_menu: Control = get_node("../UI/LabMenu") @onready var lab_menu: Control = get_node("../UI/LabMenu")
@onready var lab_vessel_buttons: Array = [ @onready var lab_vessel_buttons: Array = [
get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"), get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"),
get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"),
] ]
@onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel") @onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel")
@onready var lab_vessel_labels: Array = [ @onready var lab_vessel_labels: Array = [
get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"), get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"),
get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"),
] ]
@onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel") @onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel")
@onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel") @onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel")
@ -62,18 +62,22 @@ 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/AvailableGasBomber"),
] ]
@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/AvailableGasBomberLabel"),
] ]
@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/FleetGasBomber"),
] ]
@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/FleetGasBomberLabel"),
] ]
var build_menu_entries: Array = [] var build_menu_entries: Array = []
@ -97,11 +101,13 @@ var lab_building_id: int = 0
@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_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel") @onready var craft_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel")
@onready var craft_gas_bomber_button: IconButton = get_node("../UI/CraftMenu/GasBomberButton")
@onready var craft_gas_bomber_label: Label = get_node("../UI/CraftMenu/GasBomberLabel")
@onready var craft_vessel_buttons: Array = [ @onready var craft_vessel_buttons: Array = [
craft_interceptor_button, craft_shield_button, craft_interceptor_button, craft_shield_button, craft_gas_bomber_button,
] ]
@onready var craft_vessel_labels: Array = [ @onready var craft_vessel_labels: Array = [
craft_interceptor_label, craft_shield_label, craft_interceptor_label, craft_shield_label, craft_gas_bomber_label,
] ]
# --- the transfer panel (#24) --------------------------------------------- # --- the transfer panel (#24) ---------------------------------------------
@ -723,6 +729,25 @@ func _say(message: String) -> void:
_refresh_craft_status() _refresh_craft_status()
func _format_costs(costs: Dictionary) -> String:
var parts: PackedStringArray = PackedStringArray()
for resource_type in GameState.resource_types():
var amount: int = int(costs.get(resource_type, 0))
if amount > 0:
parts.append("%d %s" % [amount, GameState.resource_name(resource_type).to_lower()])
return " · ".join(parts) if not parts.is_empty() else "free"
func _format_missing_costs(costs: Dictionary) -> String:
var parts: PackedStringArray = PackedStringArray()
for resource_type in GameState.resource_types():
var amount: int = int(costs.get(resource_type, 0))
if amount > 0:
parts.append("%d more %s" % [amount,
GameState.resource_name(resource_type).to_lower()])
return " · ".join(parts)
func _refresh_craft_status() -> void: func _refresh_craft_status() -> void:
# Prices live with the buttons they describe. The top of the screen is # Prices live with the buttons they describe. The top of the screen is
# intentionally reserved for the shared resource bar and a short action # intentionally reserved for the shared resource bar and a short action
@ -732,7 +757,8 @@ func _refresh_craft_status() -> void:
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]
craft_vessel_labels[i].text = "%s\n%d ore" % [definition.display_name, definition.build_cost] craft_vessel_labels[i].text = "%s\n%s" % [definition.display_name,
_format_costs(GameState.product_costs(definition.definition_id))]
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)
@ -764,11 +790,10 @@ func _on_craft_vessel(vessel: String) -> void:
func _report_order(product: String) -> void: func _report_order(product: String) -> void:
var short: int = GameState.outstanding_cost(craft_building_id) var missing: Dictionary = GameState.outstanding_costs(craft_building_id)
if short > 0: if not missing.is_empty():
_say("%s queued - %d more ore must be delivered here" % [ _say("%s queued - deliver %s here" % [GameState.vessel_name(product),
GameState.vessel_name(product), short _format_missing_costs(missing)])
])
else: else:
_say("%s ordered - building now" % GameState.vessel_name(product)) _say("%s ordered - building now" % GameState.vessel_name(product))
@ -786,7 +811,8 @@ func _lab_options() -> Array:
if not GameState.vessel_is_unlocked(lab_vessel): if not GameState.vessel_is_unlocked(lab_vessel):
options.append({"unlock": true, "id": definition.unlock_id, options.append({"unlock": true, "id": definition.unlock_id,
"name": definition.unlock_name, "desc": definition.unlock_description, "name": definition.unlock_name, "desc": definition.unlock_description,
"cost": definition.unlock_cost, "icon": IconButton.Icon.UPGRADE_UNLOCK_SHIELD}) "costs": GameState.research_costs(definition.unlock_id),
"icon": definition.unlock_icon if definition.unlock_icon >= 0 else definition.icon})
return options return options
for u in GameState.upgrades_for(lab_vessel): for u in GameState.upgrades_for(lab_vessel):
options.append({"unlock": false, "id": u.id, "name": u.name, "desc": u.desc}) options.append({"unlock": false, "id": u.id, "name": u.name, "desc": u.desc})
@ -819,7 +845,8 @@ func _on_lab_buy() -> void:
if not GameState.queue_lab_research(lab_building_id, id): if not GameState.queue_lab_research(lab_building_id, id):
lab_status.text = "Research is already queued or complete" lab_status.text = "Research is already queued or complete"
return return
lab_status.text = "%s queued - deliver ore to this lab" % choice.name lab_status.text = "%s queued - deliver %s to this lab" % [choice.name,
_format_costs(GameState.research_costs(id))]
_refresh_lab() _refresh_lab()
@ -844,7 +871,8 @@ func _refresh_lab() -> void:
if not visible: if not visible:
continue continue
var option: Dictionary = options[i] var option: Dictionary = options[i]
var price_text: String = "%d ore" % (int(option.cost) if bool(option.unlock) else GameState.upgrade_cost(option.id)) var price_text: String = _format_costs(option.costs if bool(option.unlock) \
else GameState.research_costs(option.id))
if not bool(option.unlock) and GameState.upgrade_maxed(option.id): if not bool(option.unlock) and GameState.upgrade_maxed(option.id):
price_text = "MAX" price_text = "MAX"
var level_note: String = "" if bool(option.unlock) else " %d/%d" % [ var level_note: String = "" if bool(option.unlock) else " %d/%d" % [
@ -867,7 +895,7 @@ func _refresh_lab() -> void:
var queued: Dictionary = GameState.lab_research_order(lab_building_id) var queued: Dictionary = GameState.lab_research_order(lab_building_id)
lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id)) lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id))
if not queued.is_empty(): if not queued.is_empty():
lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for ore", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))]
lab_buy_button.queue_redraw() lab_buy_button.queue_redraw()
@ -989,7 +1017,7 @@ func _refresh_queue() -> void:
int(round(float(head.progress) * 100.0)), int(round(float(head.progress) * 100.0)),
] ]
else: else:
queue_detail.text = "%s waiting for ore\nTap to cancel" % \ queue_detail.text = "%s waiting for resources\nTap to cancel" % \
GameState.vessel_name(String(head.product)) GameState.vessel_name(String(head.product))
@ -1006,7 +1034,7 @@ func _on_queue_slot(index: int) -> void:
var refunded: bool = bool(orders[index].started) var refunded: bool = bool(orders[index].started)
if not GameState.cancel_order(craft_building_id, index): if not GameState.cancel_order(craft_building_id, index):
return return
_say("%s cancelled%s" % [vessel, " - ore refunded here" if refunded else ""]) _say("%s cancelled%s" % [vessel, " - resources refunded here" if refunded else ""])
_refresh_queue() _refresh_queue()

View file

@ -86,6 +86,31 @@ func vessel_types() -> PackedStringArray:
return result return result
func has_vessel_type(vessel_type: String) -> bool:
for ship in ships:
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
return true
return false
func first_vessel_of_type(vessel_type: String) -> Node2D:
for ship in ships:
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
return ship
return null
func has_capability(capability: String) -> bool:
return first_ship_with_capability(capability) != null
func first_ship_with_capability(capability: String) -> Node2D:
for ship in ships:
if is_instance_valid(ship) and ship.has_capability(capability):
return ship
return null
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.

View file

@ -21,6 +21,7 @@ extends Node2D
# Navigation section. Optional so a scene without it still works. # Navigation section. Optional so a scene without it still works.
@onready var pivot_cw_button: IconButton = get_node_or_null("UI/PivotCWButton") @onready var pivot_cw_button: IconButton = get_node_or_null("UI/PivotCWButton")
@onready var pivot_ccw_button: IconButton = get_node_or_null("UI/PivotCCWButton") @onready var pivot_ccw_button: IconButton = get_node_or_null("UI/PivotCCWButton")
@onready var gas_bomb_controller: GasBombController = get_node_or_null("GasBombController")
var touches: Dictionary = {} var touches: Dictionary = {}
var pinch_active: bool = false var pinch_active: bool = false
@ -252,6 +253,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
# The joystick gets first refusal on every touch, and only claims ones # The joystick gets first refusal on every touch, and only claims ones
# that begin inside its own rect - so pinch-to-zoom still works anywhere # that begin inside its own rect - so pinch-to-zoom still works anywhere
# else on screen. # else on screen.
if gas_bomb_controller != null and gas_bomb_controller.handle_touch(event):
return
if _handle_pivot_touch(event): if _handle_pivot_touch(event):
return return
if joystick != null and joystick.visible and joystick.handle_touch(event): if joystick != null and joystick.visible and joystick.handle_touch(event):
@ -272,6 +275,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
func _handle_drag(event: InputEventScreenDrag) -> void: func _handle_drag(event: InputEventScreenDrag) -> void:
if gas_bomb_controller != null and gas_bomb_controller.handle_drag(event):
return
if joystick != null and joystick.visible and joystick.handle_drag(event): if joystick != null and joystick.visible and joystick.handle_drag(event):
return return
if not touches.has(event.index): if not touches.has(event.index):

View file

@ -410,6 +410,7 @@ func building_has_capability(building: Dictionary, capability: String) -> bool:
# but transporters themselves belong to hubs and are never production orders. # 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_GAS_BOMBER: String = "gas_bomber"
const VESSEL_TRANSPORTER: String = "transporter" 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
@ -501,8 +502,11 @@ func unlock_vessel(vessel: String) -> bool:
var definition: Resource = vessel_definition(vessel) var definition: Resource = vessel_definition(vessel)
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab(): if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
return false return false
if not spend(definition.unlock_cost): var costs: Dictionary = research_costs(definition.unlock_id)
if not can_afford_resources(costs):
return false return false
for resource_type in costs:
bank_inventory.withdraw(String(resource_type), int(costs[resource_type]))
vessel_levels[definition.unlock_id] = 1 vessel_levels[definition.unlock_id] = 1
vessel_upgraded.emit(definition.unlock_id) vessel_upgraded.emit(definition.unlock_id)
return true return true
@ -604,8 +608,16 @@ const PRODUCT_SHIP: String = "interceptor"
func product_cost(product: String) -> int: func product_cost(product: String) -> int:
return int(product_costs(product).get(RESOURCE_ORE, 0))
func product_costs(product: String) -> Dictionary:
var definition: Resource = vessel_definition(product) var definition: Resource = vessel_definition(product)
return definition.build_cost if definition != null else 0 if definition == null:
return {}
if not definition.build_resource_costs.is_empty():
return definition.build_resource_costs.duplicate()
return {RESOURCE_ORE: definition.build_cost} if definition.build_cost > 0 else {}
func can_produce(type: String) -> bool: func can_produce(type: String) -> bool:
@ -661,34 +673,53 @@ func cancel_order(id: int, index: int) -> bool:
return false return false
var order: Dictionary = orders[index] var order: Dictionary = orders[index]
if bool(order.started): if bool(order.started):
_inventory_for_record(b).deposit(RESOURCE_ORE, product_cost(String(order.product))) var inventory: RefCounted = _inventory_for_record(b)
var costs: Dictionary = product_costs(String(order.product))
for resource_type in costs:
inventory.deposit(String(resource_type), int(costs[resource_type]))
orders.remove_at(index) orders.remove_at(index)
return true return true
func outstanding_cost(id: int) -> int: func outstanding_cost(id: int) -> int:
return outstanding_resource_cost(id, RESOURCE_ORE)
func outstanding_costs(id: int) -> Dictionary:
var result: Dictionary = {}
for resource_type in resource_types():
var need: int = outstanding_resource_cost(id, resource_type)
if need > 0:
result[resource_type] = need
return result
func outstanding_resource_cost(id: int, resource_type: String) -> 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)
if b.is_empty(): if b.is_empty():
return 0 return 0
var need: int = 0 var need: int = 0
for o in b.get("orders", []): for o in b.get("orders", []):
# A started build has already paid, so it is not still owed.
if not bool(o.started): if not bool(o.started):
need += product_cost(String(o.product)) need += int(product_costs(String(o.product)).get(resource_type, 0))
for order in b.get("research_orders", []): for order in b.get("research_orders", []):
if not bool(order.started): if not bool(order.started):
need += research_cost(String(order.id)) need += int(research_costs(String(order.id)).get(resource_type, 0))
return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 0) return maxi(need - _inventory_for_record(b).amount(resource_type), 0)
func deposit_to_building(id: int, amount: int) -> int: func deposit_to_building(id: int, amount: int) -> int:
return deposit_resource_to_building(id, RESOURCE_ORE, amount)
func deposit_resource_to_building(id: int, resource_type: String, amount: int) -> int:
if amount <= 0: if amount <= 0:
return 0 return 0
var b: Dictionary = find_building(id) var b: Dictionary = find_building(id)
if b.is_empty(): if b.is_empty():
return 0 return 0
return _inventory_for_record(b).deposit(RESOURCE_ORE, amount) return _inventory_for_record(b).deposit(resource_type, amount)
func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool: func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
@ -717,14 +748,20 @@ func _advance_production(delta: float) -> void:
# this did before #23) leaves nothing to see or cancel. # this did before #23) leaves nothing to see or cancel.
var order: Dictionary = orders[0] var order: Dictionary = orders[0]
var product: String = String(order.product) var product: String = String(order.product)
var cost: int = product_cost(product) var costs: Dictionary = product_costs(product)
var inventory: RefCounted = _inventory_for_record(b) var inventory: RefCounted = _inventory_for_record(b)
var definition: Resource = vessel_definition(product) var definition: Resource = vessel_definition(product)
if not bool(order.started): if not bool(order.started):
if inventory.amount(RESOURCE_ORE) < cost: var affordable := true
for resource_type in costs:
if inventory.amount(String(resource_type)) < int(costs[resource_type]):
affordable = false
break
if not affordable:
continue continue
inventory.withdraw(RESOURCE_ORE, cost) for resource_type in costs:
inventory.withdraw(String(resource_type), int(costs[resource_type]))
order.started = true order.started = true
order.elapsed = float(order.elapsed) + delta order.elapsed = float(order.elapsed) + delta
@ -764,8 +801,17 @@ func lab_research_order(lab_id: int) -> Dictionary:
func research_cost(id: String) -> int: func research_cost(id: String) -> int:
return int(research_costs(id).get(RESOURCE_ORE, 0))
func research_costs(id: String) -> Dictionary:
var unlock_definition: Resource = vessel_for_unlock(id) var unlock_definition: Resource = vessel_for_unlock(id)
return unlock_definition.unlock_cost if unlock_definition != null else upgrade_cost(id) if unlock_definition != null:
if not unlock_definition.unlock_resource_costs.is_empty():
return unlock_definition.unlock_resource_costs.duplicate()
return {RESOURCE_ORE: unlock_definition.unlock_cost} if unlock_definition.unlock_cost > 0 else {}
var cost: int = upgrade_cost(id)
return {RESOURCE_ORE: cost} if cost > 0 else {}
func _advance_lab_research(delta: float) -> void: func _advance_lab_research(delta: float) -> void:
@ -778,12 +824,18 @@ func _advance_lab_research(delta: float) -> void:
var order: Dictionary = orders[0] var order: Dictionary = orders[0]
var id: String = String(order.id) var id: String = String(order.id)
var unlock_definition: Resource = vessel_for_unlock(id) var unlock_definition: Resource = vessel_for_unlock(id)
var cost: int = research_cost(id) var costs: Dictionary = research_costs(id)
var inventory: RefCounted = _inventory_for_record(lab) var inventory: RefCounted = _inventory_for_record(lab)
if not bool(order.started): if not bool(order.started):
if inventory.amount(RESOURCE_ORE) < cost: var affordable := true
for resource_type in costs:
if inventory.amount(String(resource_type)) < int(costs[resource_type]):
affordable = false
break
if not affordable:
continue continue
inventory.withdraw(RESOURCE_ORE, cost) for resource_type in costs:
inventory.withdraw(String(resource_type), int(costs[resource_type]))
order.started = true order.started = true
order.elapsed = float(order.elapsed) + delta order.elapsed = float(order.elapsed) + delta
if float(order.elapsed) < lab_research_time(): if float(order.elapsed) < lab_research_time():
@ -1129,7 +1181,13 @@ func _dispatch_transporters() -> void:
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position, _begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
TRANSPORTER_TO_EXTRACTOR) TRANSPORTER_TO_EXTRACTOR)
for b in buildings: for b in buildings:
if outstanding_cost(int(b.id)) <= 0: var needed_resource := ""
for resource_type in resource_types():
if outstanding_resource_cost(int(b.id), resource_type) > 0 \
and bank_inventory.amount(resource_type) > 0:
needed_resource = resource_type
break
if needed_resource == "":
continue continue
# One at a time per building: without this every idle transporter # One at a time per building: without this every idle transporter
# would be sent after the same shortfall and they would all arrive # would be sent after the same shortfall and they would all arrive
@ -1149,6 +1207,7 @@ func _dispatch_transporters() -> void:
if chosen.is_empty(): if chosen.is_empty():
return # every transporter is busy return # every transporter is busy
chosen.target_id = int(b.id) chosen.target_id = int(b.id)
chosen.resource_type = needed_resource
_begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP) _begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP)
@ -1175,22 +1234,24 @@ func _arrive(t: Dictionary) -> void:
match t.state: match t.state:
TRANSPORTER_TO_MOTHERSHIP: TRANSPORTER_TO_MOTHERSHIP:
var target: Dictionary = find_building(int(t.target_id)) var target: Dictionary = find_building(int(t.target_id))
var want: int = outstanding_cost(int(t.target_id)) var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
var take: int = mini(mini(want, bank_inventory.amount(RESOURCE_ORE)), transporter_load_limit()) var want: int = outstanding_resource_cost(int(t.target_id), resource_type)
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
if take <= 0 or target.is_empty(): if take <= 0 or target.is_empty():
# Nothing to carry, or the building is gone. Go home empty. # Nothing to carry, or the building is gone. Go home empty.
t.target_id = 0 t.target_id = 0
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
return return
bank_inventory.withdraw(RESOURCE_ORE, take) bank_inventory.withdraw(resource_type, take)
t.carrying = take t.carrying = take
_begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING) _begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING)
TRANSPORTER_TO_BUILDING: TRANSPORTER_TO_BUILDING:
# The ore moves at exactly the moment the vessel arrives. # The cargo moves at exactly the moment the vessel arrives.
if deposit_to_building(int(t.target_id), int(t.carrying)) <= 0: var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
# Building demolished in transit - put the ore back rather if deposit_resource_to_building(int(t.target_id), resource_type, int(t.carrying)) <= 0:
# Building demolished in transit - put the cargo back rather
# than quietly destroying it. # than quietly destroying it.
bank_inventory.deposit(RESOURCE_ORE, int(t.carrying)) bank_inventory.deposit(resource_type, int(t.carrying))
t.carrying = 0 t.carrying = 0
t.target_id = 0 t.target_id = 0
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING) _begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
@ -1226,6 +1287,13 @@ func can_afford(cost: int) -> bool:
return bank_inventory.amount(RESOURCE_ORE) >= cost return bank_inventory.amount(RESOURCE_ORE) >= cost
func can_afford_resources(costs: Dictionary) -> bool:
for resource_type in costs:
if bank_inventory.amount(String(resource_type)) < int(costs[resource_type]):
return false
return true
func spend(cost: int) -> bool: func spend(cost: int) -> bool:
if not can_afford(cost): if not can_afford(cost):
return false return false

197
GasBombController.gd Normal file
View file

@ -0,0 +1,197 @@
class_name GasBombController
extends Node2D
const BOMB_COLOR := Color(0.96, 0.22, 0.72)
const TARGET_COLOR := Color(0.78, 0.32, 1.0)
const MAX_RANGE := 950.0
const AOE_RADIUS := 180.0
const DRAG_RADIUS := 260.0
const AIM_DEADZONE := 22.0
const PROJECTILE_SPEED := 720.0
const DAMAGE := 3
const COOLDOWN := 5.0
@export var minimum_zoom: float = 0.4
@onready var fleet: Node2D = get_node("../Fleet")
@onready var camera: Camera2D = get_node("../Camera2D")
@onready var button: IconButton = get_node("../UI/GasBombButton")
var aiming := false
var aim_touch := -1
var aim_start_screen := Vector2.ZERO
var aim_screen := Vector2.ZERO
var aim_target := Vector2.ZERO
var saved_zoom := 1.0
var restoring_zoom := false
var cooldown_left := 0.0
var projectile_active := false
var projectile_position := Vector2.ZERO
var projectile_target := Vector2.ZERO
var projectile_trail := PackedVector2Array()
var explosion_time := 0.0
var explosion_position := Vector2.ZERO
var last_hit_count := 0
func _ready() -> void:
button.visible = false
queue_redraw()
func _process(delta: float) -> void:
var was_exploding := explosion_time > 0.0
button.visible = fleet.has_capability("gas_bomber")
cooldown_left = maxf(cooldown_left - delta, 0.0)
button.enabled = cooldown_left <= 0.0
if not button.visible and aiming:
_cancel_aim()
if restoring_zoom and not aiming:
var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0)
camera.zoom = Vector2(z, z)
if is_equal_approx(z, saved_zoom):
restoring_zoom = false
if projectile_active:
var distance := projectile_position.distance_to(projectile_target)
if distance <= PROJECTILE_SPEED * delta:
projectile_position = projectile_target
projectile_active = false
_explode(projectile_target)
else:
projectile_position = projectile_position.move_toward(projectile_target,
PROJECTILE_SPEED * delta)
projectile_trail.append(projectile_position)
while projectile_trail.size() > 12:
projectile_trail.remove_at(0)
if explosion_time > 0.0:
explosion_time = maxf(explosion_time - delta, 0.0)
if aiming or projectile_active or explosion_time > 0.0 or was_exploding:
queue_redraw()
func can_begin_aim() -> bool:
return button.visible and button.enabled and not button._blocked_by_modal() \
and not aiming and not projectile_active
func handle_touch(event: InputEventScreenTouch) -> bool:
if event.pressed:
if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position):
return false
aiming = true
aim_touch = event.index
aim_start_screen = event.position
aim_screen = event.position
saved_zoom = camera.zoom.x
restoring_zoom = false
button.pressed_down = true
button.queue_redraw()
_update_aim()
return true
if event.index != aim_touch:
return false
var should_fire := aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE
if should_fire:
_fire()
else:
_cancel_aim()
return true
func handle_drag(event: InputEventScreenDrag) -> bool:
if not aiming or event.index != aim_touch:
return false
aim_screen = event.position
_update_aim()
return true
func _update_aim() -> void:
var drag := aim_screen - aim_start_screen
var origin: Vector2 = fleet.get_centroid()
if drag.length() < 0.001:
aim_target = origin
else:
var distance := minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE
aim_target = origin + drag.normalized() * distance
_update_aim_zoom(origin, aim_target)
queue_redraw()
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
var displacement := target - origin
var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0)
var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + AOE_RADIUS)
var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + AOE_RADIUS)
var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom)
camera.zoom = Vector2(desired, desired)
func _finish_aim() -> void:
aiming = false
aim_touch = -1
button.pressed_down = false
button.queue_redraw()
restoring_zoom = true
func _cancel_aim() -> void:
_finish_aim()
queue_redraw()
func _fire() -> void:
var bomber: Node2D = fleet.first_ship_with_capability("gas_bomber")
if bomber == null:
_cancel_aim()
return
projectile_active = true
projectile_position = bomber.global_position
projectile_target = aim_target
projectile_trail = PackedVector2Array([projectile_position])
cooldown_left = COOLDOWN
_finish_aim()
func _explode(at: Vector2) -> void:
explosion_position = at
explosion_time = 0.8
last_hit_count = 0
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(enemy) and enemy is Node2D \
and enemy.global_position.distance_to(at) <= AOE_RADIUS:
enemy.take_damage(DAMAGE)
last_hit_count += 1
func _draw() -> void:
if aiming:
var origin: Vector2 = fleet.get_centroid()
NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8)
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
_draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0)
if projectile_trail.size() >= 2 and projectile_active:
for i in range(1, projectile_trail.size()):
var alpha := float(i) / projectile_trail.size()
draw_line(projectile_trail[i - 1], projectile_trail[i],
Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true)
if projectile_active:
draw_circle(projectile_position, 12.0, Color(BOMB_COLOR, 0.35))
NeonDraw.draw_glowing_circle(self, projectile_position, 7.0, BOMB_COLOR, 1.0)
if explosion_time > 0.0:
var progress := 1.0 - explosion_time / 0.8
var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
draw_circle(explosion_position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
_draw_ring(explosion_position, radius, BOMB_COLOR, 1.0 - progress)
for i in range(7):
var angle := TAU * float(i) / 7.0 + progress
var cloud_at := explosion_position + Vector2(cos(angle), sin(angle)) * radius * 0.55
draw_circle(cloud_at, 18.0 + 14.0 * progress, Color(BOMB_COLOR, (1.0 - progress) * 0.22))
func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void:
draw_arc(center, radius, 0.0, TAU, 64, Color(color, 0.16 * alpha), 16.0, true)
draw_arc(center, radius, 0.0, TAU, 64, Color(color, 0.5 * alpha), 6.0, true)
draw_arc(center, radius, 0.0, TAU, 64, Color(1.0, 0.86, 0.96, 0.9 * alpha), 2.0, true)

1
GasBombController.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://dj1n53msflyde

View file

@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"]
[ext_resource type="PackedScene" path="res://VesselGasBomberComponent.tscn" id="2"]
[resource]
script = ExtResource("1")
definition_id = "gas_bomber"
display_name = "Gas Bomber"
description = "Area-control vessel that launches volatile gas bombs at a manually aimed target."
build_cost = 10
build_resource_costs = {"gas": 8, "ore": 10}
build_time = 18.0
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
hull_points = PackedVector2Array(0, -24, -24, 14, -10, 10, 0, 22, 10, 10, 24, 14)
fleet_eligible = true
capabilities = PackedStringArray("gas_bomber")
component_scenes = Array[PackedScene]([ExtResource("2")])
base_stats = {"max_speed": 420.0, "acceleration": 400.0, "delivery_speed": 180.0}
unlock_id = "unlock_gas_bomber"
unlock_name = "Unlock Gas Bomber"
unlock_description = "Research volatile-gas containment and unlock the Gas Bomber for ship crafters."
unlock_cost = 0
unlock_resource_costs = {"gas": 30}
unlock_icon = 31

View file

@ -30,6 +30,7 @@ enum Icon {
LEFT_ARROW, RIGHT_ARROW, LEFT_ARROW, RIGHT_ARROW,
MINUS, PLUS, MINUS, PLUS,
SHIELD_VESSEL, SHIELD_VESSEL,
GAS_BOMBER, GAS_BOMB,
} }
# Size the glyphs were originally drawn against. Every glyph dimension is # Size the glyphs were originally drawn against. Every glyph dimension is
@ -216,6 +217,10 @@ func _draw() -> void:
_draw_plus(center, pulse, s) _draw_plus(center, pulse, s)
Icon.SHIELD_VESSEL: Icon.SHIELD_VESSEL:
_draw_shield_vessel(center, pulse, s) _draw_shield_vessel(center, pulse, s)
Icon.GAS_BOMBER:
_draw_gas_bomber(center, pulse, s)
Icon.GAS_BOMB:
_draw_gas_bomb(center, pulse, s)
func _draw_close(center: Vector2, pulse: float, s: float) -> void: func _draw_close(center: Vector2, pulse: float, s: float) -> void:
@ -240,6 +245,27 @@ func _draw_shield_vessel(center: Vector2, pulse: float, s: float) -> void:
NeonDraw.draw_glowing_polygon(self, points, _active_color(), pulse, core_color) NeonDraw.draw_glowing_polygon(self, points, _active_color(), pulse, core_color)
func _draw_gas_bomber(center: Vector2, pulse: float, s: float) -> void:
var hull := PackedVector2Array([
center + Vector2(0, -12) * s,
center + Vector2(-12, 7) * s,
center + Vector2(-5, 5) * s,
center + Vector2(0, 11) * s,
center + Vector2(5, 5) * s,
center + Vector2(12, 7) * s,
])
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
NeonDraw.draw_glowing_circle(self, center + Vector2(0, 2) * s, 4.0 * s,
_active_color(), pulse, core_color)
func _draw_gas_bomb(center: Vector2, pulse: float, s: float) -> void:
NeonDraw.draw_glowing_circle(self, center, 7.0 * s, _active_color(), pulse, core_color)
for direction in [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]:
NeonDraw.draw_glowing_circle(self, center + direction * 9.0 * s, 3.5 * s,
_active_color(), pulse * 0.85, core_color)
func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void: func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void:
var r: float = 9.0 * s var r: float = 9.0 * s
var tip: Vector2 = center + Vector2(0, r * direction) var tip: Vector2 = center + Vector2(0, r * direction)

188
Main.tscn
View file

@ -1,4 +1,4 @@
[gd_scene load_steps=29 format=3] [gd_scene load_steps=30 format=3]
[ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Starfield.gd" id="1"]
[ext_resource type="Script" path="res://Fleet.gd" id="2"] [ext_resource type="Script" path="res://Fleet.gd" id="2"]
@ -27,6 +27,7 @@
[ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"] [ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"]
[ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"] [ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"]
[ext_resource type="Script" path="res://GasCargoCounter.gd" id="28"] [ext_resource type="Script" path="res://GasCargoCounter.gd" id="28"]
[ext_resource type="Script" path="res://GasBombController.gd" id="29"]
[sub_resource type="ShaderMaterial" id="1"] [sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("5") shader = ExtResource("5")
@ -55,6 +56,9 @@ script = ExtResource("1")
[node name="Fleet" type="Node2D" parent="."] [node name="Fleet" type="Node2D" parent="."]
script = ExtResource("2") script = ExtResource("2")
[node name="GasBombController" type="Node2D" parent="."]
script = ExtResource("29")
[node name="Camera2D" type="Camera2D" parent="."] [node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(0, 0) position = Vector2(0, 0)
zoom = Vector2(1, 1) zoom = Vector2(1, 1)
@ -256,6 +260,21 @@ offset_bottom = -24
script = ExtResource("10") script = ExtResource("10")
icon = 7 icon = 7
[node name="GasBombButton" type="Control" parent="UI"]
visible = false
mouse_filter = 2
anchor_left = 1.0
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -172
offset_right = -24
offset_top = -344
offset_bottom = -196
script = ExtResource("10")
icon = 32
glow_color = Color(0.96, 0.22, 0.72, 1)
[node name="PivotCWButton" type="Control" parent="UI"] [node name="PivotCWButton" type="Control" parent="UI"]
mouse_filter = 2 mouse_filter = 2
anchor_left = 1.0 anchor_left = 1.0
@ -788,8 +807,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 = -180 offset_left = -290
offset_right = -60 offset_right = -178
offset_top = 255 offset_top = 255
offset_bottom = 375 offset_bottom = 375
script = ExtResource("10") script = ExtResource("10")
@ -799,8 +818,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 = -250 offset_left = -330
offset_right = 10 offset_right = -138
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
@ -809,8 +828,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 = -56
offset_right = 180 offset_right = 56
offset_top = 255 offset_top = 255
offset_bottom = 375 offset_bottom = 375
script = ExtResource("10") script = ExtResource("10")
@ -821,8 +840,30 @@ 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 = -10 offset_left = -96
offset_right = 250 offset_right = 96
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 178
offset_right = 290
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="AvailableGasBomberLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 138
offset_right = 330
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
@ -840,8 +881,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 = -180 offset_left = -290
offset_right = -60 offset_right = -178
offset_top = 565 offset_top = 565
offset_bottom = 685 offset_bottom = 685
script = ExtResource("10") script = ExtResource("10")
@ -851,8 +892,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 = -250 offset_left = -330
offset_right = 10 offset_right = -138
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
@ -861,8 +902,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 = -56
offset_right = 180 offset_right = 56
offset_top = 565 offset_top = 565
offset_bottom = 685 offset_bottom = 685
script = ExtResource("10") script = ExtResource("10")
@ -873,8 +914,30 @@ 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 = -10 offset_left = -96
offset_right = 250 offset_right = 96
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 178
offset_right = 290
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="FleetGasBomberLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 138
offset_right = 330
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
@ -924,8 +987,8 @@ script = ExtResource("24")
[node name="VesselA" type="Control" parent="UI/LabMenu"] [node name="VesselA" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -290 offset_left = -300
offset_right = -170 offset_right = -190
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -935,8 +998,8 @@ glow_color = Color(0.25, 0.85, 1, 1)
[node name="VesselB" type="Control" parent="UI/LabMenu"] [node name="VesselB" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -60 offset_left = -145
offset_right = 60 offset_right = -35
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -946,8 +1009,19 @@ glow_color = Color(0.45, 0.8, 1, 1)
[node name="VesselC" type="Control" parent="UI/LabMenu"] [node name="VesselC" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 170 offset_left = 35
offset_right = 290 offset_right = 145
offset_top = 274
offset_bottom = 394
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="VesselD" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 190
offset_right = 300
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -971,7 +1045,7 @@ mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -330 offset_left = -330
offset_right = -130 offset_right = -160
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -982,8 +1056,8 @@ text = "Interceptor"
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -100 offset_left = -175
offset_right = 100 offset_right = -5
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -994,7 +1068,19 @@ text = "Shield"
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 130 offset_left = 5
offset_right = 175
offset_top = 400
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
text = "Gas Bomber"
[node name="VesselDLabel" type="Label" parent="UI/LabMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 160
offset_right = 330 offset_right = 330
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
@ -1246,8 +1332,8 @@ 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 = -220 offset_left = -310
offset_right = -72 offset_right = -182
offset_top = -100 offset_top = -100
offset_bottom = 48 offset_bottom = 48
script = ExtResource("10") script = ExtResource("10")
@ -1259,11 +1345,11 @@ 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 = -290 offset_left = -330
offset_right = -2 offset_right = -162
offset_top = 56 offset_top = 56
offset_bottom = 136 offset_bottom = 136
theme_override_font_sizes/font_size = 32 theme_override_font_sizes/font_size = 24
text = "Interceptor" text = "Interceptor"
horizontal_alignment = 1 horizontal_alignment = 1
@ -1272,8 +1358,8 @@ 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 = 72 offset_left = -148
offset_right = 220 offset_right = -20
offset_top = -100 offset_top = -100
offset_bottom = 48 offset_bottom = 48
script = ExtResource("10") script = ExtResource("10")
@ -1286,15 +1372,43 @@ 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 = 2 offset_left = -168
offset_right = 290 offset_right = 0
offset_top = 56 offset_top = 56
offset_bottom = 136 offset_bottom = 136
theme_override_font_sizes/font_size = 32 theme_override_font_sizes/font_size = 24
autowrap_mode = 2 autowrap_mode = 2
text = "Shield Vessel" text = "Shield Vessel"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="GasBomberButton" type="Control" parent="UI/CraftMenu"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 14
offset_right = 142
offset_top = -100
offset_bottom = 28
script = ExtResource("10")
icon = 31
glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="GasBomberLabel" 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 = -6
offset_right = 162
offset_top = 56
offset_bottom = 150
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Gas Bomber"
horizontal_alignment = 1
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"] [node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
mouse_filter = 2 mouse_filter = 2
anchor_left = 1.0 anchor_left = 1.0

View file

@ -26,6 +26,16 @@ static func draw_glowing_line(item: CanvasItem, from: Vector2, to: Vector2, glow
_draw_layers(item, PackedVector2Array([from, to]), glow_color, pulse, width_scale, core_color) _draw_layers(item, PackedVector2Array([from, to]), glow_color, pulse, width_scale, core_color)
static func draw_glowing_circle(item: CanvasItem, center: Vector2, radius: float,
glow_color: Color, pulse: float = 1.0,
core_color: Color = DEFAULT_CORE) -> void:
var points := PackedVector2Array()
for i in range(24):
var angle := TAU * float(i) / 24.0
points.append(center + Vector2(cos(angle), sin(angle)) * radius)
draw_glowing_polygon(item, points, glow_color, pulse, core_color)
# An open run of points - unlike draw_glowing_polygon, the ends are NOT # An open run of points - unlike draw_glowing_polygon, the ends are NOT
# joined. For arcs, cradle ribs and other shapes that are deliberately open. # joined. For arcs, cradle ribs and other shapes that are deliberately open.
static func draw_glowing_path(item: CanvasItem, points: PackedVector2Array, glow_color: Color, pulse: float = 1.0, width_scale: float = 1.0, core_color: Color = DEFAULT_CORE) -> void: static func draw_glowing_path(item: CanvasItem, points: PackedVector2Array, glow_color: Color, pulse: float = 1.0, width_scale: float = 1.0, core_color: Color = DEFAULT_CORE) -> void:

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=15 format=3] [gd_scene load_steps=16 format=3]
[ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://GameController.gd" id="1"]
[ext_resource type="Script" path="res://LunarBackground.gd" id="2"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"]
@ -11,6 +11,7 @@
[ext_resource type="Script" path="res://GasCounter.gd" id="11"] [ext_resource type="Script" path="res://GasCounter.gd" id="11"]
[ext_resource type="Script" path="res://CargoCounter.gd" id="12"] [ext_resource type="Script" path="res://CargoCounter.gd" id="12"]
[ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"] [ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"]
[ext_resource type="Script" path="res://GasBombController.gd" id="14"]
[sub_resource type="ShaderMaterial" id="1"] [sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("3") shader = ExtResource("3")
@ -43,6 +44,9 @@ script = ExtResource("4")
ship_glow_color = Color(0.06, 0.32, 0.5, 1) ship_glow_color = Color(0.06, 0.32, 0.5, 1)
ship_core_color = Color(0.02, 0.09, 0.15, 1) ship_core_color = Color(0.02, 0.09, 0.15, 1)
[node name="GasBombController" type="Node2D" parent="."]
script = ExtResource("14")
[node name="Camera2D" type="Camera2D" parent="."] [node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(0, 0) position = Vector2(0, 0)
zoom = Vector2(1, 1) zoom = Vector2(1, 1)
@ -180,6 +184,22 @@ icon = 7
glow_color = Color(0.06, 0.32, 0.5, 1) glow_color = Color(0.06, 0.32, 0.5, 1)
core_color = Color(0.02, 0.09, 0.15, 1) core_color = Color(0.02, 0.09, 0.15, 1)
[node name="GasBombButton" type="Control" parent="UI"]
visible = false
mouse_filter = 2
anchor_left = 1.0
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -172
offset_right = -24
offset_top = -344
offset_bottom = -196
script = ExtResource("6")
icon = 32
glow_color = Color(0.82, 0.08, 0.48, 1)
core_color = Color(0.02, 0.09, 0.15, 1)
[node name="PivotCWButton" type="Control" parent="UI"] [node name="PivotCWButton" type="Control" parent="UI"]
mouse_filter = 2 mouse_filter = 2
anchor_left = 1.0 anchor_left = 1.0

View file

@ -24,3 +24,4 @@ unlock_id = "unlock_shield"
unlock_name = "Unlock Shield Vessel" unlock_name = "Unlock Shield Vessel"
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up." unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
unlock_cost = 45 unlock_cost = 45
unlock_icon = 23

View file

@ -5,6 +5,7 @@ extends Resource
@export var display_name: String @export var display_name: String
@export_multiline var description: String @export_multiline var description: String
@export var build_cost: int @export var build_cost: int
@export var build_resource_costs: Dictionary = {}
@export var build_time: float = 10.0 @export var build_time: float = 10.0
@export var icon: int = 2 @export var icon: int = 2
@export var glow_color: Color = Color(0.25, 0.85, 1.0) @export var glow_color: Color = Color(0.25, 0.85, 1.0)
@ -21,6 +22,8 @@ extends Resource
@export var unlock_name: String @export var unlock_name: String
@export_multiline var unlock_description: String @export_multiline var unlock_description: String
@export var unlock_cost: int @export var unlock_cost: int
@export var unlock_resource_costs: Dictionary = {}
@export var unlock_icon: int = -1
func has_capability(capability: String) -> bool: func has_capability(capability: String) -> bool:

View file

@ -1,10 +1,11 @@
class_name VesselDefinitions class_name VesselDefinitions
extends RefCounted extends RefCounted
const ORDER: PackedStringArray = ["interceptor", "shield", "transporter"] const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter"]
const DEFINITIONS: Dictionary = { const DEFINITIONS: Dictionary = {
"interceptor": preload("res://InterceptorVesselDefinition.tres"), "interceptor": preload("res://InterceptorVesselDefinition.tres"),
"shield": preload("res://ShieldVesselDefinition.tres"), "shield": preload("res://ShieldVesselDefinition.tres"),
"gas_bomber": preload("res://GasBomberVesselDefinition.tres"),
"transporter": preload("res://TransporterVesselDefinition.tres"), "transporter": preload("res://TransporterVesselDefinition.tres"),
} }

View file

@ -0,0 +1,3 @@
extends Node
var capability_id := "gas_bomber"

View file

@ -0,0 +1 @@
uid://cim0c4n73qd2j

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://VesselGasBomberComponent.gd" id="1"]
[node name="GasBomberComponent" type="Node"]
script = ExtResource("1")

View file

@ -17,12 +17,19 @@ func _ready() -> void:
GameState.fleet_cargo = 24 GameState.fleet_cargo = 24
GameState.fleet_gas_cargo = 12 GameState.fleet_gas_cargo = 12
GameState.unlocked = ["shipyards", "logistics", "freight"] GameState.unlocked = ["shipyards", "logistics", "freight"]
GameState.vessel_levels["unlock_gas_bomber"] = 1
var main: Node = load("res://Main.tscn").instantiate() var main: Node = load("res://Main.tscn").instantiate()
add_child(main) add_child(main)
main.get_node("BackgroundLayer").visible = false
main.get_node("Fleet").visible = false
main.get_node("AsteroidField").visible = false
main.get_node("PlanetField").visible = false
main.get_node("BuildingField").visible = false
main.get_node("EnemySpawner").set_process(false)
main.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0)) main.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
var crafter_id: int = int(GameState.buildings[-1].id) var crafter_id: int = int(GameState.buildings[-1].id)
GameState.deposit_to_building(crafter_id, 42) GameState.deposit_to_building(crafter_id, 42)
GameState.find_building(crafter_id).gas_stock = 18 GameState.deposit_resource_to_building(crafter_id, GameState.RESOURCE_GAS, 18)
var bc: Node = main.get_node("BuildController") var bc: Node = main.get_node("BuildController")
bc.craft_building_id = crafter_id bc.craft_building_id = crafter_id
# The shared header (#24) is the thing worth eyeballing here, so give it # The shared header (#24) is the thing worth eyeballing here, so give it

View file

@ -0,0 +1,38 @@
extends Node2D
func _ready() -> void:
GameState.buildings.clear()
GameState.extractors.clear()
GameState.transporters.clear()
GameState.vessel_levels["unlock_gas_bomber"] = 1
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)
main.get_node("BackgroundLayer").visible = false
main.get_node("AsteroidField").visible = false
main.get_node("PlanetField").visible = false
main.get_node("BuildingField").visible = false
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
var fleet: Node = main.get_node("Fleet")
fleet.spawn_ships(3, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_GAS_BOMBER]))
var skill: GasBombController = main.get_node("GasBombController")
skill._process(0.0)
main.get_node("Camera2D").zoom = Vector2(1.8, 1.8)
var button: Control = main.get_node("UI/GasBombButton")
var press := InputEventScreenTouch.new()
press.index = 9
press.position = button.global_position + button.size * 0.5
press.pressed = true
skill.handle_touch(press)
var drag := InputEventScreenDrag.new()
drag.index = 9
drag.position = press.position + Vector2(-245, -235)
skill.handle_drag(drag)
for offset in [Vector2(-65, 0), Vector2(45, -45), Vector2(55, 65)]:
var enemy: Node = load("res://Enemy.tscn").instantiate()
enemy.position = skill.aim_target + offset
enemy.speed = 0.0
main.add_child(enemy)

View file

@ -0,0 +1 @@
uid://dcokscjlrguje

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_gas_bomb_aim_preview.gd" id="1"]
[node name="GasBombAimPreview" type="Node2D"]
script = ExtResource("1")

View file

@ -11,13 +11,19 @@ func _ready() -> void:
GameState.stored_vessels = [ GameState.stored_vessels = [
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD,
GameState.VESSEL_GAS_BOMBER,
] ]
var main: Node = load("res://Main.tscn").instantiate() var main: Node = load("res://Main.tscn").instantiate()
add_child(main) add_child(main)
main.get_node("BackgroundLayer").visible = false
main.get_node("AsteroidField").visible = false
main.get_node("PlanetField").visible = false
main.get_node("BuildingField").visible = false
main.get_node("AsteroidField").set_process(false) main.get_node("AsteroidField").set_process(false)
main.get_node("EnemySpawner").set_process(false) main.get_node("EnemySpawner").set_process(false)
main.get_node("Fleet").spawn_ships(3, Vector2.ZERO, PackedStringArray([ main.get_node("Fleet").spawn_ships(3, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD,
GameState.VESSEL_GAS_BOMBER,
])) ]))
main.get_node("BuildController")._on_hangar_pressed() main.get_node("BuildController")._on_hangar_pressed()

View file

@ -16,19 +16,22 @@ func _ready() -> void:
GameState.unlocked = ["shipyards", "vessel_science"] GameState.unlocked = ["shipyards", "vessel_science"]
var main: Node = load("res://Main.tscn").instantiate() var main: Node = load("res://Main.tscn").instantiate()
add_child(main) add_child(main)
main.get_node("BackgroundLayer").visible = false
main.get_node("Fleet").visible = false
main.get_node("AsteroidField").visible = false
main.get_node("PlanetField").visible = false
main.get_node("BuildingField").visible = false
main.get_node("EnemySpawner").set_process(false)
main.get_node("BuildingField").place(GameState.BUILDING_SHIP_LAB, Vector2(400.0, 0.0)) main.get_node("BuildingField").place(GameState.BUILDING_SHIP_LAB, Vector2(400.0, 0.0))
var lab_id: int = int(GameState.buildings[-1].id) var lab_id: int = int(GameState.buildings[-1].id)
GameState.deposit_to_building(lab_id, 18) GameState.deposit_to_building(lab_id, 18)
GameState.fleet_cargo = 12 GameState.fleet_cargo = 12
GameState.fleet_gas_cargo = 8 GameState.fleet_gas_cargo = 8
# A level already bought, so the list shows both a researched and an
# unresearched line rather than a uniform page of zeroes.
GameState.vessel_levels["int_damage"] = 1
var bc: Node = main.get_node("BuildController") var bc: Node = main.get_node("BuildController")
# The shared header (#24) hides its HERE column when it has no building, # The shared header (#24) hides its HERE column when it has no building,
# so bind it the way opening the panel from the building does. # so bind it the way opening the panel from the building does.
for bar in bc.resource_bars: for bar in bc.resource_bars:
bar.building_id = lab_id bar.building_id = lab_id
main.get_node("UI/LabMenu").visible = true main.get_node("UI/LabMenu").visible = true
bc.lab_vessel = GameState.VESSEL_INTERCEPTOR bc.lab_vessel = GameState.VESSEL_GAS_BOMBER
bc._refresh_lab() bc._refresh_lab()

151
tests/test_gas_bomber.gd Normal file
View file

@ -0,0 +1,151 @@
extends SceneTree
var game_state: Node
var main: Node
var fleet: Node
var skill: GasBombController
var inside_enemies: Array = []
var outside_enemy: Node
var stage := 0
var elapsed := 0.0
var original_zoom := 2.5
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.vessel_deliveries.clear()
game_state.reset_progression()
game_state.rocks = 0
game_state.gas = 0
game_state.fleet_cargo = 0
game_state.fleet_gas_cargo = 0
game_state.build_time_scale = 0.01
# Research is gas-only: any amount of ore and 29 gas must still leave it
# waiting, then the thirtieth unit starts and completes the unlock.
var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO)
if not game_state.queue_lab_research(lab_id, "unlock_gas_bomber"):
_fail_now("could not queue Gas Bomber research")
return
game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, 200)
game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_GAS, 29)
game_state._advance_lab_research(10.0)
if game_state.outstanding_resource_cost(lab_id, GameState.RESOURCE_GAS) != 1 \
or game_state.outstanding_resource_cost(lab_id, GameState.RESOURCE_ORE) != 0:
_fail_now("research shortfall was not tracked as gas")
return
if game_state.vessel_is_unlocked(GameState.VESSEL_GAS_BOMBER):
_fail_now("Gas Bomber unlocked without its full gas cost")
return
game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_GAS, 1)
game_state._advance_lab_research(10.0)
if not game_state.vessel_is_unlocked(GameState.VESSEL_GAS_BOMBER):
_fail_now("30 gas did not unlock the Gas Bomber")
return
# Crafting requires both resources and preserves the vessel type.
var crafter_id: int = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2.ZERO)
if not game_state.queue_order(crafter_id, GameState.VESSEL_GAS_BOMBER):
_fail_now("unlocked Gas Bomber could not be queued")
return
game_state.deposit_resource_to_building(crafter_id, GameState.RESOURCE_ORE, 10)
game_state.deposit_resource_to_building(crafter_id, GameState.RESOURCE_GAS, 7)
game_state._advance_production(10.0)
if game_state.outstanding_resource_cost(crafter_id, GameState.RESOURCE_GAS) != 1 \
or game_state.outstanding_resource_cost(crafter_id, GameState.RESOURCE_ORE) != 0:
_fail_now("crafting shortfall was not tracked as gas")
return
if not game_state.order_list(crafter_id).is_empty() \
and bool(game_state.order_list(crafter_id)[0].started):
_fail_now("Gas Bomber started with insufficient gas")
return
game_state.deposit_resource_to_building(crafter_id, GameState.RESOURCE_GAS, 1)
game_state._advance_production(10.0)
if game_state.stored_vessels != [GameState.VESSEL_GAS_BOMBER]:
_fail_now("crafting did not produce a typed Gas Bomber")
return
main = load("res://Main.tscn").instantiate()
root.add_child(main)
fleet = main.get_node("Fleet")
skill = main.get_node("GasBombController")
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.2:
return false
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
if main.get_node("UI/GasBombButton").visible:
return _fail_now("bomb control appeared without a Gas Bomber")
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_GAS_BOMBER]))
main.get_node("Camera2D").zoom = Vector2(original_zoom, original_zoom)
skill._process(0.0)
if not main.get_node("UI/GasBombButton").visible:
return _fail_now("fleet Gas Bomber did not reveal the bomb control")
var press := InputEventScreenTouch.new()
press.index = 7
var bomb_button: Control = main.get_node("UI/GasBombButton")
press.position = bomb_button.global_position + bomb_button.size * 0.5
press.pressed = true
main._handle_touch(press)
if not skill.aiming:
return _fail_now("bomb control did not begin aiming")
var drag := InputEventScreenDrag.new()
drag.index = 7
drag.position = Vector2(350, 700)
main._handle_drag(drag)
if main.get_node("Camera2D").zoom.x >= original_zoom:
return _fail_now("long aim did not zoom out")
for offset in [Vector2(-60, 0), Vector2(70, 35)]:
var enemy: Node = load("res://Enemy.tscn").instantiate()
enemy.position = skill.aim_target + offset
enemy.speed = 0.0
main.add_child(enemy)
inside_enemies.append(enemy)
outside_enemy = load("res://Enemy.tscn").instantiate()
outside_enemy.position = skill.aim_target + Vector2(GasBombController.AOE_RADIUS + 80.0, 0)
outside_enemy.speed = 0.0
main.add_child(outside_enemy)
var release := InputEventScreenTouch.new()
release.index = 7
release.position = drag.position
release.pressed = false
main._handle_touch(release)
if not skill.projectile_active:
return _fail_now("release did not throw a gas bomb")
elapsed = 0.0
stage = 1
1:
if elapsed < 2.5:
return false
if skill.last_hit_count != 2:
return _fail_now("AOE hit %d enemies instead of 2" % skill.last_hit_count)
for enemy in inside_enemies:
if is_instance_valid(enemy):
return _fail_now("an enemy inside the AOE survived")
if not is_instance_valid(outside_enemy) or outside_enemy.hp != outside_enemy.max_hp:
return _fail_now("the AOE damaged an enemy outside its radius")
if absf(main.get_node("Camera2D").zoom.x - original_zoom) > 0.03:
return _fail_now("camera did not restore its pre-aim zoom")
print("TEST_GAS_BOMBER: PASS")
quit(0)
return true
return false
func _fail_now(message: String) -> bool:
print("TEST_GAS_BOMBER: FAIL (%s)" % message)
quit(1)
return true

View file

@ -0,0 +1 @@
uid://c3vtwrwjpmcbm

View file

@ -0,0 +1,42 @@
extends SceneTree
func _initialize() -> void:
var state: Node = root.get_node("GameState")
state.buildings.clear()
state.extractors.clear()
state.transporters.clear()
state.stored_vessels.clear()
state.reset_progression()
state.rocks = 0
state.gas = 30
state.build_time_scale = 0.01
state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2.ZERO)
var lab_id: int = state.add_building(GameState.BUILDING_SHIP_LAB, Vector2(120, 0))
if not state.queue_lab_research(lab_id, "unlock_gas_bomber"):
_fail("could not queue gas research")
return
if state.outstanding_resource_cost(lab_id, GameState.RESOURCE_GAS) != 30:
_fail("lab did not request 30 gas")
return
# Large steps make each physical leg complete in one call; multiple calls
# still exercise dispatch -> bank pickup -> lab delivery -> hub return.
for i in range(12):
state._advance_transporters(10.0)
state._advance_lab_research(10.0)
if not state.vessel_is_unlocked(GameState.VESSEL_GAS_BOMBER):
_fail("hub transporters did not deliver gas to research")
return
if state.gas != 0 or state.building_resource(lab_id, GameState.RESOURCE_GAS) != 0:
_fail("gas was not consumed at the lab")
return
print("TEST_MULTI_RESOURCE_SUPPLY: PASS")
quit(0)
func _fail(message: String) -> void:
print("TEST_MULTI_RESOURCE_SUPPLY: FAIL (%s)" % message)
quit(1)

View file

@ -0,0 +1 @@
uid://d34djd8v1r1jd

View file

@ -23,7 +23,7 @@ func _process(delta: float) -> bool:
return false return false
var definitions: Array = VESSEL_DEFINITIONS.all() var definitions: Array = VESSEL_DEFINITIONS.all()
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet() var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
if fleet_definitions.size() != 2 \ if fleet_definitions.size() != 3 \
or fleet_definitions.any(func(definition): return definition.has_capability("transport")): or fleet_definitions.any(func(definition): return definition.has_capability("transport")):
return _fail("logistics transporters leaked into the combat-fleet catalog") return _fail("logistics transporters leaked into the combat-fleet catalog")
if ships.size() != definitions.size(): if ships.size() != definitions.size():
@ -45,7 +45,13 @@ func _process(delta: float) -> bool:
or definitions[1].hull_points.size() != 6 or definitions[1].hull_points.size() != 6
or definitions[1].icon != IconButton.Icon.SHIELD_VESSEL): or definitions[1].icon != IconButton.Icon.SHIELD_VESSEL):
return _fail("shield vessel is not an unarmed hexagon") return _fail("shield vessel is not an unarmed hexagon")
if ships[2].has_capability("weapon") or not ships[2].has_capability("transport"): var gas_index: int = VESSEL_DEFINITIONS.ORDER.find(GameState.VESSEL_GAS_BOMBER)
if gas_index < 0 or ships[gas_index].has_capability("weapon") \
or not ships[gas_index].definition.has_capability("gas_bomber"):
return _fail("gas bomber is not an unarmed skill vessel")
var transporter_index: int = VESSEL_DEFINITIONS.ORDER.find(GameState.VESSEL_TRANSPORTER)
if ships[transporter_index].has_capability("weapon") \
or not ships[transporter_index].has_capability("transport"):
return _fail("transporter capabilities were composed incorrectly") return _fail("transporter capabilities were composed incorrectly")
print("TEST_VESSEL_COMPOSITION: PASS") print("TEST_VESSEL_COMPOSITION: PASS")
quit(0) quit(0)