diff --git a/controllers/LaserController.gd b/controllers/LaserController.gd index f017811..caf9fb3 100644 --- a/controllers/LaserController.gd +++ b/controllers/LaserController.gd @@ -16,6 +16,9 @@ const BEAM_EFFECT = preload("res://vfx/LaserBeamEffect.gd") const WEAPON_COMPONENT = preload("res://vessels/VesselWeaponComponent.gd") const BEAM_COLOR := BEAM_EFFECT.BEAM_COLOR const TARGET_COLOR := Color(0.6, 0.95, 1.0) +# #203's range mark and the assist's target ring, in screen pixels. +const END_TICK_PIXELS: float = 17.0 +const TARGET_RING_PIXELS: float = 15.0 const BEAM_HALF_WIDTH := BEAM_EFFECT.BEAM_HALF_WIDTH const FLASH_DURATION := BEAM_EFFECT.FLASH_DURATION const ENERGY_PER_SHOT: int = VesselLaserComponent.ENERGY_PER_SHOT @@ -114,8 +117,48 @@ func beam_tip(beam: Node2D) -> Vector2: return beam.tip() +# #203: the aim view frames the beam's WHOLE reach, not the point the aim +# happens to sit on. The assist can settle that point on an enemy a few +# hundred units away (#201), and fitting the camera to it would say the shot +# stops there. Fitting the far end instead is also what makes the range +# visible at all: the beam is 2900 units long and the inherited floor on the +# zoom - sized for the gas bomb's 950 - left three quarters of it off screen. +func _update_aim_zoom(origin: Vector2, target: Vector2) -> void: + super(origin, origin + _aim_heading(origin, target) * _aim_max_range()) + + +func _aim_heading(origin: Vector2, target: Vector2) -> Vector2: + var direction: Vector2 = target - origin + return direction.normalized() if direction.length() > 0.001 else Vector2.UP + + func _draw() -> void: # Only the aim guide. Each beam draws itself, in its own node. - if aiming: - NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, - TARGET_COLOR, 0.7, 0.6) + if not aiming: + return + var origin: Vector2 = _aim_origin() + var reach: float = _aim_max_range() + var far_end: Vector2 = origin + _aim_heading(origin, aim_target) * reach + # Drawn to the far end rather than to aim_target, for the same reason the + # camera frames it: this is the line the beam will occupy (#203). + NeonDraw.draw_glowing_line(self, origin, far_end, TARGET_COLOR, 0.7, 0.6) + # A cross-tick where it stops, so the reach is a mark on the screen rather + # than a line that merely runs out. Sized in SCREEN pixels: framing 2900 + # units puts the camera far enough out that a mark measured in world units + # would be a few pixels of nothing at the moment it matters most. + var across: Vector2 = far_end.direction_to(origin).orthogonal() \ + * _screen_units(END_TICK_PIXELS) + NeonDraw.draw_glowing_line(self, far_end - across, far_end + across, + TARGET_COLOR, 0.7, 0.5) + # What the assist picked, when that is nearer than the far end. The line + # still runs past it - everything behind it on the same line is hit too. + if origin.distance_to(aim_target) < reach - 1.0: + NeonDraw.draw_glowing_circle(self, aim_target, + maxf(BEAM_HALF_WIDTH, _screen_units(TARGET_RING_PIXELS)), + TARGET_COLOR, 0.7) + + +# World units that cover a given number of screen pixels at the camera's +# current zoom. +func _screen_units(pixels: float) -> float: + return pixels / maxf(camera.zoom.x, 0.001) diff --git a/tests/_laser_aim_preview.gd b/tests/_laser_aim_preview.gd index b15ef31..d6d50f1 100644 --- a/tests/_laser_aim_preview.gd +++ b/tests/_laser_aim_preview.gd @@ -12,6 +12,11 @@ func _ready() -> void: GameState.extractors.clear() GameState.transporters.clear() GameState.vessel_levels["unlock_laser"] = 1 + GameState.intro_active = false + # A shot the fleet cannot pay for is refused before aiming starts, so + # without this the preview drew no guide at all - the state it exists + # to show. Deposited before Main is built, like test_laser_vessel.gd. + GameState.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 40) var main: Node = load("res://world/Main.tscn").instantiate() add_child(main) main.get_node("BackgroundLayer").visible = false @@ -21,14 +26,17 @@ func _ready() -> void: main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) var fleet: Node = main.get_node("Fleet") - fleet.spawn_ships(4, Vector2.ZERO, PackedStringArray([ - GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD, - GameState.VESSEL_LASER, GameState.VESSEL_LASER])) + # Three, not four: the starting fleet holds three hulls until the skill + # tree widens it, and a fourth entry was silently dropped - which took the + # ready laser with it and left this preview with nothing to aim. + fleet.spawn_ships(3, Vector2.ZERO, PackedStringArray([ + GameState.VESSEL_SHIELD, GameState.VESSEL_LASER, + GameState.VESSEL_LASER])) var skill: Node = main.get_node("LaserController") skill._process(0.0) # Show both per-ship states in one frame: one laser ship rearming, the # other ready and supplying the shot. - var rearming: Node = fleet.ships[2].get_component("laser") + var rearming: Node = fleet.ships[1].get_component("laser") rearming.consume_shot() rearming.tick(rearming.cooldown_duration * 0.5) main.get_node("Camera2D").zoom = Vector2(1.8, 1.8) diff --git a/tests/test_laser_vessel.gd b/tests/test_laser_vessel.gd index 5dd6239..0538403 100644 --- a/tests/test_laser_vessel.gd +++ b/tests/test_laser_vessel.gd @@ -124,6 +124,17 @@ func _process(delta: float) -> bool: return _fail_now("aiming did not zoom out") print("aiming zoomed the camera out") + # #203: far enough out to show the WHOLE beam. The drag above is + # horizontal, so the screen's narrow half is the binding one. + var half_screen: Vector2 = skill.get_viewport_rect().size * 0.5 \ + - Vector2(54.0, 54.0) + var framed: float = (2900.0 + BEAM_HALF_WIDTH) \ + * main.get_node("Camera2D").zoom.x + if framed > half_screen.x + 1.0: + return _fail_now("aiming framed %.0fpx of beam on %.0fpx of screen" \ + % [framed, half_screen.x]) + print("aiming framed the beam's full range") + # --- B. targets: two on the line, one off it, one past range --- for offset in [Vector2(200.0, 0.0), Vector2(500.0, 0.0)]: var enemy: Node = load("res://enemies/Enemy.tscn").instantiate() diff --git a/world/Main.tscn b/world/Main.tscn index 9f97912..790dbb2 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -88,6 +88,7 @@ script = ExtResource("mouse_ctrl") [node name="LaserController" type="Node2D" parent="."] script = ExtResource("30") +minimum_zoom = 0.1 [node name="DashController" type="Node2D" parent="."] script = ExtResource("dash_ctrl")