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
|
||||
|
||||
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
|
||||
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||
|
||||
# Space-side building UI. Owns:
|
||||
# - 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"),
|
||||
]
|
||||
@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_status: Label = get_node("../UI/LabMenu/StatusLabel")
|
||||
@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_transporter_label: Label = get_node("../UI/CraftMenu/TransporterLabel")
|
||||
@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) ---------------------------------------------
|
||||
# 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_close_button.pressed.connect(_close_build_menu)
|
||||
_configure_build_menu_entries()
|
||||
craft_shield_button.pressed.connect(_on_craft_shield)
|
||||
lab_close_button.pressed.connect(func(): lab_menu.visible = false)
|
||||
lab_buy_button.pressed.connect(_on_lab_buy)
|
||||
for i in range(lab_vessel_buttons.size()):
|
||||
|
|
@ -172,8 +182,9 @@ func _ready() -> void:
|
|||
action_button.pressed.connect(_on_context_action_pressed)
|
||||
building_action_button.pressed.connect(_on_action_pressed)
|
||||
|
||||
craft_interceptor_button.pressed.connect(_on_craft_interceptor)
|
||||
craft_transporter_button.pressed.connect(_on_craft_transporter)
|
||||
for i in range(mini(craft_vessel_buttons.size(), VESSEL_DEFINITIONS.all().size())):
|
||||
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)
|
||||
|
||||
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
|
||||
# intentionally reserved for the shared resource bar and a short action
|
||||
# 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 = ""
|
||||
# Nowhere to station one means the order would be refused, so say so
|
||||
# rather than letting the button look available and do nothing.
|
||||
craft_transporter_button.enabled = GameState.has_transport_hub() \
|
||||
and GameState.can_station_transporter()
|
||||
craft_transporter_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()
|
||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||
for i in range(mini(definitions.size(), craft_vessel_buttons.size())):
|
||||
var definition: Resource = definitions[i]
|
||||
var button: IconButton = craft_vessel_buttons[i]
|
||||
craft_vessel_labels[i].text = "%s\n%d ore" % [definition.display_name, definition.build_cost]
|
||||
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()
|
||||
button.queue_redraw()
|
||||
_refresh_queue()
|
||||
if craft_message != "":
|
||||
if Time.get_ticks_msec() < craft_message_until:
|
||||
|
|
@ -721,40 +725,40 @@ func _refresh_craft_status() -> void:
|
|||
|
||||
|
||||
func _on_craft_interceptor() -> void:
|
||||
_on_craft_vessel(GameState.VESSEL_INTERCEPTOR)
|
||||
|
||||
|
||||
func _on_craft_vessel(vessel: String) -> void:
|
||||
if craft_building_id == 0:
|
||||
_say("No crafter selected - reopen this panel from the building")
|
||||
return
|
||||
# No affordability check: an order the crafter cannot pay for yet is
|
||||
# QUEUED, and completes by itself once ore is delivered here (#16). But
|
||||
# queueing silently looks identical to a dead button, so it says which
|
||||
# happened and what the crafter is still short of.
|
||||
GameState.queue_order(craft_building_id)
|
||||
_report_order(GameState.PRODUCT_SHIP)
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel)
|
||||
if definition == null:
|
||||
return
|
||||
if not GameState.queue_order(craft_building_id, vessel):
|
||||
if not GameState.vessel_is_unlocked(vessel):
|
||||
_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:
|
||||
var short: int = GameState.outstanding_cost(craft_building_id)
|
||||
if short > 0:
|
||||
_say("%s queued - %d more ore must be delivered here" % [
|
||||
GameState.VESSEL_NAMES.get(product, product), short
|
||||
GameState.vessel_name(product), short
|
||||
])
|
||||
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:
|
||||
if craft_building_id == 0:
|
||||
_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()
|
||||
_on_craft_vessel(GameState.VESSEL_TRANSPORTER)
|
||||
|
||||
|
||||
# --- ship lab (#5) --------------------------------------------------------
|
||||
|
|
@ -764,10 +768,13 @@ func _lab_options() -> Array:
|
|||
# entry, because unlocking the type and upgrading it belong in the same
|
||||
# place - and its upgrades are pointless until it exists.
|
||||
var options: Array = []
|
||||
if lab_vessel == GameState.VESSEL_SHIELD and not GameState.shield_vessel_unlocked():
|
||||
options.append({"unlock": true, "name": "Unlock Shield Vessel",
|
||||
"desc": "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up.",
|
||||
"cost": GameState.UNLOCK_SHIELD_COST})
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(lab_vessel)
|
||||
if definition == null:
|
||||
return options
|
||||
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
|
||||
for u in GameState.upgrades_for(lab_vessel):
|
||||
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:
|
||||
lab_vessel = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_TRANSPORTER][index]
|
||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||
if index < 0 or index >= definitions.size():
|
||||
return
|
||||
lab_vessel = definitions[index].definition_id
|
||||
lab_choice = 0
|
||||
_refresh_lab()
|
||||
|
||||
|
|
@ -794,7 +803,7 @@ func _on_lab_buy() -> void:
|
|||
if options.is_empty():
|
||||
return
|
||||
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):
|
||||
lab_status.text = "Research is already queued or complete"
|
||||
return
|
||||
|
|
@ -803,14 +812,17 @@ func _on_lab_buy() -> void:
|
|||
|
||||
|
||||
func _refresh_lab() -> void:
|
||||
for i in range(lab_vessel_buttons.size()):
|
||||
var this_vessel: String = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_TRANSPORTER][i]
|
||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||
for i in range(mini(lab_vessel_buttons.size(), definitions.size())):
|
||||
var definition: Resource = definitions[i]
|
||||
var this_vessel: String = definition.definition_id
|
||||
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].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()
|
||||
for i in range(lab_upgrade_buttons.size()):
|
||||
|
|
@ -820,7 +832,7 @@ func _refresh_lab() -> void:
|
|||
if not visible:
|
||||
continue
|
||||
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):
|
||||
price_text = "MAX"
|
||||
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:
|
||||
if bool(option.unlock):
|
||||
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
|
||||
return int(option.get("icon", IconButton.Icon.SHIP_LAB))
|
||||
|
||||
|
||||
func _on_craft_shield() -> void:
|
||||
if craft_building_id == 0:
|
||||
_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)
|
||||
_on_craft_vessel(GameState.VESSEL_SHIELD)
|
||||
|
||||
|
||||
# --- moving ore, in both directions, at any building (#24) ----------------
|
||||
|
|
@ -957,8 +951,8 @@ func _refresh_queue() -> void:
|
|||
if not slot.visible:
|
||||
continue
|
||||
var order: Dictionary = orders[i]
|
||||
slot.icon = IconButton.Icon.TRANSPORT_HUB if order.product == GameState.PRODUCT_TRANSPORTER \
|
||||
else IconButton.Icon.SHIP
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(String(order.product))
|
||||
slot.icon = definition.icon
|
||||
slot.glow_color = _vessel_colour(String(order.product))
|
||||
# Only the one under construction reads as active; the rest are
|
||||
# waiting, and greyed says so without needing a caption each.
|
||||
|
|
@ -971,29 +965,24 @@ func _refresh_queue() -> void:
|
|||
var head: Dictionary = orders[0]
|
||||
if bool(head.started):
|
||||
queue_detail.text = "Building %s %d%%\nTap to cancel" % [
|
||||
GameState.VESSEL_NAMES.get(head.product, head.product),
|
||||
GameState.vessel_name(String(head.product)),
|
||||
int(round(float(head.progress) * 100.0)),
|
||||
]
|
||||
else:
|
||||
queue_detail.text = "%s waiting for ore\nTap to cancel" % \
|
||||
GameState.VESSEL_NAMES.get(head.product, head.product)
|
||||
GameState.vessel_name(String(head.product))
|
||||
|
||||
|
||||
func _vessel_colour(product: String) -> Color:
|
||||
match product:
|
||||
GameState.VESSEL_SHIELD:
|
||||
return Color(0.45, 0.8, 1.0)
|
||||
GameState.PRODUCT_TRANSPORTER:
|
||||
return Color(0.95, 0.55, 0.85)
|
||||
_:
|
||||
return Color(0.25, 0.85, 1.0)
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(product)
|
||||
return definition.glow_color if definition != null else Color(0.25, 0.85, 1.0)
|
||||
|
||||
|
||||
func _on_queue_slot(index: int) -> void:
|
||||
var orders: Array = GameState.order_list(craft_building_id)
|
||||
if index >= orders.size():
|
||||
return
|
||||
var vessel: String = GameState.VESSEL_NAMES.get(orders[index].product, orders[index].product)
|
||||
var vessel: String = GameState.vessel_name(String(orders[index].product))
|
||||
var refunded: bool = bool(orders[index].started)
|
||||
if not GameState.cancel_order(craft_building_id, index):
|
||||
return
|
||||
|
|
@ -1018,12 +1007,12 @@ func _on_hangar_crew_type(index: int) -> void:
|
|||
return
|
||||
var stored_index: int = GameState.stored_vessels.find(type)
|
||||
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
|
||||
if not fleet.add_ship(GameState.take_stored_vessel(stored_index)):
|
||||
hangar_status.text = "Could not crew that vessel"
|
||||
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()
|
||||
|
||||
|
||||
|
|
@ -1035,32 +1024,40 @@ func _on_hangar_store_type(index: int) -> void:
|
|||
ship_index = i
|
||||
break
|
||||
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
|
||||
var stored: String = fleet.store_ship(ship_index)
|
||||
if stored == "":
|
||||
hangar_status.text = "Keep one ship deployed"
|
||||
return
|
||||
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()
|
||||
|
||||
|
||||
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:
|
||||
var types: Array = _hangar_types()
|
||||
for i in range(types.size()):
|
||||
var type: String = types[i]
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(type)
|
||||
var available: int = GameState.stored_vessels.count(type)
|
||||
var deployed: int = 0
|
||||
for ship in fleet.ships:
|
||||
if is_instance_valid(ship) and String(ship.vessel_type) == type:
|
||||
deployed += 1
|
||||
hangar_available_labels[i].text = "%s\n%d available" % [GameState.VESSEL_NAMES.get(type, type), available]
|
||||
hangar_fleet_labels[i].text = "%s\n%d in fleet" % [GameState.VESSEL_NAMES.get(type, type), deployed]
|
||||
hangar_available_labels[i].text = "%s\n%d available" % [definition.display_name, available]
|
||||
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_fleet_slots[i].enabled = deployed > 0 and fleet.ships.size() > 1
|
||||
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 RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.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()
|
||||
|
|
@ -404,21 +405,10 @@ const VESSEL_INTERCEPTOR: String = "interceptor"
|
|||
const VESSEL_SHIELD: String = "shield"
|
||||
const VESSEL_TRANSPORTER: String = "transporter"
|
||||
|
||||
const VESSEL_NAMES: Dictionary = {
|
||||
VESSEL_INTERCEPTOR: "Interceptor",
|
||||
VESSEL_SHIELD: "Shield Vessel",
|
||||
VESSEL_TRANSPORTER: "Transporter",
|
||||
}
|
||||
|
||||
# Compatibility constants for callers and old tests. Runtime metadata is
|
||||
# resolved from VesselDefinition below.
|
||||
const SHIELD_COST: int = 14
|
||||
|
||||
# How long one vessel takes to build, once its ore is actually in the
|
||||
# building. A queue you can watch and cancel needs time to exist in.
|
||||
const BUILD_TIMES: Dictionary = {
|
||||
VESSEL_INTERCEPTOR: 10.0,
|
||||
VESSEL_SHIELD: 16.0,
|
||||
VESSEL_TRANSPORTER: 8.0,
|
||||
}
|
||||
const TRANSPORTER_COST: int = 10
|
||||
const LAB_RESEARCH_TIME: float = 8.0
|
||||
|
||||
|
||||
|
|
@ -429,57 +419,14 @@ var build_time_scale: float = 1.0
|
|||
|
||||
|
||||
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:
|
||||
return LAB_RESEARCH_TIME * build_time_scale
|
||||
|
||||
# Researched at the ship lab, per vessel type. Separate from SKILL_TREE on
|
||||
# purpose: that gates what the player may build at all and is a strategic
|
||||
# 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.
|
||||
# Compatibility constants. VesselDefinition owns the live base figures.
|
||||
const SHIP_BASE_DAMAGE: int = 1
|
||||
const SHIP_BASE_COOLDOWN: float = 0.5
|
||||
const SHIP_BASE_RANGE: float = 320.0
|
||||
|
|
@ -503,9 +450,7 @@ func has_ship_lab() -> bool:
|
|||
|
||||
|
||||
func shield_vessel_unlocked() -> bool:
|
||||
# Unlocked AT the lab, by paying for it there - a vessel type rather than
|
||||
# a stat, so it is a one-off rather than a level.
|
||||
return int(vessel_levels.get(UNLOCK_SHIELD_VESSEL, 0)) > 0
|
||||
return vessel_is_unlocked(VESSEL_SHIELD)
|
||||
|
||||
|
||||
const UNLOCK_SHIELD_VESSEL: String = "unlock_shield"
|
||||
|
|
@ -513,27 +458,69 @@ const UNLOCK_SHIELD_COST: int = 45
|
|||
|
||||
|
||||
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
|
||||
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
|
||||
vessel_levels[UNLOCK_SHIELD_VESSEL] = 1
|
||||
vessel_upgraded.emit(UNLOCK_SHIELD_VESSEL)
|
||||
if not spend(definition.unlock_cost):
|
||||
return false
|
||||
vessel_levels[definition.unlock_id] = 1
|
||||
vessel_upgraded.emit(definition.unlock_id)
|
||||
return true
|
||||
|
||||
|
||||
func upgrade_def(id: String) -> Dictionary:
|
||||
for u in VESSEL_UPGRADES:
|
||||
if u.id == id:
|
||||
return u
|
||||
for definition in VESSEL_DEFINITIONS.all():
|
||||
for upgrade in definition.upgrades:
|
||||
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 {}
|
||||
|
||||
|
||||
func upgrades_for(vessel: String) -> Array:
|
||||
var out: Array = []
|
||||
for u in VESSEL_UPGRADES:
|
||||
if u.vessel == vessel:
|
||||
out.append(u)
|
||||
var definition: Resource = vessel_definition(vessel)
|
||||
if definition == null:
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -567,66 +554,51 @@ func buy_upgrade(id: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
# --- what those upgrades actually come to ---------------------------------
|
||||
# Read live rather than baked into a vessel when it is built, so researching
|
||||
# something improves the ships already flying instead of only the next ones.
|
||||
|
||||
func ship_damage(vessel: String) -> int:
|
||||
return SHIP_BASE_DAMAGE + (upgrade_level("int_damage") if vessel == VESSEL_INTERCEPTOR else 0)
|
||||
func vessel_stat(vessel: String, stat: String) -> float:
|
||||
var definition: Resource = vessel_definition(vessel)
|
||||
if definition == null:
|
||||
return 0.0
|
||||
var value: float = float(definition.base_stats.get(stat, 0.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:
|
||||
if vessel != VESSEL_INTERCEPTOR:
|
||||
return SHIP_BASE_COOLDOWN
|
||||
return SHIP_BASE_COOLDOWN * pow(0.85, float(upgrade_level("int_rate")))
|
||||
|
||||
|
||||
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 ship_damage(vessel: String) -> int: return int(vessel_stat(vessel, "damage"))
|
||||
func ship_fire_cooldown(vessel: String) -> float: return vessel_stat(vessel, "fire_cooldown")
|
||||
func ship_fire_range(vessel: String) -> float: return vessel_stat(vessel, "fire_range")
|
||||
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 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:
|
||||
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:
|
||||
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:
|
||||
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_TRANSPORTER: String = "transporter"
|
||||
|
||||
const TRANSPORTER_COST: int = 10
|
||||
|
||||
|
||||
func product_cost(product: String) -> int:
|
||||
if product == PRODUCT_TRANSPORTER:
|
||||
return TRANSPORTER_COST
|
||||
if product == VESSEL_SHIELD:
|
||||
return SHIELD_COST
|
||||
return SHIP_COST
|
||||
var definition: Resource = vessel_definition(product)
|
||||
return definition.build_cost if definition != null else 0
|
||||
|
||||
|
||||
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)
|
||||
if b.is_empty() or not can_produce(b.type):
|
||||
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
|
||||
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
|
||||
# told now instead of watching an order sit forever.
|
||||
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).
|
||||
return 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 cost: int = product_cost(product)
|
||||
var inventory: RefCounted = _inventory_for_record(b)
|
||||
var definition: Resource = vessel_definition(product)
|
||||
|
||||
if not bool(order.started):
|
||||
if inventory.amount(RESOURCE_ORE) < cost:
|
||||
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
|
||||
# was going to use is gone. Hold rather than overfill.
|
||||
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
|
||||
inventory.withdraw(RESOURCE_ORE, cost)
|
||||
order.started = true
|
||||
|
|
@ -760,7 +734,7 @@ func _advance_production(delta: float) -> void:
|
|||
continue
|
||||
|
||||
orders.pop_front()
|
||||
if product == PRODUCT_TRANSPORTER:
|
||||
if definition.has_capability("transport"):
|
||||
# Transporters are unchanged: they take themselves to the nearest
|
||||
# hub and wait for work (#19).
|
||||
_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 == "":
|
||||
return false
|
||||
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
|
||||
orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false})
|
||||
lab.research_orders = orders
|
||||
|
|
@ -804,7 +779,8 @@ func _advance_lab_research(delta: float) -> void:
|
|||
continue
|
||||
var order: Dictionary = orders[0]
|
||||
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)
|
||||
if not bool(order.started):
|
||||
if inventory.amount(RESOURCE_ORE) < cost:
|
||||
|
|
@ -814,7 +790,7 @@ func _advance_lab_research(delta: float) -> void:
|
|||
order.elapsed = float(order.elapsed) + delta
|
||||
if float(order.elapsed) < lab_research_time():
|
||||
continue
|
||||
if id == UNLOCK_SHIELD_VESSEL:
|
||||
if unlock_definition != null:
|
||||
vessel_levels[id] = 1
|
||||
else:
|
||||
vessel_levels[id] = upgrade_level(id) + 1
|
||||
|
|
@ -1041,7 +1017,7 @@ func pending_transporter_orders() -> int:
|
|||
var n: int = 0
|
||||
for b in buildings:
|
||||
for o in b.get("orders", []):
|
||||
if String(o.product) == PRODUCT_TRANSPORTER:
|
||||
if vessel_has_capability(String(o.product), "transport"):
|
||||
n += 1
|
||||
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
|
||||
234
Ship.gd
234
Ship.gd
|
|
@ -2,224 +2,134 @@ extends Area2D
|
|||
|
||||
signal destroyed
|
||||
|
||||
# Deliberately faster than Fleet.cruise_speed: the fleet as a whole travels
|
||||
# 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.
|
||||
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||
|
||||
@export var max_speed: float = 460.0
|
||||
@export var acceleration: float = 450.0
|
||||
@export var rotation_speed: float = 8.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"
|
||||
|
||||
# --- shield (#5) ----------------------------------------------------------
|
||||
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 definition: Resource
|
||||
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
|
||||
# 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 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:
|
||||
move_target = position
|
||||
add_to_group("player_ships")
|
||||
area_entered.connect(_on_area_entered)
|
||||
if vessel_type == GameState.VESSEL_SHIELD:
|
||||
_build_shield_ring()
|
||||
shield_charges = GameState.shield_max_charges()
|
||||
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||
if definition == null:
|
||||
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()
|
||||
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:
|
||||
move_target = world_pos
|
||||
|
||||
|
||||
func apply_vessel_stats() -> void:
|
||||
# Read from GameState rather than baked in at spawn, so researching an
|
||||
# upgrade improves the ships already flying and not just the next one
|
||||
# built. Fleet calls this on every vessel when something is bought.
|
||||
max_speed = GameState.ship_max_speed(vessel_type)
|
||||
acceleration = GameState.ship_acceleration(vessel_type)
|
||||
fire_range = GameState.ship_fire_range(vessel_type)
|
||||
fire_cooldown = GameState.ship_fire_cooldown(vessel_type)
|
||||
projectile_damage = GameState.ship_damage(vessel_type)
|
||||
if vessel_type == GameState.VESSEL_SHIELD:
|
||||
# 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()
|
||||
if definition == null:
|
||||
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||
if definition == null:
|
||||
return
|
||||
max_speed = GameState.vessel_stat(vessel_type, "max_speed")
|
||||
acceleration = GameState.vessel_stat(vessel_type, "acceleration")
|
||||
for child in get_children():
|
||||
if child.has_method("apply_stats"):
|
||||
child.apply_stats()
|
||||
|
||||
|
||||
func has_shield() -> bool:
|
||||
return vessel_type == GameState.VESSEL_SHIELD and shield_charges > 0
|
||||
|
||||
|
||||
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()
|
||||
var component: Node = get_component("shield")
|
||||
return component != null and component.is_active()
|
||||
|
||||
|
||||
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 distance: float = to_target.length()
|
||||
|
||||
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
|
||||
velocity = Vector2.ZERO
|
||||
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 desired_speed: float = min(max_speed, braking_speed)
|
||||
var desired_velocity: Vector2 = to_target.normalized() * desired_speed
|
||||
velocity = velocity.move_toward(desired_velocity, acceleration * delta)
|
||||
position += velocity * delta
|
||||
|
||||
# One owner, one rule: face the way this ship is actually travelling.
|
||||
var motion: Vector2 = velocity + fleet_velocity
|
||||
if motion.length() > 8.0:
|
||||
var target_angle: float = motion.angle() + PI / 2.0
|
||||
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:
|
||||
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
||||
return
|
||||
if has_shield():
|
||||
# Absorbed. The thing that hit it is still destroyed - a shield stops
|
||||
# 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):
|
||||
area.queue_free()
|
||||
return
|
||||
for child in get_children():
|
||||
if child.has_method("absorb_hit") and child.absorb_hit():
|
||||
if area.is_in_group("asteroids") and is_instance_valid(area):
|
||||
area.queue_free()
|
||||
return
|
||||
ShipExplosionVFX.spawn(get_tree().root, position)
|
||||
destroyed.emit()
|
||||
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:
|
||||
var nose: Vector2 = Vector2(0, -22)
|
||||
var left: Vector2 = 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.
|
||||
var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([
|
||||
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||
])
|
||||
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
|
||||
|
||||
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||
|
||||
# Visual-only craft-to-mothership flight. GameState owns position/progress;
|
||||
# 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:
|
||||
|
|
@ -11,6 +13,6 @@ func _ready() -> 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 hull := PackedVector2Array([Vector2(0, -20), Vector2(13, 15), Vector2(0, 9), Vector2(-13, 15)])
|
||||
NeonDraw.draw_glowing_polygon(self, hull, color, 1.0)
|
||||
var definition: Resource = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||
if definition != null:
|
||||
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)
|
||||
if node == null or not is_instance_valid(node):
|
||||
node = delivery_script.new()
|
||||
node.vessel_type = String(delivery.type)
|
||||
get_parent().add_child(node)
|
||||
active[i] = node
|
||||
node.vessel_type = String(delivery.type)
|
||||
node.position = pos
|
||||
var direction: Vector2 = (delivery.to as Vector2) - (delivery.from as Vector2)
|
||||
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.
|
||||
var mothership: Node2D = load("res://Building.tscn").instantiate()
|
||||
mothership.set("building_type", GameState.BUILDING_MOTHERSHIP)
|
||||
mothership._ready()
|
||||
controller.current_building = mothership
|
||||
controller.context_actions_expanded = true
|
||||
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