extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_shot_reach.gd # Forgejo #86: the default gun shoots faster and further. #87: the ship that # fires throws sparks, so a shot is visibly attached to the vessel it came # from. # # The interesting part is not the numbers, it is that a bolt now stops where # the gun's range ends. It used to fly speed * lifetime - 750 units - while # the aim guide drew fire_range, 320. The guide was a promise the shot did not # keep, in the direction that matters: shots landed on things well past the # line saying where they would stop. # # Covers: # A. a bolt's travel distance IS the gun's range, whatever either is set to # B. the aim guide draws that same range, so the line and the shot agree # C. firing throws sparks from the firing ship, and they clear themselves up const PICKUP_TOLERANCE: float = 60.0 var game_state: Node var main: Node var fleet: Node var controller: Node var weapon: Node var shooter: Node2D var stage: int = 0 var elapsed: float = 0.0 var bolt: Node2D var launch_point: Vector2 = Vector2.ZERO var furthest: float = 0.0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.reset_progression() main = load("res://world/Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") controller = main.get_node("FireStickController") func _sparks() -> Array: var found: Array = [] for node in root.get_children(): if node.get_script() != null and String(node.get_script().resource_path).ends_with("MuzzleSparkVFX.gd"): found.append(node) return found func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.3: return false main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies") \ + get_nodes_in_group("player_projectiles"): if is_instance_valid(hazard): hazard.free() fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR])) shooter = fleet.ships[0] weapon = shooter.get_component("weapon") if weapon == null: return _fail("the default vessel has no weapon component") # --- B. the guide draws the gun's own range -------------------- controller.aim(Vector2(1.0, 0.0)) if not is_equal_approx(controller.guide_range(), weapon.fire_range): return _fail("the aim guide reaches %.0f but the gun's range is %.0f" \ % [controller.guide_range(), weapon.fire_range]) print("aim guide and gun range agree at %.0f" % weapon.fire_range) launch_point = shooter.global_position weapon.fire_timer = 0.0 weapon.tick(0.0) var bolts: Array = get_nodes_in_group("player_projectiles") if bolts.is_empty(): return _fail("the gun fired nothing") bolt = bolts[0] # --- C. sparks at the muzzle ----------------------------------- var sparks: Array = _sparks() if sparks.is_empty(): return _fail("firing threw no sparks") if sparks[0].global_position.distance_to(shooter.global_position) > PICKUP_TOLERANCE: return _fail("the sparks appeared %.0f from the ship that fired" \ % sparks[0].global_position.distance_to(shooter.global_position)) print("firing threw sparks at the firing ship") stage = 1 elapsed = 0.0 return false 1: # --- A. the bolt stops where the range ends -------------------- if is_instance_valid(bolt) and not bolt.is_queued_for_deletion(): furthest = maxf(furthest, launch_point.distance_to(bolt.global_position)) return false var reach: float = weapon.fire_range # One physics step of slack either way: the bolt is freed on the # tick its age passes the limit, so it lands just short of or just # past the exact figure. var slack: float = bolt_step() if absf(furthest - reach) > slack: return _fail("the bolt travelled %.0f but the gun's range is %.0f (slack %.0f)" \ % [furthest, reach, slack]) print("bolt stopped at %.0f against a range of %.0f" % [furthest, reach]) if not _sparks().is_empty(): return _fail("%d spark burst(s) still alive long after firing" % _sparks().size()) print("sparks cleared themselves up") print("TEST_SHOT_REACH: PASS") quit(0) return true return false func bolt_step() -> float: # How far a bolt moves in a single physics tick, which is the granularity # its lifetime can be enforced at. var speed: float = 850.0 var probe: Node2D = load("res://vessels/Projectile.tscn").instantiate() speed = float(probe.speed) probe.free() return speed / float(Engine.physics_ticks_per_second) * 1.5 func _fail(message: String) -> bool: print("TEST_SHOT_REACH: FAIL (%s)" % message) quit(1) return true