Refactor recent gameplay systems after code review
This commit is contained in:
parent
3f3a22227d
commit
d68f4d4f88
57 changed files with 530 additions and 572 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
|
||||
|
|
@ -738,7 +738,7 @@ func _refresh_skill_menu(id: String = "") -> void:
|
|||
var node: Dictionary = GameState.skill_node(id)
|
||||
if node.is_empty():
|
||||
return
|
||||
var points_needed: int = int(node.get("research_points", 0))
|
||||
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]
|
||||
|
|
@ -1043,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"
|
||||
|
|
@ -1056,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
|
||||
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
|
||||
17
Fleet.gd
17
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
|
||||
|
||||
|
||||
|
|
@ -283,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:
|
||||
|
|
|
|||
120
GameState.gd
120
GameState.gd
|
|
@ -387,7 +387,11 @@ func prerequisite_met(id: String) -> bool:
|
|||
func can_unlock(id: String) -> bool:
|
||||
var node: Dictionary = skill_node(id)
|
||||
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(node.cost)) \
|
||||
and research_points >= int(node.get("research_points", 0))
|
||||
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:
|
||||
|
|
@ -396,7 +400,7 @@ func unlock(id: String) -> bool:
|
|||
var node: Dictionary = skill_node(id)
|
||||
if not spend(int(node.cost)):
|
||||
return false
|
||||
var points_needed: int = int(node.get("research_points", 0))
|
||||
var points_needed: int = skill_research_point_cost(id)
|
||||
if points_needed > 0:
|
||||
research_points -= points_needed
|
||||
research_points_changed.emit(research_points)
|
||||
|
|
@ -446,6 +450,8 @@ func reset_new_game() -> void:
|
|||
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})
|
||||
|
|
@ -520,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
|
||||
|
|
@ -1044,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
|
||||
|
|
@ -1186,44 +1200,50 @@ 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:
|
||||
|
|
@ -1341,7 +1361,7 @@ 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
|
||||
|
|
@ -1500,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
|
||||
|
|
@ -1559,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
|
||||
|
|
@ -1580,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)
|
||||
|
|
@ -1610,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,79 +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
|
||||
# #60: ammunition from the fleet hold, not the bank - the fleet has to be
|
||||
# carrying its own gas to fire.
|
||||
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")
|
||||
@onready var status: Label = get_node("../UI/GasBombStatus")
|
||||
|
||||
var status_message_until: int = 0
|
||||
|
||||
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
|
||||
status.text = ""
|
||||
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 _can_afford_shot() -> bool:
|
||||
return GameState.fleet_resource(GameState.RESOURCE_GAS) >= GAS_PER_SHOT
|
||||
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
|
||||
|
||||
|
||||
func _say(message: String) -> void:
|
||||
status.text = message
|
||||
status_message_until = Time.get_ticks_msec() + 3000
|
||||
func _consume_shot(component: Node) -> bool:
|
||||
return component.consume_shot(COOLDOWN)
|
||||
|
||||
|
||||
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 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 = ""
|
||||
func _perform_shot(ship: Node2D, _component: Node) -> void:
|
||||
projectiles.append({
|
||||
"position": ship.global_position,
|
||||
"target": aim_target,
|
||||
"trail": PackedVector2Array([ship.global_position]),
|
||||
})
|
||||
|
||||
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 _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)
|
||||
|
|
@ -82,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:
|
||||
|
|
@ -94,122 +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:
|
||||
# Deliberately NOT button.enabled: an unaffordable shot still needs the
|
||||
# press to reach here so it can explain itself (IconButton's own "greyed
|
||||
# but still pressable" convention) rather than the touch silently
|
||||
# falling through to whatever's behind it. A wholly unready fleet (no
|
||||
# bomber at all) has nothing more useful to say than "greyed out".
|
||||
return button.visible and not button._blocked_by_modal() \
|
||||
and not aiming and _next_ready_bomber() != 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("Need %d gas in the fleet hold to fire (have %d)" \
|
||||
% [GAS_PER_SHOT, GameState.fleet_resource(GameState.RESOURCE_GAS)])
|
||||
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
|
||||
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
|
||||
if not _can_afford_shot():
|
||||
_say("Need %d gas in the fleet hold to fire (have %d)" \
|
||||
% [GAS_PER_SHOT, GameState.fleet_resource(GameState.RESOURCE_GAS)])
|
||||
_cancel_aim()
|
||||
return
|
||||
var component: Node = bomber.get_component("gas_bomber")
|
||||
if not component.consume_shot(COOLDOWN):
|
||||
_cancel_aim()
|
||||
return
|
||||
GameState.withdraw_fleet_resource(GameState.RESOURCE_GAS, GAS_PER_SHOT)
|
||||
projectiles.append({
|
||||
"position": bomber.global_position,
|
||||
"target": aim_target,
|
||||
"trail": PackedVector2Array([bomber.global_position]),
|
||||
})
|
||||
_finish_aim()
|
||||
button.enabled = _next_ready_bomber() != null and _can_afford_shot()
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -224,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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,195 +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
|
||||
# #61: ammunition from the fleet hold, mirroring #60's gas bomber exactly.
|
||||
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")
|
||||
@onready var status: Label = get_node("../UI/LaserStatus")
|
||||
|
||||
var status_message_until: int = 0
|
||||
|
||||
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
|
||||
status.text = ""
|
||||
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 _can_afford_shot() -> bool:
|
||||
return GameState.fleet_resource(GameState.RESOURCE_ENERGY) >= ENERGY_PER_SHOT
|
||||
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
|
||||
|
||||
|
||||
func _say(message: String) -> void:
|
||||
status.text = message
|
||||
status_message_until = Time.get_ticks_msec() + 3000
|
||||
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 _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 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 = ""
|
||||
func _consume_shot(component: Node) -> bool:
|
||||
return component.consume_shot()
|
||||
|
||||
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 _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:
|
||||
# Deliberately NOT button.enabled: an unaffordable shot still needs the
|
||||
# press to reach here so it can explain itself (#60's identical
|
||||
# reasoning on the gas bomber). A wholly unready fleet (no laser ship at
|
||||
# all) has nothing more useful to say than "greyed out".
|
||||
return button.visible and not button._blocked_by_modal() \
|
||||
and not aiming and _next_ready_laser_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("Need %d energy in the fleet hold to fire (have %d)" \
|
||||
% [ENERGY_PER_SHOT, GameState.fleet_resource(GameState.RESOURCE_ENERGY)])
|
||||
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
|
||||
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
|
||||
if not _can_afford_shot():
|
||||
_say("Need %d energy in the fleet hold to fire (have %d)" \
|
||||
% [ENERGY_PER_SHOT, GameState.fleet_resource(GameState.RESOURCE_ENERGY)])
|
||||
_cancel_aim()
|
||||
return
|
||||
var component: Node = shooter.get_component("laser")
|
||||
if not component.consume_shot():
|
||||
_cancel_aim()
|
||||
return
|
||||
GameState.withdraw_fleet_resource(GameState.RESOURCE_ENERGY, ENERGY_PER_SHOT)
|
||||
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 and _can_afford_shot()
|
||||
|
||||
|
||||
func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> void:
|
||||
|
|
@ -209,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
|
||||
1
ResearchLabPlanetVisual.gd.uid
Normal file
1
ResearchLabPlanetVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c6nasspc8ewwj
|
||||
|
|
@ -35,12 +35,10 @@ func _process(_delta: float) -> void:
|
|||
if has_distorder != visible:
|
||||
visible = has_distorder
|
||||
if not has_distorder:
|
||||
# A fleet must never be left permanently boosted by a vessel it no
|
||||
# longer has - reset the instant the capability disappears, not just
|
||||
# hide the control on top of a stale multiplier.
|
||||
# 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
|
||||
if GameState.fleet_speed_multiplier != 1.0:
|
||||
GameState.fleet_speed_multiplier = 1.0
|
||||
knob_t = 0.0
|
||||
return
|
||||
knob_t = (GameState.fleet_speed_multiplier - 1.0) \
|
||||
|
|
|
|||
1
SpeedFader.gd.uid
Normal file
1
SpeedFader.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://m16rgs2ed7w1
|
||||
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
|
||||
1
VesselDistorderComponent.gd.uid
Normal file
1
VesselDistorderComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c2una2fh0f06b
|
||||
|
|
@ -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
|
||||
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
|
||||
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
|
||||
1
tests/test_carrier_vessel.gd.uid
Normal file
1
tests/test_carrier_vessel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://gj02xu88lxpm
|
||||
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
|
||||
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
|
||||
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
|
||||
1
tests/test_gas_bomber_cooldowns.gd.uid
Normal file
1
tests/test_gas_bomber_cooldowns.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
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))
|
||||
|
|
|
|||
1
tests/test_laser_ammo.gd.uid
Normal file
1
tests/test_laser_ammo.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d24p5etger56g
|
||||
1
tests/test_laser_vessel.gd.uid
Normal file
1
tests/test_laser_vessel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dp0e71h2124c8
|
||||
1
tests/test_research_points.gd.uid
Normal file
1
tests/test_research_points.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://x08iyhs2lrrr
|
||||
1
tests/test_resource_readout.gd.uid
Normal file
1
tests/test_resource_readout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bhad0657luiec
|
||||
1
tests/test_space_time_distorder.gd.uid
Normal file
1
tests/test_space_time_distorder.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://btmmwkkgw6qwi
|
||||
1
tests/test_surface_profiles.gd.uid
Normal file
1
tests/test_surface_profiles.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://crbidmy4oqat7
|
||||
1
tests/test_turret_spawner_enemy.gd.uid
Normal file
1
tests/test_turret_spawner_enemy.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://du2i8ayup3qkd
|
||||
|
|
@ -23,8 +23,7 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
|
||||
if fleet_definitions.size() != 5 \
|
||||
or fleet_definitions.any(func(definition): return definition.has_capability("transport")):
|
||||
if fleet_definitions.any(func(definition): return definition.has_capability("transport")):
|
||||
return _fail("logistics transporters leaked into the combat-fleet catalog")
|
||||
if ships.size() != definitions.size():
|
||||
return _fail("catalog and composed fleet differ in size")
|
||||
|
|
|
|||
Loading…
Reference in a new issue