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