extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_fire_stick.gd # The fleet's guns used to pick their own targets and fire on their own, so # combat happened whether or not the player did anything. Firing is now an # action: the right-hand stick aims it, and nothing else fires it. # # Covers: # A. an idle stick means an idle gun - an enemy parked well inside fire # range takes no shots at all. This is the behaviour that CHANGED, so it # is checked against a target the old auto-aim would certainly have hit # B. pushing the stick fires, and the shots travel along the stick's # heading rather than at whatever happens to be nearest # C. releasing the stick stops the gun # D. the stick claims only touches inside its own rect, so the right half # of the screen does not swallow pinches and taps meant for the world # E. the dotted aim guide is drawn only while aiming, and reaches exactly as # far as the guns actually shoot - a guide that outruns the gun would be # worse than none const FIRE_DIRECTION := Vector2(1.0, 0.0) var game_state: Node var main: Node var fleet: Node var fire_stick: Control var controller: Node var decoy: Node var stage: int = 0 var elapsed: float = 0.0 var shots_while_idle: int = 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://Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") fire_stick = main.get_node("UI/FireStick") controller = main.get_node("FireStickController") func _projectile_count() -> int: return get_nodes_in_group("player_projectiles").size() func _push_stick(direction: Vector2) -> void: var centre: Vector2 = fire_stick.global_position + fire_stick.size * 0.5 var press := InputEventScreenTouch.new() press.index = 3 press.pressed = true press.position = centre if not fire_stick.handle_touch(press): push_error("the fire stick refused a touch at its own centre") var drag := InputEventScreenDrag.new() drag.index = 3 drag.position = centre + direction.normalized() * fire_stick.max_radius fire_stick.handle_drag(drag) func _release_stick() -> void: var release := InputEventScreenTouch.new() release.index = 3 release.pressed = false release.position = fire_stick.global_position + fire_stick.size * 0.5 fire_stick.handle_touch(release) 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"): if is_instance_valid(hazard): hazard.queue_free() for projectile in get_nodes_in_group("player_projectiles"): projectile.free() fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR])) var ship: Node2D = fleet.ships[0] var weapon: Node = ship.get_component("weapon") if weapon == null: return _fail("the default vessel has no weapon component to test") # Parked at a third of fire range, dead ahead: the old auto-aiming # gun would have opened up on this immediately. decoy = load("res://Enemy.tscn").instantiate() decoy.position = ship.global_position + Vector2(weapon.fire_range * 0.33, 0.0) main.add_child(decoy) decoy.speed = 0.0 print("a target sits %.0f away, well inside the gun's %.0f range" \ % [ship.global_position.distance_to(decoy.position), weapon.fire_range]) stage = 1 elapsed = 0.0 return false 1: # --- A. idle stick, idle gun ----------------------------------- shots_while_idle = maxi(shots_while_idle, _projectile_count()) if elapsed < 1.5: return false if shots_while_idle != 0: return _fail("the fleet fired %d shot(s) with the stick untouched - the gun is still aiming itself" \ % shots_while_idle) print("1.5s with the stick untouched: no shots fired") _push_stick(FIRE_DIRECTION) if fire_stick.direction.normalized().distance_to(FIRE_DIRECTION) > 0.01: return _fail("pushing the stick reported direction %s, expected %s" \ % [fire_stick.direction, FIRE_DIRECTION]) stage = 2 elapsed = 0.0 return false 2: # --- B. it fires, along the stick ------------------------------ if elapsed < 0.6: return false var projectiles: Array = get_nodes_in_group("player_projectiles") if projectiles.is_empty(): return _fail("pushing the fire stick did not fire anything") for projectile in projectiles: var heading: Vector2 = projectile.velocity.normalized() if heading.distance_to(FIRE_DIRECTION) > 0.05: return _fail("a shot travelled along %s, but the stick was pushed %s" \ % [heading, FIRE_DIRECTION]) print("%d shot(s) fired, every one along the stick's heading" % projectiles.size()) # --- E. the aim guide ------------------------------------------ if controller.aim_direction == Vector2.ZERO: return _fail("the aim guide has no heading while the stick is held") if controller.aim_direction.normalized().distance_to(FIRE_DIRECTION) > 0.05: return _fail("the guide points along %s but the stick was pushed %s" \ % [controller.aim_direction.normalized(), FIRE_DIRECTION]) var weapon: Node = fleet.ships[0].get_component("weapon") if not is_equal_approx(controller.guide_range(), weapon.fire_range): return _fail("the guide reaches %.0f but the gun shoots %.0f" \ % [controller.guide_range(), weapon.fire_range]) print("aim guide drawn along the stick, reaching the gun's own %.0f range" % weapon.fire_range) _release_stick() if fire_stick.direction != Vector2.ZERO: return _fail("releasing the stick left it reporting %s" % fire_stick.direction) # One frame for the controller to push the release through to the # weapons, then clear the sky so what follows is unambiguous. controller._process(0.0) for projectile in get_nodes_in_group("player_projectiles"): projectile.free() stage = 3 elapsed = 0.0 return false 3: # --- C. released means stopped --------------------------------- if _projectile_count() != 0: return _fail("the gun kept firing after the stick was released") if elapsed < 1.2: return false if controller.is_firing(): return _fail("a weapon still reports itself firing after release") if controller.aim_direction != Vector2.ZERO: return _fail("the aim guide is still drawn after the stick was released") print("1.2s after release: nothing fired") # --- D. the stick claims only its own rect --------------------- var outside := InputEventScreenTouch.new() outside.index = 5 outside.pressed = true outside.position = fire_stick.global_position - Vector2(40.0, 40.0) if fire_stick.handle_touch(outside): return _fail("the fire stick claimed a touch outside its own rect, which would swallow world gestures") print("a touch outside the stick's rect falls through") print("TEST_FIRE_STICK: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_FIRE_STICK: FAIL (%s)" % message) quit(1) return true