Compare commits
111 changed files with 632 additions and 2269 deletions
|
|
@ -1,191 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://dbahjmvwru7bp
|
|
||||||
|
|
@ -32,14 +32,12 @@ 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")
|
||||||
|
|
@ -69,7 +67,6 @@ 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"),
|
||||||
|
|
@ -77,7 +74,6 @@ 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"),
|
||||||
|
|
@ -85,7 +81,6 @@ 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"),
|
||||||
|
|
@ -93,7 +88,6 @@ 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 = []
|
||||||
|
|
||||||
|
|
@ -123,15 +117,13 @@ 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_distorder_button,
|
craft_laser_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_distorder_label,
|
craft_laser_label,
|
||||||
]
|
]
|
||||||
|
|
||||||
# --- the transfer panel (#24) ---------------------------------------------
|
# --- the transfer panel (#24) ---------------------------------------------
|
||||||
|
|
@ -680,13 +672,6 @@ 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
|
||||||
|
|
@ -730,18 +715,14 @@ 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 · Research points %d" \
|
skill_status.text = "Fleet capacity %d" % GameState.max_fleet_size()
|
||||||
% [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):
|
||||||
|
|
@ -753,12 +734,6 @@ 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
|
||||||
|
|
@ -919,8 +894,7 @@ 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 · %d RP" % [
|
lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel)
|
||||||
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()):
|
||||||
|
|
@ -932,9 +906,6 @@ 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" % [
|
||||||
|
|
@ -953,22 +924,11 @@ 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():
|
||||||
var queued_points: int = GameState.research_point_cost(String(queued.id))
|
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))]
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1043,7 +1003,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 GameState.building_has_capability(b, "bank"):
|
if b.type == GameState.BUILDING_MOTHERSHIP:
|
||||||
# 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"
|
||||||
|
|
@ -1056,7 +1016,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 GameState.building_has_capability(building, "bank"):
|
if not building.is_empty() and building.type == GameState.BUILDING_MOTHERSHIP:
|
||||||
heading = "Mothership bank"
|
heading = "Mothership bank"
|
||||||
transfer_list.set_destination(transfer_building_id, heading)
|
transfer_list.set_destination(transfer_building_id, heading)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://cydp18abylrgd
|
|
||||||
|
|
@ -7,6 +7,5 @@ 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,5 +22,4 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
[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 +0,0 @@
|
||||||
uid://coopf1u1lnb8b
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://6oj7ia3em4x0
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://eqkp0coeoh4d
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
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
|
||||||
_sync_composition_state()
|
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||||
|
|
||||||
|
|
||||||
func vessel_types() -> PackedStringArray:
|
func vessel_types() -> PackedStringArray:
|
||||||
|
|
@ -91,15 +91,6 @@ 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:
|
||||||
|
|
@ -148,7 +139,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)
|
||||||
_sync_composition_state()
|
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -166,7 +157,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()
|
||||||
_sync_composition_state()
|
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||||
return vessel_type
|
return vessel_type
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -209,25 +200,18 @@ 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. #59: multiplied
|
# Ships may exceed it individually to reach their slots.
|
||||||
# at the point of use, not baked into cruise_speed itself, so the base
|
anchor += steer_direction * cruise_speed * delta
|
||||||
# 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 * effective_cruise_speed
|
ships[i].fleet_velocity = steer_direction * cruise_speed
|
||||||
|
|
||||||
|
|
||||||
func stop() -> void:
|
func stop() -> void:
|
||||||
|
|
@ -292,7 +276,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)
|
||||||
_sync_composition_state()
|
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||||
|
|
||||||
|
|
||||||
func get_centroid() -> Vector2:
|
func get_centroid() -> Vector2:
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ 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
|
||||||
|
|
@ -264,8 +263,6 @@ 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):
|
||||||
|
|
@ -290,8 +287,6 @@ 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,7 +4,6 @@ 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()
|
||||||
|
|
@ -23,35 +22,6 @@ 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
|
||||||
|
|
@ -123,26 +93,6 @@ 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:
|
||||||
|
|
@ -280,13 +230,6 @@ 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": "",
|
||||||
|
|
@ -300,42 +243,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, "research_points": 1,
|
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER,
|
||||||
"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, "research_points": 1,
|
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB,
|
||||||
"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, "research_points": 1,
|
"x": 0.82, "unlocks": BUILDING_SHIP_LAB,
|
||||||
"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, "research_points": 2,
|
"x": 0.18, "hub_bonus": 1,
|
||||||
"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, "research_points": 2,
|
"x": 0.50, "unlocks": BUILDING_GAS_BURNER,
|
||||||
"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, "research_points": 1,
|
"x": 0.82, "fleet_bonus": 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, "research_points": 2,
|
"x": 0.50, "fleet_bonus": 1,
|
||||||
"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, "research_points": 3,
|
"x": 0.50, "fleet_bonus": 1,
|
||||||
"desc": "Permanent: +1 vessel in the fleet.",
|
"desc": "Permanent: +1 vessel in the fleet.",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -385,27 +328,16 @@ func prerequisite_met(id: String) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func can_unlock(id: String) -> bool:
|
func can_unlock(id: String) -> bool:
|
||||||
var node: Dictionary = skill_node(id)
|
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost))
|
||||||
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
|
||||||
var node: Dictionary = skill_node(id)
|
if not spend(int(skill_node(id).cost)):
|
||||||
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(node.get("hub_bonus", 0)) > 0:
|
if int(skill_node(id).get("hub_bonus", 0)) > 0:
|
||||||
_sync_hub_transporters()
|
_sync_hub_transporters()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
@ -449,10 +381,6 @@ 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()
|
||||||
|
|
@ -526,13 +454,6 @@ 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
|
||||||
|
|
@ -544,7 +465,6 @@ 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.
|
||||||
|
|
@ -636,14 +556,10 @@ 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)
|
||||||
var points_needed: int = int(definition.research_point_cost)
|
if not can_afford_resources(costs):
|
||||||
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
|
||||||
|
|
@ -690,8 +606,7 @@ 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:
|
||||||
|
|
@ -699,10 +614,6 @@ 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
|
||||||
|
|
@ -965,14 +876,6 @@ 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:
|
||||||
|
|
@ -1001,20 +904,10 @@ 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():
|
||||||
|
|
@ -1057,20 +950,19 @@ 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_hub_ids: Array = []
|
var removed_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
|
||||||
if building_has_capability(b, "transport_hub"):
|
removed_ids.append(int(b.id))
|
||||||
removed_hub_ids.append(int(b.id))
|
|
||||||
else:
|
else:
|
||||||
kept.append(b)
|
kept.append(b)
|
||||||
buildings = kept
|
buildings = kept
|
||||||
if not removed_hub_ids.is_empty():
|
if type == BUILDING_TRANSPORT_HUB and not removed_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_hub_ids:
|
if int(transporter.get("hub_id", 0)) not in removed_ids:
|
||||||
kept_transporters.append(transporter)
|
kept_transporters.append(transporter)
|
||||||
transporters = kept_transporters
|
transporters = kept_transporters
|
||||||
return removed
|
return removed
|
||||||
|
|
@ -1200,76 +1092,44 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted:
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
# --- building logistics and resource conversion ---------------------------
|
# --- gas burners (#54) -----------------------------------------------------
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
func building_logistics_deficit(id: int, resource_type: String) -> int:
|
const GAS_BURN_RATE: float = 1.0 # gas/second
|
||||||
var building: Dictionary = find_building(id)
|
const ENERGY_PER_GAS: int = 2
|
||||||
var profile: Resource = building_logistics_profile(building)
|
# Local gas reserve a burner tries to keep topped up. Transporters treat
|
||||||
if profile == null:
|
# falling short of this the same way they treat a queued production/research
|
||||||
|
# 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
|
||||||
var target: int = int(profile.input_buffers.get(resource_type, 0))
|
return maxi(0, GAS_BURNER_BUFFER - building_resource(id, RESOURCE_GAS))
|
||||||
return maxi(0, target - building_resource(id, resource_type))
|
|
||||||
|
|
||||||
|
|
||||||
func building_requested_resource(id: int, resource_type: String) -> int:
|
func _advance_gas_burners(delta: float) -> void:
|
||||||
return maxi(outstanding_resource_cost(id, resource_type),
|
for b in buildings:
|
||||||
building_logistics_deficit(id, resource_type))
|
if not building_has_capability(b, "gas_burner"):
|
||||||
|
|
||||||
|
|
||||||
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(building)
|
var inventory: RefCounted = _inventory_for_record(b)
|
||||||
if inventory.amount(profile.conversion_input) <= 0:
|
if inventory.amount(RESOURCE_GAS) <= 0:
|
||||||
continue
|
continue # stalled - no local gas to burn
|
||||||
var progress: float = float(building.get("conversion_progress", 0.0)) \
|
var progress: float = float(b.get("gas_burn_progress", 0.0)) + delta * GAS_BURN_RATE
|
||||||
+ delta * profile.conversion_rate
|
var whole: int = mini(int(floor(progress)), inventory.amount(RESOURCE_GAS))
|
||||||
var whole: int = mini(int(floor(progress)), inventory.amount(profile.conversion_input))
|
|
||||||
if whole > 0:
|
if whole > 0:
|
||||||
inventory.withdraw(profile.conversion_input, whole)
|
inventory.withdraw(RESOURCE_GAS, whole)
|
||||||
inventory.deposit(profile.conversion_output, whole * profile.conversion_yield)
|
inventory.deposit(RESOURCE_ENERGY, whole * ENERGY_PER_GAS)
|
||||||
progress -= whole
|
progress -= whole
|
||||||
building.conversion_progress = progress
|
b.gas_burn_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
|
||||||
|
|
@ -1282,7 +1142,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, definition_id: String = "") -> void:
|
func register_capture_points(planet_key: String, total: int) -> 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):
|
||||||
|
|
@ -1293,13 +1153,8 @@ func register_capture_points(planet_key: String, total: int, definition_id: Stri
|
||||||
# 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. The planet definition
|
# points still starts unconquered, exactly as before.
|
||||||
# supplies an optional conquest reward, avoiding planet-id special cases in
|
planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0}
|
||||||
# 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:
|
||||||
|
|
@ -1326,16 +1181,6 @@ 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:
|
||||||
|
|
@ -1361,8 +1206,7 @@ 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_resource_converters(delta)
|
_advance_gas_burners(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)))
|
||||||
|
|
@ -1520,7 +1364,13 @@ 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():
|
||||||
var need: int = building_requested_resource(int(b.id), resource_type)
|
# A gas burner's ongoing gas deficit is folded in alongside
|
||||||
|
# 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
|
||||||
|
|
@ -1573,11 +1423,14 @@ 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)
|
||||||
|
|
||||||
# Collect building outputs last: queued deliveries and extractor collection
|
# Collecting a gas burner's energy output, last: queued deliveries and
|
||||||
# both outrank resources that are merely ready for pickup.
|
# extractor collection both outrank it, same "supply what's needed
|
||||||
|
# before what's merely ready" ordering _dispatch_transporters() already
|
||||||
|
# holds for extractors over queued orders (f5c60f8).
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
var pickup_resource: String = _collectable_building_resource(b, load)
|
if not building_has_capability(b, "gas_burner"):
|
||||||
if pickup_resource == "" or _transporter_inbound_for_pickup(int(b.id)):
|
continue
|
||||||
|
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
|
||||||
|
|
@ -1591,7 +1444,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 = pickup_resource
|
chosen_pickup.resource_type = RESOURCE_ENERGY
|
||||||
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)
|
||||||
|
|
@ -1621,7 +1474,8 @@ 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 = building_requested_resource(int(t.target_id), resource_type)
|
var want: int = maxi(outstanding_resource_cost(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,64 +1,61 @@
|
||||||
class_name GasBombController
|
class_name GasBombController
|
||||||
extends "res://AimedFleetAbilityController.gd"
|
extends Node2D
|
||||||
|
|
||||||
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: int = 0
|
var last_hit_count := 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: float = 0.0
|
var longest := 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 _button_path() -> NodePath: return "../UI/GasBombButton"
|
func _ready() -> void:
|
||||||
func _status_path() -> NodePath: return "../UI/GasBombStatus"
|
button.visible = false
|
||||||
func _component_id() -> String: return "gas_bomber"
|
queue_redraw()
|
||||||
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 _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
func _process(delta: float) -> void:
|
||||||
if drag.length() < 0.001:
|
var had_visuals := aiming or not projectiles.is_empty() or not explosions.is_empty()
|
||||||
return origin
|
button.visible = fleet.has_capability("gas_bomber")
|
||||||
var distance: float = minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE
|
button.enabled = _next_ready_bomber() != null
|
||||||
return origin + drag.normalized() * distance
|
if not button.visible and aiming:
|
||||||
|
_cancel_aim()
|
||||||
|
|
||||||
|
if restoring_zoom and not aiming:
|
||||||
|
var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0)
|
||||||
|
camera.zoom = Vector2(z, z)
|
||||||
|
if is_equal_approx(z, saved_zoom):
|
||||||
|
restoring_zoom = false
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -67,8 +64,8 @@ func _advance_effects(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.position = (projectile.position as Vector2).move_toward(projectile.target,
|
||||||
projectile.target, PROJECTILE_SPEED * delta)
|
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:
|
||||||
|
|
@ -79,6 +76,107 @@ func _advance_effects(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:
|
||||||
|
|
@ -93,29 +191,28 @@ func _explode(at: Vector2) -> void:
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if aiming:
|
if aiming:
|
||||||
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.8)
|
var origin: Vector2 = fleet.get_centroid()
|
||||||
|
NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8)
|
||||||
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
|
_draw_ring(aim_target, 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 = float(i) / trail.size()
|
var alpha := float(i) / trail.size()
|
||||||
draw_line(trail[i - 1], trail[i], Color(BOMB_COLOR, alpha * 0.65),
|
draw_line(trail[i - 1], trail[i],
|
||||||
4.0 + alpha * 4.0, true)
|
Color(BOMB_COLOR, alpha * 0.65), 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: float = 1.0 - float(explosion.time) / 0.8
|
var progress := 1.0 - float(explosion.time) / 0.8
|
||||||
var radius: float = lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
||||||
draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
draw_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: float = TAU * float(i) / 7.0 + progress
|
var angle := TAU * float(i) / 7.0 + progress
|
||||||
var cloud_at: Vector2 = explosion.position \
|
var cloud_at: Vector2 = explosion.position + Vector2(cos(angle), sin(angle)) * radius * 0.55
|
||||||
+ 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))
|
||||||
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,5 +23,4 @@ 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,19 +1,6 @@
|
||||||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=4 format=3]
|
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 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"
|
||||||
|
|
@ -24,7 +11,6 @@ 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 +0,0 @@
|
||||||
uid://c5a74rb5s4q21
|
|
||||||
|
|
@ -41,8 +41,6 @@ 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
|
||||||
|
|
@ -241,8 +239,6 @@ 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:
|
||||||
|
|
@ -281,18 +277,6 @@ 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,6 +7,5 @@ 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,6 +7,5 @@ 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,6 +7,5 @@ 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,6 +7,5 @@ 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,60 +1,164 @@
|
||||||
class_name LaserController
|
class_name LaserController
|
||||||
extends "res://AimedFleetAbilityController.gd"
|
extends Node2D
|
||||||
|
|
||||||
|
# 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: int = 0
|
var last_hit_count := 0
|
||||||
|
|
||||||
|
|
||||||
func _button_path() -> NodePath: return "../UI/LaserButton"
|
func _ready() -> void:
|
||||||
func _status_path() -> NodePath: return "../UI/LaserStatus"
|
button.visible = false
|
||||||
func _component_id() -> String: return "laser"
|
queue_redraw()
|
||||||
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 _aim_max_range() -> float:
|
func _process(delta: float) -> void:
|
||||||
var ship: Node2D = _next_ready_ship()
|
var had_visuals := aiming or not beams.is_empty()
|
||||||
if ship == null:
|
button.visible = fleet.has_capability("laser")
|
||||||
return 950.0
|
button.enabled = _next_ready_laser_ship() != null
|
||||||
var component: Node = ship.get_component(_component_id())
|
if not button.visible and aiming:
|
||||||
return component.beam_range if component != null else 950.0
|
_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 _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:
|
||||||
|
|
@ -74,12 +178,20 @@ 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,
|
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.6)
|
||||||
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,
|
NeonDraw.draw_glowing_line(self, beam.from, beam.to, BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|
||||||
BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://cdqg3isy1lbuf
|
|
||||||
|
|
@ -7,6 +7,5 @@ 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,6 +7,5 @@ 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,6 +7,5 @@ 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,5 +26,4 @@ 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=29 format=3]
|
[gd_scene load_steps=28 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,7 +26,6 @@
|
||||||
[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")
|
||||||
|
|
@ -222,28 +221,6 @@ 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
|
||||||
|
|
@ -287,38 +264,6 @@ 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
|
||||||
|
|
@ -894,7 +839,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 = -215
|
offset_right = -198
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -905,7 +850,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 = -175
|
offset_right = -158
|
||||||
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
|
||||||
|
|
@ -914,8 +859,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 = -189
|
offset_left = -168
|
||||||
offset_right = -114
|
offset_right = -76
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -926,8 +871,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 = -229
|
offset_left = -208
|
||||||
offset_right = -74
|
offset_right = -36
|
||||||
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
|
||||||
|
|
@ -936,8 +881,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 = -88
|
offset_left = -46
|
||||||
offset_right = -13
|
offset_right = 46
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -948,8 +893,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 = -128
|
offset_left = -86
|
||||||
offset_right = 27
|
offset_right = 86
|
||||||
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
|
||||||
|
|
@ -958,8 +903,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 = 13
|
offset_left = 76
|
||||||
offset_right = 88
|
offset_right = 168
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -970,8 +915,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 = -27
|
offset_left = 36
|
||||||
offset_right = 128
|
offset_right = 208
|
||||||
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
|
||||||
|
|
@ -980,8 +925,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 = 114
|
offset_left = 198
|
||||||
offset_right = 189
|
offset_right = 290
|
||||||
offset_top = 255
|
offset_top = 255
|
||||||
offset_bottom = 375
|
offset_bottom = 375
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -992,29 +937,7 @@ 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 = 74
|
offset_left = 158
|
||||||
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
|
||||||
|
|
@ -1034,7 +957,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 = -215
|
offset_right = -198
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1045,7 +968,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 = -175
|
offset_right = -158
|
||||||
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
|
||||||
|
|
@ -1054,8 +977,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 = -189
|
offset_left = -168
|
||||||
offset_right = -114
|
offset_right = -76
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1066,8 +989,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 = -229
|
offset_left = -208
|
||||||
offset_right = -74
|
offset_right = -36
|
||||||
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
|
||||||
|
|
@ -1076,8 +999,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 = -88
|
offset_left = -46
|
||||||
offset_right = -13
|
offset_right = 46
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1088,8 +1011,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 = -128
|
offset_left = -86
|
||||||
offset_right = 27
|
offset_right = 86
|
||||||
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
|
||||||
|
|
@ -1098,8 +1021,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 = 13
|
offset_left = 76
|
||||||
offset_right = 88
|
offset_right = 168
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1110,8 +1033,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 = -27
|
offset_left = 36
|
||||||
offset_right = 128
|
offset_right = 208
|
||||||
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
|
||||||
|
|
@ -1120,8 +1043,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 = 114
|
offset_left = 198
|
||||||
offset_right = 189
|
offset_right = 290
|
||||||
offset_top = 565
|
offset_top = 565
|
||||||
offset_bottom = 685
|
offset_bottom = 685
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1132,29 +1055,7 @@ 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 = 74
|
offset_left = 158
|
||||||
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
|
||||||
|
|
@ -1206,7 +1107,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 = -240
|
offset_right = -230
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1216,8 +1117,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 = -210
|
offset_left = -194
|
||||||
offset_right = -150
|
offset_right = -124
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1227,8 +1128,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 = -120
|
offset_left = -88
|
||||||
offset_right = -60
|
offset_right = -18
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1238,8 +1139,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 = -30
|
offset_left = 18
|
||||||
offset_right = 30
|
offset_right = 88
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1249,8 +1150,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 = 60
|
offset_left = 124
|
||||||
offset_right = 120
|
offset_right = 194
|
||||||
offset_top = 274
|
offset_top = 274
|
||||||
offset_bottom = 394
|
offset_bottom = 394
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1260,25 +1161,14 @@ 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 = 150
|
offset_left = 230
|
||||||
offset_right = 210
|
offset_right = 300
|
||||||
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
|
||||||
|
|
@ -1296,7 +1186,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 = -210
|
offset_right = -200
|
||||||
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
|
||||||
|
|
@ -1307,8 +1197,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 = -240
|
offset_left = -224
|
||||||
offset_right = -120
|
offset_right = -94
|
||||||
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
|
||||||
|
|
@ -1319,8 +1209,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 = -150
|
offset_left = -118
|
||||||
offset_right = -30
|
offset_right = 12
|
||||||
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
|
||||||
|
|
@ -1331,8 +1221,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 = -60
|
offset_left = 12
|
||||||
offset_right = 60
|
offset_right = 118
|
||||||
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
|
||||||
|
|
@ -1343,8 +1233,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 = 30
|
offset_left = 94
|
||||||
offset_right = 150
|
offset_right = 224
|
||||||
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
|
||||||
|
|
@ -1355,25 +1245,13 @@ 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 = 120
|
offset_left = 200
|
||||||
offset_right = 240
|
|
||||||
offset_top = 400
|
|
||||||
offset_bottom = 460
|
|
||||||
theme_override_font_sizes/font_size = 22
|
|
||||||
horizontal_alignment = 1
|
|
||||||
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_right = 330
|
||||||
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 = "Distorder"
|
text = "Laser Ship"
|
||||||
|
|
||||||
[node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"]
|
[node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
@ -1620,7 +1498,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 = -245
|
offset_right = -232
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1633,7 +1511,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 = -225
|
offset_right = -212
|
||||||
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
|
||||||
|
|
@ -1645,8 +1523,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 = -221
|
offset_left = -202
|
||||||
offset_right = -156
|
offset_right = -124
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1659,8 +1537,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 = -241
|
offset_left = -222
|
||||||
offset_right = -136
|
offset_right = -104
|
||||||
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
|
||||||
|
|
@ -1673,8 +1551,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 = -132
|
offset_left = -94
|
||||||
offset_right = -67
|
offset_right = -16
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 28
|
offset_bottom = 28
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1687,8 +1565,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 = -152
|
offset_left = -114
|
||||||
offset_right = -47
|
offset_right = 4
|
||||||
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
|
||||||
|
|
@ -1701,8 +1579,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 = -43
|
offset_left = 14
|
||||||
offset_right = 22
|
offset_right = 92
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1715,8 +1593,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 = -63
|
offset_left = -6
|
||||||
offset_right = 42
|
offset_right = 112
|
||||||
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
|
||||||
|
|
@ -1729,8 +1607,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 = 46
|
offset_left = 122
|
||||||
offset_right = 111
|
offset_right = 200
|
||||||
offset_top = -100
|
offset_top = -100
|
||||||
offset_bottom = 48
|
offset_bottom = 48
|
||||||
script = ExtResource("10")
|
script = ExtResource("10")
|
||||||
|
|
@ -1743,41 +1621,13 @@ 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 = 26
|
offset_left = 102
|
||||||
offset_right = 131
|
|
||||||
offset_top = 56
|
|
||||||
offset_bottom = 136
|
|
||||||
theme_override_font_sizes/font_size = 24
|
|
||||||
autowrap_mode = 2
|
|
||||||
text = "Laser Ship"
|
|
||||||
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_right = 220
|
||||||
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
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
text = "Distorder"
|
text = "Laser Ship"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
|
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
|
||||||
|
|
@ -2086,9 +1936,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 = -400.0
|
offset_top = -456.0
|
||||||
offset_right = 76.0
|
offset_right = 76.0
|
||||||
offset_bottom = -336.0
|
offset_bottom = -392.0
|
||||||
script = ExtResource("19")
|
script = ExtResource("19")
|
||||||
digit_height = 52.0
|
digit_height = 52.0
|
||||||
|
|
||||||
|
|
@ -2098,9 +1948,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 = -326.0
|
offset_top = -386.0
|
||||||
offset_right = -32.0
|
offset_right = -32.0
|
||||||
offset_bottom = -250.0
|
offset_bottom = -318.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
|
||||||
|
|
@ -2112,9 +1962,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 = -236.0
|
offset_top = -304.0
|
||||||
offset_right = 74.0
|
offset_right = 74.0
|
||||||
offset_bottom = -88.0
|
offset_bottom = -156.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,7 +8,3 @@ 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,13 +4,11 @@ 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,8 +19,7 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
[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=16 format=3]
|
[gd_scene load_steps=15 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,7 +10,6 @@
|
||||||
[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"]
|
||||||
|
|
@ -158,30 +157,6 @@ 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
|
||||||
|
|
@ -229,38 +204,6 @@ 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,7 +6,6 @@ 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"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
[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
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://c6nasspc8ewwj
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
[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")
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
[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,6 +7,5 @@ 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,6 +7,5 @@ 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,6 +7,5 @@ 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,5 +24,4 @@ unlock_id = "unlock_shield"
|
||||||
unlock_name = "Unlock Shield Vessel"
|
unlock_name = "Unlock Shield Vessel"
|
||||||
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
|
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
|
||||||
unlock_cost = 45
|
unlock_cost = 45
|
||||||
research_point_cost = 1
|
|
||||||
unlock_icon = 23
|
unlock_icon = 23
|
||||||
|
|
|
||||||
21
Ship.gd
21
Ship.gd
|
|
@ -97,29 +97,16 @@ 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 * boost:
|
if distance <= arrive_radius:
|
||||||
position = move_target
|
position = move_target
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
else:
|
else:
|
||||||
var effective_acceleration: float = acceleration * boost
|
var braking_speed: float = sqrt(2.0 * acceleration * distance)
|
||||||
var braking_speed: float = sqrt(2.0 * effective_acceleration * distance)
|
var desired_speed: float = min(max_speed, braking_speed)
|
||||||
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, effective_acceleration * delta)
|
velocity = velocity.move_toward(desired_velocity, acceleration * delta)
|
||||||
position += velocity * delta
|
position += velocity * delta
|
||||||
|
|
||||||
var motion: Vector2 = velocity + fleet_velocity
|
var motion: Vector2 = velocity + fleet_velocity
|
||||||
|
|
|
||||||
125
SpeedFader.gd
125
SpeedFader.gd
|
|
@ -1,125 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://m16rgs2ed7w1
|
|
||||||
|
|
@ -7,6 +7,5 @@ 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,6 +7,5 @@ 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}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://drsagaj2wr0ni
|
|
||||||
|
|
@ -23,10 +23,6 @@ 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", "distorder"]
|
const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser"]
|
||||||
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,7 +9,6 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://c2una2fh0f06b
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
[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,19 +1,84 @@
|
||||||
class_name VesselGasBomberComponent
|
class_name VesselGasBomberComponent
|
||||||
extends "res://VesselCooldownFillComponent.gd"
|
extends Node2D
|
||||||
|
|
||||||
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 _init() -> void:
|
func setup() -> void:
|
||||||
cooldown_duration = 5.0
|
# Keep the ship's normal neon outline above the charge fill.
|
||||||
|
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:
|
||||||
return _begin_cooldown(duration)
|
if not is_ready():
|
||||||
|
return false
|
||||||
|
cooldown_duration = maxf(duration, 0.001)
|
||||||
|
cooldown_left = cooldown_duration
|
||||||
|
queue_redraw()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _ready_color() -> Color: return READY_COLOR
|
func tick(delta: float) -> void:
|
||||||
func _cooling_color() -> Color: return COOLING_COLOR
|
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 := 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,16 +1,29 @@
|
||||||
class_name VesselLaserComponent
|
class_name VesselLaserComponent
|
||||||
extends "res://VesselCooldownFillComponent.gd"
|
extends Node2D
|
||||||
|
|
||||||
|
# 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 _init() -> void:
|
func setup() -> void:
|
||||||
cooldown_duration = 4.0
|
# Keep the ship's normal neon outline above the charge fill.
|
||||||
|
show_behind_parent = true
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func apply_stats() -> void:
|
func apply_stats() -> void:
|
||||||
|
|
@ -22,9 +35,66 @@ 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:
|
||||||
return _begin_cooldown(cooldown_duration)
|
if not is_ready():
|
||||||
|
return false
|
||||||
|
cooldown_left = maxf(cooldown_duration, 0.001)
|
||||||
|
queue_redraw()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _ready_color() -> Color: return READY_COLOR
|
func tick(delta: float) -> void:
|
||||||
func _cooling_color() -> Color: return COOLING_COLOR
|
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 := 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 +0,0 @@
|
||||||
uid://bds5gccvd8dj8
|
|
||||||
|
|
@ -9,7 +9,3 @@ 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,10 +22,6 @@ 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
|
||||||
|
|
@ -99,8 +95,6 @@ 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
|
||||||
|
|
@ -110,8 +104,6 @@ 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 +0,0 @@
|
||||||
uid://bhxcvtw6tcshl
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://25xjfqi2qk7a
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://f5a8wm4wr2i4
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://b5ah2jh0i16du
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://brt82kn8lb3u5
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://bs6hguk0tkc6c
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://fd1m4excjdbn
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c0q640rosb53o
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c8lgxg63d8s52
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://cy6n62kcp7q1y
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c0yjf3qdv31eo
|
|
||||||
|
|
@ -8,13 +8,7 @@ extends Node2D
|
||||||
# ... and again at 720x960 to check a shorter screen format.
|
# ... and again at 720x960 to check a shorter screen format.
|
||||||
|
|
||||||
|
|
||||||
var configured: bool = false
|
func _ready() -> void:
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
@ -22,12 +16,9 @@ func _process(_delta: float) -> 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.research_points = 0
|
GameState.unlocked = ["shipyards", "logistics", "freight"]
|
||||||
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")
|
||||||
|
|
@ -36,5 +27,5 @@ func _process(_delta: float) -> 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 = "coordination"
|
bc.skill_tree.selected_id = "freight"
|
||||||
bc._refresh_skill_menu("coordination")
|
bc._refresh_skill_menu("freight")
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://dfi84x2tcwqwy
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://clf1bitxms0ti
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://vlcbg3juhpy3
|
|
||||||
|
|
@ -48,7 +48,6 @@ 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 +0,0 @@
|
||||||
uid://gj02xu88lxpm
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://bistn7dgiv7lw
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c18rxosfduxoa
|
|
||||||
|
|
@ -21,7 +21,6 @@ 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 +0,0 @@
|
||||||
uid://b1fyqy5i8mqf0
|
|
||||||
|
|
@ -3,10 +3,7 @@ extends SceneTree
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
var main: Node
|
var main: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
var skill: Node
|
var skill: GasBombController
|
||||||
# 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
|
||||||
|
|
@ -27,10 +24,6 @@ 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.
|
||||||
|
|
@ -76,7 +69,6 @@ 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")
|
||||||
|
|
@ -125,7 +117,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(AOE_RADIUS_FOR_TEST + 80.0, 0)
|
outside_enemy.position = skill.aim_target + Vector2(GasBombController.AOE_RADIUS + 80.0, 0)
|
||||||
main.add_child(outside_enemy)
|
main.add_child(outside_enemy)
|
||||||
outside_enemy.speed = 0.0
|
outside_enemy.speed = 0.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://cwcw77qvykk35
|
|
||||||
|
|
@ -2,7 +2,7 @@ extends SceneTree
|
||||||
|
|
||||||
var main: Node
|
var main: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
var skill: Node
|
var skill: GasBombController
|
||||||
var elapsed := 0.0
|
var elapsed := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,8 +12,6 @@ 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")
|
||||||
|
|
@ -55,7 +53,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(5.0) # matches GasBombController.COOLDOWN
|
first.tick(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://tguuracp1ewt
|
uid://cwo6ake5taias
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ 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)
|
||||||
|
|
@ -50,9 +48,6 @@ 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:
|
||||||
|
|
@ -78,10 +73,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, conversion_rate])
|
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, GameState.GAS_BURN_RATE])
|
||||||
if energy_made != gas_burned * conversion_yield:
|
if energy_made != gas_burned * GameState.ENERGY_PER_GAS:
|
||||||
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, conversion_yield
|
energy_made, gas_burned, GameState.ENERGY_PER_GAS
|
||||||
])
|
])
|
||||||
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 +0,0 @@
|
||||||
uid://dlx2eu6lw3ps6
|
|
||||||
|
|
@ -12,7 +12,6 @@ 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,7 +10,6 @@ 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,7 +12,6 @@ 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))
|
||||||
|
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
uid://d24p5etger56g
|
|
||||||
|
|
@ -20,12 +20,7 @@ extends SceneTree
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
var main: Node
|
var main: Node
|
||||||
var fleet: Node
|
var fleet: Node
|
||||||
var skill: Node
|
var skill: LaserController
|
||||||
# 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
|
||||||
|
|
@ -47,13 +42,6 @@ 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")
|
||||||
|
|
@ -118,7 +106,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, BEAM_HALF_WIDTH + 60.0)
|
+ Vector2(200.0, LaserController.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 +0,0 @@
|
||||||
uid://dp0e71h2124c8
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue