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)