Add gas bomber skill-shot vessel
This commit is contained in:
parent
d0a9f5b971
commit
1d9afcbc2e
28 changed files with 882 additions and 85 deletions
|
|
@ -30,12 +30,12 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
|||
@onready var lab_menu: Control = get_node("../UI/LabMenu")
|
||||
@onready var lab_vessel_buttons: Array = [
|
||||
get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"),
|
||||
get_node("../UI/LabMenu/VesselC"),
|
||||
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_labels: Array = [
|
||||
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_status: Label = get_node("../UI/LabMenu/StatusLabel")
|
||||
|
|
@ -62,18 +62,22 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
|||
@onready var hangar_available_slots: Array = [
|
||||
get_node("../UI/HangarMenu/AvailableInterceptor"),
|
||||
get_node("../UI/HangarMenu/AvailableShield"),
|
||||
get_node("../UI/HangarMenu/AvailableGasBomber"),
|
||||
]
|
||||
@onready var hangar_available_labels: Array = [
|
||||
get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
|
||||
get_node("../UI/HangarMenu/AvailableShieldLabel"),
|
||||
get_node("../UI/HangarMenu/AvailableGasBomberLabel"),
|
||||
]
|
||||
@onready var hangar_fleet_slots: Array = [
|
||||
get_node("../UI/HangarMenu/FleetInterceptor"),
|
||||
get_node("../UI/HangarMenu/FleetShield"),
|
||||
get_node("../UI/HangarMenu/FleetGasBomber"),
|
||||
]
|
||||
@onready var hangar_fleet_labels: Array = [
|
||||
get_node("../UI/HangarMenu/FleetInterceptorLabel"),
|
||||
get_node("../UI/HangarMenu/FleetShieldLabel"),
|
||||
get_node("../UI/HangarMenu/FleetGasBomberLabel"),
|
||||
]
|
||||
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_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel")
|
||||
@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 = [
|
||||
craft_interceptor_button, craft_shield_button,
|
||||
craft_interceptor_button, craft_shield_button, craft_gas_bomber_button,
|
||||
]
|
||||
@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) ---------------------------------------------
|
||||
|
|
@ -723,6 +729,25 @@ func _say(message: String) -> void:
|
|||
_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:
|
||||
# Prices live with the buttons they describe. The top of the screen is
|
||||
# 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())):
|
||||
var definition: Resource = definitions[i]
|
||||
var button: IconButton = craft_vessel_buttons[i]
|
||||
craft_vessel_labels[i].text = "%s\n%d ore" % [definition.display_name, definition.build_cost]
|
||||
craft_vessel_labels[i].text = "%s\n%s" % [definition.display_name,
|
||||
_format_costs(GameState.product_costs(definition.definition_id))]
|
||||
button.icon = definition.icon
|
||||
button.glow_color = definition.glow_color
|
||||
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:
|
||||
var short: int = GameState.outstanding_cost(craft_building_id)
|
||||
if short > 0:
|
||||
_say("%s queued - %d more ore must be delivered here" % [
|
||||
GameState.vessel_name(product), short
|
||||
])
|
||||
var missing: Dictionary = GameState.outstanding_costs(craft_building_id)
|
||||
if not missing.is_empty():
|
||||
_say("%s queued - deliver %s here" % [GameState.vessel_name(product),
|
||||
_format_missing_costs(missing)])
|
||||
else:
|
||||
_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):
|
||||
options.append({"unlock": true, "id": definition.unlock_id,
|
||||
"name": definition.unlock_name, "desc": definition.unlock_description,
|
||||
"cost": definition.unlock_cost, "icon": IconButton.Icon.UPGRADE_UNLOCK_SHIELD})
|
||||
"costs": GameState.research_costs(definition.unlock_id),
|
||||
"icon": definition.unlock_icon if definition.unlock_icon >= 0 else definition.icon})
|
||||
return options
|
||||
for u in GameState.upgrades_for(lab_vessel):
|
||||
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):
|
||||
lab_status.text = "Research is already queued or complete"
|
||||
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()
|
||||
|
||||
|
||||
|
|
@ -844,7 +871,8 @@ func _refresh_lab() -> void:
|
|||
if not visible:
|
||||
continue
|
||||
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):
|
||||
price_text = "MAX"
|
||||
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)
|
||||
lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id))
|
||||
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()
|
||||
|
||||
|
||||
|
|
@ -989,7 +1017,7 @@ func _refresh_queue() -> void:
|
|||
int(round(float(head.progress) * 100.0)),
|
||||
]
|
||||
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))
|
||||
|
||||
|
||||
|
|
@ -1006,7 +1034,7 @@ func _on_queue_slot(index: int) -> void:
|
|||
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 ""])
|
||||
_say("%s cancelled%s" % [vessel, " - resources refunded here" if refunded else ""])
|
||||
_refresh_queue()
|
||||
|
||||
|
||||
|
|
|
|||
25
Fleet.gd
25
Fleet.gd
|
|
@ -86,6 +86,31 @@ func vessel_types() -> PackedStringArray:
|
|||
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:
|
||||
# Adds one vessel in the next free formation slot (bought from a ship
|
||||
# crafter). Returns false when the wedge is already full.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ extends Node2D
|
|||
# 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_ccw_button: IconButton = get_node_or_null("UI/PivotCCWButton")
|
||||
@onready var gas_bomb_controller: GasBombController = get_node_or_null("GasBombController")
|
||||
|
||||
var touches: Dictionary = {}
|
||||
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
|
||||
# that begin inside its own rect - so pinch-to-zoom still works anywhere
|
||||
# else on screen.
|
||||
if gas_bomb_controller != null and gas_bomb_controller.handle_touch(event):
|
||||
return
|
||||
if _handle_pivot_touch(event):
|
||||
return
|
||||
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:
|
||||
if gas_bomb_controller != null and gas_bomb_controller.handle_drag(event):
|
||||
return
|
||||
if joystick != null and joystick.visible and joystick.handle_drag(event):
|
||||
return
|
||||
if not touches.has(event.index):
|
||||
|
|
|
|||
114
GameState.gd
114
GameState.gd
|
|
@ -410,6 +410,7 @@ func building_has_capability(building: Dictionary, capability: String) -> bool:
|
|||
# but transporters themselves belong to hubs and are never production orders.
|
||||
const VESSEL_INTERCEPTOR: String = "interceptor"
|
||||
const VESSEL_SHIELD: String = "shield"
|
||||
const VESSEL_GAS_BOMBER: String = "gas_bomber"
|
||||
const VESSEL_TRANSPORTER: String = "transporter"
|
||||
|
||||
# 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)
|
||||
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
|
||||
return false
|
||||
if not spend(definition.unlock_cost):
|
||||
var costs: Dictionary = research_costs(definition.unlock_id)
|
||||
if not can_afford_resources(costs):
|
||||
return false
|
||||
for resource_type in costs:
|
||||
bank_inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
||||
vessel_levels[definition.unlock_id] = 1
|
||||
vessel_upgraded.emit(definition.unlock_id)
|
||||
return true
|
||||
|
|
@ -604,8 +608,16 @@ const PRODUCT_SHIP: String = "interceptor"
|
|||
|
||||
|
||||
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)
|
||||
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:
|
||||
|
|
@ -661,34 +673,53 @@ func cancel_order(id: int, index: int) -> bool:
|
|||
return false
|
||||
var order: Dictionary = orders[index]
|
||||
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)
|
||||
return true
|
||||
|
||||
|
||||
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.
|
||||
var b: Dictionary = find_building(id)
|
||||
if b.is_empty():
|
||||
return 0
|
||||
var need: int = 0
|
||||
for o in b.get("orders", []):
|
||||
# A started build has already paid, so it is not still owed.
|
||||
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", []):
|
||||
if not bool(order.started):
|
||||
need += research_cost(String(order.id))
|
||||
return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 0)
|
||||
need += int(research_costs(String(order.id)).get(resource_type, 0))
|
||||
return maxi(need - _inventory_for_record(b).amount(resource_type), 0)
|
||||
|
||||
|
||||
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:
|
||||
return 0
|
||||
var b: Dictionary = find_building(id)
|
||||
if b.is_empty():
|
||||
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:
|
||||
|
|
@ -717,14 +748,20 @@ func _advance_production(delta: float) -> void:
|
|||
# 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 costs: Dictionary = product_costs(product)
|
||||
var inventory: RefCounted = _inventory_for_record(b)
|
||||
var definition: Resource = vessel_definition(product)
|
||||
|
||||
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
|
||||
inventory.withdraw(RESOURCE_ORE, cost)
|
||||
for resource_type in costs:
|
||||
inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
||||
order.started = true
|
||||
|
||||
order.elapsed = float(order.elapsed) + delta
|
||||
|
|
@ -764,8 +801,17 @@ func lab_research_order(lab_id: int) -> Dictionary:
|
|||
|
||||
|
||||
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)
|
||||
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:
|
||||
|
|
@ -778,12 +824,18 @@ func _advance_lab_research(delta: float) -> void:
|
|||
var order: Dictionary = orders[0]
|
||||
var id: String = String(order.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)
|
||||
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
|
||||
inventory.withdraw(RESOURCE_ORE, cost)
|
||||
for resource_type in costs:
|
||||
inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
||||
order.started = true
|
||||
order.elapsed = float(order.elapsed) + delta
|
||||
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,
|
||||
TRANSPORTER_TO_EXTRACTOR)
|
||||
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
|
||||
# One at a time per building: without this every idle transporter
|
||||
# would be sent after the same shortfall and they would all arrive
|
||||
|
|
@ -1149,6 +1207,7 @@ func _dispatch_transporters() -> void:
|
|||
if chosen.is_empty():
|
||||
return # every transporter is busy
|
||||
chosen.target_id = int(b.id)
|
||||
chosen.resource_type = needed_resource
|
||||
_begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP)
|
||||
|
||||
|
||||
|
|
@ -1175,22 +1234,24 @@ func _arrive(t: Dictionary) -> void:
|
|||
match t.state:
|
||||
TRANSPORTER_TO_MOTHERSHIP:
|
||||
var target: Dictionary = find_building(int(t.target_id))
|
||||
var want: int = outstanding_cost(int(t.target_id))
|
||||
var take: int = mini(mini(want, bank_inventory.amount(RESOURCE_ORE)), transporter_load_limit())
|
||||
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
||||
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():
|
||||
# Nothing to carry, or the building is gone. Go home empty.
|
||||
t.target_id = 0
|
||||
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
|
||||
return
|
||||
bank_inventory.withdraw(RESOURCE_ORE, take)
|
||||
bank_inventory.withdraw(resource_type, take)
|
||||
t.carrying = take
|
||||
_begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING)
|
||||
TRANSPORTER_TO_BUILDING:
|
||||
# The ore moves at exactly the moment the vessel arrives.
|
||||
if deposit_to_building(int(t.target_id), int(t.carrying)) <= 0:
|
||||
# Building demolished in transit - put the ore back rather
|
||||
# The cargo moves at exactly the moment the vessel arrives.
|
||||
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
||||
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.
|
||||
bank_inventory.deposit(RESOURCE_ORE, int(t.carrying))
|
||||
bank_inventory.deposit(resource_type, int(t.carrying))
|
||||
t.carrying = 0
|
||||
t.target_id = 0
|
||||
_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
|
||||
|
||||
|
||||
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:
|
||||
if not can_afford(cost):
|
||||
return false
|
||||
|
|
|
|||
197
GasBombController.gd
Normal file
197
GasBombController.gd
Normal 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
1
GasBombController.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dj1n53msflyde
|
||||
26
GasBomberVesselDefinition.tres
Normal file
26
GasBomberVesselDefinition.tres
Normal 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
|
||||
|
|
@ -30,6 +30,7 @@ enum Icon {
|
|||
LEFT_ARROW, RIGHT_ARROW,
|
||||
MINUS, PLUS,
|
||||
SHIELD_VESSEL,
|
||||
GAS_BOMBER, GAS_BOMB,
|
||||
}
|
||||
|
||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||
|
|
@ -216,6 +217,10 @@ func _draw() -> void:
|
|||
_draw_plus(center, pulse, s)
|
||||
Icon.SHIELD_VESSEL:
|
||||
_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:
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
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:
|
||||
var r: float = 9.0 * s
|
||||
var tip: Vector2 = center + Vector2(0, r * direction)
|
||||
|
|
|
|||
188
Main.tscn
188
Main.tscn
|
|
@ -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://Fleet.gd" id="2"]
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
[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://GasCargoCounter.gd" id="28"]
|
||||
[ext_resource type="Script" path="res://GasBombController.gd" id="29"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="1"]
|
||||
shader = ExtResource("5")
|
||||
|
|
@ -55,6 +56,9 @@ script = ExtResource("1")
|
|||
[node name="Fleet" type="Node2D" parent="."]
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="GasBombController" type="Node2D" parent="."]
|
||||
script = ExtResource("29")
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
position = Vector2(0, 0)
|
||||
zoom = Vector2(1, 1)
|
||||
|
|
@ -256,6 +260,21 @@ offset_bottom = -24
|
|||
script = ExtResource("10")
|
||||
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"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 1.0
|
||||
|
|
@ -788,8 +807,8 @@ horizontal_alignment = 1
|
|||
[node name="AvailableInterceptor" type="Control" parent="UI/HangarMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -180
|
||||
offset_right = -60
|
||||
offset_left = -290
|
||||
offset_right = -178
|
||||
offset_top = 255
|
||||
offset_bottom = 375
|
||||
script = ExtResource("10")
|
||||
|
|
@ -799,8 +818,8 @@ icon = 2
|
|||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -250
|
||||
offset_right = 10
|
||||
offset_left = -330
|
||||
offset_right = -138
|
||||
offset_top = 380
|
||||
offset_bottom = 460
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
|
@ -809,8 +828,8 @@ horizontal_alignment = 1
|
|||
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 60
|
||||
offset_right = 180
|
||||
offset_left = -56
|
||||
offset_right = 56
|
||||
offset_top = 255
|
||||
offset_bottom = 375
|
||||
script = ExtResource("10")
|
||||
|
|
@ -821,8 +840,30 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
|||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -10
|
||||
offset_right = 250
|
||||
offset_left = -96
|
||||
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_bottom = 460
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
|
@ -840,8 +881,8 @@ horizontal_alignment = 1
|
|||
[node name="FleetInterceptor" type="Control" parent="UI/HangarMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -180
|
||||
offset_right = -60
|
||||
offset_left = -290
|
||||
offset_right = -178
|
||||
offset_top = 565
|
||||
offset_bottom = 685
|
||||
script = ExtResource("10")
|
||||
|
|
@ -851,8 +892,8 @@ icon = 2
|
|||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -250
|
||||
offset_right = 10
|
||||
offset_left = -330
|
||||
offset_right = -138
|
||||
offset_top = 690
|
||||
offset_bottom = 770
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
|
@ -861,8 +902,8 @@ horizontal_alignment = 1
|
|||
[node name="FleetShield" type="Control" parent="UI/HangarMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 60
|
||||
offset_right = 180
|
||||
offset_left = -56
|
||||
offset_right = 56
|
||||
offset_top = 565
|
||||
offset_bottom = 685
|
||||
script = ExtResource("10")
|
||||
|
|
@ -873,8 +914,30 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
|||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -10
|
||||
offset_right = 250
|
||||
offset_left = -96
|
||||
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_bottom = 770
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
|
@ -924,8 +987,8 @@ script = ExtResource("24")
|
|||
[node name="VesselA" type="Control" parent="UI/LabMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -290
|
||||
offset_right = -170
|
||||
offset_left = -300
|
||||
offset_right = -190
|
||||
offset_top = 274
|
||||
offset_bottom = 394
|
||||
script = ExtResource("10")
|
||||
|
|
@ -935,8 +998,8 @@ glow_color = Color(0.25, 0.85, 1, 1)
|
|||
[node name="VesselB" type="Control" parent="UI/LabMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -60
|
||||
offset_right = 60
|
||||
offset_left = -145
|
||||
offset_right = -35
|
||||
offset_top = 274
|
||||
offset_bottom = 394
|
||||
script = ExtResource("10")
|
||||
|
|
@ -946,8 +1009,19 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
|||
[node name="VesselC" type="Control" parent="UI/LabMenu"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = 170
|
||||
offset_right = 290
|
||||
offset_left = 35
|
||||
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_bottom = 394
|
||||
script = ExtResource("10")
|
||||
|
|
@ -971,7 +1045,7 @@ mouse_filter = 2
|
|||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -330
|
||||
offset_right = -130
|
||||
offset_right = -160
|
||||
offset_top = 400
|
||||
offset_bottom = 460
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
|
@ -982,8 +1056,8 @@ text = "Interceptor"
|
|||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -100
|
||||
offset_right = 100
|
||||
offset_left = -175
|
||||
offset_right = -5
|
||||
offset_top = 400
|
||||
offset_bottom = 460
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
|
@ -994,7 +1068,19 @@ text = "Shield"
|
|||
mouse_filter = 2
|
||||
anchor_left = 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_top = 400
|
||||
offset_bottom = 460
|
||||
|
|
@ -1246,8 +1332,8 @@ anchor_left = 0.5
|
|||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -220
|
||||
offset_right = -72
|
||||
offset_left = -310
|
||||
offset_right = -182
|
||||
offset_top = -100
|
||||
offset_bottom = 48
|
||||
script = ExtResource("10")
|
||||
|
|
@ -1259,11 +1345,11 @@ anchor_left = 0.5
|
|||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -290
|
||||
offset_right = -2
|
||||
offset_left = -330
|
||||
offset_right = -162
|
||||
offset_top = 56
|
||||
offset_bottom = 136
|
||||
theme_override_font_sizes/font_size = 32
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "Interceptor"
|
||||
horizontal_alignment = 1
|
||||
|
||||
|
|
@ -1272,8 +1358,8 @@ anchor_left = 0.5
|
|||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 72
|
||||
offset_right = 220
|
||||
offset_left = -148
|
||||
offset_right = -20
|
||||
offset_top = -100
|
||||
offset_bottom = 48
|
||||
script = ExtResource("10")
|
||||
|
|
@ -1286,15 +1372,43 @@ anchor_left = 0.5
|
|||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 2
|
||||
offset_right = 290
|
||||
offset_left = -168
|
||||
offset_right = 0
|
||||
offset_top = 56
|
||||
offset_bottom = 136
|
||||
theme_override_font_sizes/font_size = 32
|
||||
theme_override_font_sizes/font_size = 24
|
||||
autowrap_mode = 2
|
||||
text = "Shield Vessel"
|
||||
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"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 1.0
|
||||
|
|
|
|||
10
NeonDraw.gd
10
NeonDraw.gd
|
|
@ -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)
|
||||
|
||||
|
||||
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
|
||||
# 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:
|
||||
|
|
|
|||
|
|
@ -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://LunarBackground.gd" id="2"]
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
[ext_resource type="Script" path="res://GasCounter.gd" id="11"]
|
||||
[ext_resource type="Script" path="res://CargoCounter.gd" id="12"]
|
||||
[ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"]
|
||||
[ext_resource type="Script" path="res://GasBombController.gd" id="14"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="1"]
|
||||
shader = ExtResource("3")
|
||||
|
|
@ -43,6 +44,9 @@ script = ExtResource("4")
|
|||
ship_glow_color = Color(0.06, 0.32, 0.5, 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="."]
|
||||
position = Vector2(0, 0)
|
||||
zoom = Vector2(1, 1)
|
||||
|
|
@ -180,6 +184,22 @@ icon = 7
|
|||
glow_color = Color(0.06, 0.32, 0.5, 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"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 1.0
|
||||
|
|
|
|||
|
|
@ -24,3 +24,4 @@ unlock_id = "unlock_shield"
|
|||
unlock_name = "Unlock Shield Vessel"
|
||||
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
|
||||
unlock_cost = 45
|
||||
unlock_icon = 23
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ extends Resource
|
|||
@export var display_name: String
|
||||
@export_multiline var description: String
|
||||
@export var build_cost: int
|
||||
@export var build_resource_costs: Dictionary = {}
|
||||
@export var build_time: float = 10.0
|
||||
@export var icon: int = 2
|
||||
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||
|
|
@ -21,6 +22,8 @@ extends Resource
|
|||
@export var unlock_name: String
|
||||
@export_multiline var unlock_description: String
|
||||
@export var unlock_cost: int
|
||||
@export var unlock_resource_costs: Dictionary = {}
|
||||
@export var unlock_icon: int = -1
|
||||
|
||||
|
||||
func has_capability(capability: String) -> bool:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
class_name VesselDefinitions
|
||||
extends RefCounted
|
||||
|
||||
const ORDER: PackedStringArray = ["interceptor", "shield", "transporter"]
|
||||
const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter"]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
"interceptor": preload("res://InterceptorVesselDefinition.tres"),
|
||||
"shield": preload("res://ShieldVesselDefinition.tres"),
|
||||
"gas_bomber": preload("res://GasBomberVesselDefinition.tres"),
|
||||
"transporter": preload("res://TransporterVesselDefinition.tres"),
|
||||
}
|
||||
|
||||
|
|
|
|||
3
VesselGasBomberComponent.gd
Normal file
3
VesselGasBomberComponent.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
extends Node
|
||||
|
||||
var capability_id := "gas_bomber"
|
||||
1
VesselGasBomberComponent.gd.uid
Normal file
1
VesselGasBomberComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cim0c4n73qd2j
|
||||
6
VesselGasBomberComponent.tscn
Normal file
6
VesselGasBomberComponent.tscn
Normal 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")
|
||||
|
|
@ -17,12 +17,19 @@ func _ready() -> void:
|
|||
GameState.fleet_cargo = 24
|
||||
GameState.fleet_gas_cargo = 12
|
||||
GameState.unlocked = ["shipyards", "logistics", "freight"]
|
||||
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("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))
|
||||
var crafter_id: int = int(GameState.buildings[-1].id)
|
||||
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")
|
||||
bc.craft_building_id = crafter_id
|
||||
# The shared header (#24) is the thing worth eyeballing here, so give it
|
||||
|
|
|
|||
38
tests/_gas_bomb_aim_preview.gd
Normal file
38
tests/_gas_bomb_aim_preview.gd
Normal 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)
|
||||
1
tests/_gas_bomb_aim_preview.gd.uid
Normal file
1
tests/_gas_bomb_aim_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dcokscjlrguje
|
||||
6
tests/_gas_bomb_aim_preview.tscn
Normal file
6
tests/_gas_bomb_aim_preview.tscn
Normal 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")
|
||||
|
|
@ -11,13 +11,19 @@ func _ready() -> void:
|
|||
GameState.stored_vessels = [
|
||||
GameState.VESSEL_INTERCEPTOR,
|
||||
GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_GAS_BOMBER,
|
||||
]
|
||||
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("AsteroidField").set_process(false)
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("Fleet").spawn_ships(3, Vector2.ZERO, PackedStringArray([
|
||||
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
|
||||
GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_GAS_BOMBER,
|
||||
]))
|
||||
main.get_node("BuildController")._on_hangar_pressed()
|
||||
|
|
|
|||
|
|
@ -16,19 +16,22 @@ func _ready() -> void:
|
|||
GameState.unlocked = ["shipyards", "vessel_science"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
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))
|
||||
var lab_id: int = int(GameState.buildings[-1].id)
|
||||
GameState.deposit_to_building(lab_id, 18)
|
||||
GameState.fleet_cargo = 12
|
||||
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")
|
||||
# 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.
|
||||
for bar in bc.resource_bars:
|
||||
bar.building_id = lab_id
|
||||
main.get_node("UI/LabMenu").visible = true
|
||||
bc.lab_vessel = GameState.VESSEL_INTERCEPTOR
|
||||
bc.lab_vessel = GameState.VESSEL_GAS_BOMBER
|
||||
bc._refresh_lab()
|
||||
|
|
|
|||
151
tests/test_gas_bomber.gd
Normal file
151
tests/test_gas_bomber.gd
Normal 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
|
||||
1
tests/test_gas_bomber.gd.uid
Normal file
1
tests/test_gas_bomber.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3vtwrwjpmcbm
|
||||
42
tests/test_multi_resource_supply.gd
Normal file
42
tests/test_multi_resource_supply.gd
Normal 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)
|
||||
1
tests/test_multi_resource_supply.gd.uid
Normal file
1
tests/test_multi_resource_supply.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d34djd8v1r1jd
|
||||
|
|
@ -23,7 +23,7 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||
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")):
|
||||
return _fail("logistics transporters leaked into the combat-fleet catalog")
|
||||
if ships.size() != definitions.size():
|
||||
|
|
@ -45,7 +45,13 @@ func _process(delta: float) -> bool:
|
|||
or definitions[1].hull_points.size() != 6
|
||||
or definitions[1].icon != IconButton.Icon.SHIELD_VESSEL):
|
||||
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")
|
||||
print("TEST_VESSEL_COMPOSITION: PASS")
|
||||
quit(0)
|
||||
|
|
|
|||
Loading…
Reference in a new issue