#155, #152: shots reach further and sooner, and the other explosions catch up

#155 - the projectile flew at 850, which took 1.1 seconds to cross the gun's
own range: long enough that a shot read as leaving the ship rather than as
hitting something. 1400 now, and the interceptor's range goes 950 to 1200,
so it crosses in 0.86s and comfortably outranges the 620 the standoff
enemies hold at. A projectile's lifetime is derived from its range and
speed, so faster does not mean falling short - the test pins that
arithmetic rather than trusting it.

#152 asked for VFX on three events, and the finding is worth recording: all
three already existed and were already wired. What had happened is that #130
gave a lost VESSEL thirty particles and a shockwave, which left everything
else in the game visibly smaller than it - an enemy kill, the thing the
player is being rewarded for, and a broken rock, the thing they do more than
anything else, were both still on or near the base class's twelve-particle
default with no shockwave at all.

So this is about PROPORTION rather than about adding anything, and
proportion is what the test checks: a lost vessel outweighs a kill,
a kill outweighs a rock, and nothing sits on the base defaults. Numbers get
rebalanced; an ordering that says a rock outweighs a battleship is a bug.
Particles now run 38 > 32 > 24 against a default of 12.
This commit is contained in:
Loïc Wisniewski 2026-08-29 14:39:35 +02:00
commit 7489528e60
7 changed files with 194 additions and 14 deletions

View file

@ -0,0 +1,153 @@
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_shot_and_vfx_weight.gd
# Forgejo #155 ("default vessel shots should go faster and further") and #152
# ("add a vfx for dead enemies and for exploded rock and for resource
# collected on fleet").
#
# The #152 finding is worth recording: all three effects already existed and
# were already wired. What had happened is that #130 gave a lost VESSEL
# thirty particles and a shockwave, which left every other event in the game
# visibly smaller than it - a kill, the thing the player is rewarded for,
# was a twelve-particle puff beside it. So this is about PROPORTION, and
# proportion is what is checked: numbers get rebalanced, an ordering that
# says a rock outweighs a battleship is a bug.
#
# Covers:
# A. the default gun outranges the enemies that stand off from it
# B. a shot crosses that range quickly enough to read as hitting something
# C. a shot's own lifetime is derived from its range, so faster does not
# mean falling short
# D. every one of #152's three events actually produces something
# E. and they are in proportion - a lost vessel is the biggest, a rock is
# not a puff, and nothing is on the base class's defaults
const ENEMY_STANDOFF = preload("res://enemies/EnemyStandoffComponent.gd")
const EXPLOSION_BASE = preload("res://vfx/ExplosionVFXBase.gd")
var game_state: Node
var scene: Node
var fleet: Node2D
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
# GameState is an autoload: a bare GameState.<instance member> does not
# compile in a SceneTree script, so the node is fetched by name instead.
game_state = root.get_node("GameState")
game_state.reset_new_game()
game_state.intro_active = false
game_state.reset_quests()
func _fail(message: String) -> bool:
print("TEST_SHOT_AND_VFX_WEIGHT: FAIL (%s)" % message)
quit(1)
return true
func _count(of_class) -> int:
var n: int = 0
for node in root.get_children():
if is_instance_of(node, of_class):
n += 1
return n
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# --- A, B, C. the shot ----------------------------------
var range_units: float = game_state.ship_fire_range(GameState.VESSEL_INTERCEPTOR)
var shot: Node2D = load("res://vessels/Projectile.tscn").instantiate()
var speed: float = shot.speed
shot.set_range(range_units)
var flight: float = shot.lifetime
var standoff: float = ENEMY_STANDOFF.new().preferred_range
if range_units <= standoff:
return _fail("the default gun reaches %.0f and enemies stand off at %.0f - it is outranged" \
% [range_units, standoff])
if flight > 0.9:
return _fail("a shot takes %.2fs to cross its own range - that reads as leaving, not hitting" \
% flight)
if absf(flight - range_units / speed) > 0.001:
return _fail("lifetime %.3fs does not match range %.0f at speed %.0f - shots would fall short or overshoot" \
% [flight, range_units, speed])
print("default gun: %.0f units at %.0f/s, crossing its range in %.2fs (enemies stand off at %.0f)" \
% [range_units, speed, flight, standoff])
shot.free()
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
fleet = scene.get_node("Fleet")
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.4:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
hazard.free()
# --- D. an enemy dying is seen --------------------------
var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
enemy.enemy_id = "chaser"
enemy.position = Vector2(600.0, 0.0)
scene.add_child(enemy)
var before: int = _count(EnemyExplosionVFX)
enemy.take_damage(999)
if _count(EnemyExplosionVFX) <= before:
return _fail("an enemy died with nothing to show for it")
# --- ...a rock breaking up ------------------------------
var rock: Node2D = load("res://world/Asteroid.tscn").instantiate()
rock.position = Vector2(-600.0, 0.0)
scene.add_child(rock)
var rocks_before: int = _count(AsteroidExplosionVFX)
rock.shatter(false)
if _count(AsteroidExplosionVFX) <= rocks_before:
return _fail("a rock broke up with nothing to show for it")
# --- ...and ore reaching the hold -----------------------
var pickup_script: GDScript = load("res://vfx/ResourcePickupVFX.gd")
var pickups_before: int = _count(pickup_script)
pickup_script.spawn(root, Vector2.ZERO, GameState.RESOURCE_ORE, 1)
if _count(pickup_script) <= pickups_before:
return _fail("collecting ore showed nothing")
print("all three events produce an effect")
# --- E. and they are in proportion ----------------------
var base: Node2D = EXPLOSION_BASE.new()
var ship: Node2D = ShipExplosionVFX.new()
var kill: Node2D = EnemyExplosionVFX.new()
var stone: Node2D = AsteroidExplosionVFX.new()
for pair in [["a lost vessel", ship], ["an enemy kill", kill], ["a broken rock", stone]]:
var vfx: Node2D = pair[1]
if vfx.particle_count <= base.particle_count:
return _fail("%s is still on the base class's %d particles" \
% [pair[0], base.particle_count])
if vfx.shockwave_radius <= 0.0:
return _fail("%s has no shockwave behind it" % pair[0])
if ship.particle_count < kill.particle_count:
return _fail("losing a vessel (%d) is smaller than killing an enemy (%d)" \
% [ship.particle_count, kill.particle_count])
if kill.particle_count < stone.particle_count:
return _fail("killing an enemy (%d) is smaller than breaking a rock (%d)" \
% [kill.particle_count, stone.particle_count])
print("particles: vessel %d > enemy %d > rock %d > base default %d" \
% [ship.particle_count, kill.particle_count, stone.particle_count,
base.particle_count])
for vfx in [base, ship, kill, stone]:
vfx.free()
print("TEST_SHOT_AND_VFX_WEIGHT: PASS")
quit(0)
return true
return false

View file

@ -0,0 +1 @@
uid://0ny0jxkr87ox