Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d68f4d4f88 | ||
|
|
3f3a22227d | ||
|
|
651f8b787e | ||
|
|
facbd83dd2 | ||
|
|
17d0cf7e66 | ||
|
|
116e0f97d3 |
111 changed files with 2269 additions and 632 deletions
191
AimedFleetAbilityController.gd
Normal file
191
AimedFleetAbilityController.gd
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
class_name AimedFleetAbilityController
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Shared press-drag-release input and camera behavior for manually aimed fleet
|
||||||
|
# abilities. Subclasses provide the component, ammunition, target calculation,
|
||||||
|
# shot effect, and effect visuals.
|
||||||
|
|
||||||
|
const AIM_DEADZONE: float = 22.0
|
||||||
|
|
||||||
|
@export var minimum_zoom: float = 0.4
|
||||||
|
@onready var fleet: Node2D = get_node("../Fleet")
|
||||||
|
@onready var camera: Camera2D = get_node("../Camera2D")
|
||||||
|
|
||||||
|
var button: IconButton
|
||||||
|
var status: Label
|
||||||
|
var status_message_until: int = 0
|
||||||
|
var aiming: bool = false
|
||||||
|
var aim_touch: int = -1
|
||||||
|
var aim_start_screen: Vector2 = Vector2.ZERO
|
||||||
|
var aim_screen: Vector2 = Vector2.ZERO
|
||||||
|
var aim_target: Vector2 = Vector2.ZERO
|
||||||
|
var saved_zoom: float = 1.0
|
||||||
|
var restoring_zoom: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
button = get_node(_button_path())
|
||||||
|
status = get_node(_status_path())
|
||||||
|
button.visible = false
|
||||||
|
status.text = ""
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
var had_visuals: bool = aiming or _has_effect_visuals()
|
||||||
|
button.visible = fleet.has_capability(_component_id())
|
||||||
|
button.enabled = _next_ready_ship() != null and _can_afford_shot()
|
||||||
|
if not button.visible and aiming:
|
||||||
|
_cancel_aim()
|
||||||
|
if status.text != "" and Time.get_ticks_msec() >= status_message_until:
|
||||||
|
status.text = ""
|
||||||
|
|
||||||
|
if restoring_zoom and not aiming:
|
||||||
|
var zoom: float = move_toward(camera.zoom.x, saved_zoom,
|
||||||
|
maxf(saved_zoom, 0.1) * delta * 5.0)
|
||||||
|
camera.zoom = Vector2(zoom, zoom)
|
||||||
|
if is_equal_approx(zoom, saved_zoom):
|
||||||
|
restoring_zoom = false
|
||||||
|
|
||||||
|
_advance_effects(delta)
|
||||||
|
if aiming or _has_effect_visuals() or had_visuals:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func can_begin_aim() -> bool:
|
||||||
|
# An unaffordable shot still claims the press so the status label can
|
||||||
|
# explain what is missing instead of silently passing the touch through.
|
||||||
|
return button.visible and not button._blocked_by_modal() \
|
||||||
|
and not aiming and _next_ready_ship() != null
|
||||||
|
|
||||||
|
|
||||||
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||||
|
if event.pressed:
|
||||||
|
if not can_begin_aim() \
|
||||||
|
or not Rect2(button.global_position, button.size).has_point(event.position):
|
||||||
|
return false
|
||||||
|
if not _can_afford_shot():
|
||||||
|
_say(_unaffordable_message())
|
||||||
|
return true
|
||||||
|
aiming = true
|
||||||
|
aim_touch = event.index
|
||||||
|
aim_start_screen = event.position
|
||||||
|
aim_screen = event.position
|
||||||
|
saved_zoom = camera.zoom.x
|
||||||
|
restoring_zoom = false
|
||||||
|
button.pressed_down = true
|
||||||
|
button.queue_redraw()
|
||||||
|
_update_aim()
|
||||||
|
return true
|
||||||
|
if event.index != aim_touch:
|
||||||
|
return false
|
||||||
|
if aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE:
|
||||||
|
_fire()
|
||||||
|
else:
|
||||||
|
_cancel_aim()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func handle_drag(event: InputEventScreenDrag) -> bool:
|
||||||
|
if not aiming or event.index != aim_touch:
|
||||||
|
return false
|
||||||
|
aim_screen = event.position
|
||||||
|
_update_aim()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _update_aim() -> void:
|
||||||
|
var origin: Vector2 = _aim_origin()
|
||||||
|
aim_target = _calculate_aim_target(aim_screen - aim_start_screen, origin)
|
||||||
|
_update_aim_zoom(origin, aim_target)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
||||||
|
var displacement: Vector2 = target - origin
|
||||||
|
var half_screen: Vector2 = get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0)
|
||||||
|
var padding: float = _aim_padding()
|
||||||
|
var fit_x: float = saved_zoom if absf(displacement.x) < 1.0 \
|
||||||
|
else half_screen.x / (absf(displacement.x) + padding)
|
||||||
|
var fit_y: float = saved_zoom if absf(displacement.y) < 1.0 \
|
||||||
|
else half_screen.y / (absf(displacement.y) + padding)
|
||||||
|
var desired: float = clampf(minf(saved_zoom, minf(fit_x, fit_y)),
|
||||||
|
minimum_zoom, saved_zoom)
|
||||||
|
camera.zoom = Vector2(desired, desired)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish_aim() -> void:
|
||||||
|
aiming = false
|
||||||
|
aim_touch = -1
|
||||||
|
button.pressed_down = false
|
||||||
|
button.queue_redraw()
|
||||||
|
restoring_zoom = true
|
||||||
|
|
||||||
|
|
||||||
|
func _cancel_aim() -> void:
|
||||||
|
_finish_aim()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _fire() -> void:
|
||||||
|
var ship: Node2D = _next_ready_ship()
|
||||||
|
if ship == null:
|
||||||
|
_cancel_aim()
|
||||||
|
return
|
||||||
|
if not _can_afford_shot():
|
||||||
|
_say(_unaffordable_message())
|
||||||
|
_cancel_aim()
|
||||||
|
return
|
||||||
|
var component: Node = ship.get_component(_component_id())
|
||||||
|
if component == null or not _consume_shot(component):
|
||||||
|
_cancel_aim()
|
||||||
|
return
|
||||||
|
GameState.withdraw_fleet_resource(_ammo_resource(), _ammo_cost())
|
||||||
|
_perform_shot(ship, component)
|
||||||
|
_finish_aim()
|
||||||
|
button.enabled = _next_ready_ship() != null and _can_afford_shot()
|
||||||
|
|
||||||
|
|
||||||
|
func _next_ready_ship() -> Node2D:
|
||||||
|
for ship in fleet.ships:
|
||||||
|
if not is_instance_valid(ship):
|
||||||
|
continue
|
||||||
|
var component: Node = ship.get_component(_component_id())
|
||||||
|
if component != null and component.is_ready():
|
||||||
|
return ship
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _can_afford_shot() -> bool:
|
||||||
|
return GameState.fleet_resource(_ammo_resource()) >= _ammo_cost()
|
||||||
|
|
||||||
|
|
||||||
|
func _unaffordable_message() -> String:
|
||||||
|
var resource_type: String = _ammo_resource()
|
||||||
|
return "Need %d %s in the fleet hold to fire (have %d)" % [
|
||||||
|
_ammo_cost(), GameState.resource_name(resource_type).to_lower(),
|
||||||
|
GameState.fleet_resource(resource_type),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _say(message: String) -> void:
|
||||||
|
status.text = message
|
||||||
|
status_message_until = Time.get_ticks_msec() + 3000
|
||||||
|
|
||||||
|
|
||||||
|
func _aim_origin() -> Vector2:
|
||||||
|
return fleet.get_centroid()
|
||||||
|
|
||||||
|
|
||||||
|
# Subclass hooks.
|
||||||
|
func _button_path() -> NodePath: return NodePath()
|
||||||
|
func _status_path() -> NodePath: return NodePath()
|
||||||
|
func _component_id() -> String: return ""
|
||||||
|
func _ammo_resource() -> String: return ""
|
||||||
|
func _ammo_cost() -> int: return 0
|
||||||
|
func _aim_padding() -> float: return 0.0
|
||||||
|
func _has_effect_visuals() -> bool: return false
|
||||||
|
func _advance_effects(_delta: float) -> void: pass
|
||||||
|
func _consume_shot(_component: Node) -> bool: return false
|
||||||
|
func _perform_shot(_ship: Node2D, _component: Node) -> void: pass
|
||||||
|
func _calculate_aim_target(_drag: Vector2, origin: Vector2) -> Vector2: return origin
|
||||||
1
AimedFleetAbilityController.gd.uid
Normal file
1
AimedFleetAbilityController.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dbahjmvwru7bp
|
||||||
|
|
@ -32,12 +32,14 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"),
|
get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"),
|
||||||
get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"),
|
get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"),
|
||||||
get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselF"),
|
get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselF"),
|
||||||
|
get_node("../UI/LabMenu/VesselG"),
|
||||||
]
|
]
|
||||||
@onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel")
|
@onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel")
|
||||||
@onready var lab_vessel_labels: Array = [
|
@onready var lab_vessel_labels: Array = [
|
||||||
get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"),
|
get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"),
|
||||||
get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"),
|
get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"),
|
||||||
get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselFLabel"),
|
get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselFLabel"),
|
||||||
|
get_node("../UI/LabMenu/VesselGLabel"),
|
||||||
]
|
]
|
||||||
@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")
|
||||||
|
|
@ -67,6 +69,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
get_node("../UI/HangarMenu/AvailableGasBomber"),
|
get_node("../UI/HangarMenu/AvailableGasBomber"),
|
||||||
get_node("../UI/HangarMenu/AvailableCarrier"),
|
get_node("../UI/HangarMenu/AvailableCarrier"),
|
||||||
get_node("../UI/HangarMenu/AvailableLaser"),
|
get_node("../UI/HangarMenu/AvailableLaser"),
|
||||||
|
get_node("../UI/HangarMenu/AvailableDistorder"),
|
||||||
]
|
]
|
||||||
@onready var hangar_available_labels: Array = [
|
@onready var hangar_available_labels: Array = [
|
||||||
get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
|
get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
|
||||||
|
|
@ -74,6 +77,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
get_node("../UI/HangarMenu/AvailableGasBomberLabel"),
|
get_node("../UI/HangarMenu/AvailableGasBomberLabel"),
|
||||||
get_node("../UI/HangarMenu/AvailableCarrierLabel"),
|
get_node("../UI/HangarMenu/AvailableCarrierLabel"),
|
||||||
get_node("../UI/HangarMenu/AvailableLaserLabel"),
|
get_node("../UI/HangarMenu/AvailableLaserLabel"),
|
||||||
|
get_node("../UI/HangarMenu/AvailableDistorderLabel"),
|
||||||
]
|
]
|
||||||
@onready var hangar_fleet_slots: Array = [
|
@onready var hangar_fleet_slots: Array = [
|
||||||
get_node("../UI/HangarMenu/FleetInterceptor"),
|
get_node("../UI/HangarMenu/FleetInterceptor"),
|
||||||
|
|
@ -81,6 +85,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
get_node("../UI/HangarMenu/FleetGasBomber"),
|
get_node("../UI/HangarMenu/FleetGasBomber"),
|
||||||
get_node("../UI/HangarMenu/FleetCarrier"),
|
get_node("../UI/HangarMenu/FleetCarrier"),
|
||||||
get_node("../UI/HangarMenu/FleetLaser"),
|
get_node("../UI/HangarMenu/FleetLaser"),
|
||||||
|
get_node("../UI/HangarMenu/FleetDistorder"),
|
||||||
]
|
]
|
||||||
@onready var hangar_fleet_labels: Array = [
|
@onready var hangar_fleet_labels: Array = [
|
||||||
get_node("../UI/HangarMenu/FleetInterceptorLabel"),
|
get_node("../UI/HangarMenu/FleetInterceptorLabel"),
|
||||||
|
|
@ -88,6 +93,7 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
get_node("../UI/HangarMenu/FleetGasBomberLabel"),
|
get_node("../UI/HangarMenu/FleetGasBomberLabel"),
|
||||||
get_node("../UI/HangarMenu/FleetCarrierLabel"),
|
get_node("../UI/HangarMenu/FleetCarrierLabel"),
|
||||||
get_node("../UI/HangarMenu/FleetLaserLabel"),
|
get_node("../UI/HangarMenu/FleetLaserLabel"),
|
||||||
|
get_node("../UI/HangarMenu/FleetDistorderLabel"),
|
||||||
]
|
]
|
||||||
var build_menu_entries: Array = []
|
var build_menu_entries: Array = []
|
||||||
|
|
||||||
|
|
@ -117,13 +123,15 @@ var lab_building_id: int = 0
|
||||||
@onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel")
|
@onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel")
|
||||||
@onready var craft_laser_button: IconButton = get_node("../UI/CraftMenu/LaserButton")
|
@onready var craft_laser_button: IconButton = get_node("../UI/CraftMenu/LaserButton")
|
||||||
@onready var craft_laser_label: Label = get_node("../UI/CraftMenu/LaserLabel")
|
@onready var craft_laser_label: Label = get_node("../UI/CraftMenu/LaserLabel")
|
||||||
|
@onready var craft_distorder_button: IconButton = get_node("../UI/CraftMenu/DistorderButton")
|
||||||
|
@onready var craft_distorder_label: Label = get_node("../UI/CraftMenu/DistorderLabel")
|
||||||
@onready var craft_vessel_buttons: Array = [
|
@onready var craft_vessel_buttons: Array = [
|
||||||
craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button,
|
craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button,
|
||||||
craft_laser_button,
|
craft_laser_button, craft_distorder_button,
|
||||||
]
|
]
|
||||||
@onready var craft_vessel_labels: Array = [
|
@onready var craft_vessel_labels: Array = [
|
||||||
craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label,
|
craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label,
|
||||||
craft_laser_label,
|
craft_laser_label, craft_distorder_label,
|
||||||
]
|
]
|
||||||
|
|
||||||
# --- the transfer panel (#24) ---------------------------------------------
|
# --- the transfer panel (#24) ---------------------------------------------
|
||||||
|
|
@ -672,6 +680,13 @@ func _confirm_build() -> void:
|
||||||
if not GameState.is_planet_conquered(planet.planet_id):
|
if not GameState.is_planet_conquered(planet.planet_id):
|
||||||
build_status.text = "%s is not captured yet" % planet.planet_name
|
build_status.text = "%s is not captured yet" % planet.planet_name
|
||||||
return
|
return
|
||||||
|
if String(planet.resource_type) == "":
|
||||||
|
# #63: a research_lab planet's whole point is that it yields
|
||||||
|
# nothing to mine - an extractor there would sit on an empty
|
||||||
|
# resource id with no ResourceDefinition, breaking its own
|
||||||
|
# glyph/HUD rendering the moment it produced anything.
|
||||||
|
build_status.text = "%s has nothing to extract" % planet.planet_name
|
||||||
|
return
|
||||||
if not GameState.spend(definition.cost):
|
if not GameState.spend(definition.cost):
|
||||||
build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks]
|
build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks]
|
||||||
return
|
return
|
||||||
|
|
@ -715,14 +730,18 @@ func _refresh_skill_menu(id: String = "") -> void:
|
||||||
# leaving the panel blank, which reads as broken.
|
# leaving the panel blank, which reads as broken.
|
||||||
skill_detail.text = "Select a node to see what it unlocks."
|
skill_detail.text = "Select a node to see what it unlocks."
|
||||||
skill_cost.amount = 0
|
skill_cost.amount = 0
|
||||||
skill_status.text = "Fleet capacity %d" % GameState.max_fleet_size()
|
skill_status.text = "Fleet capacity %d · Research points %d" \
|
||||||
|
% [GameState.max_fleet_size(), GameState.research_points]
|
||||||
skill_unlock_button.visible = false
|
skill_unlock_button.visible = false
|
||||||
return
|
return
|
||||||
|
|
||||||
var node: Dictionary = GameState.skill_node(id)
|
var node: Dictionary = GameState.skill_node(id)
|
||||||
if node.is_empty():
|
if node.is_empty():
|
||||||
return
|
return
|
||||||
|
var points_needed: int = GameState.skill_research_point_cost(id)
|
||||||
skill_detail.text = "%s\n%s" % [node.name, node.desc]
|
skill_detail.text = "%s\n%s" % [node.name, node.desc]
|
||||||
|
if points_needed > 0:
|
||||||
|
skill_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points_needed]
|
||||||
skill_cost.amount = int(node.cost)
|
skill_cost.amount = int(node.cost)
|
||||||
|
|
||||||
if GameState.is_unlocked(id):
|
if GameState.is_unlocked(id):
|
||||||
|
|
@ -734,6 +753,12 @@ func _refresh_skill_menu(id: String = "") -> void:
|
||||||
elif not GameState.can_afford(int(node.cost)):
|
elif not GameState.can_afford(int(node.cost)):
|
||||||
skill_status.text = "Not enough rocks - you have %d" % GameState.rocks
|
skill_status.text = "Not enough rocks - you have %d" % GameState.rocks
|
||||||
skill_unlock_button.visible = false
|
skill_unlock_button.visible = false
|
||||||
|
elif GameState.research_points < points_needed:
|
||||||
|
# #63: a distinct reason from the ore one above - conflating them
|
||||||
|
# would leave the player trying to fix the wrong shortage.
|
||||||
|
skill_status.text = "Not enough research points - you have %d, need %d" \
|
||||||
|
% [GameState.research_points, points_needed]
|
||||||
|
skill_unlock_button.visible = false
|
||||||
else:
|
else:
|
||||||
skill_status.text = ""
|
skill_status.text = ""
|
||||||
skill_unlock_button.visible = true
|
skill_unlock_button.visible = true
|
||||||
|
|
@ -894,7 +919,8 @@ func _refresh_lab() -> void:
|
||||||
lab_vessel_labels[i].text = definition.display_name
|
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_name(lab_vessel)
|
lab_vessel_label.text = "%s research · %d RP" % [
|
||||||
|
GameState.vessel_name(lab_vessel), GameState.research_points]
|
||||||
|
|
||||||
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()):
|
||||||
|
|
@ -906,6 +932,9 @@ func _refresh_lab() -> void:
|
||||||
var option: Dictionary = options[i]
|
var option: Dictionary = options[i]
|
||||||
var price_text: String = _format_costs(option.costs if bool(option.unlock) \
|
var price_text: String = _format_costs(option.costs if bool(option.unlock) \
|
||||||
else GameState.research_costs(option.id))
|
else GameState.research_costs(option.id))
|
||||||
|
var point_cost: int = GameState.research_point_cost(String(option.id))
|
||||||
|
if point_cost > 0:
|
||||||
|
price_text += " · %d RP" % point_cost
|
||||||
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" % [
|
||||||
|
|
@ -924,11 +953,22 @@ func _refresh_lab() -> void:
|
||||||
lab_choice = clampi(lab_choice, 0, options.size() - 1)
|
lab_choice = clampi(lab_choice, 0, options.size() - 1)
|
||||||
|
|
||||||
var choice: Dictionary = options[lab_choice]
|
var choice: Dictionary = options[lab_choice]
|
||||||
|
var choice_points: int = GameState.research_point_cost(String(choice.id))
|
||||||
lab_detail.text = "%s\n%s" % [choice.name, choice.desc]
|
lab_detail.text = "%s\n%s" % [choice.name, choice.desc]
|
||||||
|
if choice_points > 0:
|
||||||
|
lab_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, choice_points]
|
||||||
var queued: Dictionary = GameState.lab_research_order(lab_building_id)
|
var queued: Dictionary = GameState.lab_research_order(lab_building_id)
|
||||||
lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id))
|
lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id))
|
||||||
if not queued.is_empty():
|
if not queued.is_empty():
|
||||||
lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))]
|
var queued_points: int = GameState.research_point_cost(String(queued.id))
|
||||||
|
if not bool(queued.started) and GameState.research_points < queued_points:
|
||||||
|
# #63: distinct from "Waiting for resources" - flying more ore/gas
|
||||||
|
# to this lab cannot fix a research-point shortfall, only
|
||||||
|
# conquering a research lab elsewhere can.
|
||||||
|
lab_status.text = "Need %d research point%s (have %d)" \
|
||||||
|
% [queued_points, "" if queued_points == 1 else "s", GameState.research_points]
|
||||||
|
else:
|
||||||
|
lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))]
|
||||||
lab_buy_button.queue_redraw()
|
lab_buy_button.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1003,7 +1043,7 @@ func _transfer_place_name() -> String:
|
||||||
var b: Dictionary = GameState.find_building(transfer_building_id)
|
var b: Dictionary = GameState.find_building(transfer_building_id)
|
||||||
if b.is_empty():
|
if b.is_empty():
|
||||||
return "this building"
|
return "this building"
|
||||||
if b.type == GameState.BUILDING_MOTHERSHIP:
|
if GameState.building_has_capability(b, "bank"):
|
||||||
# Worth naming as the bank rather than as a structure: this is the
|
# Worth naming as the bank rather than as a structure: this is the
|
||||||
# one building where unloading makes the ore spendable everywhere.
|
# one building where unloading makes the ore spendable everywhere.
|
||||||
return "the bank"
|
return "the bank"
|
||||||
|
|
@ -1016,7 +1056,7 @@ func _refresh_transfer() -> void:
|
||||||
return
|
return
|
||||||
var building: Dictionary = GameState.find_building(transfer_building_id)
|
var building: Dictionary = GameState.find_building(transfer_building_id)
|
||||||
var heading: String = "This building"
|
var heading: String = "This building"
|
||||||
if not building.is_empty() and building.type == GameState.BUILDING_MOTHERSHIP:
|
if GameState.building_has_capability(building, "bank"):
|
||||||
heading = "Mothership bank"
|
heading = "Mothership bank"
|
||||||
transfer_list.set_destination(transfer_building_id, heading)
|
transfer_list.set_destination(transfer_building_id, heading)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ extends Resource
|
||||||
@export var menu_icon: int
|
@export var menu_icon: int
|
||||||
@export var visual_scene: PackedScene
|
@export var visual_scene: PackedScene
|
||||||
@export var capabilities: PackedStringArray = []
|
@export var capabilities: PackedStringArray = []
|
||||||
|
@export var logistics: Resource
|
||||||
@export var map_color: Color = Color.WHITE
|
@export var map_color: Color = Color.WHITE
|
||||||
@export var map_sides: int = 4
|
@export var map_sides: int = 4
|
||||||
@export var map_radius: float = 7.0
|
@export var map_radius: float = 7.0
|
||||||
|
|
|
||||||
13
BuildingLogisticsProfile.gd
Normal file
13
BuildingLogisticsProfile.gd
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
class_name BuildingLogisticsProfile
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
# Optional logistics and conversion behavior composed into a building
|
||||||
|
# definition. GameState consumes this data generically, so adding another
|
||||||
|
# converter does not require another resource-type branch in transport code.
|
||||||
|
|
||||||
|
@export var input_buffers: Dictionary = {}
|
||||||
|
@export var collected_outputs: PackedStringArray = []
|
||||||
|
@export var conversion_input: String = ""
|
||||||
|
@export var conversion_output: String = ""
|
||||||
|
@export var conversion_rate: float = 0.0
|
||||||
|
@export var conversion_yield: int = 1
|
||||||
1
BuildingLogisticsProfile.gd.uid
Normal file
1
BuildingLogisticsProfile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cydp18abylrgd
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Reinforced Hold"
|
||||||
description = "+30 cargo capacity per level."
|
description = "+30 cargo capacity per level."
|
||||||
base_cost = 40
|
base_cost = 40
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 21
|
icon = 21
|
||||||
additive_modifiers = {"cargo_capacity": 30.0}
|
additive_modifiers = {"cargo_capacity": 30.0}
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,5 @@ unlock_name = "Unlock Carrier"
|
||||||
unlock_description = "Research a dedicated hauler hull that trades weapons for a much larger cargo hold."
|
unlock_description = "Research a dedicated hauler hull that trades weapons for a much larger cargo hold."
|
||||||
unlock_cost = 0
|
unlock_cost = 0
|
||||||
unlock_resource_costs = {"ore": 60}
|
unlock_resource_costs = {"ore": 60}
|
||||||
|
research_point_cost = 1
|
||||||
unlock_icon = -1
|
unlock_icon = -1
|
||||||
|
|
|
||||||
25
DistorderVesselDefinition.tres
Normal file
25
DistorderVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=3 format=3]
|
||||||
|
[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://VesselDistorderComponent.tscn" id="2"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
definition_id = "distorder"
|
||||||
|
display_name = "Distorder"
|
||||||
|
description = "Unarmed space-time distortion hull. While one flies with the fleet, a fader lets you push cruise speed beyond its default."
|
||||||
|
build_cost = 14
|
||||||
|
build_resource_costs = {"ore": 14, "energy": 8}
|
||||||
|
build_time = 16.0
|
||||||
|
icon = 37
|
||||||
|
glow_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
hull_points = PackedVector2Array(0, -24, -14, -2, -6, 18, 10, 10, 12, -10)
|
||||||
|
fleet_eligible = true
|
||||||
|
capabilities = PackedStringArray("distorder")
|
||||||
|
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||||
|
base_stats = {"max_speed": 430.0, "acceleration": 410.0, "delivery_speed": 180.0}
|
||||||
|
unlock_id = "unlock_distorder"
|
||||||
|
unlock_name = "Unlock Distorder"
|
||||||
|
unlock_description = "Research a space-time distortion hull - while one flies with the fleet, a fader lets you push cruise speed beyond its default."
|
||||||
|
unlock_cost = 0
|
||||||
|
unlock_resource_costs = {"energy": 50}
|
||||||
|
research_point_cost = 1
|
||||||
|
unlock_icon = -1
|
||||||
1
EnemyBigLaserBeam.gd.uid
Normal file
1
EnemyBigLaserBeam.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://coopf1u1lnb8b
|
||||||
1
EnemyBigLaserComponent.gd.uid
Normal file
1
EnemyBigLaserComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://6oj7ia3em4x0
|
||||||
1
EnemySpawnerComponent.gd.uid
Normal file
1
EnemySpawnerComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://eqkp0coeoh4d
|
||||||
1
EnergyResourceGlyph.gd.uid
Normal file
1
EnergyResourceGlyph.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b3b64pvw4y53x
|
||||||
30
Fleet.gd
30
Fleet.gd
|
|
@ -80,7 +80,7 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO,
|
||||||
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
||||||
|
|
||||||
fleet_target = center
|
fleet_target = center
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
|
|
||||||
|
|
||||||
func vessel_types() -> PackedStringArray:
|
func vessel_types() -> PackedStringArray:
|
||||||
|
|
@ -91,6 +91,15 @@ func vessel_types() -> PackedStringArray:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_composition_state() -> void:
|
||||||
|
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||||
|
# Composition changes are authoritative; scene teardown is not. Keeping
|
||||||
|
# this out of SpeedFader prevents an outgoing scene's empty fleet from
|
||||||
|
# resetting a boost while the same vessels are being restored on a surface.
|
||||||
|
if not has_capability("distorder"):
|
||||||
|
GameState.set_fleet_speed_multiplier(1.0)
|
||||||
|
|
||||||
|
|
||||||
func has_vessel_type(vessel_type: String) -> bool:
|
func has_vessel_type(vessel_type: String) -> bool:
|
||||||
for ship in ships:
|
for ship in ships:
|
||||||
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
|
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
|
||||||
|
|
@ -139,7 +148,7 @@ func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
|
||||||
if ship.has_signal("destroyed"):
|
if ship.has_signal("destroyed"):
|
||||||
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
||||||
ship.set_move_target(fleet_target + offset)
|
ship.set_move_target(fleet_target + offset)
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,7 +166,7 @@ func store_ship(index: int) -> String:
|
||||||
formation_offsets.remove_at(index)
|
formation_offsets.remove_at(index)
|
||||||
ship.queue_free()
|
ship.queue_free()
|
||||||
apply_formation()
|
apply_formation()
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
return vessel_type
|
return vessel_type
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -200,18 +209,25 @@ func _process(delta: float) -> void:
|
||||||
if steer_direction == Vector2.ZERO:
|
if steer_direction == Vector2.ZERO:
|
||||||
return
|
return
|
||||||
# The anchor - the fleet's formation centre - travels at cruise_speed.
|
# The anchor - the fleet's formation centre - travels at cruise_speed.
|
||||||
# Ships may exceed it individually to reach their slots.
|
# Ships may exceed it individually to reach their slots. #59: multiplied
|
||||||
anchor += steer_direction * cruise_speed * delta
|
# at the point of use, not baked into cruise_speed itself, so the base
|
||||||
|
# value stays stable regardless of the current boost.
|
||||||
|
anchor += steer_direction * _effective_cruise_speed() * delta
|
||||||
fleet_target = anchor
|
fleet_target = anchor
|
||||||
_assign_slot_targets()
|
_assign_slot_targets()
|
||||||
|
|
||||||
|
|
||||||
|
func _effective_cruise_speed() -> float:
|
||||||
|
return cruise_speed * GameState.fleet_speed_multiplier
|
||||||
|
|
||||||
|
|
||||||
func _assign_slot_targets() -> void:
|
func _assign_slot_targets() -> void:
|
||||||
|
var effective_cruise_speed: float = _effective_cruise_speed()
|
||||||
for i in range(ships.size()):
|
for i in range(ships.size()):
|
||||||
ships[i].set_move_target(fleet_target + formation_offsets[i])
|
ships[i].set_move_target(fleet_target + formation_offsets[i])
|
||||||
# Context, not a command: the ship adds this to its own velocity to
|
# Context, not a command: the ship adds this to its own velocity to
|
||||||
# work out how it is really moving. See Ship.fleet_velocity.
|
# work out how it is really moving. See Ship.fleet_velocity.
|
||||||
ships[i].fleet_velocity = steer_direction * cruise_speed
|
ships[i].fleet_velocity = steer_direction * effective_cruise_speed
|
||||||
|
|
||||||
|
|
||||||
func stop() -> void:
|
func stop() -> void:
|
||||||
|
|
@ -276,7 +292,7 @@ func _on_ship_destroyed(ship: Node) -> void:
|
||||||
if idx != -1:
|
if idx != -1:
|
||||||
ships.remove_at(idx)
|
ships.remove_at(idx)
|
||||||
formation_offsets.remove_at(idx)
|
formation_offsets.remove_at(idx)
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
|
|
||||||
|
|
||||||
func get_centroid() -> Vector2:
|
func get_centroid() -> Vector2:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ extends Node2D
|
||||||
# indirectly - e.g. Main.tscn loaded from inside a _screenshot_tool.gd
|
# indirectly - e.g. Main.tscn loaded from inside a _screenshot_tool.gd
|
||||||
# preview - for a class_name this new. Node + duck-typed calls sidesteps it.
|
# preview - for a class_name this new. Node + duck-typed calls sidesteps it.
|
||||||
@onready var laser_controller: Node = get_node_or_null("LaserController")
|
@onready var laser_controller: Node = get_node_or_null("LaserController")
|
||||||
|
@onready var speed_fader: Node = get_node_or_null("UI/SpeedFader")
|
||||||
|
|
||||||
var touches: Dictionary = {}
|
var touches: Dictionary = {}
|
||||||
var pinch_active: bool = false
|
var pinch_active: bool = false
|
||||||
|
|
@ -263,6 +264,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
|
||||||
return
|
return
|
||||||
if laser_controller != null and laser_controller.handle_touch(event):
|
if laser_controller != null and laser_controller.handle_touch(event):
|
||||||
return
|
return
|
||||||
|
if speed_fader != null and speed_fader.handle_touch(event):
|
||||||
|
return
|
||||||
if _handle_pivot_touch(event):
|
if _handle_pivot_touch(event):
|
||||||
return
|
return
|
||||||
if joystick != null and joystick.visible and joystick.handle_touch(event):
|
if joystick != null and joystick.visible and joystick.handle_touch(event):
|
||||||
|
|
@ -287,6 +290,8 @@ func _handle_drag(event: InputEventScreenDrag) -> void:
|
||||||
return
|
return
|
||||||
if laser_controller != null and laser_controller.handle_drag(event):
|
if laser_controller != null and laser_controller.handle_drag(event):
|
||||||
return
|
return
|
||||||
|
if speed_fader != null and speed_fader.handle_drag(event):
|
||||||
|
return
|
||||||
if joystick != null and joystick.visible and joystick.handle_drag(event):
|
if joystick != null and joystick.visible and joystick.handle_drag(event):
|
||||||
return
|
return
|
||||||
if not touches.has(event.index):
|
if not touches.has(event.index):
|
||||||
|
|
|
||||||
284
GameState.gd
284
GameState.gd
|
|
@ -4,6 +4,7 @@ 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")
|
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
|
||||||
|
const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd")
|
||||||
|
|
||||||
|
|
||||||
# Autoload singleton - the one thing that survives change_scene_to_file()
|
# Autoload singleton - the one thing that survives change_scene_to_file()
|
||||||
|
|
@ -22,6 +23,35 @@ var planet_name: String = ""
|
||||||
var planet_id: String = ""
|
var planet_id: String = ""
|
||||||
var planet_definition_id: String = "rocky"
|
var planet_definition_id: String = "rocky"
|
||||||
|
|
||||||
|
# #59: 1.0 is today's behaviour. Applied at the point of use (Fleet's cruise
|
||||||
|
# anchor, Ship's own max_speed/acceleration) rather than baked into stored
|
||||||
|
# stats, so a speed upgrade researched while boosted can't corrupt the base
|
||||||
|
# value - see Fleet.gd/Ship.gd for the multiply sites.
|
||||||
|
const MAX_FLEET_SPEED_MULTIPLIER: float = 3.0
|
||||||
|
var fleet_speed_multiplier: float = 1.0
|
||||||
|
|
||||||
|
# #62: energy cost of sustaining a boost, scaled by how far above 1.0 the
|
||||||
|
# multiplier is - a small boost costs proportionally less, full boost costs
|
||||||
|
# ENERGY_DRAIN_PER_SECOND. Sitting at exactly 1.0 costs nothing.
|
||||||
|
const ENERGY_DRAIN_PER_SECOND: float = 2.0
|
||||||
|
var _distorder_drain_progress: float = 0.0
|
||||||
|
# One-shot notice, consumed and cleared by SpeedFader.gd: true the instant
|
||||||
|
# a sustained boost is force-reset for running the fleet hold's energy
|
||||||
|
# to zero, so the control (which may not even be part of the current
|
||||||
|
# scene tree at that exact frame) can show why on its next _process().
|
||||||
|
var boost_depleted_notice: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func set_fleet_speed_multiplier(value: float) -> void:
|
||||||
|
fleet_speed_multiplier = clampf(value, 1.0, MAX_FLEET_SPEED_MULTIPLIER)
|
||||||
|
|
||||||
|
|
||||||
|
# #63: currency gated behind conquering a research_lab planet - required by
|
||||||
|
# both progression paths (skill tree nodes, vessel/upgrade research) on top
|
||||||
|
# of their existing ore/resource costs.
|
||||||
|
signal research_points_changed(total: int)
|
||||||
|
var research_points: int = 0
|
||||||
|
|
||||||
# Explored-chunk history for the world map's fog-of-war reveal. Lives
|
# Explored-chunk history for the world map's fog-of-war reveal. Lives
|
||||||
# here (not on ExplorationTracker, which gets destroyed and recreated on
|
# here (not on ExplorationTracker, which gets destroyed and recreated on
|
||||||
# every scene swap) so exploration progress isn't lost every time the
|
# every scene swap) so exploration progress isn't lost every time the
|
||||||
|
|
@ -93,6 +123,26 @@ func fleet_resource(resource_type: String) -> int:
|
||||||
return fleet_inventory.amount(resource_type)
|
return fleet_inventory.amount(resource_type)
|
||||||
|
|
||||||
|
|
||||||
|
# #60: consumed outright (ammunition), not moved to another inventory like
|
||||||
|
# transfer_resource_to_building/_fleet - returns the amount actually taken.
|
||||||
|
func withdraw_fleet_resource(resource_type: String, amount: int) -> int:
|
||||||
|
return fleet_inventory.withdraw(resource_type, amount)
|
||||||
|
|
||||||
|
|
||||||
|
# #61: generic counterparts, for resources (energy) with no dedicated
|
||||||
|
# fleet_cargo/fleet_gas_cargo-style property of their own. Deliberately NOT
|
||||||
|
# the same as deposit_resource_to_building() on a "bank" building - that
|
||||||
|
# deposits into the building's OWN per-record inventory, not bank_inventory
|
||||||
|
# itself (only building_resource()/building_inventory() special-case "bank"
|
||||||
|
# capability to redirect there).
|
||||||
|
func deposit_fleet_resource(resource_type: String, amount: int) -> int:
|
||||||
|
return fleet_inventory.deposit(resource_type, amount)
|
||||||
|
|
||||||
|
|
||||||
|
func deposit_bank_resource(resource_type: String, amount: int) -> int:
|
||||||
|
return bank_inventory.deposit(resource_type, amount)
|
||||||
|
|
||||||
|
|
||||||
# #56: total across every resource type in the hold, and the combined cap
|
# #56: total across every resource type in the hold, and the combined cap
|
||||||
# that limits it - a HUD readout wants both, not any one resource's amount.
|
# that limits it - a HUD readout wants both, not any one resource's amount.
|
||||||
func fleet_cargo_used() -> int:
|
func fleet_cargo_used() -> int:
|
||||||
|
|
@ -230,6 +280,13 @@ var buildings: Array = []
|
||||||
# tree always reads root-at-the-bottom and cannot drift out of step with the
|
# tree always reads root-at-the-bottom and cannot drift out of step with the
|
||||||
# dependencies when nodes are added. `requires` is a single prerequisite id
|
# dependencies when nodes are added. `requires` is a single prerequisite id
|
||||||
# ("" for a root).
|
# ("" for a root).
|
||||||
|
# #63: research_points (default 0 where omitted) is a SEPARATE currency from
|
||||||
|
# cost (ore), spent only from GameState.research_points - earned by
|
||||||
|
# conquering a research_lab planet, never bought. Balance, decided
|
||||||
|
# deliberately: shipyards and logistics stay at 0 so a fresh game can still
|
||||||
|
# reach a ship crafter and an extractor with zero research points (see the
|
||||||
|
# closing comment on #63 for the reasoning); every other node - none of them
|
||||||
|
# required for that opening - costs at least 1.
|
||||||
const SKILL_TREE: Array = [
|
const SKILL_TREE: Array = [
|
||||||
{
|
{
|
||||||
"id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "",
|
"id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "",
|
||||||
|
|
@ -243,42 +300,42 @@ const SKILL_TREE: Array = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards",
|
"id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards",
|
||||||
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER,
|
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "research_points": 1,
|
||||||
"desc": "Build command centers, where the fleet formation is arranged.",
|
"desc": "Build command centers, where the fleet formation is arranged.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
||||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
|
||||||
"desc": "Build transport hubs. Each hub includes its own automatic transporters.",
|
"desc": "Build transport hubs. Each hub includes its own automatic transporters.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
|
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
|
||||||
"x": 0.82, "unlocks": BUILDING_SHIP_LAB,
|
"x": 0.82, "unlocks": BUILDING_SHIP_LAB, "research_points": 1,
|
||||||
"desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.",
|
"desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
|
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
|
||||||
"x": 0.18, "hub_bonus": 1,
|
"x": 0.18, "hub_bonus": 1, "research_points": 2,
|
||||||
"desc": "Permanent: +1 transporter stationed at every hub, including ones already built.",
|
"desc": "Permanent: +1 transporter stationed at every hub, including ones already built.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight",
|
"id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight",
|
||||||
"x": 0.50, "unlocks": BUILDING_GAS_BURNER,
|
"x": 0.50, "unlocks": BUILDING_GAS_BURNER, "research_points": 2,
|
||||||
"desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.",
|
"desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
|
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
|
||||||
"x": 0.82, "fleet_bonus": 1,
|
"x": 0.82, "fleet_bonus": 1, "research_points": 1,
|
||||||
"desc": "Permanent: +1 vessel in the fleet.",
|
"desc": "Permanent: +1 vessel in the fleet.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing",
|
"id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing",
|
||||||
"x": 0.50, "fleet_bonus": 1,
|
"x": 0.50, "fleet_bonus": 1, "research_points": 2,
|
||||||
"desc": "Permanent: +1 vessel in the fleet.",
|
"desc": "Permanent: +1 vessel in the fleet.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron",
|
"id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron",
|
||||||
"x": 0.50, "fleet_bonus": 1,
|
"x": 0.50, "fleet_bonus": 1, "research_points": 3,
|
||||||
"desc": "Permanent: +1 vessel in the fleet.",
|
"desc": "Permanent: +1 vessel in the fleet.",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -328,16 +385,27 @@ func prerequisite_met(id: String) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func can_unlock(id: String) -> bool:
|
func can_unlock(id: String) -> bool:
|
||||||
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost))
|
var node: Dictionary = skill_node(id)
|
||||||
|
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(node.cost)) \
|
||||||
|
and research_points >= skill_research_point_cost(id)
|
||||||
|
|
||||||
|
|
||||||
|
func skill_research_point_cost(id: String) -> int:
|
||||||
|
return int(skill_node(id).get("research_points", 0))
|
||||||
|
|
||||||
|
|
||||||
func unlock(id: String) -> bool:
|
func unlock(id: String) -> bool:
|
||||||
if not can_unlock(id):
|
if not can_unlock(id):
|
||||||
return false
|
return false
|
||||||
if not spend(int(skill_node(id).cost)):
|
var node: Dictionary = skill_node(id)
|
||||||
|
if not spend(int(node.cost)):
|
||||||
return false
|
return false
|
||||||
|
var points_needed: int = skill_research_point_cost(id)
|
||||||
|
if points_needed > 0:
|
||||||
|
research_points -= points_needed
|
||||||
|
research_points_changed.emit(research_points)
|
||||||
unlocked.append(id)
|
unlocked.append(id)
|
||||||
if int(skill_node(id).get("hub_bonus", 0)) > 0:
|
if int(node.get("hub_bonus", 0)) > 0:
|
||||||
_sync_hub_transporters()
|
_sync_hub_transporters()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
@ -381,6 +449,10 @@ func reset_new_game() -> void:
|
||||||
planet_name = ""
|
planet_name = ""
|
||||||
planet_id = ""
|
planet_id = ""
|
||||||
planet_definition_id = "rocky"
|
planet_definition_id = "rocky"
|
||||||
|
fleet_speed_multiplier = 1.0
|
||||||
|
_distorder_drain_progress = 0.0
|
||||||
|
boost_depleted_notice = false
|
||||||
|
research_points = 0
|
||||||
explored_chunks.clear()
|
explored_chunks.clear()
|
||||||
bank_inventory = INVENTORY.new({RESOURCE_ORE: 500})
|
bank_inventory = INVENTORY.new({RESOURCE_ORE: 500})
|
||||||
fleet_inventory = INVENTORY.new()
|
fleet_inventory = INVENTORY.new()
|
||||||
|
|
@ -454,6 +526,13 @@ func building_has_capability(building: Dictionary, capability: String) -> bool:
|
||||||
return definition != null and capability in definition.capabilities
|
return definition != null and capability in definition.capabilities
|
||||||
|
|
||||||
|
|
||||||
|
func building_logistics_profile(building: Dictionary) -> Resource:
|
||||||
|
if building.is_empty():
|
||||||
|
return null
|
||||||
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(building.type))
|
||||||
|
return null if definition == null else definition.logistics
|
||||||
|
|
||||||
|
|
||||||
# --- vessel types and their upgrades (#5) ---------------------------------
|
# --- vessel types and their upgrades (#5) ---------------------------------
|
||||||
|
|
||||||
# Combat vessels can join the player fleet. Transporter metadata lives in the
|
# Combat vessels can join the player fleet. Transporter metadata lives in the
|
||||||
|
|
@ -465,6 +544,7 @@ const VESSEL_GAS_BOMBER: String = "gas_bomber"
|
||||||
const VESSEL_TRANSPORTER: String = "transporter"
|
const VESSEL_TRANSPORTER: String = "transporter"
|
||||||
const VESSEL_CARRIER: String = "carrier"
|
const VESSEL_CARRIER: String = "carrier"
|
||||||
const VESSEL_LASER: String = "laser"
|
const VESSEL_LASER: String = "laser"
|
||||||
|
const VESSEL_DISTORDER: String = "distorder"
|
||||||
|
|
||||||
# Compatibility constants for callers and old tests. Runtime metadata is
|
# Compatibility constants for callers and old tests. Runtime metadata is
|
||||||
# resolved from VesselDefinition below.
|
# resolved from VesselDefinition below.
|
||||||
|
|
@ -556,10 +636,14 @@ func unlock_vessel(vessel: String) -> bool:
|
||||||
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
|
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
|
||||||
return false
|
return false
|
||||||
var costs: Dictionary = research_costs(definition.unlock_id)
|
var costs: Dictionary = research_costs(definition.unlock_id)
|
||||||
if not can_afford_resources(costs):
|
var points_needed: int = int(definition.research_point_cost)
|
||||||
|
if not can_afford_resources(costs) or research_points < points_needed:
|
||||||
return false
|
return false
|
||||||
for resource_type in costs:
|
for resource_type in costs:
|
||||||
bank_inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
bank_inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
||||||
|
if points_needed > 0:
|
||||||
|
research_points -= points_needed
|
||||||
|
research_points_changed.emit(research_points)
|
||||||
vessel_levels[definition.unlock_id] = 1
|
vessel_levels[definition.unlock_id] = 1
|
||||||
vessel_upgraded.emit(definition.unlock_id)
|
vessel_upgraded.emit(definition.unlock_id)
|
||||||
return true
|
return true
|
||||||
|
|
@ -606,7 +690,8 @@ func upgrade_cost(id: String) -> int:
|
||||||
|
|
||||||
|
|
||||||
func can_buy_upgrade(id: String) -> bool:
|
func can_buy_upgrade(id: String) -> bool:
|
||||||
return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id))
|
return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) \
|
||||||
|
and research_points >= research_point_cost(id)
|
||||||
|
|
||||||
|
|
||||||
func buy_upgrade(id: String) -> bool:
|
func buy_upgrade(id: String) -> bool:
|
||||||
|
|
@ -614,6 +699,10 @@ func buy_upgrade(id: String) -> bool:
|
||||||
return false
|
return false
|
||||||
if not spend(upgrade_cost(id)):
|
if not spend(upgrade_cost(id)):
|
||||||
return false
|
return false
|
||||||
|
var points_needed: int = research_point_cost(id)
|
||||||
|
if points_needed > 0:
|
||||||
|
research_points -= points_needed
|
||||||
|
research_points_changed.emit(research_points)
|
||||||
vessel_levels[id] = upgrade_level(id) + 1
|
vessel_levels[id] = upgrade_level(id) + 1
|
||||||
vessel_upgraded.emit(id)
|
vessel_upgraded.emit(id)
|
||||||
return true
|
return true
|
||||||
|
|
@ -876,6 +965,14 @@ func research_cost(id: String) -> int:
|
||||||
return int(research_costs(id).get(RESOURCE_ORE, 0))
|
return int(research_costs(id).get(RESOURCE_ORE, 0))
|
||||||
|
|
||||||
|
|
||||||
|
func research_point_cost(id: String) -> int:
|
||||||
|
var unlock_definition: Resource = vessel_for_unlock(id)
|
||||||
|
if unlock_definition != null:
|
||||||
|
return int(unlock_definition.research_point_cost)
|
||||||
|
var d: Dictionary = upgrade_def(id)
|
||||||
|
return int(d.resource.research_point_cost) if not d.is_empty() else 0
|
||||||
|
|
||||||
|
|
||||||
func research_costs(id: String) -> Dictionary:
|
func research_costs(id: String) -> Dictionary:
|
||||||
var unlock_definition: Resource = vessel_for_unlock(id)
|
var unlock_definition: Resource = vessel_for_unlock(id)
|
||||||
if unlock_definition != null:
|
if unlock_definition != null:
|
||||||
|
|
@ -904,10 +1001,20 @@ func _advance_lab_research(delta: float) -> void:
|
||||||
if inventory.amount(String(resource_type)) < int(costs[resource_type]):
|
if inventory.amount(String(resource_type)) < int(costs[resource_type]):
|
||||||
affordable = false
|
affordable = false
|
||||||
break
|
break
|
||||||
|
# #63: research points are a fleet-wide currency, not delivered
|
||||||
|
# to the lab like ore/gas - checked and spent here too so a
|
||||||
|
# shortfall stalls the order the same way an undelivered
|
||||||
|
# resource does, rather than letting it start regardless.
|
||||||
|
var points_needed: int = research_point_cost(id)
|
||||||
|
if points_needed > research_points:
|
||||||
|
affordable = false
|
||||||
if not affordable:
|
if not affordable:
|
||||||
continue
|
continue
|
||||||
for resource_type in costs:
|
for resource_type in costs:
|
||||||
inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
||||||
|
if points_needed > 0:
|
||||||
|
research_points -= points_needed
|
||||||
|
research_points_changed.emit(research_points)
|
||||||
order.started = true
|
order.started = true
|
||||||
order.elapsed = float(order.elapsed) + delta
|
order.elapsed = float(order.elapsed) + delta
|
||||||
if float(order.elapsed) < lab_research_time():
|
if float(order.elapsed) < lab_research_time():
|
||||||
|
|
@ -950,19 +1057,20 @@ func has_building_of_type(type: String) -> bool:
|
||||||
|
|
||||||
func remove_buildings_of_type(type: String) -> int:
|
func remove_buildings_of_type(type: String) -> int:
|
||||||
var kept: Array = []
|
var kept: Array = []
|
||||||
var removed_ids: Array = []
|
var removed_hub_ids: Array = []
|
||||||
var removed: int = 0
|
var removed: int = 0
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
if b.type == type:
|
if b.type == type:
|
||||||
removed += 1
|
removed += 1
|
||||||
removed_ids.append(int(b.id))
|
if building_has_capability(b, "transport_hub"):
|
||||||
|
removed_hub_ids.append(int(b.id))
|
||||||
else:
|
else:
|
||||||
kept.append(b)
|
kept.append(b)
|
||||||
buildings = kept
|
buildings = kept
|
||||||
if type == BUILDING_TRANSPORT_HUB and not removed_ids.is_empty():
|
if not removed_hub_ids.is_empty():
|
||||||
var kept_transporters: Array = []
|
var kept_transporters: Array = []
|
||||||
for transporter in transporters:
|
for transporter in transporters:
|
||||||
if int(transporter.get("hub_id", 0)) not in removed_ids:
|
if int(transporter.get("hub_id", 0)) not in removed_hub_ids:
|
||||||
kept_transporters.append(transporter)
|
kept_transporters.append(transporter)
|
||||||
transporters = kept_transporters
|
transporters = kept_transporters
|
||||||
return removed
|
return removed
|
||||||
|
|
@ -1092,44 +1200,76 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted:
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
# --- gas burners (#54) -----------------------------------------------------
|
# --- building logistics and resource conversion ---------------------------
|
||||||
#
|
|
||||||
# Converts gas into energy at the building's own site, simulated here
|
|
||||||
# alongside extractor mining and production so it keeps running whether or
|
|
||||||
# not the player is nearby. Deliberately withdraws only from the building's
|
|
||||||
# OWN inventory, never the bank - a burner with no local gas simply stalls,
|
|
||||||
# exactly like a lab or crafter waiting on materials, rather than conjuring
|
|
||||||
# energy for free.
|
|
||||||
|
|
||||||
const GAS_BURN_RATE: float = 1.0 # gas/second
|
func building_logistics_deficit(id: int, resource_type: String) -> int:
|
||||||
const ENERGY_PER_GAS: int = 2
|
var building: Dictionary = find_building(id)
|
||||||
# Local gas reserve a burner tries to keep topped up. Transporters treat
|
var profile: Resource = building_logistics_profile(building)
|
||||||
# falling short of this the same way they treat a queued production/research
|
if profile == null:
|
||||||
# order (see _dispatch_transporters()'s gas_burner_gas_deficit() use).
|
|
||||||
const GAS_BURNER_BUFFER: int = 40
|
|
||||||
|
|
||||||
|
|
||||||
func gas_burner_gas_deficit(id: int) -> int:
|
|
||||||
var b: Dictionary = find_building(id)
|
|
||||||
if not building_has_capability(b, "gas_burner"):
|
|
||||||
return 0
|
return 0
|
||||||
return maxi(0, GAS_BURNER_BUFFER - building_resource(id, RESOURCE_GAS))
|
var target: int = int(profile.input_buffers.get(resource_type, 0))
|
||||||
|
return maxi(0, target - building_resource(id, resource_type))
|
||||||
|
|
||||||
|
|
||||||
func _advance_gas_burners(delta: float) -> void:
|
func building_requested_resource(id: int, resource_type: String) -> int:
|
||||||
for b in buildings:
|
return maxi(outstanding_resource_cost(id, resource_type),
|
||||||
if not building_has_capability(b, "gas_burner"):
|
building_logistics_deficit(id, resource_type))
|
||||||
|
|
||||||
|
|
||||||
|
func _collectable_building_resource(building: Dictionary, minimum_amount: int) -> String:
|
||||||
|
var profile: Resource = building_logistics_profile(building)
|
||||||
|
if profile == null:
|
||||||
|
return ""
|
||||||
|
for resource_type in profile.collected_outputs:
|
||||||
|
if building_resource(int(building.id), resource_type) >= minimum_amount:
|
||||||
|
return resource_type
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _advance_resource_converters(delta: float) -> void:
|
||||||
|
for building in buildings:
|
||||||
|
var profile: Resource = building_logistics_profile(building)
|
||||||
|
if profile == null or profile.conversion_input == "" \
|
||||||
|
or profile.conversion_output == "" or profile.conversion_rate <= 0.0 \
|
||||||
|
or profile.conversion_yield <= 0:
|
||||||
continue
|
continue
|
||||||
var inventory: RefCounted = _inventory_for_record(b)
|
var inventory: RefCounted = _inventory_for_record(building)
|
||||||
if inventory.amount(RESOURCE_GAS) <= 0:
|
if inventory.amount(profile.conversion_input) <= 0:
|
||||||
continue # stalled - no local gas to burn
|
continue
|
||||||
var progress: float = float(b.get("gas_burn_progress", 0.0)) + delta * GAS_BURN_RATE
|
var progress: float = float(building.get("conversion_progress", 0.0)) \
|
||||||
var whole: int = mini(int(floor(progress)), inventory.amount(RESOURCE_GAS))
|
+ delta * profile.conversion_rate
|
||||||
|
var whole: int = mini(int(floor(progress)), inventory.amount(profile.conversion_input))
|
||||||
if whole > 0:
|
if whole > 0:
|
||||||
inventory.withdraw(RESOURCE_GAS, whole)
|
inventory.withdraw(profile.conversion_input, whole)
|
||||||
inventory.deposit(RESOURCE_ENERGY, whole * ENERGY_PER_GAS)
|
inventory.deposit(profile.conversion_output, whole * profile.conversion_yield)
|
||||||
progress -= whole
|
progress -= whole
|
||||||
b.gas_burn_progress = progress
|
building.conversion_progress = progress
|
||||||
|
|
||||||
|
|
||||||
|
func _advance_distorder_drain(delta: float) -> void:
|
||||||
|
if fleet_speed_multiplier <= 1.0:
|
||||||
|
# Nothing owed at 1.0x - and no stale fractional progress should
|
||||||
|
# carry over into a LATER, unrelated boost.
|
||||||
|
_distorder_drain_progress = 0.0
|
||||||
|
return
|
||||||
|
var rate: float = (fleet_speed_multiplier - 1.0) / (MAX_FLEET_SPEED_MULTIPLIER - 1.0) \
|
||||||
|
* ENERGY_DRAIN_PER_SECOND
|
||||||
|
_distorder_drain_progress += rate * delta
|
||||||
|
var whole: int = int(floor(_distorder_drain_progress))
|
||||||
|
if whole <= 0:
|
||||||
|
return
|
||||||
|
_distorder_drain_progress -= whole
|
||||||
|
var available: int = fleet_inventory.amount(RESOURCE_ENERGY)
|
||||||
|
if available >= whole:
|
||||||
|
fleet_inventory.withdraw(RESOURCE_ENERGY, whole)
|
||||||
|
return
|
||||||
|
# Ran out mid-drain: take whatever is left (down to exactly zero) and
|
||||||
|
# force the boost back to its default, rather than let the fleet keep
|
||||||
|
# travelling fast on credit.
|
||||||
|
fleet_inventory.withdraw(RESOURCE_ENERGY, available)
|
||||||
|
fleet_speed_multiplier = 1.0
|
||||||
|
_distorder_drain_progress = 0.0
|
||||||
|
boost_depleted_notice = true
|
||||||
|
|
||||||
|
|
||||||
# Planet conquest: a planet must have every one of its capture points held
|
# Planet conquest: a planet must have every one of its capture points held
|
||||||
|
|
@ -1142,7 +1282,7 @@ var planet_capture: Dictionary = {}
|
||||||
signal planet_conquered(planet_id: String)
|
signal planet_conquered(planet_id: String)
|
||||||
|
|
||||||
|
|
||||||
func register_capture_points(planet_key: String, total: int) -> void:
|
func register_capture_points(planet_key: String, total: int, definition_id: String = "") -> void:
|
||||||
# Idempotent: landing again on a planet must not wipe points already
|
# Idempotent: landing again on a planet must not wipe points already
|
||||||
# captured on a previous visit.
|
# captured on a previous visit.
|
||||||
if planet_capture.has(planet_key):
|
if planet_capture.has(planet_key):
|
||||||
|
|
@ -1153,8 +1293,13 @@ func register_capture_points(planet_key: String, total: int) -> void:
|
||||||
# A planet demanding zero points is vacuously already held (#55: a
|
# A planet demanding zero points is vacuously already held (#55: a
|
||||||
# black hole's "void" profile has no authored PlanetSetup at all, so
|
# black hole's "void" profile has no authored PlanetSetup at all, so
|
||||||
# this is the only registration it ever gets) - every planet WITH real
|
# this is the only registration it ever gets) - every planet WITH real
|
||||||
# points still starts unconquered, exactly as before.
|
# points still starts unconquered, exactly as before. The planet definition
|
||||||
planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0}
|
# supplies an optional conquest reward, avoiding planet-id special cases in
|
||||||
|
# the capture system.
|
||||||
|
var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id)
|
||||||
|
var point_reward: int = 0 if definition == null else int(definition.conquest_research_points)
|
||||||
|
planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0,
|
||||||
|
"research_point_reward": point_reward, "research_awarded": false}
|
||||||
|
|
||||||
|
|
||||||
func capture_state(planet_key: String) -> Dictionary:
|
func capture_state(planet_key: String) -> Dictionary:
|
||||||
|
|
@ -1181,6 +1326,16 @@ func mark_point_captured(planet_key: String, index: int) -> void:
|
||||||
if captured.all(func(c): return bool(c)):
|
if captured.all(func(c): return bool(c)):
|
||||||
state.conquered = true
|
state.conquered = true
|
||||||
planet_conquered.emit(planet_key)
|
planet_conquered.emit(planet_key)
|
||||||
|
# #63: definition-authored research points, exactly once per planet.
|
||||||
|
# state.conquered flipping above already makes a second call to this
|
||||||
|
# whole function a no-op via the early return above it, but
|
||||||
|
# research_awarded is its own explicit guard on the award itself
|
||||||
|
# regardless of how conquest was reached.
|
||||||
|
var reward: int = int(state.get("research_point_reward", 0))
|
||||||
|
if reward > 0 and not bool(state.get("research_awarded", false)):
|
||||||
|
state.research_awarded = true
|
||||||
|
research_points += reward
|
||||||
|
research_points_changed.emit(research_points)
|
||||||
|
|
||||||
|
|
||||||
func is_planet_conquered(planet_key: String) -> bool:
|
func is_planet_conquered(planet_key: String) -> bool:
|
||||||
|
|
@ -1206,7 +1361,8 @@ func _process(delta: float) -> void:
|
||||||
_advance_production(delta)
|
_advance_production(delta)
|
||||||
_advance_lab_research(delta)
|
_advance_lab_research(delta)
|
||||||
_advance_vessel_deliveries(delta)
|
_advance_vessel_deliveries(delta)
|
||||||
_advance_gas_burners(delta)
|
_advance_resource_converters(delta)
|
||||||
|
_advance_distorder_drain(delta)
|
||||||
for e in extractors:
|
for e in extractors:
|
||||||
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
||||||
var whole: int = int(floor(float(e.mining_progress)))
|
var whole: int = int(floor(float(e.mining_progress)))
|
||||||
|
|
@ -1364,13 +1520,7 @@ func _dispatch_transporters() -> void:
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
var needed_resource := ""
|
var needed_resource := ""
|
||||||
for resource_type in resource_types():
|
for resource_type in resource_types():
|
||||||
# A gas burner's ongoing gas deficit is folded in alongside
|
var need: int = building_requested_resource(int(b.id), resource_type)
|
||||||
# queued production/research costs (#54) - both are just "this
|
|
||||||
# building wants more of resource_type", so the one dispatch
|
|
||||||
# path already below serves both without any new transporter
|
|
||||||
# state.
|
|
||||||
var need: int = maxi(outstanding_resource_cost(int(b.id), resource_type),
|
|
||||||
gas_burner_gas_deficit(int(b.id)) if resource_type == RESOURCE_GAS else 0)
|
|
||||||
if need > 0 and bank_inventory.amount(resource_type) > 0:
|
if need > 0 and bank_inventory.amount(resource_type) > 0:
|
||||||
needed_resource = resource_type
|
needed_resource = resource_type
|
||||||
break
|
break
|
||||||
|
|
@ -1423,14 +1573,11 @@ func _dispatch_transporters() -> void:
|
||||||
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
||||||
TRANSPORTER_TO_EXTRACTOR)
|
TRANSPORTER_TO_EXTRACTOR)
|
||||||
|
|
||||||
# Collecting a gas burner's energy output, last: queued deliveries and
|
# Collect building outputs last: queued deliveries and extractor collection
|
||||||
# extractor collection both outrank it, same "supply what's needed
|
# both outrank resources that are merely ready for pickup.
|
||||||
# before what's merely ready" ordering _dispatch_transporters() already
|
|
||||||
# holds for extractors over queued orders (f5c60f8).
|
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
if not building_has_capability(b, "gas_burner"):
|
var pickup_resource: String = _collectable_building_resource(b, load)
|
||||||
continue
|
if pickup_resource == "" or _transporter_inbound_for_pickup(int(b.id)):
|
||||||
if building_resource(int(b.id), RESOURCE_ENERGY) < load or _transporter_inbound_for_pickup(int(b.id)):
|
|
||||||
continue
|
continue
|
||||||
var chosen_pickup: Dictionary = {}
|
var chosen_pickup: Dictionary = {}
|
||||||
var best_pickup_distance: float = INF
|
var best_pickup_distance: float = INF
|
||||||
|
|
@ -1444,7 +1591,7 @@ func _dispatch_transporters() -> void:
|
||||||
if chosen_pickup.is_empty():
|
if chosen_pickup.is_empty():
|
||||||
return
|
return
|
||||||
chosen_pickup.target_id = int(b.id)
|
chosen_pickup.target_id = int(b.id)
|
||||||
chosen_pickup.resource_type = RESOURCE_ENERGY
|
chosen_pickup.resource_type = pickup_resource
|
||||||
chosen_pickup.extractor_index = -1
|
chosen_pickup.extractor_index = -1
|
||||||
_begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position,
|
_begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position,
|
||||||
TRANSPORTER_TO_BUILDING_PICKUP)
|
TRANSPORTER_TO_BUILDING_PICKUP)
|
||||||
|
|
@ -1474,8 +1621,7 @@ func _arrive(t: Dictionary) -> void:
|
||||||
TRANSPORTER_TO_MOTHERSHIP:
|
TRANSPORTER_TO_MOTHERSHIP:
|
||||||
var target: Dictionary = find_building(int(t.target_id))
|
var target: Dictionary = find_building(int(t.target_id))
|
||||||
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
||||||
var want: int = maxi(outstanding_resource_cost(int(t.target_id), resource_type),
|
var want: int = building_requested_resource(int(t.target_id), resource_type)
|
||||||
gas_burner_gas_deficit(int(t.target_id)) if resource_type == RESOURCE_GAS else 0)
|
|
||||||
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
|
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
|
||||||
if take <= 0 or target.is_empty():
|
if take <= 0 or target.is_empty():
|
||||||
# Nothing to carry, or the building is gone. Go home empty.
|
# Nothing to carry, or the building is gone. Go home empty.
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,64 @@
|
||||||
class_name GasBombController
|
class_name GasBombController
|
||||||
extends Node2D
|
extends "res://AimedFleetAbilityController.gd"
|
||||||
|
|
||||||
const BOMB_COLOR := Color(0.96, 0.22, 0.72)
|
const BOMB_COLOR := Color(0.96, 0.22, 0.72)
|
||||||
const TARGET_COLOR := Color(0.78, 0.32, 1.0)
|
const TARGET_COLOR := Color(0.78, 0.32, 1.0)
|
||||||
const MAX_RANGE := 950.0
|
const MAX_RANGE := 950.0
|
||||||
const AOE_RADIUS := 180.0
|
const AOE_RADIUS := 180.0
|
||||||
const DRAG_RADIUS := 260.0
|
const DRAG_RADIUS := 260.0
|
||||||
const AIM_DEADZONE := 22.0
|
|
||||||
const PROJECTILE_SPEED := 720.0
|
const PROJECTILE_SPEED := 720.0
|
||||||
const DAMAGE := 3
|
const DAMAGE := 3
|
||||||
const COOLDOWN := 5.0
|
const COOLDOWN := 5.0
|
||||||
|
const GAS_PER_SHOT: int = 5
|
||||||
|
|
||||||
@export var minimum_zoom: float = 0.4
|
|
||||||
@onready var fleet: Node2D = get_node("../Fleet")
|
|
||||||
@onready var camera: Camera2D = get_node("../Camera2D")
|
|
||||||
@onready var button: IconButton = get_node("../UI/GasBombButton")
|
|
||||||
|
|
||||||
var aiming := false
|
|
||||||
var aim_touch := -1
|
|
||||||
var aim_start_screen := Vector2.ZERO
|
|
||||||
var aim_screen := Vector2.ZERO
|
|
||||||
var aim_target := Vector2.ZERO
|
|
||||||
var saved_zoom := 1.0
|
|
||||||
var restoring_zoom := false
|
|
||||||
var projectiles: Array = []
|
var projectiles: Array = []
|
||||||
var explosions: Array = []
|
var explosions: Array = []
|
||||||
var last_hit_count := 0
|
var last_hit_count: int = 0
|
||||||
|
|
||||||
var projectile_active: bool:
|
var projectile_active: bool:
|
||||||
get: return not projectiles.is_empty()
|
get: return not projectiles.is_empty()
|
||||||
|
|
||||||
var explosion_time: float:
|
var explosion_time: float:
|
||||||
get:
|
get:
|
||||||
var longest := 0.0
|
var longest: float = 0.0
|
||||||
for explosion in explosions:
|
for explosion in explosions:
|
||||||
longest = maxf(longest, float(explosion.time))
|
longest = maxf(longest, float(explosion.time))
|
||||||
return longest
|
return longest
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _button_path() -> NodePath: return "../UI/GasBombButton"
|
||||||
button.visible = false
|
func _status_path() -> NodePath: return "../UI/GasBombStatus"
|
||||||
queue_redraw()
|
func _component_id() -> String: return "gas_bomber"
|
||||||
|
func _ammo_resource() -> String: return GameState.RESOURCE_GAS
|
||||||
|
func _ammo_cost() -> int: return GAS_PER_SHOT
|
||||||
|
func _aim_padding() -> float: return AOE_RADIUS
|
||||||
|
func _has_effect_visuals() -> bool: return not projectiles.is_empty() or not explosions.is_empty()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
||||||
var had_visuals := aiming or not projectiles.is_empty() or not explosions.is_empty()
|
if drag.length() < 0.001:
|
||||||
button.visible = fleet.has_capability("gas_bomber")
|
return origin
|
||||||
button.enabled = _next_ready_bomber() != null
|
var distance: float = minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE
|
||||||
if not button.visible and aiming:
|
return origin + drag.normalized() * distance
|
||||||
_cancel_aim()
|
|
||||||
|
|
||||||
if restoring_zoom and not aiming:
|
|
||||||
var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0)
|
|
||||||
camera.zoom = Vector2(z, z)
|
|
||||||
if is_equal_approx(z, saved_zoom):
|
|
||||||
restoring_zoom = false
|
|
||||||
|
|
||||||
|
func _consume_shot(component: Node) -> bool:
|
||||||
|
return component.consume_shot(COOLDOWN)
|
||||||
|
|
||||||
|
|
||||||
|
func _perform_shot(ship: Node2D, _component: Node) -> void:
|
||||||
|
projectiles.append({
|
||||||
|
"position": ship.global_position,
|
||||||
|
"target": aim_target,
|
||||||
|
"trail": PackedVector2Array([ship.global_position]),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _next_ready_bomber() -> Node2D:
|
||||||
|
return _next_ready_ship()
|
||||||
|
|
||||||
|
|
||||||
|
func _advance_effects(delta: float) -> void:
|
||||||
for i in range(projectiles.size() - 1, -1, -1):
|
for i in range(projectiles.size() - 1, -1, -1):
|
||||||
var projectile: Dictionary = projectiles[i]
|
var projectile: Dictionary = projectiles[i]
|
||||||
var distance: float = (projectile.position as Vector2).distance_to(projectile.target)
|
var distance: float = (projectile.position as Vector2).distance_to(projectile.target)
|
||||||
|
|
@ -64,8 +67,8 @@ func _process(delta: float) -> void:
|
||||||
_explode(projectile.target)
|
_explode(projectile.target)
|
||||||
projectiles.remove_at(i)
|
projectiles.remove_at(i)
|
||||||
else:
|
else:
|
||||||
projectile.position = (projectile.position as Vector2).move_toward(projectile.target,
|
projectile.position = (projectile.position as Vector2).move_toward(
|
||||||
PROJECTILE_SPEED * delta)
|
projectile.target, PROJECTILE_SPEED * delta)
|
||||||
var trail: PackedVector2Array = projectile.trail
|
var trail: PackedVector2Array = projectile.trail
|
||||||
trail.append(projectile.position)
|
trail.append(projectile.position)
|
||||||
while trail.size() > 12:
|
while trail.size() > 12:
|
||||||
|
|
@ -76,107 +79,6 @@ func _process(delta: float) -> void:
|
||||||
explosion.time = maxf(float(explosion.time) - delta, 0.0)
|
explosion.time = maxf(float(explosion.time) - delta, 0.0)
|
||||||
if float(explosion.time) <= 0.0:
|
if float(explosion.time) <= 0.0:
|
||||||
explosions.remove_at(i)
|
explosions.remove_at(i)
|
||||||
if aiming or not projectiles.is_empty() or not explosions.is_empty() or had_visuals:
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func can_begin_aim() -> bool:
|
|
||||||
return button.visible and button.enabled and not button._blocked_by_modal() \
|
|
||||||
and not aiming
|
|
||||||
|
|
||||||
|
|
||||||
func handle_touch(event: InputEventScreenTouch) -> bool:
|
|
||||||
if event.pressed:
|
|
||||||
if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position):
|
|
||||||
return false
|
|
||||||
aiming = true
|
|
||||||
aim_touch = event.index
|
|
||||||
aim_start_screen = event.position
|
|
||||||
aim_screen = event.position
|
|
||||||
saved_zoom = camera.zoom.x
|
|
||||||
restoring_zoom = false
|
|
||||||
button.pressed_down = true
|
|
||||||
button.queue_redraw()
|
|
||||||
_update_aim()
|
|
||||||
return true
|
|
||||||
if event.index != aim_touch:
|
|
||||||
return false
|
|
||||||
var should_fire := aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE
|
|
||||||
if should_fire:
|
|
||||||
_fire()
|
|
||||||
else:
|
|
||||||
_cancel_aim()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func handle_drag(event: InputEventScreenDrag) -> bool:
|
|
||||||
if not aiming or event.index != aim_touch:
|
|
||||||
return false
|
|
||||||
aim_screen = event.position
|
|
||||||
_update_aim()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _update_aim() -> void:
|
|
||||||
var drag := aim_screen - aim_start_screen
|
|
||||||
var origin: Vector2 = fleet.get_centroid()
|
|
||||||
if drag.length() < 0.001:
|
|
||||||
aim_target = origin
|
|
||||||
else:
|
|
||||||
var distance := minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE
|
|
||||||
aim_target = origin + drag.normalized() * distance
|
|
||||||
_update_aim_zoom(origin, aim_target)
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
|
||||||
var displacement := target - origin
|
|
||||||
var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0)
|
|
||||||
var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + AOE_RADIUS)
|
|
||||||
var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + AOE_RADIUS)
|
|
||||||
var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom)
|
|
||||||
camera.zoom = Vector2(desired, desired)
|
|
||||||
|
|
||||||
|
|
||||||
func _finish_aim() -> void:
|
|
||||||
aiming = false
|
|
||||||
aim_touch = -1
|
|
||||||
button.pressed_down = false
|
|
||||||
button.queue_redraw()
|
|
||||||
restoring_zoom = true
|
|
||||||
|
|
||||||
|
|
||||||
func _cancel_aim() -> void:
|
|
||||||
_finish_aim()
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _fire() -> void:
|
|
||||||
var bomber: Node2D = _next_ready_bomber()
|
|
||||||
if bomber == null:
|
|
||||||
_cancel_aim()
|
|
||||||
return
|
|
||||||
var component: Node = bomber.get_component("gas_bomber")
|
|
||||||
if not component.consume_shot(COOLDOWN):
|
|
||||||
_cancel_aim()
|
|
||||||
return
|
|
||||||
projectiles.append({
|
|
||||||
"position": bomber.global_position,
|
|
||||||
"target": aim_target,
|
|
||||||
"trail": PackedVector2Array([bomber.global_position]),
|
|
||||||
})
|
|
||||||
_finish_aim()
|
|
||||||
button.enabled = _next_ready_bomber() != null
|
|
||||||
|
|
||||||
|
|
||||||
func _next_ready_bomber() -> Node2D:
|
|
||||||
for ship in fleet.ships:
|
|
||||||
if not is_instance_valid(ship):
|
|
||||||
continue
|
|
||||||
var component: Node = ship.get_component("gas_bomber")
|
|
||||||
if component != null and component.is_ready():
|
|
||||||
return ship
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
func _explode(at: Vector2) -> void:
|
func _explode(at: Vector2) -> void:
|
||||||
|
|
@ -191,28 +93,29 @@ func _explode(at: Vector2) -> void:
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if aiming:
|
if aiming:
|
||||||
var origin: Vector2 = fleet.get_centroid()
|
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.8)
|
||||||
NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8)
|
|
||||||
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
|
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
|
||||||
_draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0)
|
_draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0)
|
||||||
for projectile in projectiles:
|
for projectile in projectiles:
|
||||||
var trail: PackedVector2Array = projectile.trail
|
var trail: PackedVector2Array = projectile.trail
|
||||||
if trail.size() >= 2:
|
if trail.size() >= 2:
|
||||||
for i in range(1, trail.size()):
|
for i in range(1, trail.size()):
|
||||||
var alpha := float(i) / trail.size()
|
var alpha: float = float(i) / trail.size()
|
||||||
draw_line(trail[i - 1], trail[i],
|
draw_line(trail[i - 1], trail[i], Color(BOMB_COLOR, alpha * 0.65),
|
||||||
Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true)
|
4.0 + alpha * 4.0, true)
|
||||||
draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35))
|
draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35))
|
||||||
NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0)
|
NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0)
|
||||||
for explosion in explosions:
|
for explosion in explosions:
|
||||||
var progress := 1.0 - float(explosion.time) / 0.8
|
var progress: float = 1.0 - float(explosion.time) / 0.8
|
||||||
var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
var radius: float = lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
||||||
draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
||||||
_draw_ring(explosion.position, radius, BOMB_COLOR, 1.0 - progress)
|
_draw_ring(explosion.position, radius, BOMB_COLOR, 1.0 - progress)
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
var angle := TAU * float(i) / 7.0 + progress
|
var angle: float = TAU * float(i) / 7.0 + progress
|
||||||
var cloud_at: Vector2 = explosion.position + Vector2(cos(angle), sin(angle)) * radius * 0.55
|
var cloud_at: Vector2 = explosion.position \
|
||||||
draw_circle(cloud_at, 18.0 + 14.0 * progress, Color(BOMB_COLOR, (1.0 - progress) * 0.22))
|
+ Vector2(cos(angle), sin(angle)) * radius * 0.55
|
||||||
|
draw_circle(cloud_at, 18.0 + 14.0 * progress,
|
||||||
|
Color(BOMB_COLOR, (1.0 - progress) * 0.22))
|
||||||
|
|
||||||
|
|
||||||
func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void:
|
func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void:
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,5 @@ unlock_name = "Unlock Gas Bomber"
|
||||||
unlock_description = "Research volatile-gas containment and unlock the Gas Bomber for ship crafters."
|
unlock_description = "Research volatile-gas containment and unlock the Gas Bomber for ship crafters."
|
||||||
unlock_cost = 0
|
unlock_cost = 0
|
||||||
unlock_resource_costs = {"gas": 30}
|
unlock_resource_costs = {"gas": 30}
|
||||||
|
research_point_cost = 1
|
||||||
unlock_icon = 31
|
unlock_icon = 31
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=4 format=3]
|
||||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||||
[ext_resource type="PackedScene" path="res://GasBurnerBuildingVisual.tscn" id="2"]
|
[ext_resource type="PackedScene" path="res://GasBurnerBuildingVisual.tscn" id="2"]
|
||||||
|
[ext_resource type="Script" path="res://BuildingLogisticsProfile.gd" id="3"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_logistics"]
|
||||||
|
script = ExtResource("3")
|
||||||
|
input_buffers = {
|
||||||
|
"gas": 40
|
||||||
|
}
|
||||||
|
collected_outputs = PackedStringArray("energy")
|
||||||
|
conversion_input = "gas"
|
||||||
|
conversion_output = "energy"
|
||||||
|
conversion_rate = 1.0
|
||||||
|
conversion_yield = 2
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
definition_id = "gas_burner"
|
definition_id = "gas_burner"
|
||||||
|
|
@ -11,6 +24,7 @@ action_icon = 33
|
||||||
menu_icon = 33
|
menu_icon = 33
|
||||||
visual_scene = ExtResource("2")
|
visual_scene = ExtResource("2")
|
||||||
capabilities = PackedStringArray("gas_burner")
|
capabilities = PackedStringArray("gas_burner")
|
||||||
|
logistics = SubResource("Resource_logistics")
|
||||||
map_color = Color(0.95, 0.35, 0.15, 1)
|
map_color = Color(0.95, 0.35, 0.15, 1)
|
||||||
map_sides = 5
|
map_sides = 5
|
||||||
map_radius = 7.0
|
map_radius = 7.0
|
||||||
|
|
|
||||||
1
GasBurnerBuildingVisual.gd.uid
Normal file
1
GasBurnerBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5a74rb5s4q21
|
||||||
|
|
@ -41,6 +41,8 @@ enum Icon {
|
||||||
# #58: LASER_VESSEL is the ship glyph, LASER_SHOT the action button -
|
# #58: LASER_VESSEL is the ship glyph, LASER_SHOT the action button -
|
||||||
# same two-icon split as GAS_BOMBER/GAS_BOMB above.
|
# same two-icon split as GAS_BOMBER/GAS_BOMB above.
|
||||||
LASER_VESSEL, LASER_SHOT,
|
LASER_VESSEL, LASER_SHOT,
|
||||||
|
# #59: a warped ring reads as a space-time distortion rather than a hull.
|
||||||
|
DISTORDER_VESSEL,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||||
|
|
@ -239,6 +241,8 @@ func _draw() -> void:
|
||||||
_draw_laser_vessel(center, pulse, s)
|
_draw_laser_vessel(center, pulse, s)
|
||||||
Icon.LASER_SHOT:
|
Icon.LASER_SHOT:
|
||||||
_draw_laser_shot(center, pulse, s)
|
_draw_laser_shot(center, pulse, s)
|
||||||
|
Icon.DISTORDER_VESSEL:
|
||||||
|
_draw_distorder_vessel(center, pulse, s)
|
||||||
|
|
||||||
|
|
||||||
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
||||||
|
|
@ -277,6 +281,18 @@ func _draw_carrier(center: Vector2, pulse: float, s: float) -> void:
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
|
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_distorder_vessel(center: Vector2, pulse: float, s: float) -> void:
|
||||||
|
# A ring whose radius wobbles with angle instead of a plain circle -
|
||||||
|
# reads as warped space rather than a hull silhouette.
|
||||||
|
var points: PackedVector2Array = PackedVector2Array()
|
||||||
|
for i in range(17):
|
||||||
|
var a: float = TAU * float(i) / 16.0
|
||||||
|
var r: float = (10.0 + 3.0 * sin(a * 3.0)) * s
|
||||||
|
points.append(center + Vector2(cos(a), sin(a)) * r)
|
||||||
|
NeonDraw.draw_glowing_path(self, points, _active_color(), pulse, 0.8, core_color)
|
||||||
|
NeonDraw.draw_glowing_circle(self, center, 2.5 * s, _active_color(), pulse, core_color)
|
||||||
|
|
||||||
|
|
||||||
func _draw_laser_vessel(center: Vector2, pulse: float, s: float) -> void:
|
func _draw_laser_vessel(center: Vector2, pulse: float, s: float) -> void:
|
||||||
# A slim dart rather than the interceptor's plain triangle - the notch at
|
# A slim dart rather than the interceptor's plain triangle - the notch at
|
||||||
# the nose reads as a fixed forward-facing emitter, not a hull point.
|
# the nose reads as a fixed forward-facing emitter, not a hull point.
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Heavy Slugs"
|
||||||
description = "+1 damage per shot."
|
description = "+1 damage per shot."
|
||||||
base_cost = 30
|
base_cost = 30
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 15
|
icon = 15
|
||||||
additive_modifiers = {"damage": 1.0}
|
additive_modifiers = {"damage": 1.0}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Targeting Array"
|
||||||
description = "+60 firing range per level."
|
description = "+60 firing range per level."
|
||||||
base_cost = 20
|
base_cost = 20
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 17
|
icon = 17
|
||||||
additive_modifiers = {"fire_range": 60.0}
|
additive_modifiers = {"fire_range": 60.0}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Autoloader"
|
||||||
description = "Fires 15% faster per level."
|
description = "Fires 15% faster per level."
|
||||||
base_cost = 25
|
base_cost = 25
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 16
|
icon = 16
|
||||||
multiplicative_modifiers = {"fire_cooldown": 0.85}
|
multiplicative_modifiers = {"fire_cooldown": 0.85}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Thrusters"
|
||||||
description = "+40 top speed, +60 acceleration per level."
|
description = "+40 top speed, +60 acceleration per level."
|
||||||
base_cost = 25
|
base_cost = 25
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 18
|
icon = 18
|
||||||
additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0}
|
additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0}
|
||||||
|
|
|
||||||
|
|
@ -1,164 +1,60 @@
|
||||||
class_name LaserController
|
class_name LaserController
|
||||||
extends Node2D
|
extends "res://AimedFleetAbilityController.gd"
|
||||||
|
|
||||||
# Same press-drag-release aiming plumbing as GasBombController, adapted for
|
|
||||||
# an instant beam rather than a lobbed, timed projectile: there is no travel
|
|
||||||
# time or explosion delay - the beam strikes the instant the shot fires, and
|
|
||||||
# only a brief flash lingers for feedback. Damage/range/cooldown are read
|
|
||||||
# straight off the firing ship's own VesselLaserComponent (already resolved
|
|
||||||
# from vessel_stat/upgrades) rather than hardcoded here, unlike the bomber's
|
|
||||||
# AOE_RADIUS/DAMAGE/COOLDOWN.
|
|
||||||
#
|
|
||||||
# Reused as-is by both Main.tscn and PlanetSurface.tscn (same relative
|
|
||||||
# ../Fleet, ../Camera2D, ../UI/LaserButton paths as GasBombController), so
|
|
||||||
# it works identically in space and on a landed surface with no branching.
|
|
||||||
|
|
||||||
const BEAM_COLOR := Color(0.3, 0.9, 1.0)
|
const BEAM_COLOR := Color(0.3, 0.9, 1.0)
|
||||||
const TARGET_COLOR := Color(0.6, 0.95, 1.0)
|
const TARGET_COLOR := Color(0.6, 0.95, 1.0)
|
||||||
const BEAM_HALF_WIDTH := 22.0
|
const BEAM_HALF_WIDTH := 22.0
|
||||||
const DRAG_DEADZONE := 22.0
|
|
||||||
const FLASH_DURATION := 0.22
|
const FLASH_DURATION := 0.22
|
||||||
|
const ENERGY_PER_SHOT: int = 8
|
||||||
|
|
||||||
@export var minimum_zoom: float = 0.4
|
|
||||||
@onready var fleet: Node2D = get_node("../Fleet")
|
|
||||||
@onready var camera: Camera2D = get_node("../Camera2D")
|
|
||||||
@onready var button: IconButton = get_node("../UI/LaserButton")
|
|
||||||
|
|
||||||
var aiming := false
|
|
||||||
var aim_touch := -1
|
|
||||||
var aim_start_screen := Vector2.ZERO
|
|
||||||
var aim_screen := Vector2.ZERO
|
|
||||||
var aim_target := Vector2.ZERO
|
|
||||||
var saved_zoom := 1.0
|
|
||||||
var restoring_zoom := false
|
|
||||||
var beams: Array = []
|
var beams: Array = []
|
||||||
var last_hit_count := 0
|
var last_hit_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _button_path() -> NodePath: return "../UI/LaserButton"
|
||||||
button.visible = false
|
func _status_path() -> NodePath: return "../UI/LaserStatus"
|
||||||
queue_redraw()
|
func _component_id() -> String: return "laser"
|
||||||
|
func _ammo_resource() -> String: return GameState.RESOURCE_ENERGY
|
||||||
|
func _ammo_cost() -> int: return ENERGY_PER_SHOT
|
||||||
|
func _aim_padding() -> float: return BEAM_HALF_WIDTH
|
||||||
|
func _has_effect_visuals() -> bool: return not beams.is_empty()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _aim_max_range() -> float:
|
||||||
var had_visuals := aiming or not beams.is_empty()
|
var ship: Node2D = _next_ready_ship()
|
||||||
button.visible = fleet.has_capability("laser")
|
if ship == null:
|
||||||
button.enabled = _next_ready_laser_ship() != null
|
return 950.0
|
||||||
if not button.visible and aiming:
|
var component: Node = ship.get_component(_component_id())
|
||||||
_cancel_aim()
|
return component.beam_range if component != null else 950.0
|
||||||
|
|
||||||
if restoring_zoom and not aiming:
|
|
||||||
var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0)
|
|
||||||
camera.zoom = Vector2(z, z)
|
|
||||||
if is_equal_approx(z, saved_zoom):
|
|
||||||
restoring_zoom = false
|
|
||||||
|
|
||||||
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
||||||
|
# A beam always reaches full range; dragging chooses its direction only.
|
||||||
|
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
||||||
|
return origin + direction * _aim_max_range()
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_shot(component: Node) -> bool:
|
||||||
|
return component.consume_shot()
|
||||||
|
|
||||||
|
|
||||||
|
func _perform_shot(ship: Node2D, component: Node) -> void:
|
||||||
|
var direction: Vector2 = aim_target - _aim_origin()
|
||||||
|
direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP
|
||||||
|
var beam_to: Vector2 = ship.global_position + direction * component.beam_range
|
||||||
|
_strike(ship.global_position, beam_to, direction, int(component.damage))
|
||||||
|
|
||||||
|
|
||||||
|
func _next_ready_laser_ship() -> Node2D:
|
||||||
|
return _next_ready_ship()
|
||||||
|
|
||||||
|
|
||||||
|
func _advance_effects(delta: float) -> void:
|
||||||
for i in range(beams.size() - 1, -1, -1):
|
for i in range(beams.size() - 1, -1, -1):
|
||||||
var beam: Dictionary = beams[i]
|
var beam: Dictionary = beams[i]
|
||||||
beam.time = maxf(float(beam.time) - delta, 0.0)
|
beam.time = maxf(float(beam.time) - delta, 0.0)
|
||||||
if float(beam.time) <= 0.0:
|
if float(beam.time) <= 0.0:
|
||||||
beams.remove_at(i)
|
beams.remove_at(i)
|
||||||
if aiming or not beams.is_empty() or had_visuals:
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func can_begin_aim() -> bool:
|
|
||||||
return button.visible and button.enabled and not button._blocked_by_modal() \
|
|
||||||
and not aiming
|
|
||||||
|
|
||||||
|
|
||||||
func handle_touch(event: InputEventScreenTouch) -> bool:
|
|
||||||
if event.pressed:
|
|
||||||
if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position):
|
|
||||||
return false
|
|
||||||
aiming = true
|
|
||||||
aim_touch = event.index
|
|
||||||
aim_start_screen = event.position
|
|
||||||
aim_screen = event.position
|
|
||||||
saved_zoom = camera.zoom.x
|
|
||||||
restoring_zoom = false
|
|
||||||
button.pressed_down = true
|
|
||||||
button.queue_redraw()
|
|
||||||
_update_aim()
|
|
||||||
return true
|
|
||||||
if event.index != aim_touch:
|
|
||||||
return false
|
|
||||||
var should_fire := aim_start_screen.distance_to(aim_screen) >= DRAG_DEADZONE
|
|
||||||
if should_fire:
|
|
||||||
_fire()
|
|
||||||
else:
|
|
||||||
_cancel_aim()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func handle_drag(event: InputEventScreenDrag) -> bool:
|
|
||||||
if not aiming or event.index != aim_touch:
|
|
||||||
return false
|
|
||||||
aim_screen = event.position
|
|
||||||
_update_aim()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _aim_origin() -> Vector2:
|
|
||||||
return fleet.get_centroid()
|
|
||||||
|
|
||||||
|
|
||||||
func _aim_max_range() -> float:
|
|
||||||
var ship: Node2D = _next_ready_laser_ship()
|
|
||||||
if ship == null:
|
|
||||||
return 950.0
|
|
||||||
var component: Node = ship.get_component("laser")
|
|
||||||
return component.beam_range if component != null else 950.0
|
|
||||||
|
|
||||||
|
|
||||||
func _update_aim() -> void:
|
|
||||||
# A beam always reaches its full range - only the DIRECTION is aimed,
|
|
||||||
# unlike the bomb's drag-controlled distance.
|
|
||||||
var drag := aim_screen - aim_start_screen
|
|
||||||
var origin: Vector2 = _aim_origin()
|
|
||||||
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
|
||||||
aim_target = origin + direction * _aim_max_range()
|
|
||||||
_update_aim_zoom(origin, aim_target)
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
|
||||||
var displacement := target - origin
|
|
||||||
var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0)
|
|
||||||
var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + BEAM_HALF_WIDTH)
|
|
||||||
var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + BEAM_HALF_WIDTH)
|
|
||||||
var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom)
|
|
||||||
camera.zoom = Vector2(desired, desired)
|
|
||||||
|
|
||||||
|
|
||||||
func _finish_aim() -> void:
|
|
||||||
aiming = false
|
|
||||||
aim_touch = -1
|
|
||||||
button.pressed_down = false
|
|
||||||
button.queue_redraw()
|
|
||||||
restoring_zoom = true
|
|
||||||
|
|
||||||
|
|
||||||
func _cancel_aim() -> void:
|
|
||||||
_finish_aim()
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _fire() -> void:
|
|
||||||
var shooter: Node2D = _next_ready_laser_ship()
|
|
||||||
if shooter == null:
|
|
||||||
_cancel_aim()
|
|
||||||
return
|
|
||||||
var component: Node = shooter.get_component("laser")
|
|
||||||
if not component.consume_shot():
|
|
||||||
_cancel_aim()
|
|
||||||
return
|
|
||||||
var direction: Vector2 = (aim_target - _aim_origin())
|
|
||||||
direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP
|
|
||||||
var beam_to: Vector2 = shooter.global_position + direction * component.beam_range
|
|
||||||
_strike(shooter.global_position, beam_to, direction, int(component.damage))
|
|
||||||
_finish_aim()
|
|
||||||
button.enabled = _next_ready_laser_ship() != null
|
|
||||||
|
|
||||||
|
|
||||||
func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> void:
|
func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> void:
|
||||||
|
|
@ -178,20 +74,12 @@ func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> voi
|
||||||
last_hit_count += 1
|
last_hit_count += 1
|
||||||
|
|
||||||
|
|
||||||
func _next_ready_laser_ship() -> Node2D:
|
|
||||||
for ship in fleet.ships:
|
|
||||||
if not is_instance_valid(ship):
|
|
||||||
continue
|
|
||||||
var component: Node = ship.get_component("laser")
|
|
||||||
if component != null and component.is_ready():
|
|
||||||
return ship
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if aiming:
|
if aiming:
|
||||||
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.6)
|
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target,
|
||||||
|
TARGET_COLOR, 0.7, 0.6)
|
||||||
for beam in beams:
|
for beam in beams:
|
||||||
var progress: float = 1.0 - float(beam.time) / FLASH_DURATION
|
var progress: float = 1.0 - float(beam.time) / FLASH_DURATION
|
||||||
var alpha: float = 1.0 - progress
|
var alpha: float = 1.0 - progress
|
||||||
NeonDraw.draw_glowing_line(self, beam.from, beam.to, BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|
NeonDraw.draw_glowing_line(self, beam.from, beam.to,
|
||||||
|
BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|
||||||
|
|
|
||||||
1
LaserController.gd.uid
Normal file
1
LaserController.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cdqg3isy1lbuf
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Capacitor Bank"
|
||||||
description = "Recharges 0.6s sooner per level."
|
description = "Recharges 0.6s sooner per level."
|
||||||
base_cost = 30
|
base_cost = 30
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 20
|
icon = 20
|
||||||
additive_modifiers = {"beam_cooldown": -0.6}
|
additive_modifiers = {"beam_cooldown": -0.6}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Focusing Lens"
|
||||||
description = "+1 damage per shot."
|
description = "+1 damage per shot."
|
||||||
base_cost = 30
|
base_cost = 30
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 15
|
icon = 15
|
||||||
additive_modifiers = {"damage": 1.0}
|
additive_modifiers = {"damage": 1.0}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Beam Collimator"
|
||||||
description = "+150 beam range per level."
|
description = "+150 beam range per level."
|
||||||
base_cost = 25
|
base_cost = 25
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 17
|
icon = 17
|
||||||
additive_modifiers = {"beam_range": 150.0}
|
additive_modifiers = {"beam_range": 150.0}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,5 @@ unlock_name = "Unlock Laser Ship"
|
||||||
unlock_description = "Research a directed-energy hull for the ship crafter - manually aimed, no ammo, cools down between shots."
|
unlock_description = "Research a directed-energy hull for the ship crafter - manually aimed, no ammo, cools down between shots."
|
||||||
unlock_cost = 0
|
unlock_cost = 0
|
||||||
unlock_resource_costs = {"energy": 40}
|
unlock_resource_costs = {"energy": 40}
|
||||||
|
research_point_cost = 1
|
||||||
unlock_icon = -1
|
unlock_icon = -1
|
||||||
|
|
|
||||||
312
Main.tscn
312
Main.tscn
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=28 format=3]
|
[gd_scene load_steps=29 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://Starfield.gd" id="1"]
|
[ext_resource type="Script" path="res://Starfield.gd" id="1"]
|
||||||
[ext_resource type="Script" path="res://Fleet.gd" id="2"]
|
[ext_resource type="Script" path="res://Fleet.gd" id="2"]
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
[ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"]
|
[ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"]
|
||||||
[ext_resource type="Script" path="res://GasBombController.gd" id="29"]
|
[ext_resource type="Script" path="res://GasBombController.gd" id="29"]
|
||||||
[ext_resource type="Script" path="res://LaserController.gd" id="30"]
|
[ext_resource type="Script" path="res://LaserController.gd" id="30"]
|
||||||
|
[ext_resource type="Script" path="res://SpeedFader.gd" id="31"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="1"]
|
[sub_resource type="ShaderMaterial" id="1"]
|
||||||
shader = ExtResource("5")
|
shader = ExtResource("5")
|
||||||
|
|
@ -221,6 +222,28 @@ offset_top = -424
|
||||||
offset_bottom = -24
|
offset_bottom = -24
|
||||||
script = ExtResource("13")
|
script = ExtResource("13")
|
||||||
|
|
||||||
|
[node name="SpeedFader" type="Control" parent="UI"]
|
||||||
|
visible = false
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -94
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = 740
|
||||||
|
offset_bottom = 920
|
||||||
|
script = ExtResource("31")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="UI/SpeedFader"]
|
||||||
|
mouse_filter = 2
|
||||||
|
offset_left = -20
|
||||||
|
offset_right = 90
|
||||||
|
offset_top = -32
|
||||||
|
offset_bottom = -4
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = "x1.0"
|
||||||
|
|
||||||
[node name="LaserButton" type="Control" parent="UI"]
|
[node name="LaserButton" type="Control" parent="UI"]
|
||||||
visible = false
|
visible = false
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
@ -264,6 +287,38 @@ script = ExtResource("10")
|
||||||
icon = 32
|
icon = 32
|
||||||
glow_color = Color(0.96, 0.22, 0.72, 1)
|
glow_color = Color(0.96, 0.22, 0.72, 1)
|
||||||
|
|
||||||
|
[node name="GasBombStatus" type="Label" parent="UI"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -344
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = -480
|
||||||
|
offset_bottom = -430
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.96, 0.22, 0.72, 1)
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="LaserStatus" type="Label" parent="UI"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -344
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = -540
|
||||||
|
offset_bottom = -490
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.3, 0.9, 1, 1)
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
[node name="PivotCWButton" type="Control" parent="UI"]
|
[node name="PivotCWButton" type="Control" parent="UI"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
|
|
@ -839,7 +894,7 @@ horizontal_alignment = 1
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -290
|
offset_left = -290
|
||||||
offset_right = -198
|
offset_right = -215
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -850,7 +905,7 @@ mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -330
|
offset_left = -330
|
||||||
offset_right = -158
|
offset_right = -175
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -859,8 +914,8 @@ horizontal_alignment = 1
|
||||||
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
|
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -168
|
offset_left = -189
|
||||||
offset_right = -76
|
offset_right = -114
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -871,8 +926,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -208
|
offset_left = -229
|
||||||
offset_right = -36
|
offset_right = -74
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -881,8 +936,8 @@ horizontal_alignment = 1
|
||||||
[node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"]
|
[node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -46
|
offset_left = -88
|
||||||
offset_right = 46
|
offset_right = -13
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -893,8 +948,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -86
|
offset_left = -128
|
||||||
offset_right = 86
|
offset_right = 27
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -903,8 +958,8 @@ horizontal_alignment = 1
|
||||||
[node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"]
|
[node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 76
|
offset_left = 13
|
||||||
offset_right = 168
|
offset_right = 88
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -915,8 +970,8 @@ glow_color = Color(1, 0.78, 0.3, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 36
|
offset_left = -27
|
||||||
offset_right = 208
|
offset_right = 128
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -925,8 +980,8 @@ horizontal_alignment = 1
|
||||||
[node name="AvailableLaser" type="Control" parent="UI/HangarMenu"]
|
[node name="AvailableLaser" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 198
|
offset_left = 114
|
||||||
offset_right = 290
|
offset_right = 189
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -937,7 +992,29 @@ glow_color = Color(0.3, 0.9, 1, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 158
|
offset_left = 74
|
||||||
|
offset_right = 229
|
||||||
|
offset_top = 380
|
||||||
|
offset_bottom = 460
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="AvailableDistorder" type="Control" parent="UI/HangarMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = 215
|
||||||
|
offset_right = 290
|
||||||
|
offset_top = 255
|
||||||
|
offset_bottom = 375
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 37
|
||||||
|
glow_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
|
||||||
|
[node name="AvailableDistorderLabel" type="Label" parent="UI/HangarMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = 175
|
||||||
offset_right = 330
|
offset_right = 330
|
||||||
offset_top = 380
|
offset_top = 380
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
|
|
@ -957,7 +1034,7 @@ horizontal_alignment = 1
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -290
|
offset_left = -290
|
||||||
offset_right = -198
|
offset_right = -215
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -968,7 +1045,7 @@ mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -330
|
offset_left = -330
|
||||||
offset_right = -158
|
offset_right = -175
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -977,8 +1054,8 @@ horizontal_alignment = 1
|
||||||
[node name="FleetShield" type="Control" parent="UI/HangarMenu"]
|
[node name="FleetShield" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -168
|
offset_left = -189
|
||||||
offset_right = -76
|
offset_right = -114
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -989,8 +1066,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -208
|
offset_left = -229
|
||||||
offset_right = -36
|
offset_right = -74
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -999,8 +1076,8 @@ horizontal_alignment = 1
|
||||||
[node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"]
|
[node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -46
|
offset_left = -88
|
||||||
offset_right = 46
|
offset_right = -13
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1011,8 +1088,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -86
|
offset_left = -128
|
||||||
offset_right = 86
|
offset_right = 27
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1021,8 +1098,8 @@ horizontal_alignment = 1
|
||||||
[node name="FleetCarrier" type="Control" parent="UI/HangarMenu"]
|
[node name="FleetCarrier" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 76
|
offset_left = 13
|
||||||
offset_right = 168
|
offset_right = 88
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1033,8 +1110,8 @@ glow_color = Color(1, 0.78, 0.3, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 36
|
offset_left = -27
|
||||||
offset_right = 208
|
offset_right = 128
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1043,8 +1120,8 @@ horizontal_alignment = 1
|
||||||
[node name="FleetLaser" type="Control" parent="UI/HangarMenu"]
|
[node name="FleetLaser" type="Control" parent="UI/HangarMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 198
|
offset_left = 114
|
||||||
offset_right = 290
|
offset_right = 189
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1055,7 +1132,29 @@ glow_color = Color(0.3, 0.9, 1, 1)
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 158
|
offset_left = 74
|
||||||
|
offset_right = 229
|
||||||
|
offset_top = 690
|
||||||
|
offset_bottom = 770
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="FleetDistorder" type="Control" parent="UI/HangarMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = 215
|
||||||
|
offset_right = 290
|
||||||
|
offset_top = 565
|
||||||
|
offset_bottom = 685
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 37
|
||||||
|
glow_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
|
||||||
|
[node name="FleetDistorderLabel" type="Label" parent="UI/HangarMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = 175
|
||||||
offset_right = 330
|
offset_right = 330
|
||||||
offset_top = 690
|
offset_top = 690
|
||||||
offset_bottom = 770
|
offset_bottom = 770
|
||||||
|
|
@ -1107,7 +1206,7 @@ script = ExtResource("24")
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -300
|
offset_left = -300
|
||||||
offset_right = -230
|
offset_right = -240
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1117,8 +1216,8 @@ glow_color = Color(0.25, 0.85, 1, 1)
|
||||||
[node name="VesselB" type="Control" parent="UI/LabMenu"]
|
[node name="VesselB" type="Control" parent="UI/LabMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -194
|
offset_left = -210
|
||||||
offset_right = -124
|
offset_right = -150
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1128,8 +1227,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
|
||||||
[node name="VesselC" type="Control" parent="UI/LabMenu"]
|
[node name="VesselC" type="Control" parent="UI/LabMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -88
|
offset_left = -120
|
||||||
offset_right = -18
|
offset_right = -60
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1139,8 +1238,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1)
|
||||||
[node name="VesselD" type="Control" parent="UI/LabMenu"]
|
[node name="VesselD" type="Control" parent="UI/LabMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 18
|
offset_left = -30
|
||||||
offset_right = 88
|
offset_right = 30
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1150,8 +1249,8 @@ glow_color = Color(0.95, 0.55, 0.85, 1)
|
||||||
[node name="VesselE" type="Control" parent="UI/LabMenu"]
|
[node name="VesselE" type="Control" parent="UI/LabMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 124
|
offset_left = 60
|
||||||
offset_right = 194
|
offset_right = 120
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1161,14 +1260,25 @@ glow_color = Color(1, 0.78, 0.3, 1)
|
||||||
[node name="VesselF" type="Control" parent="UI/LabMenu"]
|
[node name="VesselF" type="Control" parent="UI/LabMenu"]
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 230
|
offset_left = 150
|
||||||
offset_right = 300
|
offset_right = 210
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 35
|
icon = 35
|
||||||
glow_color = Color(0.3, 0.9, 1, 1)
|
glow_color = Color(0.3, 0.9, 1, 1)
|
||||||
|
|
||||||
|
[node name="VesselG" type="Control" parent="UI/LabMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = 240
|
||||||
|
offset_right = 300
|
||||||
|
offset_top = 274
|
||||||
|
offset_bottom = 394
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 37
|
||||||
|
glow_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
|
||||||
[node name="VesselLabel" type="Label" parent="UI/LabMenu"]
|
[node name="VesselLabel" type="Label" parent="UI/LabMenu"]
|
||||||
visible = false
|
visible = false
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
@ -1186,7 +1296,7 @@ mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -330
|
offset_left = -330
|
||||||
offset_right = -200
|
offset_right = -210
|
||||||
offset_top = 400
|
offset_top = 400
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1197,8 +1307,8 @@ text = "Interceptor"
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -224
|
offset_left = -240
|
||||||
offset_right = -94
|
offset_right = -120
|
||||||
offset_top = 400
|
offset_top = 400
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1209,8 +1319,8 @@ text = "Shield"
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = -118
|
offset_left = -150
|
||||||
offset_right = 12
|
offset_right = -30
|
||||||
offset_top = 400
|
offset_top = 400
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1221,8 +1331,8 @@ text = "Gas Bomber"
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 12
|
offset_left = -60
|
||||||
offset_right = 118
|
offset_right = 60
|
||||||
offset_top = 400
|
offset_top = 400
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1233,8 +1343,8 @@ text = "Transporter"
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 94
|
offset_left = 30
|
||||||
offset_right = 224
|
offset_right = 150
|
||||||
offset_top = 400
|
offset_top = 400
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
@ -1245,14 +1355,26 @@ text = "Carrier"
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
offset_left = 200
|
offset_left = 120
|
||||||
offset_right = 330
|
offset_right = 240
|
||||||
offset_top = 400
|
offset_top = 400
|
||||||
offset_bottom = 460
|
offset_bottom = 460
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
text = "Laser Ship"
|
text = "Laser Ship"
|
||||||
|
|
||||||
|
[node name="VesselGLabel" type="Label" parent="UI/LabMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = 210
|
||||||
|
offset_right = 330
|
||||||
|
offset_top = 400
|
||||||
|
offset_bottom = 460
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = "Distorder"
|
||||||
|
|
||||||
[node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"]
|
[node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
|
|
@ -1498,7 +1620,7 @@ anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -310
|
offset_left = -310
|
||||||
offset_right = -232
|
offset_right = -245
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1511,7 +1633,7 @@ anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -330
|
offset_left = -330
|
||||||
offset_right = -212
|
offset_right = -225
|
||||||
offset_top = 56
|
offset_top = 56
|
||||||
offset_bottom = 136
|
offset_bottom = 136
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
|
|
@ -1523,8 +1645,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -202
|
offset_left = -221
|
||||||
offset_right = -124
|
offset_right = -156
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1537,8 +1659,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -222
|
offset_left = -241
|
||||||
offset_right = -104
|
offset_right = -136
|
||||||
offset_top = 56
|
offset_top = 56
|
||||||
offset_bottom = 136
|
offset_bottom = 136
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
|
|
@ -1551,8 +1673,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -94
|
offset_left = -132
|
||||||
offset_right = -16
|
offset_right = -67
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 28
|
offset_bottom = 28
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1565,8 +1687,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -114
|
offset_left = -152
|
||||||
offset_right = 4
|
offset_right = -47
|
||||||
offset_top = 56
|
offset_top = 56
|
||||||
offset_bottom = 150
|
offset_bottom = 150
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
|
|
@ -1579,8 +1701,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = 14
|
offset_left = -43
|
||||||
offset_right = 92
|
offset_right = 22
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1593,8 +1715,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -6
|
offset_left = -63
|
||||||
offset_right = 112
|
offset_right = 42
|
||||||
offset_top = 56
|
offset_top = 56
|
||||||
offset_bottom = 136
|
offset_bottom = 136
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
|
|
@ -1607,8 +1729,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = 122
|
offset_left = 46
|
||||||
offset_right = 200
|
offset_right = 111
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1621,8 +1743,8 @@ anchor_left = 0.5
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_top = 0.5
|
anchor_top = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = 102
|
offset_left = 26
|
||||||
offset_right = 220
|
offset_right = 131
|
||||||
offset_top = 56
|
offset_top = 56
|
||||||
offset_bottom = 136
|
offset_bottom = 136
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
|
|
@ -1630,6 +1752,34 @@ autowrap_mode = 2
|
||||||
text = "Laser Ship"
|
text = "Laser Ship"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="DistorderButton" type="Control" parent="UI/CraftMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = 135
|
||||||
|
offset_right = 200
|
||||||
|
offset_top = -100
|
||||||
|
offset_bottom = 48
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 37
|
||||||
|
glow_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
|
||||||
|
[node name="DistorderLabel" type="Label" parent="UI/CraftMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = 115
|
||||||
|
offset_right = 220
|
||||||
|
offset_top = 56
|
||||||
|
offset_bottom = 136
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
autowrap_mode = 2
|
||||||
|
text = "Distorder"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
|
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
|
|
@ -1936,9 +2086,9 @@ anchor_top = 1.0
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = -64.0
|
offset_left = -64.0
|
||||||
offset_top = -456.0
|
offset_top = -400.0
|
||||||
offset_right = 76.0
|
offset_right = 76.0
|
||||||
offset_bottom = -392.0
|
offset_bottom = -336.0
|
||||||
script = ExtResource("19")
|
script = ExtResource("19")
|
||||||
digit_height = 52.0
|
digit_height = 52.0
|
||||||
|
|
||||||
|
|
@ -1948,9 +2098,9 @@ anchor_right = 1.0
|
||||||
anchor_top = 1.0
|
anchor_top = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = 32.0
|
offset_left = 32.0
|
||||||
offset_top = -386.0
|
offset_top = -326.0
|
||||||
offset_right = -32.0
|
offset_right = -32.0
|
||||||
offset_bottom = -318.0
|
offset_bottom = -250.0
|
||||||
theme_override_font_sizes/font_size = 32
|
theme_override_font_sizes/font_size = 32
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
@ -1962,9 +2112,9 @@ anchor_top = 1.0
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = -74.0
|
offset_left = -74.0
|
||||||
offset_top = -304.0
|
offset_top = -236.0
|
||||||
offset_right = 74.0
|
offset_right = 74.0
|
||||||
offset_bottom = -156.0
|
offset_bottom = -88.0
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
icon = 5
|
icon = 5
|
||||||
glow_color = Color(0.45, 0.9, 0.55, 1)
|
glow_color = Color(0.45, 0.9, 0.55, 1)
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,7 @@ extends Resource
|
||||||
@export var surface_profile_id: String = "rocky"
|
@export var surface_profile_id: String = "rocky"
|
||||||
@export var resource_id: String = "ore"
|
@export var resource_id: String = "ore"
|
||||||
@export var map_color: Color = Color(0.7, 0.65, 0.55)
|
@export var map_color: Color = Color(0.7, 0.65, 0.55)
|
||||||
|
# Optional reward granted once when every authored capture point is claimed.
|
||||||
|
# Keeping it on the definition lets conquest remain generic as new planet
|
||||||
|
# rewards are introduced.
|
||||||
|
@export var conquest_research_points: int = 0
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ extends RefCounted
|
||||||
const ROCKY_ID: String = "rocky"
|
const ROCKY_ID: String = "rocky"
|
||||||
const GAS_GIANT_ID: String = "gas_giant"
|
const GAS_GIANT_ID: String = "gas_giant"
|
||||||
const BLACK_HOLE_ID: String = "black_hole"
|
const BLACK_HOLE_ID: String = "black_hole"
|
||||||
|
const RESEARCH_LAB_ID: String = "research_lab"
|
||||||
const DEFAULT_DEFINITION_ID: String = ROCKY_ID
|
const DEFAULT_DEFINITION_ID: String = ROCKY_ID
|
||||||
const DEFINITIONS: Dictionary = {
|
const DEFINITIONS: Dictionary = {
|
||||||
ROCKY_ID: preload("res://RockyPlanetDefinition.tres"),
|
ROCKY_ID: preload("res://RockyPlanetDefinition.tres"),
|
||||||
GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"),
|
GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"),
|
||||||
BLACK_HOLE_ID: preload("res://BlackHolePlanetDefinition.tres"),
|
BLACK_HOLE_ID: preload("res://BlackHolePlanetDefinition.tres"),
|
||||||
|
RESEARCH_LAB_ID: preload("res://ResearchLabPlanetDefinition.tres"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ func apply_setup(new_setup: Resource) -> void:
|
||||||
if setup == null:
|
if setup == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
GameState.register_capture_points(GameState.planet_id, setup.capture_points.size())
|
GameState.register_capture_points(GameState.planet_id, setup.capture_points.size(),
|
||||||
|
GameState.planet_definition_id)
|
||||||
for i in range(setup.capture_points.size()):
|
for i in range(setup.capture_points.size()):
|
||||||
_add_capture_point(i, setup.capture_points[i])
|
_add_capture_point(i, setup.capture_points[i])
|
||||||
for spawn in setup.enemy_spawns:
|
for spawn in setup.enemy_spawns:
|
||||||
|
|
|
||||||
42
PlanetSetups/ResearchLabSetup.tres
Normal file
42
PlanetSetups/ResearchLabSetup.tres
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
[gd_resource type="Resource" script_class="PlanetSetup" format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://PlanetSetupSpawn.gd" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://PlanetSetup.gd" id="2"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ta1"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "turret_spawner"
|
||||||
|
position = Vector2(-620.0, -180.0)
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ta2"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "turret_spawner"
|
||||||
|
position = Vector2(640.0, -160.0)
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ta3"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "artillery"
|
||||||
|
position = Vector2(0.0, -720.0)
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ta4"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "sniper"
|
||||||
|
position = Vector2(-380.0, 420.0)
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ta5"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "sniper"
|
||||||
|
position = Vector2(400.0, 440.0)
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ta6"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
enemy_id = "chaser"
|
||||||
|
position = Vector2(120.0, 60.0)
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("2")
|
||||||
|
setup_id = "research_lab_1"
|
||||||
|
display_name = "Research Lab Garrison"
|
||||||
|
surface_profile_id = "research_lab"
|
||||||
|
capture_points = PackedVector2Array(-450.0, -60.0, 460.0, -40.0, 0.0, 480.0)
|
||||||
|
enemy_spawns = Array[Resource]([SubResource("Resource_ta1"), SubResource("Resource_ta2"), SubResource("Resource_ta3"), SubResource("Resource_ta4"), SubResource("Resource_ta5"), SubResource("Resource_ta6")])
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=15 format=3]
|
[gd_scene load_steps=16 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://GameController.gd" id="1"]
|
[ext_resource type="Script" path="res://GameController.gd" id="1"]
|
||||||
[ext_resource type="Script" path="res://LunarBackground.gd" id="2"]
|
[ext_resource type="Script" path="res://LunarBackground.gd" id="2"]
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
[ext_resource type="Script" path="res://ResourceReadout.gd" id="9"]
|
[ext_resource type="Script" path="res://ResourceReadout.gd" id="9"]
|
||||||
[ext_resource type="Script" path="res://GasBombController.gd" id="14"]
|
[ext_resource type="Script" path="res://GasBombController.gd" id="14"]
|
||||||
[ext_resource type="Script" path="res://LaserController.gd" id="16"]
|
[ext_resource type="Script" path="res://LaserController.gd" id="16"]
|
||||||
|
[ext_resource type="Script" path="res://SpeedFader.gd" id="17"]
|
||||||
[ext_resource type="Script" path="res://CaptureLabel.gd" id="15"]
|
[ext_resource type="Script" path="res://CaptureLabel.gd" id="15"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="1"]
|
[sub_resource type="ShaderMaterial" id="1"]
|
||||||
|
|
@ -157,6 +158,30 @@ script = ExtResource("7")
|
||||||
glow_color = Color(0.06, 0.32, 0.5, 1)
|
glow_color = Color(0.06, 0.32, 0.5, 1)
|
||||||
core_color = Color(0.02, 0.09, 0.15, 1)
|
core_color = Color(0.02, 0.09, 0.15, 1)
|
||||||
|
|
||||||
|
[node name="SpeedFader" type="Control" parent="UI"]
|
||||||
|
visible = false
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -94
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = 740
|
||||||
|
offset_bottom = 920
|
||||||
|
script = ExtResource("17")
|
||||||
|
glow_color = Color(0.5, 0.18, 0.6, 1)
|
||||||
|
core_color = Color(0.02, 0.09, 0.15, 1)
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="UI/SpeedFader"]
|
||||||
|
mouse_filter = 2
|
||||||
|
offset_left = -20
|
||||||
|
offset_right = 90
|
||||||
|
offset_top = -32
|
||||||
|
offset_bottom = -4
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.3, 1, 1)
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = "x1.0"
|
||||||
|
|
||||||
[node name="LaserButton" type="Control" parent="UI"]
|
[node name="LaserButton" type="Control" parent="UI"]
|
||||||
visible = false
|
visible = false
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
@ -204,6 +229,38 @@ icon = 32
|
||||||
glow_color = Color(0.82, 0.08, 0.48, 1)
|
glow_color = Color(0.82, 0.08, 0.48, 1)
|
||||||
core_color = Color(0.02, 0.09, 0.15, 1)
|
core_color = Color(0.02, 0.09, 0.15, 1)
|
||||||
|
|
||||||
|
[node name="GasBombStatus" type="Label" parent="UI"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -344
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = -480
|
||||||
|
offset_bottom = -430
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.82, 0.08, 0.48, 1)
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="LaserStatus" type="Label" parent="UI"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -344
|
||||||
|
offset_right = -24
|
||||||
|
offset_top = -540
|
||||||
|
offset_bottom = -490
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.2, 0.6, 0.68, 1)
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
[node name="PivotCWButton" type="Control" parent="UI"]
|
[node name="PivotCWButton" type="Control" parent="UI"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const PROFILES: Dictionary = {
|
||||||
"rocky": preload("res://RockySurfaceProfile.tres"),
|
"rocky": preload("res://RockySurfaceProfile.tres"),
|
||||||
"gas": preload("res://GasSurfaceProfile.tres"),
|
"gas": preload("res://GasSurfaceProfile.tres"),
|
||||||
"void": preload("res://VoidSurfaceProfile.tres"),
|
"void": preload("res://VoidSurfaceProfile.tres"),
|
||||||
|
"research_lab": preload("res://ResearchLabSurfaceProfile.tres"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
15
ResearchLabPlanetDefinition.tres
Normal file
15
ResearchLabPlanetDefinition.tres
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ResearchLabPlanetVisual.tscn" id="2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
definition_id = "research_lab"
|
||||||
|
display_name = "Research Lab"
|
||||||
|
names = PackedStringArray("Blacksite Zero", "Cordoned Array", "Dead Signal Station", "The Vault", "Interdicted Post")
|
||||||
|
visual_scene = ExtResource("2")
|
||||||
|
surface_profile_id = "research_lab"
|
||||||
|
resource_id = ""
|
||||||
|
map_color = Color(0.7, 0.2, 0.15, 1)
|
||||||
|
conquest_research_points = 1
|
||||||
55
ResearchLabPlanetVisual.gd
Normal file
55
ResearchLabPlanetVisual.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Same informal interface as GasGiantVisual/RockyPlanetVisual - configure(),
|
||||||
|
# extractor_anchor(), animates via its own _process(). Deliberately angular
|
||||||
|
# and armoured rather than a natural silhouette (#63: this is an ENEMY
|
||||||
|
# installation, not a celestial body), with a rotating dish and blinking
|
||||||
|
# warning lights to read as occupied/hostile at a glance.
|
||||||
|
|
||||||
|
var phase: float = 0.0
|
||||||
|
|
||||||
|
const RADIUS: float = 108.0
|
||||||
|
const HULL_COLOR: Color = Color(0.55, 0.1, 0.08)
|
||||||
|
const CORE_COLOR: Color = Color(0.9, 0.25, 0.18)
|
||||||
|
const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15)
|
||||||
|
const DARK_CORE: Color = Color(0.14, 0.02, 0.02)
|
||||||
|
|
||||||
|
|
||||||
|
func configure(_shape_seed: int) -> void:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func extractor_anchor() -> Vector2:
|
||||||
|
return Vector2(0.0, -(RADIUS + 40.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
phase += delta
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var hull: PackedVector2Array = PackedVector2Array()
|
||||||
|
for i in range(6):
|
||||||
|
var angle: float = TAU * float(i) / 6.0
|
||||||
|
hull.append(Vector2(cos(angle), sin(angle)) * RADIUS)
|
||||||
|
NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 0.85, DARK_CORE)
|
||||||
|
|
||||||
|
var inner: PackedVector2Array = PackedVector2Array()
|
||||||
|
for i in range(6):
|
||||||
|
var angle: float = TAU * float(i) / 6.0 + PI / 6.0
|
||||||
|
inner.append(Vector2(cos(angle), sin(angle)) * RADIUS * 0.55)
|
||||||
|
NeonDraw.draw_glowing_polygon(self, inner, CORE_COLOR, 0.9, DARK_CORE)
|
||||||
|
|
||||||
|
# A slow-rotating dish - reads as actively transmitting, not derelict.
|
||||||
|
var dish_angle: float = phase * 0.6
|
||||||
|
var dish_tip: Vector2 = Vector2(cos(dish_angle), sin(dish_angle)) * RADIUS * 1.35
|
||||||
|
NeonDraw.draw_glowing_line(self, Vector2.ZERO, dish_tip, CORE_COLOR, 0.8, 0.55, DARK_CORE)
|
||||||
|
NeonDraw.draw_glowing_circle(self, dish_tip, 11.0, CORE_COLOR, 1.0, DARK_CORE)
|
||||||
|
|
||||||
|
# Blinking hull-corner warning lights.
|
||||||
|
var blink: float = 0.4 + 0.6 * absf(sin(phase * 4.0))
|
||||||
|
for i in range(6):
|
||||||
|
var angle: float = TAU * float(i) / 6.0
|
||||||
|
var at: Vector2 = Vector2(cos(angle), sin(angle)) * RADIUS
|
||||||
|
NeonDraw.draw_glowing_circle(self, at, 5.0, WARNING_COLOR, blink, DARK_CORE)
|
||||||
1
ResearchLabPlanetVisual.gd.uid
Normal file
1
ResearchLabPlanetVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c6nasspc8ewwj
|
||||||
6
ResearchLabPlanetVisual.tscn
Normal file
6
ResearchLabPlanetVisual.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ResearchLabPlanetVisual.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Visual" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
12
ResearchLabSurfaceProfile.tres
Normal file
12
ResearchLabSurfaceProfile.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
profile_id = "research_lab"
|
||||||
|
base_color = Color(0.13, 0.1, 0.1, 1)
|
||||||
|
crater_color = Color(0.05, 0.03, 0.03, 1)
|
||||||
|
highlight_color = Color(0.55, 0.15, 0.1, 1)
|
||||||
|
caption_color = Color(0.9, 0.6, 0.55, 1)
|
||||||
|
caption_outline_color = Color(0.05, 0.02, 0.02, 0.9)
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Layered Emitters"
|
||||||
description = "+1 hit absorbed before the shield drops."
|
description = "+1 hit absorbed before the shield drops."
|
||||||
base_cost = 60
|
base_cost = 60
|
||||||
max_level = 2
|
max_level = 2
|
||||||
|
research_point_cost = 1
|
||||||
icon = 19
|
icon = 19
|
||||||
additive_modifiers = {"shield_charges": 1.0}
|
additive_modifiers = {"shield_charges": 1.0}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Fast Cycling"
|
||||||
description = "Shield returns 1.2s sooner per level."
|
description = "Shield returns 1.2s sooner per level."
|
||||||
base_cost = 35
|
base_cost = 35
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 20
|
icon = 20
|
||||||
additive_modifiers = {"shield_recharge": -1.2}
|
additive_modifiers = {"shield_recharge": -1.2}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Thrusters"
|
||||||
description = "+40 top speed, +60 acceleration per level."
|
description = "+40 top speed, +60 acceleration per level."
|
||||||
base_cost = 25
|
base_cost = 25
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 18
|
icon = 18
|
||||||
additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0}
|
additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0}
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,5 @@ unlock_id = "unlock_shield"
|
||||||
unlock_name = "Unlock Shield Vessel"
|
unlock_name = "Unlock Shield Vessel"
|
||||||
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
|
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
|
||||||
unlock_cost = 45
|
unlock_cost = 45
|
||||||
|
research_point_cost = 1
|
||||||
unlock_icon = 23
|
unlock_icon = 23
|
||||||
|
|
|
||||||
21
Ship.gd
21
Ship.gd
|
|
@ -97,16 +97,29 @@ func _physics_process(delta: float) -> void:
|
||||||
if child.has_method("tick"):
|
if child.has_method("tick"):
|
||||||
child.tick(delta)
|
child.tick(delta)
|
||||||
|
|
||||||
|
# #59: multiplied here, at the point of use, rather than baked into the
|
||||||
|
# cached max_speed/acceleration fields apply_vessel_stats() sets - those
|
||||||
|
# stay the true base values, so a stat upgrade researched mid-boost can't
|
||||||
|
# corrupt them, and the fader takes effect the instant it moves with no
|
||||||
|
# re-apply needed. arrive_radius is scaled too: at N physics ticks/sec a
|
||||||
|
# ship's per-tick step is `speed * delta`, and once that step outgrows
|
||||||
|
# the (otherwise fixed) arrive_radius it can overshoot the snap-to-target
|
||||||
|
# zone every tick while chasing a target that is ALSO now receding N
|
||||||
|
# times faster - a small, boost-proportional radius keeps the same
|
||||||
|
# "snaps onto its slot" formation tightness at any boost level that
|
||||||
|
# 1.0x already has.
|
||||||
|
var boost: float = GameState.fleet_speed_multiplier
|
||||||
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 * boost:
|
||||||
position = move_target
|
position = move_target
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
else:
|
else:
|
||||||
var braking_speed: float = sqrt(2.0 * acceleration * distance)
|
var effective_acceleration: float = acceleration * boost
|
||||||
var desired_speed: float = min(max_speed, braking_speed)
|
var braking_speed: float = sqrt(2.0 * effective_acceleration * distance)
|
||||||
|
var desired_speed: float = min(max_speed * boost, 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, effective_acceleration * delta)
|
||||||
position += velocity * delta
|
position += velocity * delta
|
||||||
|
|
||||||
var motion: Vector2 = velocity + fleet_velocity
|
var motion: Vector2 = velocity + fleet_velocity
|
||||||
|
|
|
||||||
125
SpeedFader.gd
Normal file
125
SpeedFader.gd
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
class_name SpeedFader
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
# Vertical drag control for GameState.fleet_speed_multiplier. Same raw-touch
|
||||||
|
# idiom as TouchJoystick (handle_touch/handle_drag offered first by
|
||||||
|
# GameController, not _gui_input) and the same NeonDraw look, but a fader
|
||||||
|
# rather than a stick: the knob's Y position along a fixed vertical track IS
|
||||||
|
# the value, no spring-back to centre.
|
||||||
|
#
|
||||||
|
# Self-contained: writes straight to GameState.fleet_speed_multiplier rather
|
||||||
|
# than reporting a value for something else to apply, since the multiplier
|
||||||
|
# already lives in the one place (the autoload) that needs to survive scene
|
||||||
|
# swaps.
|
||||||
|
|
||||||
|
@export var glow_color: Color = Color(0.7, 0.3, 1.0)
|
||||||
|
@export var core_color: Color = Color(1.0, 1.0, 1.0)
|
||||||
|
@export var track_margin: float = 28.0
|
||||||
|
|
||||||
|
@onready var fleet: Node2D = get_node("../../Fleet")
|
||||||
|
@onready var label: Label = get_node("Label")
|
||||||
|
|
||||||
|
var active_touch_index: int = -1
|
||||||
|
# 0 = bottom of the track (1.0x), 1 = top (GameState.MAX_FLEET_SPEED_MULTIPLIER).
|
||||||
|
var knob_t: float = 0.0
|
||||||
|
var status_message_until: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
visible = false
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
var has_distorder: bool = fleet.has_capability("distorder")
|
||||||
|
if has_distorder != visible:
|
||||||
|
visible = has_distorder
|
||||||
|
if not has_distorder:
|
||||||
|
# Fleet owns persistent composition state. This outgoing UI node can
|
||||||
|
# briefly observe an empty fleet during a scene swap, so it must only
|
||||||
|
# clear its own interaction state here.
|
||||||
|
active_touch_index = -1
|
||||||
|
knob_t = 0.0
|
||||||
|
return
|
||||||
|
knob_t = (GameState.fleet_speed_multiplier - 1.0) \
|
||||||
|
/ maxf(GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0, 0.001)
|
||||||
|
# #62: GameState._process() (not this node's own _process()) is what
|
||||||
|
# drains energy and force-resets the multiplier, since it has to keep
|
||||||
|
# running across the space<->surface swap and while this control might
|
||||||
|
# not even be in the current scene tree. It leaves a one-shot flag for
|
||||||
|
# whichever SpeedFader picks it up next.
|
||||||
|
if GameState.boost_depleted_notice:
|
||||||
|
GameState.boost_depleted_notice = false
|
||||||
|
# Still holding the drag at the moment energy ran out would
|
||||||
|
# otherwise re-boost on the very next drag event, from a knob
|
||||||
|
# position the finger never actually moved away from.
|
||||||
|
active_touch_index = -1
|
||||||
|
label.text = "Out of energy!"
|
||||||
|
status_message_until = Time.get_ticks_msec() + 3000
|
||||||
|
elif Time.get_ticks_msec() >= status_message_until:
|
||||||
|
label.text = "x%.1f" % GameState.fleet_speed_multiplier
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return active_touch_index != -1
|
||||||
|
|
||||||
|
|
||||||
|
func _blocked_by_modal() -> bool:
|
||||||
|
# Same check as IconButton._blocked_by_modal() - panels stay transparent
|
||||||
|
# to raw input (the skill tree/formation editor need _unhandled_input),
|
||||||
|
# so every raw-touch control checks for itself instead.
|
||||||
|
for menu in get_tree().get_nodes_in_group("modal_menus"):
|
||||||
|
if menu.is_visible_in_tree() and not menu.is_ancestor_of(self):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||||
|
var local: Vector2 = event.position - global_position
|
||||||
|
if event.pressed:
|
||||||
|
if not visible or active_touch_index != -1 or _blocked_by_modal():
|
||||||
|
return false
|
||||||
|
if not Rect2(Vector2.ZERO, size).has_point(local):
|
||||||
|
return false
|
||||||
|
active_touch_index = event.index
|
||||||
|
_update_from_local(local)
|
||||||
|
return true
|
||||||
|
if event.index != active_touch_index:
|
||||||
|
return false
|
||||||
|
active_touch_index = -1
|
||||||
|
queue_redraw()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func handle_drag(event: InputEventScreenDrag) -> bool:
|
||||||
|
if event.index != active_touch_index:
|
||||||
|
return false
|
||||||
|
_update_from_local(event.position - global_position)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _update_from_local(local: Vector2) -> void:
|
||||||
|
var usable: float = maxf(size.y - track_margin * 2.0, 1.0)
|
||||||
|
# Up is 1.0 (top of the track), matching how a real fader/throttle reads.
|
||||||
|
var t: float = 1.0 - clampf((local.y - track_margin) / usable, 0.0, 1.0)
|
||||||
|
knob_t = t
|
||||||
|
GameState.set_fleet_speed_multiplier(1.0 + t * (GameState.MAX_FLEET_SPEED_MULTIPLIER - 1.0))
|
||||||
|
label.text = "x%.1f" % GameState.fleet_speed_multiplier
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var top: Vector2 = Vector2(size.x * 0.5, track_margin)
|
||||||
|
var bottom: Vector2 = Vector2(size.x * 0.5, size.y - track_margin)
|
||||||
|
NeonDraw.draw_glowing_line(self, bottom, top, glow_color, 0.55, 0.5, core_color)
|
||||||
|
|
||||||
|
# Tick marks at the extremes read as "min" and "max" at a glance.
|
||||||
|
for t in [0.0, 1.0]:
|
||||||
|
var y: float = lerpf(bottom.y, top.y, t)
|
||||||
|
NeonDraw.draw_glowing_line(self, Vector2(size.x * 0.5 - 12.0, y),
|
||||||
|
Vector2(size.x * 0.5 + 12.0, y), glow_color, 0.4, 0.35, core_color)
|
||||||
|
|
||||||
|
var knob_pos: Vector2 = bottom.lerp(top, clampf(knob_t, 0.0, 1.0))
|
||||||
|
NeonDraw.draw_glowing_circle(self, knob_pos, 16.0, glow_color,
|
||||||
|
1.0 if is_active() else 0.8, core_color)
|
||||||
1
SpeedFader.gd.uid
Normal file
1
SpeedFader.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://m16rgs2ed7w1
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Expanded Hold"
|
||||||
description = "Carries 8 more resources per supply run."
|
description = "Carries 8 more resources per supply run."
|
||||||
base_cost = 30
|
base_cost = 30
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 21
|
icon = 21
|
||||||
additive_modifiers = {"cargo_capacity": 8.0}
|
additive_modifiers = {"cargo_capacity": 8.0}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ display_name = "Freight Engines"
|
||||||
description = "+70 speed on supply runs."
|
description = "+70 speed on supply runs."
|
||||||
base_cost = 30
|
base_cost = 30
|
||||||
max_level = 3
|
max_level = 3
|
||||||
|
research_point_cost = 1
|
||||||
icon = 22
|
icon = 22
|
||||||
additive_modifiers = {"transport_speed": 70.0}
|
additive_modifiers = {"transport_speed": 70.0}
|
||||||
|
|
|
||||||
84
VesselCooldownFillComponent.gd
Normal file
84
VesselCooldownFillComponent.gd
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
class_name VesselCooldownFillComponent
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Shared per-vessel cooldown state and hull-fill feedback for abilities that
|
||||||
|
# grant one independently recharging shot per ship.
|
||||||
|
|
||||||
|
var cooldown_duration: float = 1.0
|
||||||
|
var cooldown_left: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func setup() -> void:
|
||||||
|
show_behind_parent = true
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func is_ready() -> bool:
|
||||||
|
return cooldown_left <= 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _begin_cooldown(duration: float) -> bool:
|
||||||
|
if not is_ready():
|
||||||
|
return false
|
||||||
|
cooldown_duration = maxf(duration, 0.001)
|
||||||
|
cooldown_left = cooldown_duration
|
||||||
|
queue_redraw()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func tick(delta: float) -> void:
|
||||||
|
if cooldown_left <= 0.0:
|
||||||
|
return
|
||||||
|
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func cooldown_progress() -> float:
|
||||||
|
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
||||||
|
|
||||||
|
|
||||||
|
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
|
||||||
|
if points.size() < 3 or progress <= 0.001:
|
||||||
|
return PackedVector2Array()
|
||||||
|
if progress >= 0.999:
|
||||||
|
return points.duplicate()
|
||||||
|
var top: float = INF
|
||||||
|
var bottom: float = -INF
|
||||||
|
for point in points:
|
||||||
|
top = minf(top, point.y)
|
||||||
|
bottom = maxf(bottom, point.y)
|
||||||
|
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
||||||
|
var clipped := PackedVector2Array()
|
||||||
|
var previous: Vector2 = points[-1]
|
||||||
|
var previous_inside: bool = previous.y >= fill_y
|
||||||
|
for current in points:
|
||||||
|
var current_inside: bool = current.y >= fill_y
|
||||||
|
if current_inside != previous_inside:
|
||||||
|
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
|
||||||
|
clipped.append(previous.lerp(current, amount))
|
||||||
|
if current_inside:
|
||||||
|
clipped.append(current)
|
||||||
|
previous = current
|
||||||
|
previous_inside = current_inside
|
||||||
|
return clipped
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var ship: Node = get_parent()
|
||||||
|
if ship == null or ship.definition == null:
|
||||||
|
return
|
||||||
|
var hull: PackedVector2Array = ship.definition.hull_points
|
||||||
|
var ready_color: Color = _ready_color()
|
||||||
|
if is_ready():
|
||||||
|
draw_colored_polygon(hull, Color(ready_color, 0.42))
|
||||||
|
NeonDraw.draw_glowing_polygon(self, hull, ready_color, 0.75,
|
||||||
|
ship.core_color, 0.75)
|
||||||
|
return
|
||||||
|
draw_colored_polygon(hull, Color(_cooling_color(), 0.58))
|
||||||
|
var filled: PackedVector2Array = _filled_hull(hull, cooldown_progress())
|
||||||
|
if filled.size() >= 3:
|
||||||
|
draw_colored_polygon(filled, Color(ready_color, 0.68))
|
||||||
|
|
||||||
|
|
||||||
|
func _ready_color() -> Color: return Color.WHITE
|
||||||
|
func _cooling_color() -> Color: return Color(0.2, 0.2, 0.2)
|
||||||
1
VesselCooldownFillComponent.gd.uid
Normal file
1
VesselCooldownFillComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://drsagaj2wr0ni
|
||||||
|
|
@ -23,6 +23,10 @@ extends Resource
|
||||||
@export_multiline var unlock_description: String
|
@export_multiline var unlock_description: String
|
||||||
@export var unlock_cost: int
|
@export var unlock_cost: int
|
||||||
@export var unlock_resource_costs: Dictionary = {}
|
@export var unlock_resource_costs: Dictionary = {}
|
||||||
|
# #63: spent from GameState.research_points, alongside unlock_resource_costs
|
||||||
|
# rather than instead of it. 0 (the default) means this unlock needs no
|
||||||
|
# research points at all.
|
||||||
|
@export var research_point_cost: int = 0
|
||||||
@export var unlock_icon: int = -1
|
@export var unlock_icon: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
class_name VesselDefinitions
|
class_name VesselDefinitions
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser"]
|
const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser", "distorder"]
|
||||||
const DEFINITIONS: Dictionary = {
|
const DEFINITIONS: Dictionary = {
|
||||||
"interceptor": preload("res://InterceptorVesselDefinition.tres"),
|
"interceptor": preload("res://InterceptorVesselDefinition.tres"),
|
||||||
"shield": preload("res://ShieldVesselDefinition.tres"),
|
"shield": preload("res://ShieldVesselDefinition.tres"),
|
||||||
|
|
@ -9,6 +9,7 @@ const DEFINITIONS: Dictionary = {
|
||||||
"transporter": preload("res://TransporterVesselDefinition.tres"),
|
"transporter": preload("res://TransporterVesselDefinition.tres"),
|
||||||
"carrier": preload("res://CarrierVesselDefinition.tres"),
|
"carrier": preload("res://CarrierVesselDefinition.tres"),
|
||||||
"laser": preload("res://LaserVesselDefinition.tres"),
|
"laser": preload("res://LaserVesselDefinition.tres"),
|
||||||
|
"distorder": preload("res://DistorderVesselDefinition.tres"),
|
||||||
}
|
}
|
||||||
|
|
||||||
static func get_definition(definition_id: String) -> Resource:
|
static func get_definition(definition_id: String) -> Resource:
|
||||||
|
|
|
||||||
9
VesselDistorderComponent.gd
Normal file
9
VesselDistorderComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
class_name VesselDistorderComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Pure presence marker (#59) - no per-shot behaviour, no tick(). Its only
|
||||||
|
# job is to make Ship.has_capability("distorder") true, which is what
|
||||||
|
# SpeedFader.gd/Fleet.gd key off to reveal and apply the speed boost.
|
||||||
|
# Energy drain while boosted is a later ticket (#62).
|
||||||
|
|
||||||
|
var capability_id := "distorder"
|
||||||
1
VesselDistorderComponent.gd.uid
Normal file
1
VesselDistorderComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c2una2fh0f06b
|
||||||
6
VesselDistorderComponent.tscn
Normal file
6
VesselDistorderComponent.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://VesselDistorderComponent.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="DistorderComponent" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
@ -1,84 +1,19 @@
|
||||||
class_name VesselGasBomberComponent
|
class_name VesselGasBomberComponent
|
||||||
extends Node2D
|
extends "res://VesselCooldownFillComponent.gd"
|
||||||
|
|
||||||
var capability_id := "gas_bomber"
|
var capability_id := "gas_bomber"
|
||||||
var cooldown_duration := 5.0
|
|
||||||
var cooldown_left := 0.0
|
|
||||||
|
|
||||||
const READY_COLOR := Color(1.0, 0.25, 0.78)
|
const READY_COLOR := Color(1.0, 0.25, 0.78)
|
||||||
const COOLING_COLOR := Color(0.38, 0.16, 0.32)
|
const COOLING_COLOR := Color(0.38, 0.16, 0.32)
|
||||||
|
|
||||||
|
|
||||||
func setup() -> void:
|
func _init() -> void:
|
||||||
# Keep the ship's normal neon outline above the charge fill.
|
cooldown_duration = 5.0
|
||||||
show_behind_parent = true
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func is_ready() -> bool:
|
|
||||||
return cooldown_left <= 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func consume_shot(duration: float) -> bool:
|
func consume_shot(duration: float) -> bool:
|
||||||
if not is_ready():
|
return _begin_cooldown(duration)
|
||||||
return false
|
|
||||||
cooldown_duration = maxf(duration, 0.001)
|
|
||||||
cooldown_left = cooldown_duration
|
|
||||||
queue_redraw()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func tick(delta: float) -> void:
|
func _ready_color() -> Color: return READY_COLOR
|
||||||
if cooldown_left <= 0.0:
|
func _cooling_color() -> Color: return COOLING_COLOR
|
||||||
return
|
|
||||||
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func cooldown_progress() -> float:
|
|
||||||
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
|
||||||
|
|
||||||
|
|
||||||
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
|
|
||||||
if points.size() < 3 or progress <= 0.001:
|
|
||||||
return PackedVector2Array()
|
|
||||||
if progress >= 0.999:
|
|
||||||
return points.duplicate()
|
|
||||||
var top := INF
|
|
||||||
var bottom := -INF
|
|
||||||
for point in points:
|
|
||||||
top = minf(top, point.y)
|
|
||||||
bottom = maxf(bottom, point.y)
|
|
||||||
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
|
||||||
var clipped := PackedVector2Array()
|
|
||||||
var previous: Vector2 = points[-1]
|
|
||||||
var previous_inside := previous.y >= fill_y
|
|
||||||
for current in points:
|
|
||||||
var current_inside := current.y >= fill_y
|
|
||||||
if current_inside != previous_inside:
|
|
||||||
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
|
|
||||||
clipped.append(previous.lerp(current, amount))
|
|
||||||
if current_inside:
|
|
||||||
clipped.append(current)
|
|
||||||
previous = current
|
|
||||||
previous_inside = current_inside
|
|
||||||
return clipped
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
var parent_ship: Node = get_parent()
|
|
||||||
if parent_ship == null or parent_ship.definition == null:
|
|
||||||
return
|
|
||||||
var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE
|
|
||||||
var hull: PackedVector2Array = parent_ship.definition.hull_points
|
|
||||||
if is_ready():
|
|
||||||
# A fully lit hull means this individual bomber owns an available shot.
|
|
||||||
draw_colored_polygon(hull, Color(READY_COLOR, 0.42))
|
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75)
|
|
||||||
return
|
|
||||||
# Rearming is shown inside the bomber instead of as a shield-like ring:
|
|
||||||
# the muted silhouette fills from its engines toward its nose.
|
|
||||||
draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58))
|
|
||||||
var filled := _filled_hull(hull, cooldown_progress())
|
|
||||||
if filled.size() >= 3:
|
|
||||||
draw_colored_polygon(filled, Color(READY_COLOR, 0.68))
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,16 @@
|
||||||
class_name VesselLaserComponent
|
class_name VesselLaserComponent
|
||||||
extends Node2D
|
extends "res://VesselCooldownFillComponent.gd"
|
||||||
|
|
||||||
# Same shape as VesselGasBomberComponent (per-ship cooldown, ready/cooling
|
|
||||||
# hull fill) - #58's own note: two laser vessels must give two shots, so
|
|
||||||
# this lives on the SHIP, not the fleet-wide LaserController.
|
|
||||||
#
|
|
||||||
# Unlike the gas bomber, damage/beam_range/cooldown are resolved from the
|
|
||||||
# vessel's own base_stats/upgrades (apply_stats(), the same pattern
|
|
||||||
# VesselWeaponComponent uses) rather than hardcoded in the controller, so
|
|
||||||
# ship-lab upgrades actually reach them.
|
|
||||||
|
|
||||||
var capability_id := "laser"
|
var capability_id := "laser"
|
||||||
var damage: int = 2
|
var damage: int = 2
|
||||||
var beam_range: float = 950.0
|
var beam_range: float = 950.0
|
||||||
var cooldown_duration: float = 4.0
|
|
||||||
var cooldown_left: float = 0.0
|
|
||||||
|
|
||||||
const READY_COLOR := Color(0.3, 0.9, 1.0)
|
const READY_COLOR := Color(0.3, 0.9, 1.0)
|
||||||
const COOLING_COLOR := Color(0.08, 0.28, 0.34)
|
const COOLING_COLOR := Color(0.08, 0.28, 0.34)
|
||||||
|
|
||||||
|
|
||||||
func setup() -> void:
|
func _init() -> void:
|
||||||
# Keep the ship's normal neon outline above the charge fill.
|
cooldown_duration = 4.0
|
||||||
show_behind_parent = true
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func apply_stats() -> void:
|
func apply_stats() -> void:
|
||||||
|
|
@ -35,66 +22,9 @@ func apply_stats() -> void:
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func is_ready() -> bool:
|
|
||||||
return cooldown_left <= 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func consume_shot() -> bool:
|
func consume_shot() -> bool:
|
||||||
if not is_ready():
|
return _begin_cooldown(cooldown_duration)
|
||||||
return false
|
|
||||||
cooldown_left = maxf(cooldown_duration, 0.001)
|
|
||||||
queue_redraw()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func tick(delta: float) -> void:
|
func _ready_color() -> Color: return READY_COLOR
|
||||||
if cooldown_left <= 0.0:
|
func _cooling_color() -> Color: return COOLING_COLOR
|
||||||
return
|
|
||||||
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func cooldown_progress() -> float:
|
|
||||||
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
|
||||||
|
|
||||||
|
|
||||||
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
|
|
||||||
if points.size() < 3 or progress <= 0.001:
|
|
||||||
return PackedVector2Array()
|
|
||||||
if progress >= 0.999:
|
|
||||||
return points.duplicate()
|
|
||||||
var top := INF
|
|
||||||
var bottom := -INF
|
|
||||||
for point in points:
|
|
||||||
top = minf(top, point.y)
|
|
||||||
bottom = maxf(bottom, point.y)
|
|
||||||
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
|
||||||
var clipped := PackedVector2Array()
|
|
||||||
var previous: Vector2 = points[-1]
|
|
||||||
var previous_inside := previous.y >= fill_y
|
|
||||||
for current in points:
|
|
||||||
var current_inside := current.y >= fill_y
|
|
||||||
if current_inside != previous_inside:
|
|
||||||
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
|
|
||||||
clipped.append(previous.lerp(current, amount))
|
|
||||||
if current_inside:
|
|
||||||
clipped.append(current)
|
|
||||||
previous = current
|
|
||||||
previous_inside = current_inside
|
|
||||||
return clipped
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
var parent_ship: Node = get_parent()
|
|
||||||
if parent_ship == null or parent_ship.definition == null:
|
|
||||||
return
|
|
||||||
var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE
|
|
||||||
var hull: PackedVector2Array = parent_ship.definition.hull_points
|
|
||||||
if is_ready():
|
|
||||||
draw_colored_polygon(hull, Color(READY_COLOR, 0.42))
|
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75)
|
|
||||||
return
|
|
||||||
draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58))
|
|
||||||
var filled := _filled_hull(hull, cooldown_progress())
|
|
||||||
if filled.size() >= 3:
|
|
||||||
draw_colored_polygon(filled, Color(READY_COLOR, 0.68))
|
|
||||||
|
|
|
||||||
1
VesselLaserComponent.gd.uid
Normal file
1
VesselLaserComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bds5gccvd8dj8
|
||||||
|
|
@ -9,3 +9,7 @@ extends Resource
|
||||||
@export var icon: int
|
@export var icon: int
|
||||||
@export var additive_modifiers: Dictionary = {}
|
@export var additive_modifiers: Dictionary = {}
|
||||||
@export var multiplicative_modifiers: Dictionary = {}
|
@export var multiplicative_modifiers: Dictionary = {}
|
||||||
|
# #63: spent from GameState.research_points per level bought, alongside
|
||||||
|
# base_cost (ore) rather than instead of it. 0 (the default) means this
|
||||||
|
# level needs no research points at all.
|
||||||
|
@export var research_point_cost: int = 0
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ const GAS_GIANT_THRESHOLD: float = 0.55
|
||||||
# chance independent of biome - not a second smooth region to tune against
|
# chance independent of biome - not a second smooth region to tune against
|
||||||
# the first.
|
# the first.
|
||||||
const BLACK_HOLE_CHANCE: float = 0.05
|
const BLACK_HOLE_CHANCE: float = 0.05
|
||||||
|
# #63: findable without excessive exploration - research points gate mid/
|
||||||
|
# late skill-tree nodes and vessel research, so a player needs to run into
|
||||||
|
# one of these reasonably often, not just as a rare curiosity.
|
||||||
|
const RESEARCH_LAB_CHANCE: float = 0.08
|
||||||
|
|
||||||
# A predictable early gas source for play-testing. The origin chunk is
|
# A predictable early gas source for play-testing. The origin chunk is
|
||||||
# deterministically normal space. Its explicit in-chunk position puts it
|
# deterministically normal space. Its explicit in-chunk position puts it
|
||||||
|
|
@ -95,6 +99,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
|
||||||
# roll doesn't shift which random values any LATER draw (planet_name,
|
# roll doesn't shift which random values any LATER draw (planet_name,
|
||||||
# below) consumes for chunks that would have existed before it.
|
# below) consumes for chunks that would have existed before it.
|
||||||
var black_hole_roll: float = rng.randf()
|
var black_hole_roll: float = rng.randf()
|
||||||
|
# Same principle, its own independent roll (#63).
|
||||||
|
var research_lab_roll: float = rng.randf()
|
||||||
# A stable per-chunk roll makes gas giants persistent landmarks rather
|
# A stable per-chunk roll makes gas giants persistent landmarks rather
|
||||||
# than a different resource whenever the scene reloads.
|
# than a different resource whenever the scene reloads.
|
||||||
var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE
|
var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE
|
||||||
|
|
@ -104,6 +110,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
|
||||||
# that chunk's whole purpose is to always be the same reachable source.
|
# that chunk's whole purpose is to always be the same reachable source.
|
||||||
if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE:
|
if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE:
|
||||||
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
|
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
|
||||||
|
elif not guaranteed_nearby_giant and research_lab_roll < RESEARCH_LAB_CHANCE:
|
||||||
|
definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID
|
||||||
var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id)
|
var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id)
|
||||||
var names: PackedStringArray = definition.names
|
var names: PackedStringArray = definition.names
|
||||||
var planet_name: String = names[rng.randi() % names.size()]
|
var planet_name: String = names[rng.randi() % names.size()]
|
||||||
|
|
|
||||||
1
gas_surface.gdshader.uid
Normal file
1
gas_surface.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bhxcvtw6tcshl
|
||||||
1
tests/_black_hole_space_preview.gd.uid
Normal file
1
tests/_black_hole_space_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://25xjfqi2qk7a
|
||||||
1
tests/_carrier_crafter_preview.gd.uid
Normal file
1
tests/_carrier_crafter_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://f5a8wm4wr2i4
|
||||||
1
tests/_carrier_hangar_preview.gd.uid
Normal file
1
tests/_carrier_hangar_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b5ah2jh0i16du
|
||||||
1
tests/_fleet_cargo_limit_preview.gd.uid
Normal file
1
tests/_fleet_cargo_limit_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://brt82kn8lb3u5
|
||||||
1
tests/_gas_burner_build_menu_preview.gd.uid
Normal file
1
tests/_gas_burner_build_menu_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bs6hguk0tkc6c
|
||||||
1
tests/_gas_burner_transfer_preview.gd.uid
Normal file
1
tests/_gas_burner_transfer_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://fd1m4excjdbn
|
||||||
1
tests/_laser_aim_preview.gd.uid
Normal file
1
tests/_laser_aim_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c0q640rosb53o
|
||||||
1
tests/_mothership_beam_preview.gd.uid
Normal file
1
tests/_mothership_beam_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c8lgxg63d8s52
|
||||||
1
tests/_resource_readout_surface_preview.gd.uid
Normal file
1
tests/_resource_readout_surface_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cy6n62kcp7q1y
|
||||||
1
tests/_rocky_surface_preview.gd.uid
Normal file
1
tests/_rocky_surface_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c0yjf3qdv31eo
|
||||||
|
|
@ -8,7 +8,13 @@ extends Node2D
|
||||||
# ... and again at 720x960 to check a shorter screen format.
|
# ... and again at 720x960 to check a shorter screen format.
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
var configured: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if configured:
|
||||||
|
return
|
||||||
|
configured = true
|
||||||
GameState.buildings.clear()
|
GameState.buildings.clear()
|
||||||
GameState.extractors.clear()
|
GameState.extractors.clear()
|
||||||
GameState.transporters.clear()
|
GameState.transporters.clear()
|
||||||
|
|
@ -16,9 +22,12 @@ func _ready() -> void:
|
||||||
GameState.gas = 36
|
GameState.gas = 36
|
||||||
GameState.fleet_cargo = 24
|
GameState.fleet_cargo = 24
|
||||||
GameState.fleet_gas_cargo = 12
|
GameState.fleet_gas_cargo = 12
|
||||||
GameState.unlocked = ["shipyards", "logistics", "freight"]
|
GameState.research_points = 0
|
||||||
|
GameState.unlocked = ["shipyards"]
|
||||||
var main: Node = load("res://Main.tscn").instantiate()
|
var main: Node = load("res://Main.tscn").instantiate()
|
||||||
add_child(main)
|
add_child(main)
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
|
main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
|
||||||
var mothership_id: int = int(GameState.buildings[-1].id)
|
var mothership_id: int = int(GameState.buildings[-1].id)
|
||||||
var bc: Node = main.get_node("BuildController")
|
var bc: Node = main.get_node("BuildController")
|
||||||
|
|
@ -27,5 +36,5 @@ func _ready() -> void:
|
||||||
main.get_node("UI/SkillMenu").visible = true
|
main.get_node("UI/SkillMenu").visible = true
|
||||||
# Select a node so the description, price and status all have text - an
|
# Select a node so the description, price and status all have text - an
|
||||||
# empty description cannot collide with anything.
|
# empty description cannot collide with anything.
|
||||||
bc.skill_tree.selected_id = "freight"
|
bc.skill_tree.selected_id = "coordination"
|
||||||
bc._refresh_skill_menu("freight")
|
bc._refresh_skill_menu("coordination")
|
||||||
|
|
|
||||||
1
tests/_turret_vs_chaser_preview.gd.uid
Normal file
1
tests/_turret_vs_chaser_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dfi84x2tcwqwy
|
||||||
1
tests/_void_surface_preview.gd.uid
Normal file
1
tests/_void_surface_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://clf1bitxms0ti
|
||||||
1
tests/test_black_hole.gd.uid
Normal file
1
tests/test_black_hole.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://vlcbg3juhpy3
|
||||||
|
|
@ -48,6 +48,7 @@ func _initialize() -> void:
|
||||||
game_state.reset_progression()
|
game_state.reset_progression()
|
||||||
game_state.build_time_scale = 0.02
|
game_state.build_time_scale = 0.02
|
||||||
game_state.rocks = 500
|
game_state.rocks = 500
|
||||||
|
game_state.research_points = 1
|
||||||
scene = load("res://Main.tscn").instantiate()
|
scene = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
fleet = scene.get_node("Fleet")
|
fleet = scene.get_node("Fleet")
|
||||||
|
|
|
||||||
1
tests/test_carrier_vessel.gd.uid
Normal file
1
tests/test_carrier_vessel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://gj02xu88lxpm
|
||||||
150
tests/test_distorder_energy.gd
Normal file
150
tests/test_distorder_energy.gd
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless gameplay test: run with
|
||||||
|
# godot --headless -s res://tests/test_distorder_energy.gd
|
||||||
|
# Forgejo #62: sustaining a boost above the fleet's default speed drains
|
||||||
|
# energy from the fleet hold, continuously, proportional to how far above
|
||||||
|
# 1.0x the multiplier is.
|
||||||
|
#
|
||||||
|
# Most of this is pure GameState math (GameState._advance_distorder_drain,
|
||||||
|
# called directly the same way other tests call _advance_lab_research/
|
||||||
|
# _advance_production) - no Main.tscn/Fleet needed for it, which is itself
|
||||||
|
# proof the drain is scene-independent (it lives in the autoload's own
|
||||||
|
# _process(), unlike a component's tick() which only runs while its ship's
|
||||||
|
# scene is loaded) and so "continues correctly across a landing and
|
||||||
|
# return" without a scene-swap simulation to re-prove that.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. sustained boost drains energy at the configured rate; half boost
|
||||||
|
# drains proportionally less than full boost
|
||||||
|
# B. a small multiplier still drains a nonzero amount over time - no
|
||||||
|
# fractional rounding loss
|
||||||
|
# D. at exactly 1.0x, energy is never consumed
|
||||||
|
# C. hitting zero energy forces the multiplier back to 1.0, and the
|
||||||
|
# SpeedFader visibly follows it down with the reason shown
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var main: Node
|
||||||
|
var fleet: Node
|
||||||
|
var fader: Node
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.fleet_speed_multiplier = 1.0
|
||||||
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 1000)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
# --- D. nothing drains at exactly 1.0x ----------------------------
|
||||||
|
var before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
for i in range(60):
|
||||||
|
game_state._advance_distorder_drain(1.0)
|
||||||
|
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != before:
|
||||||
|
return _fail_now("energy drained at 1.0x: %d -> %d" \
|
||||||
|
% [before, game_state.fleet_resource(GameState.RESOURCE_ENERGY)])
|
||||||
|
print("no drain at 1.0x, even over 60 simulated seconds")
|
||||||
|
|
||||||
|
# --- A. full boost drains at the configured rate ------------------
|
||||||
|
game_state.fleet_speed_multiplier = GameState.MAX_FLEET_SPEED_MULTIPLIER
|
||||||
|
var full_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
for i in range(10):
|
||||||
|
game_state._advance_distorder_drain(1.0)
|
||||||
|
var full_drained: int = full_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
if full_drained != 20:
|
||||||
|
return _fail_now("full boost drained %d over 10s, expected 20 (ENERGY_DRAIN_PER_SECOND=2.0)" \
|
||||||
|
% full_drained)
|
||||||
|
print("full boost drained %d over 10s, matches ENERGY_DRAIN_PER_SECOND" % full_drained)
|
||||||
|
|
||||||
|
# --- A continued: half boost drains proportionally less -----------
|
||||||
|
game_state.fleet_speed_multiplier = 2.0
|
||||||
|
var half_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
for i in range(10):
|
||||||
|
game_state._advance_distorder_drain(1.0)
|
||||||
|
var half_drained: int = half_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
if half_drained <= 0 or half_drained >= full_drained:
|
||||||
|
return _fail_now("half boost drained %d over 10s, expected between 0 and full's %d" \
|
||||||
|
% [half_drained, full_drained])
|
||||||
|
print("half boost drained %d over 10s - less than full's %d, still nonzero" \
|
||||||
|
% [half_drained, full_drained])
|
||||||
|
|
||||||
|
# --- B. a small multiplier still drains a nonzero amount ----------
|
||||||
|
game_state.fleet_speed_multiplier = 1.05
|
||||||
|
var small_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
for i in range(600):
|
||||||
|
game_state._advance_distorder_drain(0.1)
|
||||||
|
var small_drained: int = small_before - game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
if small_drained <= 0:
|
||||||
|
return _fail_now("a small (1.05x) sustained boost drained nothing over 60s - fractional rounding loss")
|
||||||
|
print("small boost (1.05x) still drained %d over 60s (no rounding loss)" % small_drained)
|
||||||
|
|
||||||
|
# --- C. running out resets the multiplier and raises the notice ---
|
||||||
|
game_state.withdraw_fleet_resource(GameState.RESOURCE_ENERGY,
|
||||||
|
game_state.fleet_resource(GameState.RESOURCE_ENERGY))
|
||||||
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 5)
|
||||||
|
game_state.fleet_speed_multiplier = GameState.MAX_FLEET_SPEED_MULTIPLIER
|
||||||
|
game_state.boost_depleted_notice = false
|
||||||
|
for i in range(10):
|
||||||
|
game_state._advance_distorder_drain(1.0)
|
||||||
|
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != 0:
|
||||||
|
return _fail_now("fleet energy did not reach exactly 0, got %d" \
|
||||||
|
% game_state.fleet_resource(GameState.RESOURCE_ENERGY))
|
||||||
|
if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0):
|
||||||
|
return _fail_now("multiplier was not forced back to 1.0 after running out: %f" \
|
||||||
|
% game_state.fleet_speed_multiplier)
|
||||||
|
if not game_state.boost_depleted_notice:
|
||||||
|
return _fail_now("no depleted notice was raised when energy ran out")
|
||||||
|
print("running out of energy dropped the multiplier to 1.0 and raised the notice")
|
||||||
|
|
||||||
|
stage = 1
|
||||||
|
elapsed = 0.0
|
||||||
|
return false
|
||||||
|
1:
|
||||||
|
# --- C continued: the SpeedFader visibly follows it down ----------
|
||||||
|
if elapsed < 0.2:
|
||||||
|
return false
|
||||||
|
main = load("res://Main.tscn").instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
fleet = main.get_node("Fleet")
|
||||||
|
fader = main.get_node("UI/SpeedFader")
|
||||||
|
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_DISTORDER]))
|
||||||
|
fader._process(0.0)
|
||||||
|
if not fader.visible:
|
||||||
|
return _fail_now("fader hidden with a distorder ship in the fleet")
|
||||||
|
|
||||||
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 4)
|
||||||
|
game_state.set_fleet_speed_multiplier(GameState.MAX_FLEET_SPEED_MULTIPLIER)
|
||||||
|
fader._process(0.0)
|
||||||
|
if fader.knob_t < 0.9:
|
||||||
|
return _fail_now("fader knob did not reflect the boosted multiplier before draining: %f" \
|
||||||
|
% fader.knob_t)
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
game_state._advance_distorder_drain(1.0)
|
||||||
|
if not is_equal_approx(game_state.fleet_speed_multiplier, 1.0):
|
||||||
|
return _fail_now("multiplier was not forced back to 1.0 after running out (with a live fleet)")
|
||||||
|
fader._process(0.0)
|
||||||
|
if fader.knob_t > 0.05:
|
||||||
|
return _fail_now("fader knob did not snap down after the boost was depleted: %f" % fader.knob_t)
|
||||||
|
if fader.label.text != "Out of energy!":
|
||||||
|
return _fail_now("fader did not show the depleted reason, text was '%s'" % fader.label.text)
|
||||||
|
if game_state.boost_depleted_notice:
|
||||||
|
return _fail_now("boost_depleted_notice was not consumed by the fader")
|
||||||
|
print("fader visibly snapped its knob down and explained the depletion")
|
||||||
|
|
||||||
|
print("TEST_DISTORDER_ENERGY: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _fail_now(message: String) -> bool:
|
||||||
|
print("TEST_DISTORDER_ENERGY: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
1
tests/test_distorder_energy.gd.uid
Normal file
1
tests/test_distorder_energy.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bistn7dgiv7lw
|
||||||
1
tests/test_enemy_mothership.gd.uid
Normal file
1
tests/test_enemy_mothership.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c18rxosfduxoa
|
||||||
|
|
@ -21,6 +21,7 @@ func _initialize() -> void:
|
||||||
game_state.extractors.clear()
|
game_state.extractors.clear()
|
||||||
game_state.transporters.clear()
|
game_state.transporters.clear()
|
||||||
game_state.reset_progression()
|
game_state.reset_progression()
|
||||||
|
game_state.research_points = 1
|
||||||
game_state.add_extractor("Test Planet", EXTRACTOR_AT)
|
game_state.add_extractor("Test Planet", EXTRACTOR_AT)
|
||||||
extractor_index = 0
|
extractor_index = 0
|
||||||
|
|
||||||
|
|
|
||||||
1
tests/test_fleet_cargo_limit.gd.uid
Normal file
1
tests/test_fleet_cargo_limit.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b1fyqy5i8mqf0
|
||||||
|
|
@ -3,7 +3,10 @@ extends SceneTree
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
var main: Node
|
var main: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
var skill: GasBombController
|
var skill: Node
|
||||||
|
# Mirrors GasBombController.AOE_RADIUS's value rather than referencing the
|
||||||
|
# class constant directly - see test_gas_bomber_cooldowns.gd's identical fix.
|
||||||
|
const AOE_RADIUS_FOR_TEST: float = 180.0
|
||||||
var inside_enemies: Array = []
|
var inside_enemies: Array = []
|
||||||
var outside_enemy: Node
|
var outside_enemy: Node
|
||||||
var stage := 0
|
var stage := 0
|
||||||
|
|
@ -24,6 +27,10 @@ func _initialize() -> void:
|
||||||
game_state.fleet_cargo = 0
|
game_state.fleet_cargo = 0
|
||||||
game_state.fleet_gas_cargo = 0
|
game_state.fleet_gas_cargo = 0
|
||||||
game_state.build_time_scale = 0.01
|
game_state.build_time_scale = 0.01
|
||||||
|
game_state.research_points = 1
|
||||||
|
# #60: after research/crafting resets the fleet hold below, top it up
|
||||||
|
# with fleet-carried gas for the shot itself (ammunition, not the ore/gas
|
||||||
|
# spent researching or crafting the bomber).
|
||||||
|
|
||||||
# Research is gas-only: any amount of ore and 29 gas must still leave it
|
# Research is gas-only: any amount of ore and 29 gas must still leave it
|
||||||
# waiting, then the thirtieth unit starts and completes the unlock.
|
# waiting, then the thirtieth unit starts and completes the unlock.
|
||||||
|
|
@ -69,6 +76,7 @@ func _initialize() -> void:
|
||||||
_fail_now("crafting did not produce a typed Gas Bomber")
|
_fail_now("crafting did not produce a typed Gas Bomber")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
game_state.fleet_gas_cargo = 20
|
||||||
main = load("res://Main.tscn").instantiate()
|
main = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(main)
|
root.add_child(main)
|
||||||
fleet = main.get_node("Fleet")
|
fleet = main.get_node("Fleet")
|
||||||
|
|
@ -117,7 +125,7 @@ func _process(delta: float) -> bool:
|
||||||
enemy.speed = 0.0
|
enemy.speed = 0.0
|
||||||
inside_enemies.append(enemy)
|
inside_enemies.append(enemy)
|
||||||
outside_enemy = load("res://Enemy.tscn").instantiate()
|
outside_enemy = load("res://Enemy.tscn").instantiate()
|
||||||
outside_enemy.position = skill.aim_target + Vector2(GasBombController.AOE_RADIUS + 80.0, 0)
|
outside_enemy.position = skill.aim_target + Vector2(AOE_RADIUS_FOR_TEST + 80.0, 0)
|
||||||
main.add_child(outside_enemy)
|
main.add_child(outside_enemy)
|
||||||
outside_enemy.speed = 0.0
|
outside_enemy.speed = 0.0
|
||||||
|
|
||||||
|
|
|
||||||
142
tests/test_gas_bomber_ammo.gd
Normal file
142
tests/test_gas_bomber_ammo.gd
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless gameplay test: run with
|
||||||
|
# godot --headless -s res://tests/test_gas_bomber_ammo.gd
|
||||||
|
# Forgejo #60: gas bomb shots draw ammunition from the FLEET HOLD
|
||||||
|
# (GameState.fleet_inventory), not the bank - the fleet has to be carrying
|
||||||
|
# its own gas.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. gas sitting in the bank does not enable firing - only the fleet
|
||||||
|
# hold counts
|
||||||
|
# B. with insufficient fleet gas the button is disabled, pressing it
|
||||||
|
# explains why, and no projectile is created / no cooldown consumed
|
||||||
|
# C. firing drains exactly GAS_PER_SHOT from the fleet hold and nothing
|
||||||
|
# from the bank
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var main: Node
|
||||||
|
var fleet: Node
|
||||||
|
var skill: Node
|
||||||
|
var bomber_component: Node
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
# Mirrors GasBombController.GAS_PER_SHOT's value rather than referencing
|
||||||
|
# the class constant directly - a bare ClassName.CONST reference elsewhere
|
||||||
|
# in a test
|
||||||
|
# script has been observed to corrupt method resolution for the WHOLE
|
||||||
|
# script (an unrelated skill._process() call earlier in the same file then
|
||||||
|
# fails with "Nonexistent function '_process' in base 'Nil'"), root cause
|
||||||
|
# unconfirmed. See tests/test_gas_bomber_cooldowns.gd's identical fix.
|
||||||
|
const GAS_PER_SHOT: int = 5
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
game_state.buildings.clear()
|
||||||
|
game_state.extractors.clear()
|
||||||
|
game_state.transporters.clear()
|
||||||
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.vessel_levels["unlock_gas_bomber"] = 1
|
||||||
|
game_state.rocks = 0
|
||||||
|
game_state.gas = 0
|
||||||
|
game_state.fleet_gas_cargo = 0
|
||||||
|
main = load("res://Main.tscn").instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
fleet = main.get_node("Fleet")
|
||||||
|
skill = main.get_node("GasBombController")
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if elapsed < 0.2:
|
||||||
|
return false
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
|
# An interceptor rides along purely for its "cargo" capability
|
||||||
|
# (#56): a fleet made only of a gas bomber has zero cargo
|
||||||
|
# capacity, which would silently clamp every fleet_gas_cargo
|
||||||
|
# assignment below to 0.
|
||||||
|
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
|
||||||
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_GAS_BOMBER]))
|
||||||
|
bomber_component = fleet.ships[1].get_component("gas_bomber")
|
||||||
|
skill._process(0.0)
|
||||||
|
|
||||||
|
# --- A. bank gas does not count -----------------------------------
|
||||||
|
game_state.gas = 500
|
||||||
|
game_state.fleet_gas_cargo = 0
|
||||||
|
skill._process(0.0)
|
||||||
|
if skill.button.enabled:
|
||||||
|
return _fail_now("500 banked gas (0 in the fleet hold) left the control enabled")
|
||||||
|
print("500 banked gas, 0 fleet gas: control correctly disabled")
|
||||||
|
|
||||||
|
# --- B. insufficient fleet gas --------------------------------------
|
||||||
|
game_state.fleet_gas_cargo = GAS_PER_SHOT - 1
|
||||||
|
skill._process(0.0)
|
||||||
|
if skill.button.enabled:
|
||||||
|
return _fail_now("fleet gas one short of GAS_PER_SHOT left the control enabled")
|
||||||
|
|
||||||
|
var press := InputEventScreenTouch.new()
|
||||||
|
press.index = 3
|
||||||
|
press.position = skill.button.global_position + skill.button.size * 0.5
|
||||||
|
press.pressed = true
|
||||||
|
main._handle_touch(press)
|
||||||
|
if skill.aiming:
|
||||||
|
return _fail_now("aiming began despite insufficient fleet gas")
|
||||||
|
if skill.status.text == "":
|
||||||
|
return _fail_now("an unaffordable press did not explain itself")
|
||||||
|
print("insufficient fleet gas: disabled, press explains itself, no aiming began")
|
||||||
|
if not bomber_component.is_ready():
|
||||||
|
return _fail_now("a refused press consumed the bomber's cooldown")
|
||||||
|
if game_state.fleet_resource(GameState.RESOURCE_GAS) != GAS_PER_SHOT - 1:
|
||||||
|
return _fail_now("a refused press spent fleet gas anyway")
|
||||||
|
print("no cooldown consumed, no gas spent on a refused press")
|
||||||
|
|
||||||
|
# --- C. exact ammo cost on a real shot ------------------------------
|
||||||
|
game_state.fleet_gas_cargo = GAS_PER_SHOT + 7
|
||||||
|
skill._process(0.0)
|
||||||
|
if not skill.button.enabled:
|
||||||
|
return _fail_now("enough fleet gas still left the control disabled")
|
||||||
|
|
||||||
|
var press2 := InputEventScreenTouch.new()
|
||||||
|
press2.index = 4
|
||||||
|
press2.position = skill.button.global_position + skill.button.size * 0.5
|
||||||
|
press2.pressed = true
|
||||||
|
main._handle_touch(press2)
|
||||||
|
if not skill.aiming:
|
||||||
|
return _fail_now("an affordable press did not begin aiming")
|
||||||
|
var drag := InputEventScreenDrag.new()
|
||||||
|
drag.index = 4
|
||||||
|
drag.position = press2.position + Vector2(-300.0, 0.0)
|
||||||
|
main._handle_drag(drag)
|
||||||
|
var release := InputEventScreenTouch.new()
|
||||||
|
release.index = 4
|
||||||
|
release.position = drag.position
|
||||||
|
release.pressed = false
|
||||||
|
main._handle_touch(release)
|
||||||
|
|
||||||
|
if skill.projectiles.size() != 1:
|
||||||
|
return _fail_now("a paid-for shot did not launch a projectile")
|
||||||
|
if bomber_component.is_ready():
|
||||||
|
return _fail_now("firing did not consume the bomber's cooldown")
|
||||||
|
if game_state.fleet_resource(GameState.RESOURCE_GAS) != 7:
|
||||||
|
return _fail_now("fleet gas after firing is %d, expected 7 (paid exactly %d)" \
|
||||||
|
% [game_state.fleet_resource(GameState.RESOURCE_GAS), GAS_PER_SHOT])
|
||||||
|
if game_state.gas != 500:
|
||||||
|
return _fail_now("firing touched the bank (%d), expected it untouched at 500" % game_state.gas)
|
||||||
|
print("firing drained exactly %d from the fleet hold, bank untouched" % GAS_PER_SHOT)
|
||||||
|
|
||||||
|
print("TEST_GAS_BOMBER_AMMO: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _fail_now(message: String) -> bool:
|
||||||
|
print("TEST_GAS_BOMBER_AMMO: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
1
tests/test_gas_bomber_ammo.gd.uid
Normal file
1
tests/test_gas_bomber_ammo.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cwcw77qvykk35
|
||||||
|
|
@ -2,7 +2,7 @@ extends SceneTree
|
||||||
|
|
||||||
var main: Node
|
var main: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
var skill: GasBombController
|
var skill: Node
|
||||||
var elapsed := 0.0
|
var elapsed := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,6 +12,8 @@ func _initialize() -> void:
|
||||||
state.extractors.clear()
|
state.extractors.clear()
|
||||||
state.transporters.clear()
|
state.transporters.clear()
|
||||||
state.vessel_levels["unlock_gas_bomber"] = 1
|
state.vessel_levels["unlock_gas_bomber"] = 1
|
||||||
|
# #60: shots draw from the fleet hold now - stock enough for both fires.
|
||||||
|
state.fleet_gas_cargo = 20
|
||||||
main = load("res://Main.tscn").instantiate()
|
main = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(main)
|
root.add_child(main)
|
||||||
fleet = main.get_node("Fleet")
|
fleet = main.get_node("Fleet")
|
||||||
|
|
@ -53,7 +55,7 @@ func _process(delta: float) -> bool:
|
||||||
|
|
||||||
# Rearming one bomber restores one available shot without touching the
|
# Rearming one bomber restores one available shot without touching the
|
||||||
# other bomber's cooldown.
|
# other bomber's cooldown.
|
||||||
first.tick(GasBombController.COOLDOWN)
|
first.tick(5.0) # matches GasBombController.COOLDOWN
|
||||||
skill._process(0.0)
|
skill._process(0.0)
|
||||||
if not first.is_ready() or second.is_ready() or not skill.button.enabled:
|
if not first.is_ready() or second.is_ready() or not skill.button.enabled:
|
||||||
return _fail("one completed cooldown did not restore exactly one shot")
|
return _fail("one completed cooldown did not restore exactly one shot")
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
uid://cwo6ake5taias
|
uid://tguuracp1ewt
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ var hub_id: int = 0
|
||||||
var burner_id: int = 0
|
var burner_id: int = 0
|
||||||
var load_limit: int = 0
|
var load_limit: int = 0
|
||||||
var energy_before_stall: int = 0
|
var energy_before_stall: int = 0
|
||||||
|
var conversion_rate: float = 0.0
|
||||||
|
var conversion_yield: int = 0
|
||||||
|
|
||||||
const MOTHERSHIP_AT := Vector2.ZERO
|
const MOTHERSHIP_AT := Vector2.ZERO
|
||||||
const HUB_AT := Vector2(150.0, 0.0)
|
const HUB_AT := Vector2(150.0, 0.0)
|
||||||
|
|
@ -48,6 +50,9 @@ func _initialize() -> void:
|
||||||
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||||
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
|
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
|
||||||
game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20)
|
game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20)
|
||||||
|
var logistics: Resource = game_state.building_logistics_profile(game_state.find_building(burner_id))
|
||||||
|
conversion_rate = logistics.conversion_rate
|
||||||
|
conversion_yield = logistics.conversion_yield
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
|
|
@ -73,10 +78,10 @@ func _process(delta: float) -> bool:
|
||||||
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
||||||
var gas_burned: int = 20 - gas_left
|
var gas_burned: int = 20 - gas_left
|
||||||
if gas_burned < 2:
|
if gas_burned < 2:
|
||||||
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, GameState.GAS_BURN_RATE])
|
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, conversion_rate])
|
||||||
if energy_made != gas_burned * GameState.ENERGY_PER_GAS:
|
if energy_made != gas_burned * conversion_yield:
|
||||||
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
|
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
|
||||||
energy_made, gas_burned, GameState.ENERGY_PER_GAS
|
energy_made, gas_burned, conversion_yield
|
||||||
])
|
])
|
||||||
if game_state.rocks != 0 or game_state.gas != 0:
|
if game_state.rocks != 0 or game_state.gas != 0:
|
||||||
return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [
|
return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [
|
||||||
|
|
|
||||||
1
tests/test_gas_burner.gd.uid
Normal file
1
tests/test_gas_burner.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dlx2eu6lw3ps6
|
||||||
|
|
@ -12,6 +12,7 @@ func _initialize() -> void:
|
||||||
state.transporters.clear()
|
state.transporters.clear()
|
||||||
state.reset_progression()
|
state.reset_progression()
|
||||||
state.rocks = 900
|
state.rocks = 900
|
||||||
|
state.research_points = state.skill_research_point_cost("freight_yards")
|
||||||
state.unlocked = ["shipyards", "logistics", "freight"]
|
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||||
|
|
||||||
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
|
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ func _initialize() -> void:
|
||||||
game.vessel_levels.clear()
|
game.vessel_levels.clear()
|
||||||
game.build_time_scale = 0.02
|
game.build_time_scale = 0.02
|
||||||
game.rocks = 500
|
game.rocks = 500
|
||||||
|
game.research_points = 1
|
||||||
lab_id = game.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO)
|
lab_id = game.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO)
|
||||||
if not game.queue_lab_research(lab_id, "int_damage"):
|
if not game.queue_lab_research(lab_id, "int_damage"):
|
||||||
_fail("could not queue research")
|
_fail("could not queue research")
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ func _initialize() -> void:
|
||||||
state.transporters.clear()
|
state.transporters.clear()
|
||||||
state.reset_progression()
|
state.reset_progression()
|
||||||
state.rocks = 100
|
state.rocks = 100
|
||||||
|
state.research_points = 1
|
||||||
state.build_time_scale = 0.02
|
state.build_time_scale = 0.02
|
||||||
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||||
state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
|
state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
|
||||||
|
|
|
||||||
141
tests/test_laser_ammo.gd
Normal file
141
tests/test_laser_ammo.gd
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless gameplay test: run with
|
||||||
|
# godot --headless -s res://tests/test_laser_ammo.gd
|
||||||
|
# Forgejo #61: laser shots draw ammunition from the FLEET HOLD
|
||||||
|
# (GameState.fleet_inventory), the same way gas bomb shots do (#60) - the
|
||||||
|
# fleet has to be carrying its own energy.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. banked energy does not enable firing - only the fleet hold counts
|
||||||
|
# B. with insufficient fleet energy the button is disabled, pressing it
|
||||||
|
# explains why, and no beam is fired / no cooldown consumed
|
||||||
|
# C. firing drains exactly ENERGY_PER_SHOT from the fleet hold and
|
||||||
|
# nothing from the bank
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var main: Node
|
||||||
|
var fleet: Node
|
||||||
|
var skill: Node
|
||||||
|
var laser_component: Node
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
# Mirrors LaserController.ENERGY_PER_SHOT's value rather than referencing
|
||||||
|
# the class constant directly - a bare ClassName.CONST reference elsewhere
|
||||||
|
# in a test script has been observed to corrupt method resolution for the
|
||||||
|
# WHOLE script (an unrelated skill._process() call earlier in the same
|
||||||
|
# file then fails with "Nonexistent function '_process' in base 'Nil'"),
|
||||||
|
# root cause unconfirmed. See tests/test_gas_bomber_cooldowns.gd's
|
||||||
|
# identical fix (#60).
|
||||||
|
const ENERGY_PER_SHOT: int = 8
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
game_state.buildings.clear()
|
||||||
|
game_state.extractors.clear()
|
||||||
|
game_state.transporters.clear()
|
||||||
|
game_state.stored_vessels.clear()
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.vessel_levels["unlock_laser"] = 1
|
||||||
|
game_state.rocks = 0
|
||||||
|
game_state.gas = 0
|
||||||
|
game_state.fleet_gas_cargo = 0
|
||||||
|
main = load("res://Main.tscn").instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
fleet = main.get_node("Fleet")
|
||||||
|
skill = main.get_node("LaserController")
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if elapsed < 0.2:
|
||||||
|
return false
|
||||||
|
main.get_node("EnemySpawner").set_process(false)
|
||||||
|
main.get_node("AsteroidField").set_process(false)
|
||||||
|
# An interceptor rides along purely for its "cargo" capability
|
||||||
|
# (#56): a fleet made only of a laser ship has zero cargo
|
||||||
|
# capacity, which would silently clamp every fleet energy
|
||||||
|
# deposit below to 0.
|
||||||
|
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
|
||||||
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_LASER]))
|
||||||
|
laser_component = fleet.ships[1].get_component("laser")
|
||||||
|
skill._process(0.0)
|
||||||
|
|
||||||
|
# --- A. bank energy does not count ----------------------------
|
||||||
|
game_state.deposit_bank_resource(GameState.RESOURCE_ENERGY, 500)
|
||||||
|
skill._process(0.0)
|
||||||
|
if skill.button.enabled:
|
||||||
|
return _fail_now("500 banked energy (0 in the fleet hold) left the control enabled")
|
||||||
|
print("500 banked energy, 0 fleet energy: control correctly disabled")
|
||||||
|
|
||||||
|
# --- B. insufficient fleet energy -------------------------------
|
||||||
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, ENERGY_PER_SHOT - 1)
|
||||||
|
skill._process(0.0)
|
||||||
|
if skill.button.enabled:
|
||||||
|
return _fail_now("fleet energy one short of ENERGY_PER_SHOT left the control enabled")
|
||||||
|
|
||||||
|
var press := InputEventScreenTouch.new()
|
||||||
|
press.index = 3
|
||||||
|
press.position = skill.button.global_position + skill.button.size * 0.5
|
||||||
|
press.pressed = true
|
||||||
|
main._handle_touch(press)
|
||||||
|
if skill.aiming:
|
||||||
|
return _fail_now("aiming began despite insufficient fleet energy")
|
||||||
|
if skill.status.text == "":
|
||||||
|
return _fail_now("an unaffordable press did not explain itself")
|
||||||
|
print("insufficient fleet energy: disabled, press explains itself, no aiming began")
|
||||||
|
if not laser_component.is_ready():
|
||||||
|
return _fail_now("a refused press consumed the laser ship's cooldown")
|
||||||
|
if game_state.fleet_resource(GameState.RESOURCE_ENERGY) != ENERGY_PER_SHOT - 1:
|
||||||
|
return _fail_now("a refused press spent fleet energy anyway")
|
||||||
|
print("no cooldown consumed, no energy spent on a refused press")
|
||||||
|
|
||||||
|
# --- C. exact ammo cost on a real shot -----------------------------
|
||||||
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 8)
|
||||||
|
skill._process(0.0)
|
||||||
|
if not skill.button.enabled:
|
||||||
|
return _fail_now("enough fleet energy still left the control disabled")
|
||||||
|
var fleet_energy_before: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
|
||||||
|
var press2 := InputEventScreenTouch.new()
|
||||||
|
press2.index = 4
|
||||||
|
press2.position = skill.button.global_position + skill.button.size * 0.5
|
||||||
|
press2.pressed = true
|
||||||
|
main._handle_touch(press2)
|
||||||
|
if not skill.aiming:
|
||||||
|
return _fail_now("an affordable press did not begin aiming")
|
||||||
|
var drag := InputEventScreenDrag.new()
|
||||||
|
drag.index = 4
|
||||||
|
drag.position = press2.position + Vector2(-300.0, 0.0)
|
||||||
|
main._handle_drag(drag)
|
||||||
|
var release := InputEventScreenTouch.new()
|
||||||
|
release.index = 4
|
||||||
|
release.position = drag.position
|
||||||
|
release.pressed = false
|
||||||
|
main._handle_touch(release)
|
||||||
|
|
||||||
|
if laser_component.is_ready():
|
||||||
|
return _fail_now("firing did not consume the laser ship's cooldown")
|
||||||
|
var fleet_energy_after: int = game_state.fleet_resource(GameState.RESOURCE_ENERGY)
|
||||||
|
if fleet_energy_before - fleet_energy_after != ENERGY_PER_SHOT:
|
||||||
|
return _fail_now("firing spent %d fleet energy, expected exactly %d" \
|
||||||
|
% [fleet_energy_before - fleet_energy_after, ENERGY_PER_SHOT])
|
||||||
|
if game_state.banked_resource(GameState.RESOURCE_ENERGY) != 500:
|
||||||
|
return _fail_now("firing touched the bank (%d), expected it untouched at 500" \
|
||||||
|
% game_state.banked_resource(GameState.RESOURCE_ENERGY))
|
||||||
|
print("firing drained exactly %d from the fleet hold, bank untouched" % ENERGY_PER_SHOT)
|
||||||
|
|
||||||
|
print("TEST_LASER_AMMO: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _fail_now(message: String) -> bool:
|
||||||
|
print("TEST_LASER_AMMO: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
1
tests/test_laser_ammo.gd.uid
Normal file
1
tests/test_laser_ammo.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d24p5etger56g
|
||||||
|
|
@ -20,7 +20,12 @@ extends SceneTree
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
var main: Node
|
var main: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
var skill: LaserController
|
var skill: Node
|
||||||
|
# Mirrors LaserController.BEAM_HALF_WIDTH's value rather than referencing
|
||||||
|
# the class constant directly - a bare ClassName.CONST reference elsewhere
|
||||||
|
# in a test script has been observed to corrupt method resolution for the
|
||||||
|
# WHOLE script. See tests/test_gas_bomber_cooldowns.gd's identical fix (#60).
|
||||||
|
const BEAM_HALF_WIDTH: float = 22.0
|
||||||
var on_line_enemies: Array = []
|
var on_line_enemies: Array = []
|
||||||
var off_line_enemy: Node
|
var off_line_enemy: Node
|
||||||
var beyond_range_enemy: Node
|
var beyond_range_enemy: Node
|
||||||
|
|
@ -42,6 +47,13 @@ func _initialize() -> void:
|
||||||
# this ticket is about the laser's own behaviour, not re-proving the
|
# this ticket is about the laser's own behaviour, not re-proving the
|
||||||
# ship-lab research flow (already covered generically elsewhere).
|
# ship-lab research flow (already covered generically elsewhere).
|
||||||
game_state.vessel_levels["unlock_laser"] = 1
|
game_state.vessel_levels["unlock_laser"] = 1
|
||||||
|
# #61: shots draw energy from the fleet hold now - stock enough for two
|
||||||
|
# fires. Set BEFORE the first spawn_ships() call (inside Main.tscn's own
|
||||||
|
# _ready(), triggered by instantiate() below): the fleet hold has no
|
||||||
|
# capacity cap yet at this point (#56's cap is only computed once a
|
||||||
|
# fleet composition exists), so this deposit can't be clamped by the
|
||||||
|
# soon-to-exist all-laser fleet having zero "cargo" capability.
|
||||||
|
game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 20)
|
||||||
main = load("res://Main.tscn").instantiate()
|
main = load("res://Main.tscn").instantiate()
|
||||||
root.add_child(main)
|
root.add_child(main)
|
||||||
fleet = main.get_node("Fleet")
|
fleet = main.get_node("Fleet")
|
||||||
|
|
@ -106,7 +118,7 @@ func _process(delta: float) -> bool:
|
||||||
|
|
||||||
off_line_enemy = load("res://Enemy.tscn").instantiate()
|
off_line_enemy = load("res://Enemy.tscn").instantiate()
|
||||||
off_line_enemy.position = first_shooter.global_position \
|
off_line_enemy.position = first_shooter.global_position \
|
||||||
+ Vector2(200.0, LaserController.BEAM_HALF_WIDTH + 60.0)
|
+ Vector2(200.0, BEAM_HALF_WIDTH + 60.0)
|
||||||
main.add_child(off_line_enemy)
|
main.add_child(off_line_enemy)
|
||||||
off_line_enemy.speed = 0.0
|
off_line_enemy.speed = 0.0
|
||||||
|
|
||||||
|
|
|
||||||
1
tests/test_laser_vessel.gd.uid
Normal file
1
tests/test_laser_vessel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dp0e71h2124c8
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue