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