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)
|
var node: Dictionary = GameState.skill_node(id)
|
||||||
if node.is_empty():
|
if node.is_empty():
|
||||||
return
|
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]
|
skill_detail.text = "%s\n%s" % [node.name, node.desc]
|
||||||
if points_needed > 0:
|
if points_needed > 0:
|
||||||
skill_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points_needed]
|
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)
|
var b: Dictionary = GameState.find_building(transfer_building_id)
|
||||||
if b.is_empty():
|
if b.is_empty():
|
||||||
return "this building"
|
return "this building"
|
||||||
if b.type == GameState.BUILDING_MOTHERSHIP:
|
if GameState.building_has_capability(b, "bank"):
|
||||||
# Worth naming as the bank rather than as a structure: this is the
|
# Worth naming as the bank rather than as a structure: this is the
|
||||||
# one building where unloading makes the ore spendable everywhere.
|
# one building where unloading makes the ore spendable everywhere.
|
||||||
return "the bank"
|
return "the bank"
|
||||||
|
|
@ -1056,7 +1056,7 @@ func _refresh_transfer() -> void:
|
||||||
return
|
return
|
||||||
var building: Dictionary = GameState.find_building(transfer_building_id)
|
var building: Dictionary = GameState.find_building(transfer_building_id)
|
||||||
var heading: String = "This building"
|
var heading: String = "This building"
|
||||||
if not building.is_empty() and building.type == GameState.BUILDING_MOTHERSHIP:
|
if GameState.building_has_capability(building, "bank"):
|
||||||
heading = "Mothership bank"
|
heading = "Mothership bank"
|
||||||
transfer_list.set_destination(transfer_building_id, heading)
|
transfer_list.set_destination(transfer_building_id, heading)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ extends Resource
|
||||||
@export var menu_icon: int
|
@export var menu_icon: int
|
||||||
@export var visual_scene: PackedScene
|
@export var visual_scene: PackedScene
|
||||||
@export var capabilities: PackedStringArray = []
|
@export var capabilities: PackedStringArray = []
|
||||||
|
@export var logistics: Resource
|
||||||
@export var map_color: Color = Color.WHITE
|
@export var map_color: Color = Color.WHITE
|
||||||
@export var map_sides: int = 4
|
@export var map_sides: int = 4
|
||||||
@export var map_radius: float = 7.0
|
@export var map_radius: float = 7.0
|
||||||
|
|
|
||||||
13
BuildingLogisticsProfile.gd
Normal file
13
BuildingLogisticsProfile.gd
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
class_name BuildingLogisticsProfile
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
# Optional logistics and conversion behavior composed into a building
|
||||||
|
# definition. GameState consumes this data generically, so adding another
|
||||||
|
# converter does not require another resource-type branch in transport code.
|
||||||
|
|
||||||
|
@export var input_buffers: Dictionary = {}
|
||||||
|
@export var collected_outputs: PackedStringArray = []
|
||||||
|
@export var conversion_input: String = ""
|
||||||
|
@export var conversion_output: String = ""
|
||||||
|
@export var conversion_rate: float = 0.0
|
||||||
|
@export var conversion_yield: int = 1
|
||||||
1
BuildingLogisticsProfile.gd.uid
Normal file
1
BuildingLogisticsProfile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cydp18abylrgd
|
||||||
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))
|
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
||||||
|
|
||||||
fleet_target = center
|
fleet_target = center
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
|
|
||||||
|
|
||||||
func vessel_types() -> PackedStringArray:
|
func vessel_types() -> PackedStringArray:
|
||||||
|
|
@ -91,6 +91,15 @@ func vessel_types() -> PackedStringArray:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_composition_state() -> void:
|
||||||
|
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||||
|
# Composition changes are authoritative; scene teardown is not. Keeping
|
||||||
|
# this out of SpeedFader prevents an outgoing scene's empty fleet from
|
||||||
|
# resetting a boost while the same vessels are being restored on a surface.
|
||||||
|
if not has_capability("distorder"):
|
||||||
|
GameState.set_fleet_speed_multiplier(1.0)
|
||||||
|
|
||||||
|
|
||||||
func has_vessel_type(vessel_type: String) -> bool:
|
func has_vessel_type(vessel_type: String) -> bool:
|
||||||
for ship in ships:
|
for ship in ships:
|
||||||
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
|
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
|
||||||
|
|
@ -139,7 +148,7 @@ func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
|
||||||
if ship.has_signal("destroyed"):
|
if ship.has_signal("destroyed"):
|
||||||
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
||||||
ship.set_move_target(fleet_target + offset)
|
ship.set_move_target(fleet_target + offset)
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,7 +166,7 @@ func store_ship(index: int) -> String:
|
||||||
formation_offsets.remove_at(index)
|
formation_offsets.remove_at(index)
|
||||||
ship.queue_free()
|
ship.queue_free()
|
||||||
apply_formation()
|
apply_formation()
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
return vessel_type
|
return vessel_type
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -283,7 +292,7 @@ func _on_ship_destroyed(ship: Node) -> void:
|
||||||
if idx != -1:
|
if idx != -1:
|
||||||
ships.remove_at(idx)
|
ships.remove_at(idx)
|
||||||
formation_offsets.remove_at(idx)
|
formation_offsets.remove_at(idx)
|
||||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
_sync_composition_state()
|
||||||
|
|
||||||
|
|
||||||
func get_centroid() -> Vector2:
|
func get_centroid() -> Vector2:
|
||||||
|
|
|
||||||
120
GameState.gd
120
GameState.gd
|
|
@ -387,7 +387,11 @@ func prerequisite_met(id: String) -> bool:
|
||||||
func can_unlock(id: String) -> bool:
|
func can_unlock(id: String) -> bool:
|
||||||
var node: Dictionary = skill_node(id)
|
var node: Dictionary = skill_node(id)
|
||||||
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(node.cost)) \
|
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:
|
func unlock(id: String) -> bool:
|
||||||
|
|
@ -396,7 +400,7 @@ func unlock(id: String) -> bool:
|
||||||
var node: Dictionary = skill_node(id)
|
var node: Dictionary = skill_node(id)
|
||||||
if not spend(int(node.cost)):
|
if not spend(int(node.cost)):
|
||||||
return false
|
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:
|
if points_needed > 0:
|
||||||
research_points -= points_needed
|
research_points -= points_needed
|
||||||
research_points_changed.emit(research_points)
|
research_points_changed.emit(research_points)
|
||||||
|
|
@ -446,6 +450,8 @@ func reset_new_game() -> void:
|
||||||
planet_id = ""
|
planet_id = ""
|
||||||
planet_definition_id = "rocky"
|
planet_definition_id = "rocky"
|
||||||
fleet_speed_multiplier = 1.0
|
fleet_speed_multiplier = 1.0
|
||||||
|
_distorder_drain_progress = 0.0
|
||||||
|
boost_depleted_notice = false
|
||||||
research_points = 0
|
research_points = 0
|
||||||
explored_chunks.clear()
|
explored_chunks.clear()
|
||||||
bank_inventory = INVENTORY.new({RESOURCE_ORE: 500})
|
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
|
return definition != null and capability in definition.capabilities
|
||||||
|
|
||||||
|
|
||||||
|
func building_logistics_profile(building: Dictionary) -> Resource:
|
||||||
|
if building.is_empty():
|
||||||
|
return null
|
||||||
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(String(building.type))
|
||||||
|
return null if definition == null else definition.logistics
|
||||||
|
|
||||||
|
|
||||||
# --- vessel types and their upgrades (#5) ---------------------------------
|
# --- vessel types and their upgrades (#5) ---------------------------------
|
||||||
|
|
||||||
# Combat vessels can join the player fleet. Transporter metadata lives in the
|
# Combat vessels can join the player fleet. Transporter metadata lives in the
|
||||||
|
|
@ -1044,19 +1057,20 @@ func has_building_of_type(type: String) -> bool:
|
||||||
|
|
||||||
func remove_buildings_of_type(type: String) -> int:
|
func remove_buildings_of_type(type: String) -> int:
|
||||||
var kept: Array = []
|
var kept: Array = []
|
||||||
var removed_ids: Array = []
|
var removed_hub_ids: Array = []
|
||||||
var removed: int = 0
|
var removed: int = 0
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
if b.type == type:
|
if b.type == type:
|
||||||
removed += 1
|
removed += 1
|
||||||
removed_ids.append(int(b.id))
|
if building_has_capability(b, "transport_hub"):
|
||||||
|
removed_hub_ids.append(int(b.id))
|
||||||
else:
|
else:
|
||||||
kept.append(b)
|
kept.append(b)
|
||||||
buildings = kept
|
buildings = kept
|
||||||
if type == BUILDING_TRANSPORT_HUB and not removed_ids.is_empty():
|
if not removed_hub_ids.is_empty():
|
||||||
var kept_transporters: Array = []
|
var kept_transporters: Array = []
|
||||||
for transporter in transporters:
|
for transporter in transporters:
|
||||||
if int(transporter.get("hub_id", 0)) not in removed_ids:
|
if int(transporter.get("hub_id", 0)) not in removed_hub_ids:
|
||||||
kept_transporters.append(transporter)
|
kept_transporters.append(transporter)
|
||||||
transporters = kept_transporters
|
transporters = kept_transporters
|
||||||
return removed
|
return removed
|
||||||
|
|
@ -1186,44 +1200,50 @@ func _extractor_inventory(extractor: Dictionary) -> RefCounted:
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
# --- gas burners (#54) -----------------------------------------------------
|
# --- building logistics and resource conversion ---------------------------
|
||||||
#
|
|
||||||
# Converts gas into energy at the building's own site, simulated here
|
|
||||||
# alongside extractor mining and production so it keeps running whether or
|
|
||||||
# not the player is nearby. Deliberately withdraws only from the building's
|
|
||||||
# OWN inventory, never the bank - a burner with no local gas simply stalls,
|
|
||||||
# exactly like a lab or crafter waiting on materials, rather than conjuring
|
|
||||||
# energy for free.
|
|
||||||
|
|
||||||
const GAS_BURN_RATE: float = 1.0 # gas/second
|
func building_logistics_deficit(id: int, resource_type: String) -> int:
|
||||||
const ENERGY_PER_GAS: int = 2
|
var building: Dictionary = find_building(id)
|
||||||
# Local gas reserve a burner tries to keep topped up. Transporters treat
|
var profile: Resource = building_logistics_profile(building)
|
||||||
# falling short of this the same way they treat a queued production/research
|
if profile == null:
|
||||||
# order (see _dispatch_transporters()'s gas_burner_gas_deficit() use).
|
|
||||||
const GAS_BURNER_BUFFER: int = 40
|
|
||||||
|
|
||||||
|
|
||||||
func gas_burner_gas_deficit(id: int) -> int:
|
|
||||||
var b: Dictionary = find_building(id)
|
|
||||||
if not building_has_capability(b, "gas_burner"):
|
|
||||||
return 0
|
return 0
|
||||||
return maxi(0, GAS_BURNER_BUFFER - building_resource(id, RESOURCE_GAS))
|
var target: int = int(profile.input_buffers.get(resource_type, 0))
|
||||||
|
return maxi(0, target - building_resource(id, resource_type))
|
||||||
|
|
||||||
|
|
||||||
func _advance_gas_burners(delta: float) -> void:
|
func building_requested_resource(id: int, resource_type: String) -> int:
|
||||||
for b in buildings:
|
return maxi(outstanding_resource_cost(id, resource_type),
|
||||||
if not building_has_capability(b, "gas_burner"):
|
building_logistics_deficit(id, resource_type))
|
||||||
|
|
||||||
|
|
||||||
|
func _collectable_building_resource(building: Dictionary, minimum_amount: int) -> String:
|
||||||
|
var profile: Resource = building_logistics_profile(building)
|
||||||
|
if profile == null:
|
||||||
|
return ""
|
||||||
|
for resource_type in profile.collected_outputs:
|
||||||
|
if building_resource(int(building.id), resource_type) >= minimum_amount:
|
||||||
|
return resource_type
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _advance_resource_converters(delta: float) -> void:
|
||||||
|
for building in buildings:
|
||||||
|
var profile: Resource = building_logistics_profile(building)
|
||||||
|
if profile == null or profile.conversion_input == "" \
|
||||||
|
or profile.conversion_output == "" or profile.conversion_rate <= 0.0 \
|
||||||
|
or profile.conversion_yield <= 0:
|
||||||
continue
|
continue
|
||||||
var inventory: RefCounted = _inventory_for_record(b)
|
var inventory: RefCounted = _inventory_for_record(building)
|
||||||
if inventory.amount(RESOURCE_GAS) <= 0:
|
if inventory.amount(profile.conversion_input) <= 0:
|
||||||
continue # stalled - no local gas to burn
|
continue
|
||||||
var progress: float = float(b.get("gas_burn_progress", 0.0)) + delta * GAS_BURN_RATE
|
var progress: float = float(building.get("conversion_progress", 0.0)) \
|
||||||
var whole: int = mini(int(floor(progress)), inventory.amount(RESOURCE_GAS))
|
+ delta * profile.conversion_rate
|
||||||
|
var whole: int = mini(int(floor(progress)), inventory.amount(profile.conversion_input))
|
||||||
if whole > 0:
|
if whole > 0:
|
||||||
inventory.withdraw(RESOURCE_GAS, whole)
|
inventory.withdraw(profile.conversion_input, whole)
|
||||||
inventory.deposit(RESOURCE_ENERGY, whole * ENERGY_PER_GAS)
|
inventory.deposit(profile.conversion_output, whole * profile.conversion_yield)
|
||||||
progress -= whole
|
progress -= whole
|
||||||
b.gas_burn_progress = progress
|
building.conversion_progress = progress
|
||||||
|
|
||||||
|
|
||||||
func _advance_distorder_drain(delta: float) -> void:
|
func _advance_distorder_drain(delta: float) -> void:
|
||||||
|
|
@ -1341,7 +1361,7 @@ func _process(delta: float) -> void:
|
||||||
_advance_production(delta)
|
_advance_production(delta)
|
||||||
_advance_lab_research(delta)
|
_advance_lab_research(delta)
|
||||||
_advance_vessel_deliveries(delta)
|
_advance_vessel_deliveries(delta)
|
||||||
_advance_gas_burners(delta)
|
_advance_resource_converters(delta)
|
||||||
_advance_distorder_drain(delta)
|
_advance_distorder_drain(delta)
|
||||||
for e in extractors:
|
for e in extractors:
|
||||||
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
|
||||||
|
|
@ -1500,13 +1520,7 @@ func _dispatch_transporters() -> void:
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
var needed_resource := ""
|
var needed_resource := ""
|
||||||
for resource_type in resource_types():
|
for resource_type in resource_types():
|
||||||
# A gas burner's ongoing gas deficit is folded in alongside
|
var need: int = building_requested_resource(int(b.id), resource_type)
|
||||||
# queued production/research costs (#54) - both are just "this
|
|
||||||
# building wants more of resource_type", so the one dispatch
|
|
||||||
# path already below serves both without any new transporter
|
|
||||||
# state.
|
|
||||||
var need: int = maxi(outstanding_resource_cost(int(b.id), resource_type),
|
|
||||||
gas_burner_gas_deficit(int(b.id)) if resource_type == RESOURCE_GAS else 0)
|
|
||||||
if need > 0 and bank_inventory.amount(resource_type) > 0:
|
if need > 0 and bank_inventory.amount(resource_type) > 0:
|
||||||
needed_resource = resource_type
|
needed_resource = resource_type
|
||||||
break
|
break
|
||||||
|
|
@ -1559,14 +1573,11 @@ func _dispatch_transporters() -> void:
|
||||||
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
|
||||||
TRANSPORTER_TO_EXTRACTOR)
|
TRANSPORTER_TO_EXTRACTOR)
|
||||||
|
|
||||||
# Collecting a gas burner's energy output, last: queued deliveries and
|
# Collect building outputs last: queued deliveries and extractor collection
|
||||||
# extractor collection both outrank it, same "supply what's needed
|
# both outrank resources that are merely ready for pickup.
|
||||||
# before what's merely ready" ordering _dispatch_transporters() already
|
|
||||||
# holds for extractors over queued orders (f5c60f8).
|
|
||||||
for b in buildings:
|
for b in buildings:
|
||||||
if not building_has_capability(b, "gas_burner"):
|
var pickup_resource: String = _collectable_building_resource(b, load)
|
||||||
continue
|
if pickup_resource == "" or _transporter_inbound_for_pickup(int(b.id)):
|
||||||
if building_resource(int(b.id), RESOURCE_ENERGY) < load or _transporter_inbound_for_pickup(int(b.id)):
|
|
||||||
continue
|
continue
|
||||||
var chosen_pickup: Dictionary = {}
|
var chosen_pickup: Dictionary = {}
|
||||||
var best_pickup_distance: float = INF
|
var best_pickup_distance: float = INF
|
||||||
|
|
@ -1580,7 +1591,7 @@ func _dispatch_transporters() -> void:
|
||||||
if chosen_pickup.is_empty():
|
if chosen_pickup.is_empty():
|
||||||
return
|
return
|
||||||
chosen_pickup.target_id = int(b.id)
|
chosen_pickup.target_id = int(b.id)
|
||||||
chosen_pickup.resource_type = RESOURCE_ENERGY
|
chosen_pickup.resource_type = pickup_resource
|
||||||
chosen_pickup.extractor_index = -1
|
chosen_pickup.extractor_index = -1
|
||||||
_begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position,
|
_begin_leg(chosen_pickup, transporter_position(chosen_pickup), b.position,
|
||||||
TRANSPORTER_TO_BUILDING_PICKUP)
|
TRANSPORTER_TO_BUILDING_PICKUP)
|
||||||
|
|
@ -1610,8 +1621,7 @@ func _arrive(t: Dictionary) -> void:
|
||||||
TRANSPORTER_TO_MOTHERSHIP:
|
TRANSPORTER_TO_MOTHERSHIP:
|
||||||
var target: Dictionary = find_building(int(t.target_id))
|
var target: Dictionary = find_building(int(t.target_id))
|
||||||
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
var resource_type: String = String(t.get("resource_type", RESOURCE_ORE))
|
||||||
var want: int = maxi(outstanding_resource_cost(int(t.target_id), resource_type),
|
var want: int = building_requested_resource(int(t.target_id), resource_type)
|
||||||
gas_burner_gas_deficit(int(t.target_id)) if resource_type == RESOURCE_GAS else 0)
|
|
||||||
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
|
var take: int = mini(mini(want, bank_inventory.amount(resource_type)), transporter_load_limit())
|
||||||
if take <= 0 or target.is_empty():
|
if take <= 0 or target.is_empty():
|
||||||
# Nothing to carry, or the building is gone. Go home empty.
|
# Nothing to carry, or the building is gone. Go home empty.
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,64 @@
|
||||||
class_name GasBombController
|
class_name GasBombController
|
||||||
extends Node2D
|
extends "res://AimedFleetAbilityController.gd"
|
||||||
|
|
||||||
const BOMB_COLOR := Color(0.96, 0.22, 0.72)
|
const BOMB_COLOR := Color(0.96, 0.22, 0.72)
|
||||||
const TARGET_COLOR := Color(0.78, 0.32, 1.0)
|
const TARGET_COLOR := Color(0.78, 0.32, 1.0)
|
||||||
const MAX_RANGE := 950.0
|
const MAX_RANGE := 950.0
|
||||||
const AOE_RADIUS := 180.0
|
const AOE_RADIUS := 180.0
|
||||||
const DRAG_RADIUS := 260.0
|
const DRAG_RADIUS := 260.0
|
||||||
const AIM_DEADZONE := 22.0
|
|
||||||
const PROJECTILE_SPEED := 720.0
|
const PROJECTILE_SPEED := 720.0
|
||||||
const DAMAGE := 3
|
const DAMAGE := 3
|
||||||
const COOLDOWN := 5.0
|
const COOLDOWN := 5.0
|
||||||
# #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
|
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 projectiles: Array = []
|
||||||
var explosions: Array = []
|
var explosions: Array = []
|
||||||
var last_hit_count := 0
|
var last_hit_count: int = 0
|
||||||
|
|
||||||
var projectile_active: bool:
|
var projectile_active: bool:
|
||||||
get: return not projectiles.is_empty()
|
get: return not projectiles.is_empty()
|
||||||
|
|
||||||
var explosion_time: float:
|
var explosion_time: float:
|
||||||
get:
|
get:
|
||||||
var longest := 0.0
|
var longest: float = 0.0
|
||||||
for explosion in explosions:
|
for explosion in explosions:
|
||||||
longest = maxf(longest, float(explosion.time))
|
longest = maxf(longest, float(explosion.time))
|
||||||
return longest
|
return longest
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _button_path() -> NodePath: return "../UI/GasBombButton"
|
||||||
button.visible = false
|
func _status_path() -> NodePath: return "../UI/GasBombStatus"
|
||||||
status.text = ""
|
func _component_id() -> String: return "gas_bomber"
|
||||||
queue_redraw()
|
func _ammo_resource() -> String: return GameState.RESOURCE_GAS
|
||||||
|
func _ammo_cost() -> int: return GAS_PER_SHOT
|
||||||
|
func _aim_padding() -> float: return AOE_RADIUS
|
||||||
|
func _has_effect_visuals() -> bool: return not projectiles.is_empty() or not explosions.is_empty()
|
||||||
|
|
||||||
|
|
||||||
func _can_afford_shot() -> bool:
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
||||||
return GameState.fleet_resource(GameState.RESOURCE_GAS) >= GAS_PER_SHOT
|
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:
|
func _consume_shot(component: Node) -> bool:
|
||||||
status.text = message
|
return component.consume_shot(COOLDOWN)
|
||||||
status_message_until = Time.get_ticks_msec() + 3000
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _perform_shot(ship: Node2D, _component: Node) -> void:
|
||||||
var had_visuals := aiming or not projectiles.is_empty() or not explosions.is_empty()
|
projectiles.append({
|
||||||
button.visible = fleet.has_capability("gas_bomber")
|
"position": ship.global_position,
|
||||||
button.enabled = _next_ready_bomber() != null and _can_afford_shot()
|
"target": aim_target,
|
||||||
if not button.visible and aiming:
|
"trail": PackedVector2Array([ship.global_position]),
|
||||||
_cancel_aim()
|
})
|
||||||
if status.text != "" and Time.get_ticks_msec() >= status_message_until:
|
|
||||||
status.text = ""
|
|
||||||
|
|
||||||
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):
|
for i in range(projectiles.size() - 1, -1, -1):
|
||||||
var projectile: Dictionary = projectiles[i]
|
var projectile: Dictionary = projectiles[i]
|
||||||
var distance: float = (projectile.position as Vector2).distance_to(projectile.target)
|
var distance: float = (projectile.position as Vector2).distance_to(projectile.target)
|
||||||
|
|
@ -82,8 +67,8 @@ func _process(delta: float) -> void:
|
||||||
_explode(projectile.target)
|
_explode(projectile.target)
|
||||||
projectiles.remove_at(i)
|
projectiles.remove_at(i)
|
||||||
else:
|
else:
|
||||||
projectile.position = (projectile.position as Vector2).move_toward(projectile.target,
|
projectile.position = (projectile.position as Vector2).move_toward(
|
||||||
PROJECTILE_SPEED * delta)
|
projectile.target, PROJECTILE_SPEED * delta)
|
||||||
var trail: PackedVector2Array = projectile.trail
|
var trail: PackedVector2Array = projectile.trail
|
||||||
trail.append(projectile.position)
|
trail.append(projectile.position)
|
||||||
while trail.size() > 12:
|
while trail.size() > 12:
|
||||||
|
|
@ -94,122 +79,6 @@ func _process(delta: float) -> void:
|
||||||
explosion.time = maxf(float(explosion.time) - delta, 0.0)
|
explosion.time = maxf(float(explosion.time) - delta, 0.0)
|
||||||
if float(explosion.time) <= 0.0:
|
if float(explosion.time) <= 0.0:
|
||||||
explosions.remove_at(i)
|
explosions.remove_at(i)
|
||||||
if aiming or not projectiles.is_empty() or not explosions.is_empty() or had_visuals:
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func can_begin_aim() -> bool:
|
|
||||||
# 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:
|
func _explode(at: Vector2) -> void:
|
||||||
|
|
@ -224,28 +93,29 @@ func _explode(at: Vector2) -> void:
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if aiming:
|
if aiming:
|
||||||
var origin: Vector2 = fleet.get_centroid()
|
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.8)
|
||||||
NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8)
|
|
||||||
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
|
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
|
||||||
_draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0)
|
_draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0)
|
||||||
for projectile in projectiles:
|
for projectile in projectiles:
|
||||||
var trail: PackedVector2Array = projectile.trail
|
var trail: PackedVector2Array = projectile.trail
|
||||||
if trail.size() >= 2:
|
if trail.size() >= 2:
|
||||||
for i in range(1, trail.size()):
|
for i in range(1, trail.size()):
|
||||||
var alpha := float(i) / trail.size()
|
var alpha: float = float(i) / trail.size()
|
||||||
draw_line(trail[i - 1], trail[i],
|
draw_line(trail[i - 1], trail[i], Color(BOMB_COLOR, alpha * 0.65),
|
||||||
Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true)
|
4.0 + alpha * 4.0, true)
|
||||||
draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35))
|
draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35))
|
||||||
NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0)
|
NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0)
|
||||||
for explosion in explosions:
|
for explosion in explosions:
|
||||||
var progress := 1.0 - float(explosion.time) / 0.8
|
var progress: float = 1.0 - float(explosion.time) / 0.8
|
||||||
var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
var radius: float = lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
||||||
draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
||||||
_draw_ring(explosion.position, radius, BOMB_COLOR, 1.0 - progress)
|
_draw_ring(explosion.position, radius, BOMB_COLOR, 1.0 - progress)
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
var angle := TAU * float(i) / 7.0 + progress
|
var angle: float = TAU * float(i) / 7.0 + progress
|
||||||
var cloud_at: Vector2 = explosion.position + Vector2(cos(angle), sin(angle)) * radius * 0.55
|
var cloud_at: Vector2 = explosion.position \
|
||||||
draw_circle(cloud_at, 18.0 + 14.0 * progress, Color(BOMB_COLOR, (1.0 - progress) * 0.22))
|
+ Vector2(cos(angle), sin(angle)) * radius * 0.55
|
||||||
|
draw_circle(cloud_at, 18.0 + 14.0 * progress,
|
||||||
|
Color(BOMB_COLOR, (1.0 - progress) * 0.22))
|
||||||
|
|
||||||
|
|
||||||
func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void:
|
func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=4 format=3]
|
||||||
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
[ext_resource type="Script" path="res://BuildingDefinition.gd" id="1"]
|
||||||
[ext_resource type="PackedScene" path="res://GasBurnerBuildingVisual.tscn" id="2"]
|
[ext_resource type="PackedScene" path="res://GasBurnerBuildingVisual.tscn" id="2"]
|
||||||
|
[ext_resource type="Script" path="res://BuildingLogisticsProfile.gd" id="3"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_logistics"]
|
||||||
|
script = ExtResource("3")
|
||||||
|
input_buffers = {
|
||||||
|
"gas": 40
|
||||||
|
}
|
||||||
|
collected_outputs = PackedStringArray("energy")
|
||||||
|
conversion_input = "gas"
|
||||||
|
conversion_output = "energy"
|
||||||
|
conversion_rate = 1.0
|
||||||
|
conversion_yield = 2
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
definition_id = "gas_burner"
|
definition_id = "gas_burner"
|
||||||
|
|
@ -11,6 +24,7 @@ action_icon = 33
|
||||||
menu_icon = 33
|
menu_icon = 33
|
||||||
visual_scene = ExtResource("2")
|
visual_scene = ExtResource("2")
|
||||||
capabilities = PackedStringArray("gas_burner")
|
capabilities = PackedStringArray("gas_burner")
|
||||||
|
logistics = SubResource("Resource_logistics")
|
||||||
map_color = Color(0.95, 0.35, 0.15, 1)
|
map_color = Color(0.95, 0.35, 0.15, 1)
|
||||||
map_sides = 5
|
map_sides = 5
|
||||||
map_radius = 7.0
|
map_radius = 7.0
|
||||||
|
|
|
||||||
1
GasBurnerBuildingVisual.gd.uid
Normal file
1
GasBurnerBuildingVisual.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5a74rb5s4q21
|
||||||
|
|
@ -1,195 +1,60 @@
|
||||||
class_name LaserController
|
class_name LaserController
|
||||||
extends Node2D
|
extends "res://AimedFleetAbilityController.gd"
|
||||||
|
|
||||||
# Same press-drag-release aiming plumbing as GasBombController, adapted for
|
|
||||||
# an instant beam rather than a lobbed, timed projectile: there is no travel
|
|
||||||
# time or explosion delay - the beam strikes the instant the shot fires, and
|
|
||||||
# only a brief flash lingers for feedback. Damage/range/cooldown are read
|
|
||||||
# straight off the firing ship's own VesselLaserComponent (already resolved
|
|
||||||
# from vessel_stat/upgrades) rather than hardcoded here, unlike the bomber's
|
|
||||||
# AOE_RADIUS/DAMAGE/COOLDOWN.
|
|
||||||
#
|
|
||||||
# Reused as-is by both Main.tscn and PlanetSurface.tscn (same relative
|
|
||||||
# ../Fleet, ../Camera2D, ../UI/LaserButton paths as GasBombController), so
|
|
||||||
# it works identically in space and on a landed surface with no branching.
|
|
||||||
|
|
||||||
const BEAM_COLOR := Color(0.3, 0.9, 1.0)
|
const BEAM_COLOR := Color(0.3, 0.9, 1.0)
|
||||||
const TARGET_COLOR := Color(0.6, 0.95, 1.0)
|
const TARGET_COLOR := Color(0.6, 0.95, 1.0)
|
||||||
const BEAM_HALF_WIDTH := 22.0
|
const BEAM_HALF_WIDTH := 22.0
|
||||||
const DRAG_DEADZONE := 22.0
|
|
||||||
const FLASH_DURATION := 0.22
|
const FLASH_DURATION := 0.22
|
||||||
# #61: ammunition from the fleet hold, mirroring #60's gas bomber exactly.
|
|
||||||
const ENERGY_PER_SHOT: int = 8
|
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 beams: Array = []
|
||||||
var last_hit_count := 0
|
var last_hit_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _button_path() -> NodePath: return "../UI/LaserButton"
|
||||||
button.visible = false
|
func _status_path() -> NodePath: return "../UI/LaserStatus"
|
||||||
status.text = ""
|
func _component_id() -> String: return "laser"
|
||||||
queue_redraw()
|
func _ammo_resource() -> String: return GameState.RESOURCE_ENERGY
|
||||||
|
func _ammo_cost() -> int: return ENERGY_PER_SHOT
|
||||||
|
func _aim_padding() -> float: return BEAM_HALF_WIDTH
|
||||||
|
func _has_effect_visuals() -> bool: return not beams.is_empty()
|
||||||
|
|
||||||
|
|
||||||
func _can_afford_shot() -> bool:
|
func _aim_max_range() -> float:
|
||||||
return GameState.fleet_resource(GameState.RESOURCE_ENERGY) >= ENERGY_PER_SHOT
|
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:
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
||||||
status.text = message
|
# A beam always reaches full range; dragging chooses its direction only.
|
||||||
status_message_until = Time.get_ticks_msec() + 3000
|
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
||||||
|
return origin + direction * _aim_max_range()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _consume_shot(component: Node) -> bool:
|
||||||
var had_visuals := aiming or not beams.is_empty()
|
return component.consume_shot()
|
||||||
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 = ""
|
|
||||||
|
|
||||||
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):
|
for i in range(beams.size() - 1, -1, -1):
|
||||||
var beam: Dictionary = beams[i]
|
var beam: Dictionary = beams[i]
|
||||||
beam.time = maxf(float(beam.time) - delta, 0.0)
|
beam.time = maxf(float(beam.time) - delta, 0.0)
|
||||||
if float(beam.time) <= 0.0:
|
if float(beam.time) <= 0.0:
|
||||||
beams.remove_at(i)
|
beams.remove_at(i)
|
||||||
if aiming or not beams.is_empty() or had_visuals:
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func can_begin_aim() -> bool:
|
|
||||||
# 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:
|
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
|
last_hit_count += 1
|
||||||
|
|
||||||
|
|
||||||
func _next_ready_laser_ship() -> Node2D:
|
|
||||||
for ship in fleet.ships:
|
|
||||||
if not is_instance_valid(ship):
|
|
||||||
continue
|
|
||||||
var component: Node = ship.get_component("laser")
|
|
||||||
if component != null and component.is_ready():
|
|
||||||
return ship
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if aiming:
|
if aiming:
|
||||||
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.6)
|
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target,
|
||||||
|
TARGET_COLOR, 0.7, 0.6)
|
||||||
for beam in beams:
|
for beam in beams:
|
||||||
var progress: float = 1.0 - float(beam.time) / FLASH_DURATION
|
var progress: float = 1.0 - float(beam.time) / FLASH_DURATION
|
||||||
var alpha: float = 1.0 - progress
|
var alpha: float = 1.0 - progress
|
||||||
NeonDraw.draw_glowing_line(self, beam.from, beam.to, BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|
NeonDraw.draw_glowing_line(self, beam.from, beam.to,
|
||||||
|
BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|
||||||
|
|
|
||||||
1
LaserController.gd.uid
Normal file
1
LaserController.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cdqg3isy1lbuf
|
||||||
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:
|
if has_distorder != visible:
|
||||||
visible = has_distorder
|
visible = has_distorder
|
||||||
if not has_distorder:
|
if not has_distorder:
|
||||||
# A fleet must never be left permanently boosted by a vessel it no
|
# Fleet owns persistent composition state. This outgoing UI node can
|
||||||
# longer has - reset the instant the capability disappears, not just
|
# briefly observe an empty fleet during a scene swap, so it must only
|
||||||
# hide the control on top of a stale multiplier.
|
# clear its own interaction state here.
|
||||||
active_touch_index = -1
|
active_touch_index = -1
|
||||||
if GameState.fleet_speed_multiplier != 1.0:
|
|
||||||
GameState.fleet_speed_multiplier = 1.0
|
|
||||||
knob_t = 0.0
|
knob_t = 0.0
|
||||||
return
|
return
|
||||||
knob_t = (GameState.fleet_speed_multiplier - 1.0) \
|
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
|
class_name VesselGasBomberComponent
|
||||||
extends Node2D
|
extends "res://VesselCooldownFillComponent.gd"
|
||||||
|
|
||||||
var capability_id := "gas_bomber"
|
var capability_id := "gas_bomber"
|
||||||
var cooldown_duration := 5.0
|
|
||||||
var cooldown_left := 0.0
|
|
||||||
|
|
||||||
const READY_COLOR := Color(1.0, 0.25, 0.78)
|
const READY_COLOR := Color(1.0, 0.25, 0.78)
|
||||||
const COOLING_COLOR := Color(0.38, 0.16, 0.32)
|
const COOLING_COLOR := Color(0.38, 0.16, 0.32)
|
||||||
|
|
||||||
|
|
||||||
func setup() -> void:
|
func _init() -> void:
|
||||||
# Keep the ship's normal neon outline above the charge fill.
|
cooldown_duration = 5.0
|
||||||
show_behind_parent = true
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func is_ready() -> bool:
|
|
||||||
return cooldown_left <= 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func consume_shot(duration: float) -> bool:
|
func consume_shot(duration: float) -> bool:
|
||||||
if not is_ready():
|
return _begin_cooldown(duration)
|
||||||
return false
|
|
||||||
cooldown_duration = maxf(duration, 0.001)
|
|
||||||
cooldown_left = cooldown_duration
|
|
||||||
queue_redraw()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func tick(delta: float) -> void:
|
func _ready_color() -> Color: return READY_COLOR
|
||||||
if cooldown_left <= 0.0:
|
func _cooling_color() -> Color: return COOLING_COLOR
|
||||||
return
|
|
||||||
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func cooldown_progress() -> float:
|
|
||||||
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
|
||||||
|
|
||||||
|
|
||||||
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
|
|
||||||
if points.size() < 3 or progress <= 0.001:
|
|
||||||
return PackedVector2Array()
|
|
||||||
if progress >= 0.999:
|
|
||||||
return points.duplicate()
|
|
||||||
var top := INF
|
|
||||||
var bottom := -INF
|
|
||||||
for point in points:
|
|
||||||
top = minf(top, point.y)
|
|
||||||
bottom = maxf(bottom, point.y)
|
|
||||||
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
|
||||||
var clipped := PackedVector2Array()
|
|
||||||
var previous: Vector2 = points[-1]
|
|
||||||
var previous_inside := previous.y >= fill_y
|
|
||||||
for current in points:
|
|
||||||
var current_inside := current.y >= fill_y
|
|
||||||
if current_inside != previous_inside:
|
|
||||||
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
|
|
||||||
clipped.append(previous.lerp(current, amount))
|
|
||||||
if current_inside:
|
|
||||||
clipped.append(current)
|
|
||||||
previous = current
|
|
||||||
previous_inside = current_inside
|
|
||||||
return clipped
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
var parent_ship: Node = get_parent()
|
|
||||||
if parent_ship == null or parent_ship.definition == null:
|
|
||||||
return
|
|
||||||
var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE
|
|
||||||
var hull: PackedVector2Array = parent_ship.definition.hull_points
|
|
||||||
if is_ready():
|
|
||||||
# A fully lit hull means this individual bomber owns an available shot.
|
|
||||||
draw_colored_polygon(hull, Color(READY_COLOR, 0.42))
|
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75)
|
|
||||||
return
|
|
||||||
# Rearming is shown inside the bomber instead of as a shield-like ring:
|
|
||||||
# the muted silhouette fills from its engines toward its nose.
|
|
||||||
draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58))
|
|
||||||
var filled := _filled_hull(hull, cooldown_progress())
|
|
||||||
if filled.size() >= 3:
|
|
||||||
draw_colored_polygon(filled, Color(READY_COLOR, 0.68))
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,16 @@
|
||||||
class_name VesselLaserComponent
|
class_name VesselLaserComponent
|
||||||
extends Node2D
|
extends "res://VesselCooldownFillComponent.gd"
|
||||||
|
|
||||||
# Same shape as VesselGasBomberComponent (per-ship cooldown, ready/cooling
|
|
||||||
# hull fill) - #58's own note: two laser vessels must give two shots, so
|
|
||||||
# this lives on the SHIP, not the fleet-wide LaserController.
|
|
||||||
#
|
|
||||||
# Unlike the gas bomber, damage/beam_range/cooldown are resolved from the
|
|
||||||
# vessel's own base_stats/upgrades (apply_stats(), the same pattern
|
|
||||||
# VesselWeaponComponent uses) rather than hardcoded in the controller, so
|
|
||||||
# ship-lab upgrades actually reach them.
|
|
||||||
|
|
||||||
var capability_id := "laser"
|
var capability_id := "laser"
|
||||||
var damage: int = 2
|
var damage: int = 2
|
||||||
var beam_range: float = 950.0
|
var beam_range: float = 950.0
|
||||||
var cooldown_duration: float = 4.0
|
|
||||||
var cooldown_left: float = 0.0
|
|
||||||
|
|
||||||
const READY_COLOR := Color(0.3, 0.9, 1.0)
|
const READY_COLOR := Color(0.3, 0.9, 1.0)
|
||||||
const COOLING_COLOR := Color(0.08, 0.28, 0.34)
|
const COOLING_COLOR := Color(0.08, 0.28, 0.34)
|
||||||
|
|
||||||
|
|
||||||
func setup() -> void:
|
func _init() -> void:
|
||||||
# Keep the ship's normal neon outline above the charge fill.
|
cooldown_duration = 4.0
|
||||||
show_behind_parent = true
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func apply_stats() -> void:
|
func apply_stats() -> void:
|
||||||
|
|
@ -35,66 +22,9 @@ func apply_stats() -> void:
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func is_ready() -> bool:
|
|
||||||
return cooldown_left <= 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func consume_shot() -> bool:
|
func consume_shot() -> bool:
|
||||||
if not is_ready():
|
return _begin_cooldown(cooldown_duration)
|
||||||
return false
|
|
||||||
cooldown_left = maxf(cooldown_duration, 0.001)
|
|
||||||
queue_redraw()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func tick(delta: float) -> void:
|
func _ready_color() -> Color: return READY_COLOR
|
||||||
if cooldown_left <= 0.0:
|
func _cooling_color() -> Color: return COOLING_COLOR
|
||||||
return
|
|
||||||
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func cooldown_progress() -> float:
|
|
||||||
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
|
||||||
|
|
||||||
|
|
||||||
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
|
|
||||||
if points.size() < 3 or progress <= 0.001:
|
|
||||||
return PackedVector2Array()
|
|
||||||
if progress >= 0.999:
|
|
||||||
return points.duplicate()
|
|
||||||
var top := INF
|
|
||||||
var bottom := -INF
|
|
||||||
for point in points:
|
|
||||||
top = minf(top, point.y)
|
|
||||||
bottom = maxf(bottom, point.y)
|
|
||||||
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
|
|
||||||
var clipped := PackedVector2Array()
|
|
||||||
var previous: Vector2 = points[-1]
|
|
||||||
var previous_inside := previous.y >= fill_y
|
|
||||||
for current in points:
|
|
||||||
var current_inside := current.y >= fill_y
|
|
||||||
if current_inside != previous_inside:
|
|
||||||
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
|
|
||||||
clipped.append(previous.lerp(current, amount))
|
|
||||||
if current_inside:
|
|
||||||
clipped.append(current)
|
|
||||||
previous = current
|
|
||||||
previous_inside = current_inside
|
|
||||||
return clipped
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
var parent_ship: Node = get_parent()
|
|
||||||
if parent_ship == null or parent_ship.definition == null:
|
|
||||||
return
|
|
||||||
var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE
|
|
||||||
var hull: PackedVector2Array = parent_ship.definition.hull_points
|
|
||||||
if is_ready():
|
|
||||||
draw_colored_polygon(hull, Color(READY_COLOR, 0.42))
|
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75)
|
|
||||||
return
|
|
||||||
draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58))
|
|
||||||
var filled := _filled_hull(hull, cooldown_progress())
|
|
||||||
if filled.size() >= 3:
|
|
||||||
draw_colored_polygon(filled, Color(READY_COLOR, 0.68))
|
|
||||||
|
|
|
||||||
1
VesselLaserComponent.gd.uid
Normal file
1
VesselLaserComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bds5gccvd8dj8
|
||||||
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 burner_id: int = 0
|
||||||
var load_limit: int = 0
|
var load_limit: int = 0
|
||||||
var energy_before_stall: int = 0
|
var energy_before_stall: int = 0
|
||||||
|
var conversion_rate: float = 0.0
|
||||||
|
var conversion_yield: int = 0
|
||||||
|
|
||||||
const MOTHERSHIP_AT := Vector2.ZERO
|
const MOTHERSHIP_AT := Vector2.ZERO
|
||||||
const HUB_AT := Vector2(150.0, 0.0)
|
const HUB_AT := Vector2(150.0, 0.0)
|
||||||
|
|
@ -48,6 +50,9 @@ func _initialize() -> void:
|
||||||
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||||
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
|
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
|
||||||
game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20)
|
game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20)
|
||||||
|
var logistics: Resource = game_state.building_logistics_profile(game_state.find_building(burner_id))
|
||||||
|
conversion_rate = logistics.conversion_rate
|
||||||
|
conversion_yield = logistics.conversion_yield
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
func _process(delta: float) -> bool:
|
||||||
|
|
@ -73,10 +78,10 @@ func _process(delta: float) -> bool:
|
||||||
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
|
||||||
var gas_burned: int = 20 - gas_left
|
var gas_burned: int = 20 - gas_left
|
||||||
if gas_burned < 2:
|
if gas_burned < 2:
|
||||||
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, GameState.GAS_BURN_RATE])
|
return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, conversion_rate])
|
||||||
if energy_made != gas_burned * GameState.ENERGY_PER_GAS:
|
if energy_made != gas_burned * conversion_yield:
|
||||||
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
|
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
|
||||||
energy_made, gas_burned, GameState.ENERGY_PER_GAS
|
energy_made, gas_burned, conversion_yield
|
||||||
])
|
])
|
||||||
if game_state.rocks != 0 or game_state.gas != 0:
|
if game_state.rocks != 0 or game_state.gas != 0:
|
||||||
return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [
|
return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [
|
||||||
|
|
|
||||||
1
tests/test_gas_burner.gd.uid
Normal file
1
tests/test_gas_burner.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dlx2eu6lw3ps6
|
||||||
|
|
@ -12,6 +12,7 @@ func _initialize() -> void:
|
||||||
state.transporters.clear()
|
state.transporters.clear()
|
||||||
state.reset_progression()
|
state.reset_progression()
|
||||||
state.rocks = 900
|
state.rocks = 900
|
||||||
|
state.research_points = state.skill_research_point_cost("freight_yards")
|
||||||
state.unlocked = ["shipyards", "logistics", "freight"]
|
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||||
|
|
||||||
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
|
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
|
||||||
|
|
|
||||||
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
|
return false
|
||||||
var definitions: Array = VESSEL_DEFINITIONS.all()
|
var definitions: Array = VESSEL_DEFINITIONS.all()
|
||||||
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
|
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
|
||||||
if fleet_definitions.size() != 5 \
|
if fleet_definitions.any(func(definition): return definition.has_capability("transport")):
|
||||||
or fleet_definitions.any(func(definition): return definition.has_capability("transport")):
|
|
||||||
return _fail("logistics transporters leaked into the combat-fleet catalog")
|
return _fail("logistics transporters leaked into the combat-fleet catalog")
|
||||||
if ships.size() != definitions.size():
|
if ships.size() != definitions.size():
|
||||||
return _fail("catalog and composed fleet differ in size")
|
return _fail("catalog and composed fleet differ in size")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue