Compose vessels from definitions and capabilities
This commit is contained in:
parent
0504b24d8d
commit
14081ad230
39 changed files with 705 additions and 386 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
||||||
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
|
|
||||||
# Space-side building UI. Owns:
|
# Space-side building UI. Owns:
|
||||||
# - the build menu (place a mothership, ship crafter or command centre)
|
# - the build menu (place a mothership, ship crafter or command centre)
|
||||||
|
|
@ -32,6 +33,10 @@ const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
||||||
get_node("../UI/LabMenu/VesselC"),
|
get_node("../UI/LabMenu/VesselC"),
|
||||||
]
|
]
|
||||||
@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 = [
|
||||||
|
get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"),
|
||||||
|
get_node("../UI/LabMenu/VesselCLabel"),
|
||||||
|
]
|
||||||
@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")
|
||||||
@onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton")
|
@onready var lab_buy_button: IconButton = get_node("../UI/LabMenu/BuyButton")
|
||||||
|
|
@ -98,6 +103,12 @@ var lab_building_id: int = 0
|
||||||
@onready var craft_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel")
|
@onready var craft_interceptor_label: Label = get_node("../UI/CraftMenu/InterceptorLabel")
|
||||||
@onready var craft_transporter_label: Label = get_node("../UI/CraftMenu/TransporterLabel")
|
@onready var craft_transporter_label: Label = get_node("../UI/CraftMenu/TransporterLabel")
|
||||||
@onready var craft_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel")
|
@onready var craft_shield_label: Label = get_node("../UI/CraftMenu/ShieldLabel")
|
||||||
|
@onready var craft_vessel_buttons: Array = [
|
||||||
|
craft_interceptor_button, craft_shield_button, craft_transporter_button,
|
||||||
|
]
|
||||||
|
@onready var craft_vessel_labels: Array = [
|
||||||
|
craft_interceptor_label, craft_shield_label, craft_transporter_label,
|
||||||
|
]
|
||||||
|
|
||||||
# --- the transfer panel (#24) ---------------------------------------------
|
# --- the transfer panel (#24) ---------------------------------------------
|
||||||
# Ore used to be moved from inside whichever panel happened to be open: "load
|
# Ore used to be moved from inside whichever panel happened to be open: "load
|
||||||
|
|
@ -159,7 +170,6 @@ func _ready() -> void:
|
||||||
build_button.pressed.connect(_on_build_pressed)
|
build_button.pressed.connect(_on_build_pressed)
|
||||||
build_close_button.pressed.connect(_close_build_menu)
|
build_close_button.pressed.connect(_close_build_menu)
|
||||||
_configure_build_menu_entries()
|
_configure_build_menu_entries()
|
||||||
craft_shield_button.pressed.connect(_on_craft_shield)
|
|
||||||
lab_close_button.pressed.connect(func(): lab_menu.visible = false)
|
lab_close_button.pressed.connect(func(): lab_menu.visible = false)
|
||||||
lab_buy_button.pressed.connect(_on_lab_buy)
|
lab_buy_button.pressed.connect(_on_lab_buy)
|
||||||
for i in range(lab_vessel_buttons.size()):
|
for i in range(lab_vessel_buttons.size()):
|
||||||
|
|
@ -172,8 +182,9 @@ func _ready() -> void:
|
||||||
action_button.pressed.connect(_on_context_action_pressed)
|
action_button.pressed.connect(_on_context_action_pressed)
|
||||||
building_action_button.pressed.connect(_on_action_pressed)
|
building_action_button.pressed.connect(_on_action_pressed)
|
||||||
|
|
||||||
craft_interceptor_button.pressed.connect(_on_craft_interceptor)
|
for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.all().size())):
|
||||||
craft_transporter_button.pressed.connect(_on_craft_transporter)
|
var definition: Resource = VESSEL_DEFINITIONS.all()[i]
|
||||||
|
craft_vessel_buttons[i].pressed.connect(_on_craft_vessel.bind(definition.definition_id))
|
||||||
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
craft_close_button.pressed.connect(func(): craft_menu.visible = false)
|
||||||
|
|
||||||
transfer_button.pressed.connect(_on_transfer_pressed)
|
transfer_button.pressed.connect(_on_transfer_pressed)
|
||||||
|
|
@ -692,26 +703,19 @@ 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
|
||||||
# message; repeating fleet, queue and transport counts here buried the UI.
|
# message; repeating fleet, queue and transport counts here buried the UI.
|
||||||
craft_interceptor_label.text = "Interceptor\n%d ore" % GameState.SHIP_COST
|
|
||||||
craft_transporter_label.text = "Transporter\n%d ore" % GameState.TRANSPORTER_COST
|
|
||||||
craft_shield_label.text = "Shield Vessel\n%d ore" % GameState.SHIELD_COST
|
|
||||||
craft_status.text = ""
|
craft_status.text = ""
|
||||||
# Nowhere to station one means the order would be refused, so say so
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||||
# rather than letting the button look available and do nothing.
|
for i in range(mini(definitions.size(), craft_vessel_buttons.size())):
|
||||||
craft_transporter_button.enabled = GameState.has_transport_hub() \
|
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]
|
||||||
|
button.icon = definition.icon
|
||||||
|
button.glow_color = definition.glow_color
|
||||||
|
button.enabled = GameState.vessel_is_unlocked(definition.definition_id)
|
||||||
|
if definition.has_capability("transport"):
|
||||||
|
button.enabled = button.enabled and GameState.has_transport_hub() \
|
||||||
and GameState.can_station_transporter()
|
and GameState.can_station_transporter()
|
||||||
craft_transporter_button.queue_redraw()
|
button.queue_redraw()
|
||||||
|
|
||||||
craft_shield_button.enabled = GameState.shield_vessel_unlocked()
|
|
||||||
craft_shield_button.queue_redraw()
|
|
||||||
|
|
||||||
# Fleet capacity no longer gates ORDERING (#23). A finished vessel waits
|
|
||||||
# at the mothership until the player crews it, so a full fleet is a
|
|
||||||
# reason not to collect one yet - not a reason the crafter cannot build.
|
|
||||||
# This is also what stopped the crafter being inert on a fresh save,
|
|
||||||
# where the fleet starts at exactly its cap.
|
|
||||||
craft_interceptor_button.enabled = true
|
|
||||||
craft_interceptor_button.queue_redraw()
|
|
||||||
_refresh_queue()
|
_refresh_queue()
|
||||||
if craft_message != "":
|
if craft_message != "":
|
||||||
if Time.get_ticks_msec() < craft_message_until:
|
if Time.get_ticks_msec() < craft_message_until:
|
||||||
|
|
@ -721,40 +725,40 @@ func _refresh_craft_status() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_craft_interceptor() -> void:
|
func _on_craft_interceptor() -> void:
|
||||||
|
_on_craft_vessel(GameState.VESSEL_INTERCEPTOR)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_craft_vessel(vessel: String) -> void:
|
||||||
if craft_building_id == 0:
|
if craft_building_id == 0:
|
||||||
_say("No crafter selected - reopen this panel from the building")
|
_say("No crafter selected - reopen this panel from the building")
|
||||||
return
|
return
|
||||||
# No affordability check: an order the crafter cannot pay for yet is
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel)
|
||||||
# QUEUED, and completes by itself once ore is delivered here (#16). But
|
if definition == null:
|
||||||
# queueing silently looks identical to a dead button, so it says which
|
return
|
||||||
# happened and what the crafter is still short of.
|
if not GameState.queue_order(craft_building_id, vessel):
|
||||||
GameState.queue_order(craft_building_id)
|
if not GameState.vessel_is_unlocked(vessel):
|
||||||
_report_order(GameState.PRODUCT_SHIP)
|
_say("%s must be unlocked at a ship lab first" % definition.display_name)
|
||||||
|
elif definition.has_capability("transport") and not GameState.has_transport_hub():
|
||||||
|
_say("Build a Transport Hub first - a transporter needs somewhere to be stationed")
|
||||||
|
elif definition.has_capability("transport"):
|
||||||
|
_say("All %d berths are taken - build another hub, or research Freight Yards" %
|
||||||
|
GameState.transport_capacity())
|
||||||
|
return
|
||||||
|
_report_order(vessel)
|
||||||
|
|
||||||
|
|
||||||
func _report_order(product: String) -> void:
|
func _report_order(product: String) -> void:
|
||||||
var short: int = GameState.outstanding_cost(craft_building_id)
|
var short: int = GameState.outstanding_cost(craft_building_id)
|
||||||
if short > 0:
|
if short > 0:
|
||||||
_say("%s queued - %d more ore must be delivered here" % [
|
_say("%s queued - %d more ore must be delivered here" % [
|
||||||
GameState.VESSEL_NAMES.get(product, product), short
|
GameState.vessel_name(product), short
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
_say("%s ordered - building now" % GameState.VESSEL_NAMES.get(product, product))
|
_say("%s ordered - building now" % GameState.vessel_name(product))
|
||||||
|
|
||||||
|
|
||||||
func _on_craft_transporter() -> void:
|
func _on_craft_transporter() -> void:
|
||||||
if craft_building_id == 0:
|
_on_craft_vessel(GameState.VESSEL_TRANSPORTER)
|
||||||
_say("No crafter selected - reopen this panel from the building")
|
|
||||||
return
|
|
||||||
if not GameState.queue_order(craft_building_id, GameState.PRODUCT_TRANSPORTER):
|
|
||||||
# Two different refusals, and the player needs to know WHICH.
|
|
||||||
if not GameState.has_transport_hub():
|
|
||||||
_say("Build a Transport Hub first - a transporter needs somewhere to be stationed")
|
|
||||||
else:
|
|
||||||
_say("All %d berths are taken - build another hub, or research Freight Yards" %
|
|
||||||
GameState.transport_capacity())
|
|
||||||
return
|
|
||||||
_refresh_craft_status()
|
|
||||||
|
|
||||||
|
|
||||||
# --- ship lab (#5) --------------------------------------------------------
|
# --- ship lab (#5) --------------------------------------------------------
|
||||||
|
|
@ -764,10 +768,13 @@ func _lab_options() -> Array:
|
||||||
# entry, because unlocking the type and upgrading it belong in the same
|
# entry, because unlocking the type and upgrading it belong in the same
|
||||||
# place - and its upgrades are pointless until it exists.
|
# place - and its upgrades are pointless until it exists.
|
||||||
var options: Array = []
|
var options: Array = []
|
||||||
if lab_vessel == GameState.VESSEL_SHIELD and not GameState.shield_vessel_unlocked():
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel)
|
||||||
options.append({"unlock": true, "name": "Unlock Shield Vessel",
|
if definition == null:
|
||||||
"desc": "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up.",
|
return options
|
||||||
"cost": GameState.UNLOCK_SHIELD_COST})
|
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})
|
||||||
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})
|
||||||
|
|
@ -775,8 +782,10 @@ func _lab_options() -> Array:
|
||||||
|
|
||||||
|
|
||||||
func _on_lab_vessel(index: int) -> void:
|
func _on_lab_vessel(index: int) -> void:
|
||||||
lab_vessel = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||||
GameState.VESSEL_TRANSPORTER][index]
|
if index < 0 or index >= definitions.size():
|
||||||
|
return
|
||||||
|
lab_vessel = definitions[index].definition_id
|
||||||
lab_choice = 0
|
lab_choice = 0
|
||||||
_refresh_lab()
|
_refresh_lab()
|
||||||
|
|
||||||
|
|
@ -794,7 +803,7 @@ func _on_lab_buy() -> void:
|
||||||
if options.is_empty():
|
if options.is_empty():
|
||||||
return
|
return
|
||||||
var choice: Dictionary = options[lab_choice]
|
var choice: Dictionary = options[lab_choice]
|
||||||
var id: String = GameState.UNLOCK_SHIELD_VESSEL if choice.unlock else String(choice.id)
|
var id: String = String(choice.id)
|
||||||
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
|
||||||
|
|
@ -803,14 +812,17 @@ func _on_lab_buy() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _refresh_lab() -> void:
|
func _refresh_lab() -> void:
|
||||||
for i in range(lab_vessel_buttons.size()):
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||||
var this_vessel: String = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
for i in range(mini(lab_vessel_buttons.size(), definitions.size())):
|
||||||
GameState.VESSEL_TRANSPORTER][i]
|
var definition: Resource = definitions[i]
|
||||||
|
var this_vessel: String = definition.definition_id
|
||||||
lab_vessel_buttons[i].enabled = true
|
lab_vessel_buttons[i].enabled = true
|
||||||
lab_vessel_buttons[i].glow_color = _vessel_colour(this_vessel)
|
lab_vessel_buttons[i].icon = definition.icon
|
||||||
|
lab_vessel_buttons[i].glow_color = definition.glow_color
|
||||||
|
lab_vessel_labels[i].text = definition.display_name
|
||||||
lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel
|
lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel
|
||||||
lab_vessel_buttons[i].queue_redraw()
|
lab_vessel_buttons[i].queue_redraw()
|
||||||
lab_vessel_label.text = "%s research" % GameState.VESSEL_NAMES.get(lab_vessel, lab_vessel)
|
lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel)
|
||||||
|
|
||||||
var options: Array = _lab_options()
|
var options: Array = _lab_options()
|
||||||
for i in range(lab_upgrade_buttons.size()):
|
for i in range(lab_upgrade_buttons.size()):
|
||||||
|
|
@ -820,7 +832,7 @@ 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" % (GameState.UNLOCK_SHIELD_COST if bool(option.unlock) else GameState.upgrade_cost(option.id))
|
var price_text: String = "%d ore" % (int(option.cost) if bool(option.unlock) else GameState.upgrade_cost(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" % [
|
||||||
|
|
@ -848,29 +860,11 @@ func _refresh_lab() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _upgrade_icon(option: Dictionary) -> IconButton.Icon:
|
func _upgrade_icon(option: Dictionary) -> IconButton.Icon:
|
||||||
if bool(option.unlock):
|
return int(option.get("icon", IconButton.Icon.SHIP_LAB))
|
||||||
return IconButton.Icon.UPGRADE_UNLOCK_SHIELD
|
|
||||||
match String(option.id):
|
|
||||||
"int_damage": return IconButton.Icon.UPGRADE_DAMAGE
|
|
||||||
"int_rate": return IconButton.Icon.UPGRADE_RATE
|
|
||||||
"int_range": return IconButton.Icon.UPGRADE_RANGE
|
|
||||||
"int_speed", "shd_speed": return IconButton.Icon.UPGRADE_THRUSTERS
|
|
||||||
"shd_hits": return IconButton.Icon.UPGRADE_SHIELD_HITS
|
|
||||||
"shd_recharge": return IconButton.Icon.UPGRADE_SHIELD_RECHARGE
|
|
||||||
"trn_capacity": return IconButton.Icon.UPGRADE_CARGO
|
|
||||||
"trn_speed": return IconButton.Icon.UPGRADE_ENGINES
|
|
||||||
_: return IconButton.Icon.SHIP_LAB
|
|
||||||
|
|
||||||
|
|
||||||
func _on_craft_shield() -> void:
|
func _on_craft_shield() -> void:
|
||||||
if craft_building_id == 0:
|
_on_craft_vessel(GameState.VESSEL_SHIELD)
|
||||||
_say("No crafter selected - reopen this panel from the building")
|
|
||||||
return
|
|
||||||
if not GameState.shield_vessel_unlocked():
|
|
||||||
_say("Shield vessels must be unlocked at a ship lab first")
|
|
||||||
return
|
|
||||||
GameState.queue_order(craft_building_id, GameState.VESSEL_SHIELD)
|
|
||||||
_report_order(GameState.VESSEL_SHIELD)
|
|
||||||
|
|
||||||
|
|
||||||
# --- moving ore, in both directions, at any building (#24) ----------------
|
# --- moving ore, in both directions, at any building (#24) ----------------
|
||||||
|
|
@ -957,8 +951,8 @@ func _refresh_queue() -> void:
|
||||||
if not slot.visible:
|
if not slot.visible:
|
||||||
continue
|
continue
|
||||||
var order: Dictionary = orders[i]
|
var order: Dictionary = orders[i]
|
||||||
slot.icon = IconButton.Icon.TRANSPORT_HUB if order.product == GameState.PRODUCT_TRANSPORTER \
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product))
|
||||||
else IconButton.Icon.SHIP
|
slot.icon = definition.icon
|
||||||
slot.glow_color = _vessel_colour(String(order.product))
|
slot.glow_color = _vessel_colour(String(order.product))
|
||||||
# Only the one under construction reads as active; the rest are
|
# Only the one under construction reads as active; the rest are
|
||||||
# waiting, and greyed says so without needing a caption each.
|
# waiting, and greyed says so without needing a caption each.
|
||||||
|
|
@ -971,29 +965,24 @@ func _refresh_queue() -> void:
|
||||||
var head: Dictionary = orders[0]
|
var head: Dictionary = orders[0]
|
||||||
if bool(head.started):
|
if bool(head.started):
|
||||||
queue_detail.text = "Building %s %d%%\nTap to cancel" % [
|
queue_detail.text = "Building %s %d%%\nTap to cancel" % [
|
||||||
GameState.VESSEL_NAMES.get(head.product, head.product),
|
GameState.vessel_name(String(head.product)),
|
||||||
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 ore\nTap to cancel" % \
|
||||||
GameState.VESSEL_NAMES.get(head.product, head.product)
|
GameState.vessel_name(String(head.product))
|
||||||
|
|
||||||
|
|
||||||
func _vessel_colour(product: String) -> Color:
|
func _vessel_colour(product: String) -> Color:
|
||||||
match product:
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
|
||||||
GameState.VESSEL_SHIELD:
|
return definition.glow_color if definition != null else Color(0.25, 0.85, 1.0)
|
||||||
return Color(0.45, 0.8, 1.0)
|
|
||||||
GameState.PRODUCT_TRANSPORTER:
|
|
||||||
return Color(0.95, 0.55, 0.85)
|
|
||||||
_:
|
|
||||||
return Color(0.25, 0.85, 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_queue_slot(index: int) -> void:
|
func _on_queue_slot(index: int) -> void:
|
||||||
var orders: Array = GameState.order_list(craft_building_id)
|
var orders: Array = GameState.order_list(craft_building_id)
|
||||||
if index >= orders.size():
|
if index >= orders.size():
|
||||||
return
|
return
|
||||||
var vessel: String = GameState.VESSEL_NAMES.get(orders[index].product, orders[index].product)
|
var vessel: String = GameState.vessel_name(String(orders[index].product))
|
||||||
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
|
||||||
|
|
@ -1018,12 +1007,12 @@ func _on_hangar_crew_type(index: int) -> void:
|
||||||
return
|
return
|
||||||
var stored_index: int = GameState.stored_vessels.find(type)
|
var stored_index: int = GameState.stored_vessels.find(type)
|
||||||
if stored_index < 0:
|
if stored_index < 0:
|
||||||
hangar_status.text = "No %s available" % GameState.VESSEL_NAMES.get(type, type)
|
hangar_status.text = "No %s available" % GameState.vessel_name(type)
|
||||||
return
|
return
|
||||||
if not fleet.add_ship(GameState.take_stored_vessel(stored_index)):
|
if not fleet.add_ship(GameState.take_stored_vessel(stored_index)):
|
||||||
hangar_status.text = "Could not crew that vessel"
|
hangar_status.text = "Could not crew that vessel"
|
||||||
return
|
return
|
||||||
hangar_status.text = "%s joined the fleet" % GameState.VESSEL_NAMES.get(type, type)
|
hangar_status.text = "%s joined the fleet" % GameState.vessel_name(type)
|
||||||
_refresh_hangar()
|
_refresh_hangar()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1035,32 +1024,40 @@ func _on_hangar_store_type(index: int) -> void:
|
||||||
ship_index = i
|
ship_index = i
|
||||||
break
|
break
|
||||||
if ship_index < 0:
|
if ship_index < 0:
|
||||||
hangar_status.text = "No %s in the fleet" % GameState.VESSEL_NAMES.get(type, type)
|
hangar_status.text = "No %s in the fleet" % GameState.vessel_name(type)
|
||||||
return
|
return
|
||||||
var stored: String = fleet.store_ship(ship_index)
|
var stored: String = fleet.store_ship(ship_index)
|
||||||
if stored == "":
|
if stored == "":
|
||||||
hangar_status.text = "Keep one ship deployed"
|
hangar_status.text = "Keep one ship deployed"
|
||||||
return
|
return
|
||||||
GameState.stored_vessels.append(stored)
|
GameState.stored_vessels.append(stored)
|
||||||
hangar_status.text = "%s stored in the hangar" % GameState.VESSEL_NAMES.get(stored, stored)
|
hangar_status.text = "%s stored in the hangar" % GameState.vessel_name(stored)
|
||||||
_refresh_hangar()
|
_refresh_hangar()
|
||||||
|
|
||||||
|
|
||||||
func _hangar_types() -> Array:
|
func _hangar_types() -> Array:
|
||||||
return [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]
|
var types: Array = []
|
||||||
|
for definition in VESSEL_DEFINITIONS.all():
|
||||||
|
types.append(definition.definition_id)
|
||||||
|
return types
|
||||||
|
|
||||||
|
|
||||||
func _refresh_hangar() -> void:
|
func _refresh_hangar() -> void:
|
||||||
var types: Array = _hangar_types()
|
var types: Array = _hangar_types()
|
||||||
for i in range(types.size()):
|
for i in range(types.size()):
|
||||||
var type: String = types[i]
|
var type: String = types[i]
|
||||||
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(type)
|
||||||
var available: int = GameState.stored_vessels.count(type)
|
var available: int = GameState.stored_vessels.count(type)
|
||||||
var deployed: int = 0
|
var deployed: int = 0
|
||||||
for ship in fleet.ships:
|
for ship in fleet.ships:
|
||||||
if is_instance_valid(ship) and String(ship.vessel_type) == type:
|
if is_instance_valid(ship) and String(ship.vessel_type) == type:
|
||||||
deployed += 1
|
deployed += 1
|
||||||
hangar_available_labels[i].text = "%s\n%d available" % [GameState.VESSEL_NAMES.get(type, type), available]
|
hangar_available_labels[i].text = "%s\n%d available" % [definition.display_name, available]
|
||||||
hangar_fleet_labels[i].text = "%s\n%d in fleet" % [GameState.VESSEL_NAMES.get(type, type), deployed]
|
hangar_fleet_labels[i].text = "%s\n%d in fleet" % [definition.display_name, deployed]
|
||||||
|
hangar_available_slots[i].icon = definition.icon
|
||||||
|
hangar_fleet_slots[i].icon = definition.icon
|
||||||
|
hangar_available_slots[i].glow_color = definition.glow_color
|
||||||
|
hangar_fleet_slots[i].glow_color = definition.glow_color
|
||||||
hangar_available_slots[i].enabled = available > 0 and fleet.ships.size() < GameState.max_fleet_size()
|
hangar_available_slots[i].enabled = available > 0 and fleet.ships.size() < GameState.max_fleet_size()
|
||||||
hangar_fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1
|
hangar_fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1
|
||||||
hangar_available_slots[i].queue_redraw()
|
hangar_available_slots[i].queue_redraw()
|
||||||
|
|
|
||||||
228
GameState.gd
228
GameState.gd
|
|
@ -3,6 +3,7 @@ extends Node
|
||||||
const INVENTORY = preload("res://Inventory.gd")
|
const INVENTORY = preload("res://Inventory.gd")
|
||||||
const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd")
|
const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd")
|
||||||
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
||||||
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
|
|
||||||
|
|
||||||
# Autoload singleton - the one thing that survives change_scene_to_file()
|
# Autoload singleton - the one thing that survives change_scene_to_file()
|
||||||
|
|
@ -404,21 +405,10 @@ const VESSEL_INTERCEPTOR: String = "interceptor"
|
||||||
const VESSEL_SHIELD: String = "shield"
|
const VESSEL_SHIELD: String = "shield"
|
||||||
const VESSEL_TRANSPORTER: String = "transporter"
|
const VESSEL_TRANSPORTER: String = "transporter"
|
||||||
|
|
||||||
const VESSEL_NAMES: Dictionary = {
|
# Compatibility constants for callers and old tests. Runtime metadata is
|
||||||
VESSEL_INTERCEPTOR: "Interceptor",
|
# resolved from VesselDefinition below.
|
||||||
VESSEL_SHIELD: "Shield Vessel",
|
|
||||||
VESSEL_TRANSPORTER: "Transporter",
|
|
||||||
}
|
|
||||||
|
|
||||||
const SHIELD_COST: int = 14
|
const SHIELD_COST: int = 14
|
||||||
|
const TRANSPORTER_COST: int = 10
|
||||||
# How long one vessel takes to build, once its ore is actually in the
|
|
||||||
# building. A queue you can watch and cancel needs time to exist in.
|
|
||||||
const BUILD_TIMES: Dictionary = {
|
|
||||||
VESSEL_INTERCEPTOR: 10.0,
|
|
||||||
VESSEL_SHIELD: 16.0,
|
|
||||||
VESSEL_TRANSPORTER: 8.0,
|
|
||||||
}
|
|
||||||
const LAB_RESEARCH_TIME: float = 8.0
|
const LAB_RESEARCH_TIME: float = 8.0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -429,57 +419,14 @@ var build_time_scale: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
func build_time(product: String) -> float:
|
func build_time(product: String) -> float:
|
||||||
return float(BUILD_TIMES.get(product, 10.0)) * build_time_scale
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
|
||||||
|
return (definition.build_time if definition != null else 10.0) * build_time_scale
|
||||||
|
|
||||||
|
|
||||||
func lab_research_time() -> float:
|
func lab_research_time() -> float:
|
||||||
return LAB_RESEARCH_TIME * build_time_scale
|
return LAB_RESEARCH_TIME * build_time_scale
|
||||||
|
|
||||||
# Researched at the ship lab, per vessel type. Separate from SKILL_TREE on
|
# Compatibility constants. VesselDefinition owns the live base figures.
|
||||||
# purpose: that gates what the player may build at all and is a strategic
|
|
||||||
# spine, while these are repeatable tuning on a vessel you already have.
|
|
||||||
# `cost` is the FIRST level; each level after costs cost * (level + 1), so
|
|
||||||
# the third point in something is not the same price as the first.
|
|
||||||
const VESSEL_UPGRADES: Array = [
|
|
||||||
{
|
|
||||||
"id": "int_damage", "vessel": VESSEL_INTERCEPTOR, "name": "Heavy Slugs",
|
|
||||||
"cost": 30, "max_level": 3, "desc": "+1 damage per shot.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "int_rate", "vessel": VESSEL_INTERCEPTOR, "name": "Autoloader",
|
|
||||||
"cost": 25, "max_level": 3, "desc": "Fires 15% faster per level.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "int_range", "vessel": VESSEL_INTERCEPTOR, "name": "Targeting Array",
|
|
||||||
"cost": 20, "max_level": 3, "desc": "+60 firing range per level.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "int_speed", "vessel": VESSEL_INTERCEPTOR, "name": "Thrusters",
|
|
||||||
"cost": 25, "max_level": 3, "desc": "+40 top speed, +60 acceleration per level.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "shd_hits", "vessel": VESSEL_SHIELD, "name": "Layered Emitters",
|
|
||||||
"cost": 60, "max_level": 2, "desc": "+1 hit absorbed before the shield drops.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "shd_recharge", "vessel": VESSEL_SHIELD, "name": "Fast Cycling",
|
|
||||||
"cost": 35, "max_level": 3, "desc": "Shield returns 1.2s sooner per level.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "shd_speed", "vessel": VESSEL_SHIELD, "name": "Thrusters",
|
|
||||||
"cost": 25, "max_level": 3, "desc": "+40 top speed, +60 acceleration per level.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "trn_capacity", "vessel": VESSEL_TRANSPORTER, "name": "Expanded Hold",
|
|
||||||
"cost": 30, "max_level": 3, "desc": "Carries 8 more ore per supply run.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "trn_speed", "vessel": VESSEL_TRANSPORTER, "name": "Freight Engines",
|
|
||||||
"cost": 30, "max_level": 3, "desc": "+70 speed on supply runs.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Base figures every vessel starts from, before anything is researched.
|
|
||||||
const SHIP_BASE_DAMAGE: int = 1
|
const SHIP_BASE_DAMAGE: int = 1
|
||||||
const SHIP_BASE_COOLDOWN: float = 0.5
|
const SHIP_BASE_COOLDOWN: float = 0.5
|
||||||
const SHIP_BASE_RANGE: float = 320.0
|
const SHIP_BASE_RANGE: float = 320.0
|
||||||
|
|
@ -503,9 +450,7 @@ func has_ship_lab() -> bool:
|
||||||
|
|
||||||
|
|
||||||
func shield_vessel_unlocked() -> bool:
|
func shield_vessel_unlocked() -> bool:
|
||||||
# Unlocked AT the lab, by paying for it there - a vessel type rather than
|
return vessel_is_unlocked(VESSEL_SHIELD)
|
||||||
# a stat, so it is a one-off rather than a level.
|
|
||||||
return int(vessel_levels.get(UNLOCK_SHIELD_VESSEL, 0)) > 0
|
|
||||||
|
|
||||||
|
|
||||||
const UNLOCK_SHIELD_VESSEL: String = "unlock_shield"
|
const UNLOCK_SHIELD_VESSEL: String = "unlock_shield"
|
||||||
|
|
@ -513,27 +458,69 @@ const UNLOCK_SHIELD_COST: int = 45
|
||||||
|
|
||||||
|
|
||||||
func unlock_shield_vessel() -> bool:
|
func unlock_shield_vessel() -> bool:
|
||||||
if shield_vessel_unlocked() or not has_ship_lab():
|
return unlock_vessel(VESSEL_SHIELD)
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_definition(vessel: String) -> Resource:
|
||||||
|
return VESSEL_DEFINITIONS.get_definition(vessel)
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_name(vessel: String) -> String:
|
||||||
|
var definition: Resource = vessel_definition(vessel)
|
||||||
|
return definition.display_name if definition != null else vessel.capitalize()
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_has_capability(vessel: String, capability: String) -> bool:
|
||||||
|
var definition: Resource = vessel_definition(vessel)
|
||||||
|
return definition != null and definition.has_capability(capability)
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_is_unlocked(vessel: String) -> bool:
|
||||||
|
var definition: Resource = vessel_definition(vessel)
|
||||||
|
if definition == null:
|
||||||
return false
|
return false
|
||||||
if not spend(UNLOCK_SHIELD_COST):
|
return definition.unlock_id == "" or int(vessel_levels.get(definition.unlock_id, 0)) > 0
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_for_unlock(unlock_id: String) -> Resource:
|
||||||
|
for definition in VESSEL_DEFINITIONS.all():
|
||||||
|
if definition.unlock_id == unlock_id:
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
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
|
return false
|
||||||
vessel_levels[UNLOCK_SHIELD_VESSEL] = 1
|
if not spend(definition.unlock_cost):
|
||||||
vessel_upgraded.emit(UNLOCK_SHIELD_VESSEL)
|
return false
|
||||||
|
vessel_levels[definition.unlock_id] = 1
|
||||||
|
vessel_upgraded.emit(definition.unlock_id)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
func upgrade_def(id: String) -> Dictionary:
|
func upgrade_def(id: String) -> Dictionary:
|
||||||
for u in VESSEL_UPGRADES:
|
for definition in VESSEL_DEFINITIONS.all():
|
||||||
if u.id == id:
|
for upgrade in definition.upgrades:
|
||||||
return u
|
if upgrade.upgrade_id == id:
|
||||||
|
return {"id": upgrade.upgrade_id, "vessel": definition.definition_id,
|
||||||
|
"name": upgrade.display_name, "desc": upgrade.description,
|
||||||
|
"cost": upgrade.base_cost, "max_level": upgrade.max_level,
|
||||||
|
"icon": upgrade.icon, "resource": upgrade}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
func upgrades_for(vessel: String) -> Array:
|
func upgrades_for(vessel: String) -> Array:
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
for u in VESSEL_UPGRADES:
|
var definition: Resource = vessel_definition(vessel)
|
||||||
if u.vessel == vessel:
|
if definition == null:
|
||||||
out.append(u)
|
return out
|
||||||
|
for upgrade in definition.upgrades:
|
||||||
|
out.append({"id": upgrade.upgrade_id, "vessel": vessel,
|
||||||
|
"name": upgrade.display_name, "desc": upgrade.description,
|
||||||
|
"cost": upgrade.base_cost, "max_level": upgrade.max_level,
|
||||||
|
"icon": upgrade.icon, "resource": upgrade})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -567,66 +554,51 @@ func buy_upgrade(id: String) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
# --- what those upgrades actually come to ---------------------------------
|
func vessel_stat(vessel: String, stat: String) -> float:
|
||||||
# Read live rather than baked into a vessel when it is built, so researching
|
var definition: Resource = vessel_definition(vessel)
|
||||||
# something improves the ships already flying instead of only the next ones.
|
if definition == null:
|
||||||
|
return 0.0
|
||||||
func ship_damage(vessel: String) -> int:
|
var value: float = float(definition.base_stats.get(stat, 0.0))
|
||||||
return SHIP_BASE_DAMAGE + (upgrade_level("int_damage") if vessel == VESSEL_INTERCEPTOR else 0)
|
for upgrade in definition.upgrades:
|
||||||
|
var level: int = upgrade_level(upgrade.upgrade_id)
|
||||||
|
value += float(upgrade.additive_modifiers.get(stat, 0.0)) * level
|
||||||
|
if upgrade.multiplicative_modifiers.has(stat):
|
||||||
|
value *= pow(float(upgrade.multiplicative_modifiers[stat]), level)
|
||||||
|
if definition.minimum_stats.has(stat):
|
||||||
|
value = maxf(value, float(definition.minimum_stats[stat]))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
func ship_fire_cooldown(vessel: String) -> float:
|
func ship_damage(vessel: String) -> int: return int(vessel_stat(vessel, "damage"))
|
||||||
if vessel != VESSEL_INTERCEPTOR:
|
func ship_fire_cooldown(vessel: String) -> float: return vessel_stat(vessel, "fire_cooldown")
|
||||||
return SHIP_BASE_COOLDOWN
|
func ship_fire_range(vessel: String) -> float: return vessel_stat(vessel, "fire_range")
|
||||||
return SHIP_BASE_COOLDOWN * pow(0.85, float(upgrade_level("int_rate")))
|
func ship_max_speed(vessel: String) -> float: return vessel_stat(vessel, "max_speed")
|
||||||
|
func ship_acceleration(vessel: String) -> float: return vessel_stat(vessel, "acceleration")
|
||||||
|
|
||||||
func ship_fire_range(vessel: String) -> float:
|
|
||||||
return SHIP_BASE_RANGE + (60.0 * upgrade_level("int_range") if vessel == VESSEL_INTERCEPTOR else 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _speed_levels(vessel: String) -> int:
|
|
||||||
if vessel == VESSEL_SHIELD:
|
|
||||||
return upgrade_level("shd_speed")
|
|
||||||
return upgrade_level("int_speed")
|
|
||||||
|
|
||||||
|
|
||||||
func ship_max_speed(vessel: String) -> float:
|
|
||||||
return SHIP_BASE_SPEED + 40.0 * _speed_levels(vessel)
|
|
||||||
|
|
||||||
|
|
||||||
func ship_acceleration(vessel: String) -> float:
|
|
||||||
return SHIP_BASE_ACCEL + 60.0 * _speed_levels(vessel)
|
|
||||||
|
|
||||||
|
|
||||||
func shield_max_charges() -> int:
|
func shield_max_charges() -> int:
|
||||||
return SHIELD_BASE_CHARGES + upgrade_level("shd_hits")
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield")
|
||||||
|
return int(vessel_stat(definition.definition_id, "shield_charges"))
|
||||||
|
|
||||||
func shield_recharge_time() -> float:
|
func shield_recharge_time() -> float:
|
||||||
return maxf(2.0, SHIELD_BASE_RECHARGE - 1.2 * upgrade_level("shd_recharge"))
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("shield")
|
||||||
|
return vessel_stat(definition.definition_id, "shield_recharge")
|
||||||
|
|
||||||
func transporter_load_limit() -> int:
|
func transporter_load_limit() -> int:
|
||||||
return TRANSPORTER_BASE_LOAD + 8 * upgrade_level("trn_capacity")
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport")
|
||||||
|
return int(vessel_stat(definition.definition_id, "cargo_capacity"))
|
||||||
|
|
||||||
func transporter_speed() -> float:
|
func transporter_speed() -> float:
|
||||||
return TRANSPORTER_BASE_SPEED + 70.0 * upgrade_level("trn_speed")
|
var definition: Resource = VESSEL_DEFINITIONS.with_capability("transport")
|
||||||
|
return vessel_stat(definition.definition_id, "transport_speed")
|
||||||
|
|
||||||
|
|
||||||
const PRODUCT_SHIP: String = "interceptor"
|
const PRODUCT_SHIP: String = "interceptor"
|
||||||
const PRODUCT_TRANSPORTER: String = "transporter"
|
const PRODUCT_TRANSPORTER: String = "transporter"
|
||||||
|
|
||||||
const TRANSPORTER_COST: int = 10
|
|
||||||
|
|
||||||
|
|
||||||
func product_cost(product: String) -> int:
|
func product_cost(product: String) -> int:
|
||||||
if product == PRODUCT_TRANSPORTER:
|
var definition: Resource = vessel_definition(product)
|
||||||
return TRANSPORTER_COST
|
return definition.build_cost if definition != null else 0
|
||||||
if product == VESSEL_SHIELD:
|
|
||||||
return SHIELD_COST
|
|
||||||
return SHIP_COST
|
|
||||||
|
|
||||||
|
|
||||||
func can_produce(type: String) -> bool:
|
func can_produce(type: String) -> bool:
|
||||||
|
|
@ -713,13 +685,14 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
|
||||||
var b: Dictionary = find_building(id)
|
var b: Dictionary = find_building(id)
|
||||||
if b.is_empty() or not can_produce(b.type):
|
if b.is_empty() or not can_produce(b.type):
|
||||||
return false
|
return false
|
||||||
if product == VESSEL_SHIELD and not shield_vessel_unlocked():
|
var definition: Resource = vessel_definition(product)
|
||||||
|
if definition == null or not vessel_is_unlocked(product):
|
||||||
return false
|
return false
|
||||||
if product == PRODUCT_TRANSPORTER and not has_transport_hub():
|
if definition.has_capability("transport") and not has_transport_hub():
|
||||||
# Nowhere to station it. Refused rather than queued, so the player is
|
# Nowhere to station it. Refused rather than queued, so the player is
|
||||||
# told now instead of watching an order sit forever.
|
# told now instead of watching an order sit forever.
|
||||||
return false
|
return false
|
||||||
if product == PRODUCT_TRANSPORTER and not can_station_transporter():
|
if definition.has_capability("transport") and not can_station_transporter():
|
||||||
# Every berth is taken or spoken for (#20).
|
# Every berth is taken or spoken for (#20).
|
||||||
return false
|
return false
|
||||||
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
|
(b.orders as Array).append({"product": product, "elapsed": 0.0, "started": false})
|
||||||
|
|
@ -742,15 +715,16 @@ func _advance_production(delta: float) -> void:
|
||||||
var product: String = String(order.product)
|
var product: String = String(order.product)
|
||||||
var cost: int = product_cost(product)
|
var cost: int = product_cost(product)
|
||||||
var inventory: RefCounted = _inventory_for_record(b)
|
var inventory: RefCounted = _inventory_for_record(b)
|
||||||
|
var definition: Resource = vessel_definition(product)
|
||||||
|
|
||||||
if not bool(order.started):
|
if not bool(order.started):
|
||||||
if inventory.amount(RESOURCE_ORE) < cost:
|
if inventory.amount(RESOURCE_ORE) < cost:
|
||||||
continue
|
continue
|
||||||
if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity():
|
if definition.has_capability("transport") and transporters.size() >= transport_capacity():
|
||||||
# A hub was demolished after this was ordered, so the berth it
|
# A hub was demolished after this was ordered, so the berth it
|
||||||
# was going to use is gone. Hold rather than overfill.
|
# was going to use is gone. Hold rather than overfill.
|
||||||
continue
|
continue
|
||||||
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
|
if definition.has_capability("transport") and nearest_hub_to(b.position).is_empty():
|
||||||
continue
|
continue
|
||||||
inventory.withdraw(RESOURCE_ORE, cost)
|
inventory.withdraw(RESOURCE_ORE, cost)
|
||||||
order.started = true
|
order.started = true
|
||||||
|
|
@ -760,7 +734,7 @@ func _advance_production(delta: float) -> void:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
orders.pop_front()
|
orders.pop_front()
|
||||||
if product == PRODUCT_TRANSPORTER:
|
if definition.has_capability("transport"):
|
||||||
# Transporters are unchanged: they take themselves to the nearest
|
# Transporters are unchanged: they take themselves to the nearest
|
||||||
# hub and wait for work (#19).
|
# hub and wait for work (#19).
|
||||||
_station_transporter(b.position)
|
_station_transporter(b.position)
|
||||||
|
|
@ -782,7 +756,8 @@ func queue_lab_research(lab_id: int, upgrade_id: String) -> bool:
|
||||||
if not building_has_capability(lab, "vessel_research") or upgrade_id == "":
|
if not building_has_capability(lab, "vessel_research") or upgrade_id == "":
|
||||||
return false
|
return false
|
||||||
var orders: Array = lab.get("research_orders", [])
|
var orders: Array = lab.get("research_orders", [])
|
||||||
if not orders.is_empty() or (upgrade_id != UNLOCK_SHIELD_VESSEL and upgrade_maxed(upgrade_id)):
|
var unlock_definition: Resource = vessel_for_unlock(upgrade_id)
|
||||||
|
if not orders.is_empty() or (unlock_definition == null and upgrade_maxed(upgrade_id)):
|
||||||
return false
|
return false
|
||||||
orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false})
|
orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false})
|
||||||
lab.research_orders = orders
|
lab.research_orders = orders
|
||||||
|
|
@ -804,7 +779,8 @@ func _advance_lab_research(delta: float) -> void:
|
||||||
continue
|
continue
|
||||||
var order: Dictionary = orders[0]
|
var order: Dictionary = orders[0]
|
||||||
var id: String = String(order.id)
|
var id: String = String(order.id)
|
||||||
var cost: int = UNLOCK_SHIELD_COST if id == UNLOCK_SHIELD_VESSEL else upgrade_cost(id)
|
var unlock_definition: Resource = vessel_for_unlock(id)
|
||||||
|
var cost: int = unlock_definition.unlock_cost if unlock_definition != null else upgrade_cost(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:
|
if inventory.amount(RESOURCE_ORE) < cost:
|
||||||
|
|
@ -814,7 +790,7 @@ func _advance_lab_research(delta: float) -> void:
|
||||||
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():
|
||||||
continue
|
continue
|
||||||
if id == UNLOCK_SHIELD_VESSEL:
|
if unlock_definition != null:
|
||||||
vessel_levels[id] = 1
|
vessel_levels[id] = 1
|
||||||
else:
|
else:
|
||||||
vessel_levels[id] = upgrade_level(id) + 1
|
vessel_levels[id] = upgrade_level(id) + 1
|
||||||
|
|
@ -1041,7 +1017,7 @@ func pending_transporter_orders() -> int:
|
||||||
var n: int = 0
|
var n: int = 0
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
for o in b.get("orders", []):
|
for o in b.get("orders", []):
|
||||||
if String(o.product) == PRODUCT_TRANSPORTER:
|
if vessel_has_capability(String(o.product), "transport"):
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
|
||||||
11
InterceptorDamageUpgrade.tres
Normal file
11
InterceptorDamageUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "int_damage"
|
||||||
|
display_name = "Heavy Slugs"
|
||||||
|
description = "+1 damage per shot."
|
||||||
|
base_cost = 30
|
||||||
|
max_level = 3
|
||||||
|
icon = 15
|
||||||
|
additive_modifiers = {"damage": 1.0}
|
||||||
11
InterceptorRangeUpgrade.tres
Normal file
11
InterceptorRangeUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "int_range"
|
||||||
|
display_name = "Targeting Array"
|
||||||
|
description = "+60 firing range per level."
|
||||||
|
base_cost = 20
|
||||||
|
max_level = 3
|
||||||
|
icon = 17
|
||||||
|
additive_modifiers = {"fire_range": 60.0}
|
||||||
11
InterceptorRateUpgrade.tres
Normal file
11
InterceptorRateUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "int_rate"
|
||||||
|
display_name = "Autoloader"
|
||||||
|
description = "Fires 15% faster per level."
|
||||||
|
base_cost = 25
|
||||||
|
max_level = 3
|
||||||
|
icon = 16
|
||||||
|
multiplicative_modifiers = {"fire_cooldown": 0.85}
|
||||||
11
InterceptorSpeedUpgrade.tres
Normal file
11
InterceptorSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "int_speed"
|
||||||
|
display_name = "Thrusters"
|
||||||
|
description = "+40 top speed, +60 acceleration per level."
|
||||||
|
base_cost = 25
|
||||||
|
max_level = 3
|
||||||
|
icon = 18
|
||||||
|
additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0}
|
||||||
20
InterceptorVesselDefinition.tres
Normal file
20
InterceptorVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=7 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://VesselWeaponComponent.tscn" id="2"]
|
||||||
|
[ext_resource type="Resource" path="res://InterceptorDamageUpgrade.tres" id="3"]
|
||||||
|
[ext_resource type="Resource" path="res://InterceptorRateUpgrade.tres" id="4"]
|
||||||
|
[ext_resource type="Resource" path="res://InterceptorRangeUpgrade.tres" id="5"]
|
||||||
|
[ext_resource type="Resource" path="res://InterceptorSpeedUpgrade.tres" id="6"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
definition_id = "interceptor"
|
||||||
|
display_name = "Interceptor"
|
||||||
|
description = "Fast combat vessel with a rapid-firing cannon."
|
||||||
|
build_cost = 6
|
||||||
|
build_time = 10.0
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
|
capabilities = PackedStringArray("weapon")
|
||||||
|
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||||
|
base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 450.0}
|
||||||
|
upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
|
||||||
11
ShieldHitsUpgrade.tres
Normal file
11
ShieldHitsUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "shd_hits"
|
||||||
|
display_name = "Layered Emitters"
|
||||||
|
description = "+1 hit absorbed before the shield drops."
|
||||||
|
base_cost = 60
|
||||||
|
max_level = 2
|
||||||
|
icon = 19
|
||||||
|
additive_modifiers = {"shield_charges": 1.0}
|
||||||
11
ShieldRechargeUpgrade.tres
Normal file
11
ShieldRechargeUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "shd_recharge"
|
||||||
|
display_name = "Fast Cycling"
|
||||||
|
description = "Shield returns 1.2s sooner per level."
|
||||||
|
base_cost = 35
|
||||||
|
max_level = 3
|
||||||
|
icon = 20
|
||||||
|
additive_modifiers = {"shield_recharge": -1.2}
|
||||||
11
ShieldSpeedUpgrade.tres
Normal file
11
ShieldSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "shd_speed"
|
||||||
|
display_name = "Thrusters"
|
||||||
|
description = "+40 top speed, +60 acceleration per level."
|
||||||
|
base_cost = 25
|
||||||
|
max_level = 3
|
||||||
|
icon = 18
|
||||||
|
additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0}
|
||||||
25
ShieldVesselDefinition.tres
Normal file
25
ShieldVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=7 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://VesselWeaponComponent.tscn" id="2"]
|
||||||
|
[ext_resource type="PackedScene" path="res://VesselShieldComponent.tscn" id="3"]
|
||||||
|
[ext_resource type="Resource" path="res://ShieldHitsUpgrade.tres" id="4"]
|
||||||
|
[ext_resource type="Resource" path="res://ShieldRechargeUpgrade.tres" id="5"]
|
||||||
|
[ext_resource type="Resource" path="res://ShieldSpeedUpgrade.tres" id="6"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
definition_id = "shield"
|
||||||
|
display_name = "Shield Vessel"
|
||||||
|
description = "Combat vessel whose rechargeable personal shield absorbs lethal impacts."
|
||||||
|
build_cost = 14
|
||||||
|
build_time = 16.0
|
||||||
|
icon = 2
|
||||||
|
glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
|
capabilities = PackedStringArray("weapon", "shield")
|
||||||
|
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3")])
|
||||||
|
base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 450.0, "shield_charges": 1.0, "shield_recharge": 6.0}
|
||||||
|
minimum_stats = {"shield_recharge": 2.0}
|
||||||
|
upgrades = Array[Resource]([ExtResource("4"), ExtResource("5"), ExtResource("6")])
|
||||||
|
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
|
||||||
228
Ship.gd
228
Ship.gd
|
|
@ -2,196 +2,124 @@ extends Area2D
|
||||||
|
|
||||||
signal destroyed
|
signal destroyed
|
||||||
|
|
||||||
# Deliberately faster than Fleet.cruise_speed: the fleet as a whole travels
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
# at the cruise rate, and this headroom is what lets an individual ship
|
|
||||||
# close on its formation slot - so a pivot mid-flight resolves quickly
|
|
||||||
# instead of the ship having no speed budget left over for it.
|
|
||||||
@export var max_speed: float = 460.0
|
@export var max_speed: float = 460.0
|
||||||
@export var acceleration: float = 450.0
|
@export var acceleration: float = 450.0
|
||||||
@export var rotation_speed: float = 8.0
|
@export var rotation_speed: float = 8.0
|
||||||
@export var arrive_radius: float = 6.0
|
@export var arrive_radius: float = 6.0
|
||||||
|
|
||||||
@export var fire_range: float = 320.0
|
|
||||||
@export var fire_cooldown: float = 0.5
|
|
||||||
# How long to wait before looking for targets again after finding none.
|
|
||||||
@export var idle_rescan_interval: float = 0.25
|
|
||||||
@export var projectile_damage: int = 1
|
|
||||||
|
|
||||||
# Which kind of vessel this is (GameState.VESSEL_*). Drives both its
|
|
||||||
# researched statistics and whether it carries a shield (#5).
|
|
||||||
@export var vessel_type: String = "interceptor"
|
@export var vessel_type: String = "interceptor"
|
||||||
|
|
||||||
# --- shield (#5) ----------------------------------------------------------
|
var definition: Resource
|
||||||
const SHIELD_COLOR: Color = Color(0.45, 0.8, 1.0)
|
|
||||||
const SHIELD_RADIUS: float = 30.0
|
|
||||||
|
|
||||||
# A shield vessel survives a hit that would destroy any other ship, and is
|
|
||||||
# then defenceless until it cycles back up. Personal, not a bubble: it
|
|
||||||
# protects the vessel carrying it and nothing else.
|
|
||||||
var shield_charges: int = 0
|
|
||||||
var shield_recharge_left: float = 0.0
|
|
||||||
var _shield_ring: Node2D = null
|
|
||||||
|
|
||||||
# Neon colours, assigned by Fleet on spawn so a scene can tint its fleet.
|
|
||||||
# The lunar surface is a bright grey, where the default near-white core all
|
|
||||||
# but vanishes - PlanetSurface.tscn passes a dark core there. See NeonDraw.
|
|
||||||
var glow_color: Color = Color(0.25, 0.85, 1.0)
|
var glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||||
var core_color: Color = Color(1.0, 1.0, 1.0)
|
var core_color: Color = Color.WHITE
|
||||||
|
|
||||||
var velocity: Vector2 = Vector2.ZERO
|
var velocity: Vector2 = Vector2.ZERO
|
||||||
# The fleet's own velocity, supplied by Fleet as CONTEXT rather than as a
|
|
||||||
# facing command - the ship works out where to point from it.
|
|
||||||
#
|
|
||||||
# It is needed because `velocity` below is not the ship's true motion while
|
|
||||||
# it is holding formation: the slot it chases sits inside arrive_radius, so
|
|
||||||
# the ship snaps onto it and reports zero velocity even though it is being
|
|
||||||
# carried along at cruise speed. Facing derived from `velocity` alone
|
|
||||||
# therefore froze at whatever heading the ship last happened to travel.
|
|
||||||
# Adding the two gives the ship's ACTUAL motion through the world, which is
|
|
||||||
# the honest thing to point along in every case: it equals the fleet's
|
|
||||||
# heading when station-keeping, the ship's own heading when manoeuvring
|
|
||||||
# alone, and the true blend of both while strafing into a new slot.
|
|
||||||
var fleet_velocity: Vector2 = Vector2.ZERO
|
var fleet_velocity: Vector2 = Vector2.ZERO
|
||||||
var move_target: Vector2
|
var move_target: Vector2
|
||||||
var fire_timer: float = 0.0
|
|
||||||
var projectile_scene: PackedScene = preload("res://Projectile.tscn")
|
# Compatibility accessors keep old saves/tests readable while shield state
|
||||||
|
# itself belongs exclusively to the optional ShieldComponent.
|
||||||
|
var shield_charges: int:
|
||||||
|
get:
|
||||||
|
var component: Node = get_component("shield")
|
||||||
|
return component.charges if component != null else 0
|
||||||
|
set(value):
|
||||||
|
var component: Node = get_component("shield")
|
||||||
|
if component != null:
|
||||||
|
component.set_charges(value)
|
||||||
|
|
||||||
|
var shield_recharge_left: float:
|
||||||
|
get:
|
||||||
|
var component: Node = get_component("shield")
|
||||||
|
return component.recharge_left if component != null else 0.0
|
||||||
|
set(value):
|
||||||
|
var component: Node = get_component("shield")
|
||||||
|
if component != null:
|
||||||
|
component.recharge_left = value
|
||||||
|
|
||||||
|
var projectile_damage: int:
|
||||||
|
get:
|
||||||
|
var component: Node = get_component("weapon")
|
||||||
|
return component.projectile_damage if component != null else 0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
move_target = position
|
move_target = position
|
||||||
add_to_group("player_ships")
|
add_to_group("player_ships")
|
||||||
area_entered.connect(_on_area_entered)
|
area_entered.connect(_on_area_entered)
|
||||||
if vessel_type == GameState.VESSEL_SHIELD:
|
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||||
_build_shield_ring()
|
if definition == null:
|
||||||
shield_charges = GameState.shield_max_charges()
|
return
|
||||||
|
glow_color = definition.glow_color
|
||||||
|
for component_scene in definition.component_scenes:
|
||||||
|
var component: Node = component_scene.instantiate()
|
||||||
|
add_child(component)
|
||||||
|
if component.has_method("setup"):
|
||||||
|
component.setup()
|
||||||
apply_vessel_stats()
|
apply_vessel_stats()
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func get_component(capability: String) -> Node:
|
||||||
|
for child in get_children():
|
||||||
|
if child.get("capability_id") == capability:
|
||||||
|
return child
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func has_capability(capability: String) -> bool:
|
||||||
|
return get_component(capability) != null
|
||||||
|
|
||||||
|
|
||||||
func set_move_target(world_pos: Vector2) -> void:
|
func set_move_target(world_pos: Vector2) -> void:
|
||||||
move_target = world_pos
|
move_target = world_pos
|
||||||
|
|
||||||
|
|
||||||
func apply_vessel_stats() -> void:
|
func apply_vessel_stats() -> void:
|
||||||
# Read from GameState rather than baked in at spawn, so researching an
|
if definition == null:
|
||||||
# upgrade improves the ships already flying and not just the next one
|
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||||
# built. Fleet calls this on every vessel when something is bought.
|
if definition == null:
|
||||||
max_speed = GameState.ship_max_speed(vessel_type)
|
return
|
||||||
acceleration = GameState.ship_acceleration(vessel_type)
|
max_speed = GameState.vessel_stat(vessel_type, "max_speed")
|
||||||
fire_range = GameState.ship_fire_range(vessel_type)
|
acceleration = GameState.vessel_stat(vessel_type, "acceleration")
|
||||||
fire_cooldown = GameState.ship_fire_cooldown(vessel_type)
|
for child in get_children():
|
||||||
projectile_damage = GameState.ship_damage(vessel_type)
|
if child.has_method("apply_stats"):
|
||||||
if vessel_type == GameState.VESSEL_SHIELD:
|
child.apply_stats()
|
||||||
# Topping up a live shield with researched capacity, without
|
|
||||||
# refilling one that is currently down and cycling.
|
|
||||||
var was_up: bool = shield_charges > 0
|
|
||||||
shield_charges = GameState.shield_max_charges() if was_up else shield_charges
|
|
||||||
_update_shield_visual()
|
|
||||||
|
|
||||||
|
|
||||||
func has_shield() -> bool:
|
func has_shield() -> bool:
|
||||||
return vessel_type == GameState.VESSEL_SHIELD and shield_charges > 0
|
var component: Node = get_component("shield")
|
||||||
|
return component != null and component.is_active()
|
||||||
|
|
||||||
func _update_shield_visual() -> void:
|
|
||||||
if _shield_ring == null:
|
|
||||||
return
|
|
||||||
# visible only - per #10 the ring is rasterised once and never redrawn to
|
|
||||||
# turn it on and off.
|
|
||||||
_shield_ring.visible = has_shield()
|
|
||||||
|
|
||||||
|
|
||||||
func _tick_shield(delta: float) -> void:
|
|
||||||
if vessel_type != GameState.VESSEL_SHIELD:
|
|
||||||
return
|
|
||||||
if shield_charges >= GameState.shield_max_charges():
|
|
||||||
return
|
|
||||||
shield_recharge_left -= delta
|
|
||||||
if shield_recharge_left <= 0.0:
|
|
||||||
shield_charges += 1
|
|
||||||
shield_recharge_left = GameState.shield_recharge_time()
|
|
||||||
_update_shield_visual()
|
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
_tick_shield(delta)
|
for child in get_children():
|
||||||
|
if child.has_method("tick"):
|
||||||
|
child.tick(delta)
|
||||||
|
|
||||||
var to_target: Vector2 = move_target - position
|
var to_target: Vector2 = move_target - position
|
||||||
var distance: float = to_target.length()
|
var distance: float = to_target.length()
|
||||||
|
|
||||||
if distance <= arrive_radius:
|
if distance <= arrive_radius:
|
||||||
# Snap and stop dead - prevents residual float-level jitter that
|
|
||||||
# would otherwise keep nudging back and forth around the target.
|
|
||||||
position = move_target
|
position = move_target
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
else:
|
else:
|
||||||
# Braking-distance formula: the fastest speed we can be going right
|
|
||||||
# now and still be able to decelerate to 0 exactly at the target,
|
|
||||||
# given our acceleration. Without this the ship approaches at full
|
|
||||||
# speed, overshoots (stopping distance = max_speed^2 / (2*accel)),
|
|
||||||
# and oscillates back and forth forever instead of settling.
|
|
||||||
var braking_speed: float = sqrt(2.0 * acceleration * distance)
|
var braking_speed: float = sqrt(2.0 * acceleration * distance)
|
||||||
var desired_speed: float = min(max_speed, braking_speed)
|
var desired_speed: float = min(max_speed, braking_speed)
|
||||||
var desired_velocity: Vector2 = to_target.normalized() * desired_speed
|
var desired_velocity: Vector2 = to_target.normalized() * desired_speed
|
||||||
velocity = velocity.move_toward(desired_velocity, acceleration * delta)
|
velocity = velocity.move_toward(desired_velocity, acceleration * delta)
|
||||||
position += velocity * delta
|
position += velocity * delta
|
||||||
|
|
||||||
# One owner, one rule: face the way this ship is actually travelling.
|
|
||||||
var motion: Vector2 = velocity + fleet_velocity
|
var motion: Vector2 = velocity + fleet_velocity
|
||||||
if motion.length() > 8.0:
|
if motion.length() > 8.0:
|
||||||
var target_angle: float = motion.angle() + PI / 2.0
|
var target_angle: float = motion.angle() + PI / 2.0
|
||||||
rotation = lerp_angle(rotation, target_angle, rotation_speed * delta)
|
rotation = lerp_angle(rotation, target_angle, rotation_speed * delta)
|
||||||
|
|
||||||
_update_weapon(delta)
|
|
||||||
|
|
||||||
|
|
||||||
func _update_weapon(delta: float) -> void:
|
|
||||||
fire_timer -= delta
|
|
||||||
if fire_timer > 0.0:
|
|
||||||
return
|
|
||||||
|
|
||||||
var target: Node2D = _find_nearest_enemy()
|
|
||||||
if target == null:
|
|
||||||
# Back off before scanning again. Without this, fire_timer stays at
|
|
||||||
# or below zero and every ship queries the whole "enemies" group and
|
|
||||||
# loops it on EVERY physics frame whenever nothing is in range -
|
|
||||||
# which is most of the time.
|
|
||||||
fire_timer = idle_rescan_interval
|
|
||||||
return
|
|
||||||
|
|
||||||
fire_timer = fire_cooldown
|
|
||||||
var proj: Node2D = projectile_scene.instantiate()
|
|
||||||
proj.position = position
|
|
||||||
proj.damage = projectile_damage
|
|
||||||
get_tree().root.add_child(proj)
|
|
||||||
proj.launch(target.position - position)
|
|
||||||
|
|
||||||
|
|
||||||
func _find_nearest_enemy() -> Node2D:
|
|
||||||
var enemies: Array = get_tree().get_nodes_in_group("enemies")
|
|
||||||
var nearest: Node2D = null
|
|
||||||
var best_dist: float = fire_range
|
|
||||||
|
|
||||||
for e in enemies:
|
|
||||||
if not is_instance_valid(e):
|
|
||||||
continue
|
|
||||||
var d: float = position.distance_to(e.position)
|
|
||||||
if d <= best_dist:
|
|
||||||
best_dist = d
|
|
||||||
nearest = e
|
|
||||||
|
|
||||||
return nearest
|
|
||||||
|
|
||||||
|
|
||||||
func _on_area_entered(area: Area2D) -> void:
|
func _on_area_entered(area: Area2D) -> void:
|
||||||
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
||||||
return
|
return
|
||||||
if has_shield():
|
for child in get_children():
|
||||||
# Absorbed. The thing that hit it is still destroyed - a shield stops
|
if child.has_method("absorb_hit") and child.absorb_hit():
|
||||||
# the vessel dying, it does not make the rock harmless - but the
|
|
||||||
# vessel survives and is defenceless until the shield cycles back.
|
|
||||||
shield_charges -= 1
|
|
||||||
shield_recharge_left = GameState.shield_recharge_time()
|
|
||||||
_update_shield_visual()
|
|
||||||
if area.is_in_group("asteroids") and is_instance_valid(area):
|
if area.is_in_group("asteroids") and is_instance_valid(area):
|
||||||
area.queue_free()
|
area.queue_free()
|
||||||
return
|
return
|
||||||
|
|
@ -200,26 +128,8 @@ func _on_area_entered(area: Area2D) -> void:
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _build_shield_ring() -> void:
|
|
||||||
# Its own child, drawn once, toggled with `visible` (#10). A shield that
|
|
||||||
# came and went by redrawing the ship would re-tessellate the hull every
|
|
||||||
# time it cycled.
|
|
||||||
_shield_ring = NeonPiece.create(self, Vector2.ZERO, SHIELD_COLOR)
|
|
||||||
var points: PackedVector2Array = PackedVector2Array()
|
|
||||||
for i in range(14):
|
|
||||||
var a: float = (float(i) / 14.0) * TAU
|
|
||||||
points.append(Vector2(cos(a), sin(a)) * SHIELD_RADIUS)
|
|
||||||
_shield_ring.add_polygon(points, 0.7)
|
|
||||||
_shield_ring.queue_redraw()
|
|
||||||
_shield_ring.visible = false
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var nose: Vector2 = Vector2(0, -22)
|
var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([
|
||||||
var left: Vector2 = Vector2(-14, 16)
|
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||||
var right: Vector2 = Vector2(14, 16)
|
])
|
||||||
var points: PackedVector2Array = PackedVector2Array([nose, left, right])
|
|
||||||
# Constant glow, drawn once. No pulse means nothing changes frame to
|
|
||||||
# frame, so the ship never needs redrawing after _ready() - which also
|
|
||||||
# removes four antialiased polyline rebuilds per ship per frame.
|
|
||||||
NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color)
|
NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color)
|
||||||
|
|
|
||||||
11
TransportCapacityUpgrade.tres
Normal file
11
TransportCapacityUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "trn_capacity"
|
||||||
|
display_name = "Expanded Hold"
|
||||||
|
description = "Carries 8 more resources per supply run."
|
||||||
|
base_cost = 30
|
||||||
|
max_level = 3
|
||||||
|
icon = 21
|
||||||
|
additive_modifiers = {"cargo_capacity": 8.0}
|
||||||
11
TransportSpeedUpgrade.tres
Normal file
11
TransportSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
upgrade_id = "trn_speed"
|
||||||
|
display_name = "Freight Engines"
|
||||||
|
description = "+70 speed on supply runs."
|
||||||
|
base_cost = 30
|
||||||
|
max_level = 3
|
||||||
|
icon = 22
|
||||||
|
additive_modifiers = {"transport_speed": 70.0}
|
||||||
19
TransporterVesselDefinition.tres
Normal file
19
TransporterVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=6 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://VesselCargoComponent.tscn" id="2"]
|
||||||
|
[ext_resource type="PackedScene" path="res://VesselTransportComponent.tscn" id="3"]
|
||||||
|
[ext_resource type="Resource" path="res://TransportCapacityUpgrade.tres" id="4"]
|
||||||
|
[ext_resource type="Resource" path="res://TransportSpeedUpgrade.tres" id="5"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
definition_id = "transporter"
|
||||||
|
display_name = "Transporter"
|
||||||
|
description = "Unarmed cargo vessel stationed at a transport hub to move resources."
|
||||||
|
build_cost = 10
|
||||||
|
build_time = 8.0
|
||||||
|
icon = 12
|
||||||
|
glow_color = Color(0.95, 0.55, 0.85, 1)
|
||||||
|
capabilities = PackedStringArray("cargo", "transport")
|
||||||
|
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3")])
|
||||||
|
base_stats = {"max_speed": 460.0, "acceleration": 450.0, "cargo_capacity": 20.0, "transport_speed": 340.0}
|
||||||
|
upgrades = Array[Resource]([ExtResource("4"), ExtResource("5")])
|
||||||
9
VesselCargoComponent.gd
Normal file
9
VesselCargoComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
class_name VesselCargoComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var capability_id: String = "cargo"
|
||||||
|
var capacity: int
|
||||||
|
|
||||||
|
func apply_stats() -> void:
|
||||||
|
var state: Node = get_tree().root.get_node("GameState")
|
||||||
|
capacity = int(state.vessel_stat(get_parent().vessel_type, "cargo_capacity"))
|
||||||
1
VesselCargoComponent.gd.uid
Normal file
1
VesselCargoComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bo16x4wq6hg5k
|
||||||
4
VesselCargoComponent.tscn
Normal file
4
VesselCargoComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselCargoComponent.gd" id="1"]
|
||||||
|
[node name="CargoComponent" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
26
VesselDefinition.gd
Normal file
26
VesselDefinition.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
class_name VesselDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var definition_id: String
|
||||||
|
@export var display_name: String
|
||||||
|
@export_multiline var description: String
|
||||||
|
@export var build_cost: int
|
||||||
|
@export var build_time: float = 10.0
|
||||||
|
@export var icon: int = 2
|
||||||
|
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||||
|
@export var hull_points: PackedVector2Array = PackedVector2Array([
|
||||||
|
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||||
|
])
|
||||||
|
@export var capabilities: PackedStringArray = []
|
||||||
|
@export var component_scenes: Array[PackedScene] = []
|
||||||
|
@export var base_stats: Dictionary = {}
|
||||||
|
@export var minimum_stats: Dictionary = {}
|
||||||
|
@export var upgrades: Array[Resource] = []
|
||||||
|
@export var unlock_id: String
|
||||||
|
@export var unlock_name: String
|
||||||
|
@export_multiline var unlock_description: String
|
||||||
|
@export var unlock_cost: int
|
||||||
|
|
||||||
|
|
||||||
|
func has_capability(capability: String) -> bool:
|
||||||
|
return capability in capabilities
|
||||||
1
VesselDefinition.gd.uid
Normal file
1
VesselDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c6txy3v61blm8
|
||||||
24
VesselDefinitions.gd
Normal file
24
VesselDefinitions.gd
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
class_name VesselDefinitions
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const ORDER: PackedStringArray = ["interceptor", "shield", "transporter"]
|
||||||
|
const DEFINITIONS: Dictionary = {
|
||||||
|
"interceptor": preload("res://InterceptorVesselDefinition.tres"),
|
||||||
|
"shield": preload("res://ShieldVesselDefinition.tres"),
|
||||||
|
"transporter": preload("res://TransporterVesselDefinition.tres"),
|
||||||
|
}
|
||||||
|
|
||||||
|
static func get_definition(definition_id: String) -> Resource:
|
||||||
|
return DEFINITIONS.get(definition_id)
|
||||||
|
|
||||||
|
static func all() -> Array:
|
||||||
|
var result: Array = []
|
||||||
|
for definition_id in ORDER:
|
||||||
|
result.append(DEFINITIONS[definition_id])
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func with_capability(capability: String) -> Resource:
|
||||||
|
for definition in all():
|
||||||
|
if definition.has_capability(capability):
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
1
VesselDefinitions.gd.uid
Normal file
1
VesselDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cgwj1q4nf3ar7
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
|
|
||||||
# Visual-only craft-to-mothership flight. GameState owns position/progress;
|
# Visual-only craft-to-mothership flight. GameState owns position/progress;
|
||||||
# this node just renders a finished hull headed toward the hangar.
|
# this node just renders a finished hull headed toward the hangar.
|
||||||
var vessel_type: String = GameState.VESSEL_INTERCEPTOR
|
var vessel_type: String = "interceptor"
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -11,6 +13,6 @@ func _ready() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
var color: Color = Color(0.45, 0.8, 1.0) if vessel_type == GameState.VESSEL_SHIELD else Color(0.95, 0.55, 0.85) if vessel_type == GameState.VESSEL_TRANSPORTER else Color(0.25, 0.85, 1.0)
|
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||||
var hull := PackedVector2Array([Vector2(0, -20), Vector2(13, 15), Vector2(0, 9), Vector2(-13, 15)])
|
if definition != null:
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, color, 1.0)
|
NeonDraw.draw_glowing_polygon(self, definition.hull_points, definition.glow_color, 1.0)
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ func _process(_delta: float) -> void:
|
||||||
var node: Node2D = active.get(i)
|
var node: Node2D = active.get(i)
|
||||||
if node == null or not is_instance_valid(node):
|
if node == null or not is_instance_valid(node):
|
||||||
node = delivery_script.new()
|
node = delivery_script.new()
|
||||||
|
node.vessel_type = String(delivery.type)
|
||||||
get_parent().add_child(node)
|
get_parent().add_child(node)
|
||||||
active[i] = node
|
active[i] = node
|
||||||
node.vessel_type = String(delivery.type)
|
|
||||||
node.position = pos
|
node.position = pos
|
||||||
var direction: Vector2 = (delivery.to as Vector2) - (delivery.from as Vector2)
|
var direction: Vector2 = (delivery.to as Vector2) - (delivery.from as Vector2)
|
||||||
if direction.length() > 0.1:
|
if direction.length() > 0.1:
|
||||||
|
|
|
||||||
69
VesselShieldComponent.gd
Normal file
69
VesselShieldComponent.gd
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
class_name VesselShieldComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const SHIELD_COLOR: Color = Color(0.45, 0.8, 1.0)
|
||||||
|
const SHIELD_RADIUS: float = 30.0
|
||||||
|
|
||||||
|
var capability_id: String = "shield"
|
||||||
|
var charges: int = 0
|
||||||
|
var recharge_left: float = 0.0
|
||||||
|
var ring: Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func setup() -> void:
|
||||||
|
var ship: Node2D = get_parent()
|
||||||
|
var state: Node = ship.get_tree().root.get_node("GameState")
|
||||||
|
ring = NeonPiece.create(ship, Vector2.ZERO, SHIELD_COLOR)
|
||||||
|
var points := PackedVector2Array()
|
||||||
|
for i in range(14):
|
||||||
|
var angle: float = float(i) / 14.0 * TAU
|
||||||
|
points.append(Vector2(cos(angle), sin(angle)) * SHIELD_RADIUS)
|
||||||
|
ring.add_polygon(points, 0.7)
|
||||||
|
ring.queue_redraw()
|
||||||
|
charges = int(state.vessel_stat(ship.vessel_type, "shield_charges"))
|
||||||
|
_update_visual()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_stats() -> void:
|
||||||
|
var was_up: bool = charges > 0
|
||||||
|
if was_up:
|
||||||
|
charges = int(_state().vessel_stat(get_parent().vessel_type, "shield_charges"))
|
||||||
|
_update_visual()
|
||||||
|
|
||||||
|
|
||||||
|
func tick(delta: float) -> void:
|
||||||
|
var maximum: int = int(_state().vessel_stat(get_parent().vessel_type, "shield_charges"))
|
||||||
|
if charges >= maximum:
|
||||||
|
return
|
||||||
|
recharge_left -= delta
|
||||||
|
if recharge_left <= 0.0:
|
||||||
|
charges += 1
|
||||||
|
recharge_left = _state().vessel_stat(get_parent().vessel_type, "shield_recharge")
|
||||||
|
_update_visual()
|
||||||
|
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return charges > 0
|
||||||
|
|
||||||
|
|
||||||
|
func absorb_hit() -> bool:
|
||||||
|
if not is_active():
|
||||||
|
return false
|
||||||
|
charges -= 1
|
||||||
|
recharge_left = _state().vessel_stat(get_parent().vessel_type, "shield_recharge")
|
||||||
|
_update_visual()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func set_charges(value: int) -> void:
|
||||||
|
charges = value
|
||||||
|
_update_visual()
|
||||||
|
|
||||||
|
|
||||||
|
func _state() -> Node:
|
||||||
|
return get_tree().root.get_node("GameState")
|
||||||
|
|
||||||
|
|
||||||
|
func _update_visual() -> void:
|
||||||
|
if ring != null:
|
||||||
|
ring.visible = is_active()
|
||||||
1
VesselShieldComponent.gd.uid
Normal file
1
VesselShieldComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ghacatobxl11
|
||||||
4
VesselShieldComponent.tscn
Normal file
4
VesselShieldComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselShieldComponent.gd" id="1"]
|
||||||
|
[node name="ShieldComponent" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
9
VesselTransportComponent.gd
Normal file
9
VesselTransportComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
class_name VesselTransportComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var capability_id: String = "transport"
|
||||||
|
var travel_speed: float
|
||||||
|
|
||||||
|
func apply_stats() -> void:
|
||||||
|
var state: Node = get_tree().root.get_node("GameState")
|
||||||
|
travel_speed = state.vessel_stat(get_parent().vessel_type, "transport_speed")
|
||||||
1
VesselTransportComponent.gd.uid
Normal file
1
VesselTransportComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://tsapg8ld1vl8
|
||||||
4
VesselTransportComponent.tscn
Normal file
4
VesselTransportComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselTransportComponent.gd" id="1"]
|
||||||
|
[node name="TransportComponent" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
11
VesselUpgradeDefinition.gd
Normal file
11
VesselUpgradeDefinition.gd
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
class_name VesselUpgradeDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var upgrade_id: String
|
||||||
|
@export var display_name: String
|
||||||
|
@export_multiline var description: String
|
||||||
|
@export var base_cost: int
|
||||||
|
@export var max_level: int = 1
|
||||||
|
@export var icon: int
|
||||||
|
@export var additive_modifiers: Dictionary = {}
|
||||||
|
@export var multiplicative_modifiers: Dictionary = {}
|
||||||
1
VesselUpgradeDefinition.gd.uid
Normal file
1
VesselUpgradeDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dw41jux1cn3sw
|
||||||
47
VesselWeaponComponent.gd
Normal file
47
VesselWeaponComponent.gd
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
class_name VesselWeaponComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var capability_id: String = "weapon"
|
||||||
|
var fire_range: float
|
||||||
|
var fire_cooldown: float
|
||||||
|
var projectile_damage: int
|
||||||
|
var fire_timer: float = 0.0
|
||||||
|
var projectile_scene: PackedScene = preload("res://Projectile.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func apply_stats() -> void:
|
||||||
|
var ship: Node = get_parent()
|
||||||
|
var state: Node = ship.get_tree().root.get_node("GameState")
|
||||||
|
fire_range = state.vessel_stat(ship.vessel_type, "fire_range")
|
||||||
|
fire_cooldown = state.vessel_stat(ship.vessel_type, "fire_cooldown")
|
||||||
|
projectile_damage = int(state.vessel_stat(ship.vessel_type, "damage"))
|
||||||
|
|
||||||
|
|
||||||
|
func tick(delta: float) -> void:
|
||||||
|
fire_timer -= delta
|
||||||
|
if fire_timer > 0.0:
|
||||||
|
return
|
||||||
|
var ship: Node2D = get_parent()
|
||||||
|
var target: Node2D = _find_nearest_enemy(ship)
|
||||||
|
if target == null:
|
||||||
|
fire_timer = 0.25
|
||||||
|
return
|
||||||
|
var projectile: Node2D = projectile_scene.instantiate()
|
||||||
|
projectile.position = ship.position
|
||||||
|
projectile.damage = projectile_damage
|
||||||
|
ship.get_tree().root.add_child(projectile)
|
||||||
|
projectile.launch(target.position - ship.position)
|
||||||
|
fire_timer = fire_cooldown
|
||||||
|
|
||||||
|
|
||||||
|
func _find_nearest_enemy(ship: Node2D) -> Node2D:
|
||||||
|
var nearest: Node2D = null
|
||||||
|
var best_distance: float = fire_range
|
||||||
|
for enemy in ship.get_tree().get_nodes_in_group("enemies"):
|
||||||
|
if not is_instance_valid(enemy):
|
||||||
|
continue
|
||||||
|
var distance: float = ship.position.distance_to(enemy.position)
|
||||||
|
if distance <= best_distance:
|
||||||
|
best_distance = distance
|
||||||
|
nearest = enemy
|
||||||
|
return nearest
|
||||||
1
VesselWeaponComponent.gd.uid
Normal file
1
VesselWeaponComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cwo0nkmr4e1sl
|
||||||
4
VesselWeaponComponent.tscn
Normal file
4
VesselWeaponComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselWeaponComponent.gd" id="1"]
|
||||||
|
[node name="WeaponComponent" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
1
tests/test_building_composition.gd.uid
Normal file
1
tests/test_building_composition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bljnijxm3n20e
|
||||||
|
|
@ -68,6 +68,7 @@ func _process(delta: float) -> bool:
|
||||||
# own non-overlapping berths to the right only after expansion.
|
# own non-overlapping berths to the right only after expansion.
|
||||||
var mothership: Node2D = load("res://Building.tscn").instantiate()
|
var mothership: Node2D = load("res://Building.tscn").instantiate()
|
||||||
mothership.set("building_type", GameState.BUILDING_MOTHERSHIP)
|
mothership.set("building_type", GameState.BUILDING_MOTHERSHIP)
|
||||||
|
mothership._ready()
|
||||||
controller.current_building = mothership
|
controller.current_building = mothership
|
||||||
controller.context_actions_expanded = true
|
controller.context_actions_expanded = true
|
||||||
controller._layout_context_buttons()
|
controller._layout_context_buttons()
|
||||||
|
|
|
||||||
50
tests/test_vessel_composition.gd
Normal file
50
tests/test_vessel_composition.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
|
|
||||||
|
var ships: Array = []
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
var game_state: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
var scene: PackedScene = load("res://Ship.tscn")
|
||||||
|
for definition in VESSEL_DEFINITIONS.all():
|
||||||
|
var ship: Area2D = scene.instantiate()
|
||||||
|
ship.vessel_type = definition.definition_id
|
||||||
|
root.add_child(ship)
|
||||||
|
ships.append(ship)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
if elapsed < 0.1:
|
||||||
|
return false
|
||||||
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||||
|
if ships.size() != definitions.size():
|
||||||
|
return _fail("catalog and composed fleet differ in size")
|
||||||
|
for i in range(definitions.size()):
|
||||||
|
var definition: Resource = definitions[i]
|
||||||
|
var ship: Node = ships[i]
|
||||||
|
if ship.definition == null or ship.definition.definition_id != definition.definition_id:
|
||||||
|
return _fail("%s did not resolve its definition" % definition.definition_id)
|
||||||
|
for capability in definition.capabilities:
|
||||||
|
if not ship.has_capability(capability):
|
||||||
|
return _fail("%s did not compose %s" % [definition.definition_id, capability])
|
||||||
|
if game_state.product_cost(definition.definition_id) != definition.build_cost \
|
||||||
|
or game_state.vessel_name(definition.definition_id) != definition.display_name:
|
||||||
|
return _fail("%s metadata did not come from its definition" % definition.definition_id)
|
||||||
|
if ships[0].has_capability("shield") or not ships[1].has_capability("shield"):
|
||||||
|
return _fail("shield behavior is not controlled by component presence")
|
||||||
|
if ships[2].has_capability("weapon") or not ships[2].has_capability("transport"):
|
||||||
|
return _fail("transporter capabilities were composed incorrectly")
|
||||||
|
print("TEST_VESSEL_COMPOSITION: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_VESSEL_COMPOSITION: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
1
tests/test_vessel_composition.gd.uid
Normal file
1
tests/test_vessel_composition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cq73poxld1e35
|
||||||
Loading…
Reference in a new issue