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_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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
25
Fleet.gd
25
Fleet.gd
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
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.
|
# 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
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,
|
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
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://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
|
||||||
|
|
|
||||||
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)
|
_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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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_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
|
||||||
|
|
|
||||||
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.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()
|
||||||
|
|
|
||||||
|
|
@ -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
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
|
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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue