extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_laser_auto_aim.gd # Tapping the laser button without dragging used to do nothing whatsoever: # the press was claimed and the aim begun, then releasing inside the aim # deadzone threw the whole thing away. It reads as a dead button. A tap now # fires at the nearest enemy in range; dragging is still how a shot is placed # precisely. # # Covers: # A. a tap picks the NEAREST enemy, not merely any enemy, and fires # B. it fires at all - the shot lands and the target takes the hit # C. a tap with nothing in range does not fire, says so, and leaves the # shot unspent rather than wasting it on empty space # D. a tap does not consume the drag-aim path: dragging past the deadzone # still aims where it was dragged, not at the nearest enemy var game_state: Node var main: Node var fleet: Node var skill: Node var shooter: Node2D var near_enemy: Node var far_enemy: Node var near_position: Vector2 = Vector2.ZERO var far_position: Vector2 = Vector2.ZERO var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.stored_vessels.clear() game_state.reset_progression() game_state.vessel_levels["unlock_laser"] = 1 game_state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500) main = load("res://Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") skill = main.get_node("LaserController") func _tap() -> void: # Press and release at the same point - inside AIM_DEADZONE, so this is a # tap and not a drag. var button: Control = main.get_node("UI/LaserButton") var at: Vector2 = button.global_position + button.size * 0.5 for pressed in [true, false]: var event := InputEventScreenTouch.new() event.index = 7 event.position = at event.pressed = pressed main._handle_touch(event) func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.2: return false main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) # Whatever the world spawned would be a second, unintended target. for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"): if is_instance_valid(hazard): hazard.queue_free() fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_LASER])) skill._process(0.0) if not main.get_node("UI/LaserButton").visible: return _fail("a laser ship in the fleet did not reveal the laser control") shooter = skill._next_ready_laser_ship() if shooter == null: return _fail("no laser ship was ready right after spawning") stage = 1 elapsed = 0.0 return false 1: if elapsed < 0.1: return false # --- C. nothing in range: no shot, and it says why --------------- _tap() if not skill.beams.is_empty(): return _fail("tapping with no enemy in range fired anyway") if skill.aiming: return _fail("tapping with no target left the control stuck in aiming") if main.get_node("UI/LaserStatus").text == "": return _fail("tapping with no target fired nothing and said nothing") print("no target in range: no shot fired, status explains why") # --- A/B. two targets, the nearer one is picked ------------------ # Deliberately placed in DIFFERENT directions, so hitting the near # one cannot be explained by the beam happening to pass through # both. Both are well inside range. far_enemy = _spawn_enemy(Vector2(0.0, -900.0)) near_enemy = _spawn_enemy(Vector2(400.0, 0.0)) far_position = far_enemy.global_position near_position = near_enemy.global_position stage = 2 elapsed = 0.0 return false 2: if elapsed < 0.1: return false _tap() if skill.beams.is_empty(): return _fail("tapping with an enemy in range did not fire") if skill.last_hit_count < 1: return _fail("the auto-aimed shot hit nothing") # Asserted on where it AIMED, not on a body count. Enemies have # hit points and a beam damages rather than always destroys, so # "did it die" would be measuring the damage numbers instead of # the targeting this test is about. if skill.aim_target.distance_to(near_position) > 1.0: return _fail("the tap aimed at %s, but the nearest enemy is at %s" \ % [skill.aim_target, near_position]) print("tap aimed at the nearest enemy (%.0f away), not the one %.0f away in another direction" \ % [shooter.global_position.distance_to(near_position), shooter.global_position.distance_to(far_position)]) # --- D. dragging still aims where it is dragged ------------------ stage = 3 elapsed = 0.0 return false 3: # Wait out the per-ship cooldown so the next shot is available. if elapsed < 6.0: return false if skill._next_ready_laser_ship() == null: return _fail("the laser ship never came off cooldown") var button: Control = main.get_node("UI/LaserButton") var at: Vector2 = button.global_position + button.size * 0.5 var press := InputEventScreenTouch.new() press.index = 8 press.position = at press.pressed = true main._handle_touch(press) # Dragged straight UP, towards the far enemy and away from where # auto-aim would have pointed. var drag := InputEventScreenDrag.new() drag.index = 8 drag.position = at + Vector2(0.0, -300.0) main._handle_drag(drag) var release := InputEventScreenTouch.new() release.index = 8 release.position = drag.position release.pressed = false main._handle_touch(release) # Dragged up, so the shot must go UP - not to the nearest enemy, # which lies to the right. Checked as a direction because the # laser always extends to its full range along the aimed heading. var heading: Vector2 = (skill.aim_target - shooter.global_position).normalized() if heading.y > -0.9: return _fail("dragging upward aimed along %s - auto-aim overrode a deliberate aim" % heading) print("dragging still aims where it is dragged (heading %s), not at the nearest enemy" % heading) print("TEST_LASER_AUTO_AIM: PASS") quit(0) return true return false func _spawn_enemy(offset: Vector2) -> Node: var enemy: Node = load("res://Enemy.tscn").instantiate() enemy.position = shooter.global_position + offset main.add_child(enemy) enemy.speed = 0.0 return enemy func _fail(message: String) -> bool: print("TEST_LASER_AUTO_AIM: FAIL (%s)" % message) quit(1) return true