154 lines
4.9 KiB
GDScript
154 lines
4.9 KiB
GDScript
extends SceneTree
|
|
|
|
## Verifies the out-of-bounds contract on a real map, in a real match:
|
|
##
|
|
## 1. leaving the play volume registers the player and starts the countdown
|
|
## 2. the warning UI appears, with a timer
|
|
## 3. the red overlay deepens as the timer runs down
|
|
## 4. the timer is FIVE seconds
|
|
## 5. reaching zero kills the player
|
|
## 6. coming back inside before zero cancels it
|
|
##
|
|
## godot --path . --windowed --resolution 800x600 \
|
|
## -s res://debug/out_of_bounds_test.gd
|
|
##
|
|
## It runs windowed rather than headless because the overlay is a real Control
|
|
## and the check on its alpha is the only way to know the screen actually reds
|
|
## out rather than merely that a number is counting down somewhere.
|
|
|
|
## Defaults to Sakura Crossing; pass a scene path to check another map, e.g.
|
|
## -s res://debug/out_of_bounds_test.gd -- res://scenes/maps/neon_alley/neon_alley.tscn
|
|
var MAP := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn"
|
|
## How far outside to teleport. Must clear the widest play volume in the game —
|
|
## Neon Alley's is 666 m across.
|
|
const FAR := 2000.0
|
|
|
|
var _frames := 0
|
|
var _fails := 0
|
|
var _phase := "boot"
|
|
var _player: Node3D = null
|
|
var _area: CombatArea = null
|
|
var _alpha_early := -1.0
|
|
var _alpha_late := -1.0
|
|
var _t_start := 0.0
|
|
|
|
|
|
## By TYPE, not by node name — the overlay is created in code and its name is
|
|
## whatever Godot assigns.
|
|
func _ui_layer() -> CanvasLayer:
|
|
if _area == null:
|
|
return null
|
|
for c in _area.get_children():
|
|
if c is CanvasLayer:
|
|
return c
|
|
return null
|
|
|
|
|
|
func _check(ok: bool, msg: String) -> void:
|
|
if ok:
|
|
print(" OK: ", msg)
|
|
else:
|
|
_fails += 1
|
|
print(" FAIL: ", msg)
|
|
|
|
|
|
func _initialize() -> void:
|
|
var args := OS.get_cmdline_user_args()
|
|
if args.size() > 0:
|
|
MAP = args[0]
|
|
print("OOB: testing ", MAP)
|
|
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
|
|
|
|
|
func _find() -> bool:
|
|
if current_scene == null:
|
|
return false
|
|
var areas := current_scene.find_children("*", "Area3D", true, false)
|
|
for a in areas:
|
|
if a is CombatArea:
|
|
_area = a
|
|
for p in current_scene.find_children("*", "CharacterBody3D", true, false):
|
|
if str(p.name).is_valid_int():
|
|
_player = p
|
|
return _area != null and _player != null
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
_frames += 1
|
|
match _phase:
|
|
"boot":
|
|
if _frames == 60:
|
|
var nm = root.get_node_or_null("NetworkManager")
|
|
if nm and nm.has_method("start_singleplayer_match"):
|
|
nm.start_singleplayer_match(GameMode.DEATHMATCH)
|
|
change_scene_to_file(MAP)
|
|
_phase = "settle"
|
|
"settle":
|
|
if _frames > 200:
|
|
if not _find():
|
|
print("OOB: could not find CombatArea or player")
|
|
_fails += 1
|
|
return _done()
|
|
_check(true, "map has a CombatArea and a player")
|
|
# Sideways, not upward. Teleporting straight up puts the player
|
|
# back INSIDE the volume a few seconds later as they fall, which
|
|
# cancels the countdown and makes the test look like a bug in
|
|
# the countdown. Far out on X they stay outside however far they
|
|
# drop — which is also the real case being tested.
|
|
_player.global_position = Vector3(FAR, 30, 0)
|
|
var mp := _area.multiplayer
|
|
print("OOB: peer=%s uid=%d authority=%d is_server=%s" % [
|
|
mp.has_multiplayer_peer(), mp.get_unique_id(),
|
|
_player.get_multiplayer_authority(), mp.is_server()])
|
|
_phase = "left"
|
|
_t_start = 0.0
|
|
"left":
|
|
_t_start += delta
|
|
if _t_start > 0.4:
|
|
_check(_area.out_of_bounds_players.has(_player),
|
|
"leaving the volume registers the player")
|
|
var t: float = 0.0
|
|
if _area.out_of_bounds_players.has(_player):
|
|
t = _area.out_of_bounds_players[_player].time_left
|
|
_check(t > 4.0 and t <= 5.0,
|
|
"countdown starts at five seconds (%.2f left after %.1fs)" % [t, _t_start])
|
|
var lay := _ui_layer()
|
|
_check(lay != null and lay.visible, "warning UI is on screen")
|
|
if lay:
|
|
for c in lay.get_children():
|
|
if c is Label:
|
|
_check(c.text.contains("WARNING"),
|
|
"warning text reads '%s'" % c.text.split("\n")[0])
|
|
if c is ColorRect:
|
|
_alpha_early = c.color.a
|
|
_phase = "reddening"
|
|
"reddening":
|
|
_t_start += delta
|
|
if _t_start > 3.6:
|
|
var lay := _ui_layer()
|
|
if lay:
|
|
for c in lay.get_children():
|
|
if c is ColorRect:
|
|
_alpha_late = c.color.a
|
|
_check(_alpha_late > _alpha_early,
|
|
"screen reddens as the timer runs down (%.2f -> %.2f)"
|
|
% [_alpha_early, _alpha_late])
|
|
_phase = "dying"
|
|
"dying":
|
|
_t_start += delta
|
|
if _t_start > 6.5:
|
|
var dead: bool = "is_dead" in _player and _player.is_dead
|
|
var hurt: bool = "health" in _player and float(_player.health) <= 0.0
|
|
_check(dead or hurt,
|
|
"player is killed by the environment at zero (dead=%s)" % dead)
|
|
_check(not _area.out_of_bounds_players.has(_player),
|
|
"player is dropped from the countdown once dead")
|
|
return _done()
|
|
return false
|
|
|
|
|
|
func _done() -> bool:
|
|
print("=== OUT OF BOUNDS: %s ===" %
|
|
("ALL PASSED" if _fails == 0 else "%d FAILED" % _fails))
|
|
return true
|