Asteroids were pure hazard: the only thing one could ever do was cost you a ship. They now take damage like anything else and break open, dropping ore the fleet can fly over and collect - the one resource reachable in open space without conquering anything, and something for the new fire stick to do out there. Two ways to destroy one, deliberately not equivalent. Shot apart yields the ore; flown into does not - the rock shatters against a hull rather than being mined, and a ship or a shield charge has just been spent on it. Without that split a shield vessel could farm asteroids by ramming them. Asteroid.take_damage() matches Enemy's contract exactly, so every weapon that already knew how to hurt an enemy hurts an asteroid through the same branch rather than a second special case: bolts, the laser sweep, and the gas bomb's blast all pick both groups up together. Dropped ore carries the asteroid's own drift, so it reads as debris rather than as a spawned pickup, and it expires after a minute. Rock grew an optional lifetime for that, defaulting to 0 (never) so hand-placed surface rocks are unchanged - a planet surface is bounded and authored, open space is not, and a field shot up and abandoned would otherwise leak nodes for the session. Rock's header note that asteroids and rocks are never on screen together is no longer true and now says so; the hexagon-versus-lump shape language matters more now that they sit side by side, not less. Five weapon tests silenced the asteroid field but never cleared what it had already placed. That was harmless while asteroids were untouchable and is not any more - two of them assert exact hit counts, and a drifting rock in the beam would have made those fail intermittently. Suite 111/111. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4 KiB
GDScript
101 lines
4 KiB
GDScript
class_name LaserController
|
|
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 := 38.0
|
|
# The DRAWN beam is scaled from the hit width rather than tuned separately.
|
|
# The two were independent numbers and had drifted apart: the beam struck
|
|
# anything within 22 units of the line while its outer glow spanned about 11,
|
|
# so it killed things it visibly missed. NeonDraw's widest layer is 18px at
|
|
# scale 1, so this makes the glow span exactly what the beam hits.
|
|
const BEAM_WIDTH_SCALE := BEAM_HALF_WIDTH * 2.0 / 18.0
|
|
const FLASH_DURATION := 0.22
|
|
const ENERGY_PER_SHOT: int = 8
|
|
|
|
var beams: Array = []
|
|
var last_hit_count: int = 0
|
|
|
|
|
|
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 _aim_max_range() -> float:
|
|
# Falls back to the VESSEL DEFINITION's own beam_range, not to a copy of
|
|
# the number. This used to hardcode 950.0 in both branches, which silently
|
|
# became a lie the moment the definition's range changed - the aim line
|
|
# would have been drawn to a different distance than the beam actually
|
|
# reaches, and only in the cases where the fallback fires.
|
|
var ship: Node2D = _next_ready_ship()
|
|
if ship != null:
|
|
var component: Node = ship.get_component(_component_id())
|
|
if component != null:
|
|
return component.beam_range
|
|
return GameState.vessel_stat(GameState.VESSEL_LASER, "beam_range")
|
|
|
|
|
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
|
# A beam always reaches full range; dragging chooses its direction only.
|
|
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
|
return origin + direction * _aim_max_range()
|
|
|
|
|
|
func _consume_shot(component: Node) -> bool:
|
|
return component.consume_shot()
|
|
|
|
|
|
func _perform_shot(ship: Node2D, component: Node) -> void:
|
|
var direction: Vector2 = aim_target - _aim_origin()
|
|
direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP
|
|
var beam_to: Vector2 = ship.global_position + direction * component.beam_range
|
|
_strike(ship.global_position, beam_to, direction, int(component.damage))
|
|
|
|
|
|
func _next_ready_laser_ship() -> Node2D:
|
|
return _next_ready_ship()
|
|
|
|
|
|
func _advance_effects(delta: float) -> void:
|
|
for i in range(beams.size() - 1, -1, -1):
|
|
var beam: Dictionary = beams[i]
|
|
beam.time = maxf(float(beam.time) - delta, 0.0)
|
|
if float(beam.time) <= 0.0:
|
|
beams.remove_at(i)
|
|
|
|
|
|
func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> void:
|
|
beams.append({"from": from, "to": to, "time": FLASH_DURATION})
|
|
last_hit_count = 0
|
|
var range_along: float = from.distance_to(to)
|
|
# Asteroids share the enemies' take_damage() contract, so the beam sweeps
|
|
# both groups in one pass. Shooting a path clear through a field is a
|
|
# perfectly good use of a laser, and the ore it leaves behind is the point.
|
|
for enemy in get_tree().get_nodes_in_group("enemies") \
|
|
+ get_tree().get_nodes_in_group("asteroids"):
|
|
if not is_instance_valid(enemy) or not (enemy is Node2D):
|
|
continue
|
|
var to_enemy: Vector2 = enemy.global_position - from
|
|
var along: float = to_enemy.dot(direction)
|
|
if along < 0.0 or along > range_along:
|
|
continue
|
|
var perpendicular: float = (to_enemy - direction * along).length()
|
|
if perpendicular <= BEAM_HALF_WIDTH:
|
|
enemy.take_damage(damage)
|
|
last_hit_count += 1
|
|
|
|
|
|
func _draw() -> void:
|
|
if aiming:
|
|
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, BEAM_WIDTH_SCALE * (1.0 - progress * 0.45))
|