This commit is contained in:
Nicholas Butzke
2026-08-02 02:20:02 -04:00
parent 61669627db
commit 922983429e
226 changed files with 34032 additions and 18521 deletions
+60 -14
View File
@@ -1,6 +1,37 @@
extends Area3D
class_name CombatArea
## The play volume. Leaving it starts a five second countdown with an on-screen
## warning and a reddening overlay; reaching zero kills the player.
##
## ── Every map needs one ──────────────────────────────────────────────────────
##
## This existed for a long while and only dust2 installed it, which meant that
## on every other map a player who got over a boundary wall — and this game has
## a grapple and a dash — simply fell out of the world and kept falling, with no
## warning, no death and no way back. `add_to()` exists so wiring it up is one
## line at the bottom of a level builder rather than fifteen, because the reason
## it was missing everywhere was that it was fiddly to add.
##
## Size it GENEROUSLY: the volume is the point at which a player is told they
## have left, not the edge of the level geometry. A box tight to the boundary
## walls fires the warning while someone is still standing on a legitimate
## rooftop.
static func add_to(level: Node, size: Vector3, centre: Vector3 = Vector3.ZERO,
node_name: String = "CombatArea") -> CombatArea:
if level.has_node(NodePath(node_name)):
return level.get_node(NodePath(node_name)) as CombatArea
var area := CombatArea.new()
area.name = node_name
var shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = size
shape.shape = box
area.add_child(shape)
area.position = centre
level.add_child(area)
return area
var out_of_bounds_players: Dictionary = {} # body -> { time_left: 5.0 }
var recovery_players: Dictionary = {} # body -> { time_safe: 0.0, time_left: float }
@@ -15,6 +46,12 @@ func _ready() -> void:
collision_mask = 4294967295 # All layers
body_exited.connect(_on_body_exited)
body_entered.connect(_on_body_entered)
# Own the warning UI for the same lifetime as the combat volume. Creating it
# from body_exited races level teardown: removing the player from physics can
# emit that signal while the scene is already being freed, leaving the
# deferred overlay orphaned.
_create_ui()
_ui_layer.visible = false
set_process(false)
func _process(delta: float) -> void:
@@ -72,11 +109,17 @@ func _on_body_exited(body: Node3D) -> void:
out_of_bounds_players[body] = { "time_left": start_time }
var is_local = false
# Singleplayer goes through OfflineMultiplayerPeer, so it takes the
# branch below and works. This default covers the case with NO peer at
# all — a level opened directly, without NetworkManager having started a
# match, which is how the debug and capture tools run it. There is one
# player then and they are the local one; without this they would get
# the silent countdown and no warning.
var is_local := true
if multiplayer.has_multiplayer_peer():
if body.get_multiplayer_authority() == multiplayer.get_unique_id():
is_local = true
is_local = body.get_multiplayer_authority() == multiplayer.get_unique_id()
if is_local:
_is_local_player_out = true
_local_player_body = body
@@ -99,7 +142,9 @@ func _on_body_entered(body: Node3D) -> void:
_local_player_body = null
func _create_ui() -> void:
if _ui_layer: return
if _ui_layer:
_ui_layer.visible = true
return
_ui_layer = CanvasLayer.new()
_ui_layer.layer = 100 # Put it on top of everything
@@ -126,21 +171,22 @@ func _create_ui() -> void:
_ui_layer.add_child(_ui_label)
if get_tree().current_scene:
get_tree().current_scene.call_deferred("add_child", _ui_layer)
add_child(_ui_layer)
func _remove_ui() -> void:
if _ui_layer:
_ui_layer.queue_free()
_ui_layer = null
_ui_label = null
_ui_overlay = null
_ui_layer.visible = false
func _kill_player(body: Node3D) -> void:
if body:
if multiplayer.is_server():
if body.has_method("take_damage"):
body.take_damage(9999, Vector3.ZERO, null, Vector3.ZERO)
# Damage is the server's call. With no peer there is no server to defer
# to and `is_server()` answers false, so without this a level opened
# outside a match would count down to zero and then quietly drop the
# player from its own list without ever killing them.
var authoritative := not multiplayer.has_multiplayer_peer() \
or multiplayer.is_server()
if authoritative and body.has_method("take_damage"):
body.take_damage(9999, Vector3.ZERO, null, Vector3.ZERO)
out_of_bounds_players.erase(body)
if body == _local_player_body and _is_local_player_out: