From 7489528e6088ce4fcfaa6aee86c17cbc1d1fe5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Sat, 29 Aug 2026 14:39:35 +0200 Subject: [PATCH] #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. --- tests/test_shot_and_vfx_weight.gd | 153 +++++++++++++++++++++++ tests/test_shot_and_vfx_weight.gd.uid | 1 + vessels/InterceptorVesselDefinition.tres | 2 +- vessels/Projectile.gd | 6 +- vfx/AsteroidExplosionVFX.gd | 16 ++- vfx/EnemyExplosionVFX.gd | 16 ++- vfx/ShipExplosionVFX.gd | 14 ++- 7 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 tests/test_shot_and_vfx_weight.gd create mode 100644 tests/test_shot_and_vfx_weight.gd.uid diff --git a/tests/test_shot_and_vfx_weight.gd b/tests/test_shot_and_vfx_weight.gd new file mode 100644 index 0000000..a599321 --- /dev/null +++ b/tests/test_shot_and_vfx_weight.gd @@ -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. 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 diff --git a/tests/test_shot_and_vfx_weight.gd.uid b/tests/test_shot_and_vfx_weight.gd.uid new file mode 100644 index 0000000..8704365 --- /dev/null +++ b/tests/test_shot_and_vfx_weight.gd.uid @@ -0,0 +1 @@ +uid://0ny0jxkr87ox diff --git a/vessels/InterceptorVesselDefinition.tres b/vessels/InterceptorVesselDefinition.tres index b99b0cb..c3e8c05 100644 --- a/vessels/InterceptorVesselDefinition.tres +++ b/vessels/InterceptorVesselDefinition.tres @@ -17,5 +17,5 @@ glow_color = Color(0.25, 0.85, 1, 1) fleet_eligible = true capabilities = PackedStringArray("weapon", "cargo") component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("7")]) -base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 950.0, "max_speed": 690.0, "acceleration": 2025.0, "delivery_speed": 180.0, "cargo_capacity": 20.0} +base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 1200.0, "max_speed": 690.0, "acceleration": 2025.0, "delivery_speed": 180.0, "cargo_capacity": 20.0} upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5")]) diff --git a/vessels/Projectile.gd b/vessels/Projectile.gd index aa966a9..26281ae 100644 --- a/vessels/Projectile.gd +++ b/vessels/Projectile.gd @@ -2,7 +2,11 @@ extends Area2D # #86: faster. The bolt used to crawl far enough behind a moving target that # leading a shot was guesswork rather than skill. -@export var speed: float = 850.0 +# #155: was 850, which took 1.1s to cross the gun's own range - long enough +# that a shot read as leaving the ship rather than as hitting something. The +# range below is derived from this (see set_range), so raising it makes shots +# faster without making them fall short. +@export var speed: float = 1400.0 # How long a bolt lives if nothing sets a range. VesselWeaponComponent always # does - see set_range() - so this only covers a projectile fired by something # that has no range of its own. diff --git a/vfx/AsteroidExplosionVFX.gd b/vfx/AsteroidExplosionVFX.gd index 9830458..a377667 100644 --- a/vfx/AsteroidExplosionVFX.gd +++ b/vfx/AsteroidExplosionVFX.gd @@ -7,6 +7,20 @@ extends ExplosionVFXBase const COLOR: Color = Color(0.65, 0.5, 0.35) +func _init() -> void: + # #152: this took the base class's defaults - 12 chunks over half a + # second, no shockwave - which is why breaking a rock, the thing the + # player does more than anything else in the game, barely registered. + # Matter rather than light, so it throws more and slower than an energy + # burst does, and its shockwave is dust rather than a flash. + particle_count = 24 + lifetime = 0.7 + min_speed = 70.0 + max_speed = 330.0 + shockwave_radius = 72.0 + shockwave_color = Color(0.72, 0.6, 0.44) + + static func spawn(parent: Node, world_position: Vector2) -> void: var vfx: AsteroidExplosionVFX = AsteroidExplosionVFX.new() vfx.position = world_position @@ -14,7 +28,7 @@ static func spawn(parent: Node, world_position: Vector2) -> void: func _random_particle_size() -> float: - return randf_range(3.0, 7.0) + return randf_range(4.0, 11.0) # Debris tumbles independently of its flight path, unlike the light-burst diff --git a/vfx/EnemyExplosionVFX.gd b/vfx/EnemyExplosionVFX.gd index 1588a58..a0ca24d 100644 --- a/vfx/EnemyExplosionVFX.gd +++ b/vfx/EnemyExplosionVFX.gd @@ -7,13 +7,17 @@ func _init() -> void: # as a small red puff that was easy to miss in a fight - the moment the # player is being rewarded for should be the loudest thing on screen for # an instant. + # #152: and again. #130 gave a lost VESSEL 30 particles and a 120-unit + # shockwave, which left a kill - the thing the player is being rewarded + # for - visibly the smaller event of the two. Back in proportion: still + # below a vessel's own death, but no longer a puff beside it. color = Color(1.0, 0.25, 0.18) - particle_count = 22 - lifetime = 0.62 - min_speed = 120.0 - max_speed = 460.0 - shockwave_radius = 86.0 - shockwave_color = Color(1.0, 0.55, 0.3) + particle_count = 32 + lifetime = 0.75 + min_speed = 170.0 + max_speed = 620.0 + shockwave_radius = 130.0 + shockwave_color = Color(1.0, 0.6, 0.32) static func spawn(parent: Node, world_position: Vector2) -> void: diff --git a/vfx/ShipExplosionVFX.gd b/vfx/ShipExplosionVFX.gd index 317325c..df701c7 100644 --- a/vfx/ShipExplosionVFX.gd +++ b/vfx/ShipExplosionVFX.gd @@ -10,12 +10,16 @@ func _init() -> void: # had already been told to be the loudest thing in the frame. Now it is # the louder of the two, which is the right way round: the enemy's is a # reward, this one is a loss. + # + # #152 lifted the enemy's and the asteroid's to match; this goes up with + # them, because the ORDER is the point rather than the numbers. Losing a + # vessel must stay the bigger event than killing something. color = Color(0.25, 0.85, 1.0) - particle_count = 30 - lifetime = 0.8 - min_speed = 150.0 - max_speed = 540.0 - shockwave_radius = 120.0 + particle_count = 38 + lifetime = 0.95 + min_speed = 190.0 + max_speed = 640.0 + shockwave_radius = 150.0 shockwave_color = Color(0.6, 0.95, 1.0)