Sharpen the gas bomb and the laser

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Loïc Wisniewski 2026-09-04 22:34:29 +02:00
commit 1cb8fc54e9
6 changed files with 76 additions and 5 deletions

View file

@ -11,6 +11,9 @@ extends "res://controllers/AimedFleetAbilityController.gd"
# while they aim it.
const BEAM_EFFECT = preload("res://vfx/LaserBeamEffect.gd")
# #201: the beam borrows the fire stick's aim helper, so it is loaded here
# for its static assist rather than for anything a laser hull carries.
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)
const BEAM_HALF_WIDTH := BEAM_EFFECT.BEAM_HALF_WIDTH
@ -51,7 +54,25 @@ func _aim_max_range() -> float:
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()
return _assisted_target(origin, origin + direction * _aim_max_range())
# The mouse path (#80) names a world point directly instead of dragging one
# into existence. It has a real cursor, so it is assisted from that rather
# than from a virtual one at the end of the beam - otherwise the crosshair
# would be the only aim in the game the helper does not touch.
func fire_at(world_target: Vector2) -> bool:
return super(_assisted_target(_aim_origin(), world_target))
# #201: ordinary fire's aim helper, applied to the beam. It answers with a
# PLACE, not a heading - the same thing the fire stick resolves and every
# hull then converges on - so a drag that ends near an enemy, or merely
# inside the assist cone, passes through that enemy instead of a few degrees
# past it. The beam still runs on to its full range from there.
func _assisted_target(origin: Vector2, point: Vector2) -> Vector2:
return WEAPON_COMPONENT.assisted_point(get_tree(), origin, point,
_aim_max_range())
func _consume_shot(component: Node) -> bool:
@ -62,7 +83,12 @@ func _perform_shot(ship: Node2D, component: Node) -> void:
# The cooldown was already spent by _consume_shot() above, so this fires
# the effect directly rather than going through fire_ability() and
# spending it twice.
var direction: Vector2 = aim_target - _aim_origin()
# Aimed from the HULL, not from the fleet centre. aim_target is a place
# now that the assist can move it onto an enemy (#201), and ordinary fire
# converges on such a place by letting each hull find its own heading to
# it - a shared heading measured at the centroid would walk this beam off
# the very target the assist just picked, by the ship's formation offset.
var direction: Vector2 = aim_target - ship.global_position
direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP
var effect: Node2D = BEAM_EFFECT.fire(get_tree().root, ship.global_position,
direction, component.beam_range, int(component.damage))

View file

@ -144,6 +144,12 @@ func _process(delta: float) -> bool:
main._handle_touch(release)
if not skill.projectile_active:
return _fail_now("release did not throw a gas bomb")
# #201 doubled the blast. Read off the bomb actually in flight, so
# a constant that stops reaching the effect fails here rather than
# passing on the strength of its own declaration.
if int(skill.projectiles[0].damage) != 6:
return _fail_now("gas bomb carries %d damage, expected the doubled 6" \
% int(skill.projectiles[0].damage))
elapsed = 0.0
stage = 1
1:

View file

@ -99,6 +99,12 @@ func _process(delta: float) -> bool:
return _fail_now("no laser ship is ready right after spawning")
if absf(first_shooter.get_component("laser").beam_range - 2900.0) > 0.1:
return _fail_now("laser ship beam range was not doubled to 2900")
# #201 tripled the beam's damage. Read off the COMPONENT rather
# than off the definition, so a stat that stops reaching the hull
# fails here instead of passing on the strength of its own file.
if first_shooter.get_component("laser").damage != 6:
return _fail_now("laser damage is %d, expected the tripled 6" \
% first_shooter.get_component("laser").damage)
var press := InputEventScreenTouch.new()
press.index = 7
@ -203,6 +209,37 @@ func _process(delta: float) -> bool:
if absf(main.get_node("Camera2D").zoom.x - original_zoom) > 0.03:
return _fail_now("camera did not restore its pre-aim zoom")
print("camera zoom restored")
# --- E. #201: the beam gets ordinary fire's aim helper ----------
# Checked on the resolution itself rather than by firing again:
# both hulls are still cooling down, and what the ticket asks for
# is exactly that a drag resolves onto a nearby enemy.
for spent in on_line_enemies + [off_line_enemy, beyond_range_enemy]:
if is_instance_valid(spent):
spent.free()
var origin: Vector2 = skill._aim_origin()
var in_cone: Node = load("res://enemies/Enemy.tscn").instantiate()
in_cone.position = origin + Vector2(800.0, 0.0).rotated(deg_to_rad(20.0))
main.add_child(in_cone)
in_cone.speed = 0.0
# Nearer than the one in the cone, and deliberately outside it:
# the assist must not treat "closest" as "aimed at".
var out_of_cone: Node = load("res://enemies/Enemy.tscn").instantiate()
out_of_cone.position = origin + Vector2(600.0, 0.0).rotated(deg_to_rad(60.0))
main.add_child(out_of_cone)
out_of_cone.speed = 0.0
var assisted: Vector2 = skill._calculate_aim_target(Vector2(300.0, 0.0), origin)
if assisted.distance_to(in_cone.global_position) > 1.0:
return _fail_now("a drag past an enemy inside the assist cone did not snap onto it")
print("aiming snapped onto the enemy inside the assist cone")
in_cone.free()
var unassisted: Vector2 = skill._calculate_aim_target(Vector2(300.0, 0.0), origin)
if unassisted.distance_to(origin + Vector2(2900.0, 0.0)) > 1.0:
return _fail_now("the aim bent toward an enemy outside the assist cone")
print("an enemy outside the cone left the aim where it was pointed")
print("TEST_LASER_VESSEL: PASS")
quit(0)
return true

View file

@ -18,7 +18,7 @@ hull_points = PackedVector2Array(0, -26, -9, 14, -4, 10, 4, 10, 9, 14)
fleet_eligible = true
capabilities = PackedStringArray("laser")
component_scenes = Array[PackedScene]([ExtResource("2")])
base_stats = {"max_speed": 660.0, "acceleration": 1890.0, "delivery_speed": 180.0, "damage": 2.0, "beam_range": 2900.0, "beam_cooldown": 4.0}
base_stats = {"max_speed": 660.0, "acceleration": 1890.0, "delivery_speed": 180.0, "damage": 6.0, "beam_range": 2900.0, "beam_cooldown": 4.0}
minimum_stats = {"beam_cooldown": 1.5}
upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5")])
unlock_id = "unlock_laser"

View file

@ -14,7 +14,9 @@ func _init() -> void:
const BOMB_EFFECT = preload("res://vfx/GasBombEffect.gd")
const GAS_PER_SHOT: int = 5
const MAX_RANGE: float = 950.0
const BLAST_DAMAGE: int = 3
# #201: doubled. The bomb is the fleet's only area weapon and one slow,
# expensive throw was worth less than a couple of seconds of ordinary fire.
const BLAST_DAMAGE: int = 6
const SHOT_COOLDOWN: float = 5.0

View file

@ -2,7 +2,7 @@ class_name VesselLaserComponent
extends "res://vessels/VesselCooldownFillComponent.gd"
var capability_id := "laser"
var damage: int = 2
var damage: int = 6
var beam_range: float = 2900.0
const READY_COLOR := Color(0.3, 0.9, 1.0)