Fresh start: replace with naxIO/netfox-cs-sample foundation

Complete replacement of the tactical-shooter project with the
netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built
with Godot 4 and netfox.

## What's new
- Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu
- 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP
- Bomb plant/defuse with 2 bombsites
- Flashbang & smoke grenades
- Proper netfox rollback netcode at 64 tick
- Network popup UI for host/join
- HUD, crosshair, round timer, scoreboard
- All netfox singletons registered as autoloads (works in exported builds)

## Architecture
- Listen-server (host from client, no dedicated server binary)
- Multiplayer-fps game lives at examples/multiplayer-fps/
- Netfox addons registered as autoloads for exported build compat
- Godot 4.7 with Forward+ renderer

## Removed
- Old headless-server architecture (client_main, server_main, player.gd, etc.)
- Custom netfox bootstrap with ENet fallback
- Old ChaffGames FPS template (2,420 lines, 844 KB)
- SimulationServer GDExtension stub
- Godot-jolt physics (netfox sample uses default Godot physics)
- Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc.
- Server browser API Python venv (87 MB)
- test_range map and modular assets

## Preserved
- Git history
- Server config at config/default_server_config.cfg
- Windows export preset
- Build directory (gitignored)

Co-authored-by: naxIO <naxIO@users.noreply.github.com>
This commit is contained in:
2026-07-02 20:55:20 -04:00
parent ce39b237c3
commit b0c83af092
4416 changed files with 57418 additions and 902676 deletions
@@ -0,0 +1,49 @@
@tool
extends ArrayMesh
class_name AuraSphereMesh
@export var segments: int = 64:
set(v):
segments = v
regenerate()
@export var rings: int = 32:
set(v):
rings = v
regenerate()
@export var radius: float = 1.0:
set(v):
radius = v
regenerate()
@export var material: Material:
set(v):
material = v
surface_set_material(0, material)
func _init():
regenerate()
func regenerate():
clear_surfaces()
var sphere = SphereMesh.new()
sphere.radial_segments = segments
sphere.rings = rings
sphere.radius = radius
sphere.height = 2 * radius
sphere.is_hemisphere = false
var arrays = sphere.get_mesh_arrays()
var positions: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
for i in range(uvs.size()):
var uv = positions[i]
uv = Vector2(uv.x, uv.y) / radius
uv = (Vector2.ONE + uv) / 2.0
uvs.set(i, uv)
add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
surface_set_material(0, material)
@@ -0,0 +1 @@
uid://b4kr6im1bxf3t
@@ -0,0 +1,59 @@
extends ShapeCast3D
class_name BombProjectile
@export var speed: float = 12.0
@export var strength: float = 2.0
@export var effect: PackedScene
@export var distance: float = 128.0
var distance_left: float
var fired_by: Node
var is_first_tick: bool = true
func _ready():
NetworkTime.on_tick.connect(_tick)
distance_left = distance
func _tick(delta, _t):
var dst = speed * delta
var motion = transform.basis.z * dst
target_position = Vector3.FORWARD * dst
distance_left -= dst
if distance_left < 0:
queue_free()
# Check if we've hit anyone
force_shapecast_update()
# Find the closest point of contact
var space := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.motion = motion
query.shape = shape
query.transform = global_transform
var hit_interval := space.cast_motion(query)
if hit_interval[0] != 1.0 or hit_interval[1] != 1.0 and not is_first_tick:
# Move to collision
position += motion * hit_interval[1]
_explode()
else:
position += motion
# Skip collisions for a single tick, no more
is_first_tick = false
func _explode():
queue_free()
NetworkTime.on_tick.disconnect(_tick)
if effect:
var spawn = effect.instantiate() as Node3D
get_tree().root.add_child(spawn)
spawn.global_position = global_position
spawn.fired_by = fired_by
spawn.set_multiplayer_authority(get_multiplayer_authority())
if spawn is CPUParticles3D:
(spawn as CPUParticles3D).emitting = true
@@ -0,0 +1 @@
uid://jouclb3iqnku
@@ -0,0 +1,181 @@
extends CharacterBody3D
class_name BrawlerController
# Stats
@export var speed: float = 5.0
@export var jump_velocity: float = 4.5
@export var mass: float = 4.0
# Spawn
@export var spawn_point: Vector3 = Vector3(0, 4, 0)
@export var death_depth: float = 4.0
@export var respawn_time: float = 4.0
# Dependencies
@onready var input := $Input as BrawlerInput
@onready var rollback_synchronizer := $RollbackSynchronizer as RollbackSynchronizer
@onready var animation_tree := $AnimationTree as AnimationTree
@onready var weapon := $Weapon as BrawlerWeapon
@onready var mesh := $"bomber-guy/rig/Skeleton3D/Cube_008" as MeshInstance3D
@onready var nametag := $Nametag as Label3D
@onready var fall_sound := $"Fall Sound" as PlayRandomStream3D
var player_name: String = "":
set(p_name):
if p_name.length() > 24:
p_name = p_name.substr(0, 21) + "..."
player_name = p_name
nametag.text = p_name
var player_id: int = -1
var last_hit_player: BrawlerController
var last_hit_tick: int = -1
var gravity = ProjectSettings.get_setting(&"physics/3d/default_gravity")
var respawn_tick: int = -1
var respawn_count: int = 0
func register_hit(from: BrawlerController):
if from == self:
push_error("Player %s (#%s) trying to register hit on themselves!" % [player_name, player_id])
return
last_hit_player = from
last_hit_tick = NetworkRollback.tick if NetworkRollback.is_rollback() else NetworkTime.tick
func shove(motion: Vector3):
move_and_collide(motion / mass)
func _ready():
if not input:
input = $Input
_snap_to_spawn()
GameEvents.on_brawler_spawn.emit(self)
NetworkTime.on_tick.connect(_tick)
if not player_name:
player_name = "Nameless Brawler #%s" % [player_id]
# Set player color
var color = Color.from_hsv((hash(player_id) % 256) / 256.0, 1.0, 1.0)
var material: StandardMaterial3D = mesh.get_active_material(0)
material = material.duplicate()
material.albedo_color = color
mesh.set_surface_override_material(0, material)
rollback_synchronizer.set_schema({
":transform": NetworkSchemas.transform3f32(),
":velocity": NetworkSchemas.vec3f32(),
":speed": NetworkSchemas.float32(),
":mass": NetworkSchemas.float32(),
"Input:movement": NetworkSchemas.vec3f32(),
"Input:aim": NetworkSchemas.vec3f32()
})
func _process(delta):
# Update animation
# Running
var movement = Vector3(velocity.x, 0, velocity.z) * speed
var relative_velocity = quaternion.inverse() * movement
relative_velocity.y = 0
relative_velocity /= speed
relative_velocity = Vector2(relative_velocity.x, relative_velocity.z)
var animated_velocity = animation_tree.get("parameters/Move/blend_position") as Vector2
animation_tree.set("parameters/Move/blend_position", animated_velocity.move_toward(relative_velocity, delta / 0.2))
# Float
_force_update_is_on_floor()
var animated_float = animation_tree.get("parameters/Float/blend_amount") as float
var actual_float = 1.0 if not is_on_floor() else 0.0
animation_tree.set("parameters/Float/blend_amount", move_toward(animated_float, actual_float, delta / 0.2))
# Speed
animation_tree.set("parameters/MoveScale/scale", speed / 3.75)
animation_tree.set("parameters/ThrowScale/scale", min(weapon.fire_cooldown / (10. / 24.), 1.0))
func _tick(_delta, tick):
# Run throw animation if firing
if weapon.last_fire == tick:
animation_tree.set("parameters/Throw/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
func _rollback_tick(delta, tick, is_fresh):
# Respawn
if tick == respawn_tick:
_snap_to_spawn()
velocity = Vector3.ZERO
last_hit_tick = -1
if is_fresh:
GameEvents.on_brawler_respawn.emit(self)
# Skip predictions
if rollback_synchronizer.is_predicting():
rollback_synchronizer.ignore_prediction(self)
return
# Apply gravity
_force_update_is_on_floor()
if not is_on_floor():
velocity.y -= gravity * delta
# Stick to moving platforms
var platform_velocity := Vector3.ZERO
var collision_result := KinematicCollision3D.new()
if test_move(global_transform, Vector3.DOWN * delta, collision_result):
var collider := collision_result.get_collider()
if collider is MovingPlatform:
var platform := collider as MovingPlatform
platform_velocity = platform.get_velocity()
# Jump
if input.movement.y > 0 and is_on_floor():
velocity.y = jump_velocity * input.movement.y
# Movement
var direction = Vector3(input.movement.x, 0, input.movement.z).normalized()
if direction:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
# Aim
if input.aim:
transform = transform.looking_at(position + Vector3(input.aim.x, 0, input.aim.z), Vector3.UP, true).scaled_local(scale)
# Apply movement
velocity += platform_velocity
velocity *= NetworkTime.physics_factor
move_and_slide()
velocity /= NetworkTime.physics_factor
velocity -= platform_velocity
# Death
if position.y < -death_depth and tick > respawn_tick and is_fresh:
var respawn_cooldown = respawn_time * NetworkTime.tickrate
respawn_tick = tick + respawn_cooldown
respawn_count += 1
fall_sound.play_random()
GameEvents.on_brawler_fall.emit(self)
func _exit_tree():
GameEvents.on_brawler_despawn.emit(self)
func _snap_to_spawn():
var spawns = get_tree().get_nodes_in_group("Spawn Points")
var idx = hash(player_id + respawn_count * 39) % spawns.size()
var spawn = spawns[idx] as Node3D
global_transform = spawn.global_transform
func _force_update_is_on_floor():
var old_velocity = velocity
velocity *= 0
move_and_slide()
velocity = old_velocity
@@ -0,0 +1 @@
uid://8iahb2i2wylj
@@ -0,0 +1,50 @@
extends Node3D
@export var offset: Vector3 = Vector3(0, 2.5, 0)
@export var tween_time: float = 0.2
var is_enabled: bool = true
var target: BrawlerController
func _ready():
# Start as hidden
scale = Vector3.ZERO
is_enabled = false
# Start animation
$"brawler-crown/AnimationPlayer".play("crown_rotate")
GameEvents.on_scores_updated.connect(_handle_scores)
func _process(delta):
if not target:
is_enabled = false
else:
var target_pos = target.global_position + offset
var dst = global_position.distance_squared_to(target_pos)
global_position = global_position.move_toward(target_pos, dst / tween_time * delta)
scale = scale.move_toward(Vector3.ONE if is_enabled else Vector3.ZERO, delta / tween_time)
visible = scale.length_squared() > 0.05
func _handle_scores(scores: Dictionary):
is_enabled = false
# No crown in single player
if scores.size() == 1:
return
var max_score = scores.values().max()
var max_players = scores.keys().filter(func(p): return scores[p] == max_score)
# Multiple players share the crown
if max_players.size() > 1:
return
var player_id = max_players[0]
var player = get_tree().get_nodes_in_group("Brawlers")\
.filter(func(it): return it.player_id == player_id)\
.pop_back()
target = player
is_enabled = true
@@ -0,0 +1 @@
uid://bb7o1olebaly8
@@ -0,0 +1,91 @@
extends BaseNetInput
class_name BrawlerInput
@export var switch_time: float = 1.0
var camera: Camera3D
@onready var _player: Node3D = get_parent()
@onready var _confine_mouse: bool = DisplayServer.mouse_get_mode() == DisplayServer.MOUSE_MODE_CONFINED
var movement: Vector3 = Vector3.ZERO
var aim: Vector3 = Vector3.ZERO
var is_firing: bool = false
var _last_mouse_input: float = 0.0
var _aim_target: Vector3
var _projected_target: Vector3
var _has_aim: bool = false
func _input(event):
if event is InputEventMouse:
_last_mouse_input = NetworkTime.local_time
func _gather():
# Movement
movement = Vector3(
Input.get_axis("move_west", "move_east"),
Input.get_action_strength("move_jump"),
Input.get_axis("move_north", "move_south")
)
# Aim
aim = Vector3(
Input.get_axis("aim_west", "aim_east"),
0.0,
Input.get_axis("aim_north", "aim_south")
)
if aim.length():
# Prefer gamepad
# Reset timeout for mouse motion
_last_mouse_input = NetworkTime.local_time - switch_time
elif NetworkTime.local_time - _last_mouse_input > switch_time:
# Use movement if no mouse motion recently
aim = movement
elif _has_aim:
# Use mouse raycast
aim = (_aim_target - _player.global_position).normalized()
else:
# Fall back to mouse projected to player height
aim = (_projected_target - _player.global_position).normalized()
# Always aim horizontally, never up or down
aim.y = 0
aim = aim.normalized()
# Hide mouse if inactive
if NetworkTime.local_time - _last_mouse_input >= switch_time:
DisplayServer.mouse_set_mode(
DisplayServer.MOUSE_MODE_CONFINED_HIDDEN if _confine_mouse else DisplayServer.MOUSE_MODE_HIDDEN
)
else:
DisplayServer.mouse_set_mode(
DisplayServer.MOUSE_MODE_CONFINED if _confine_mouse else DisplayServer.MOUSE_MODE_VISIBLE
)
is_firing = Input.is_action_pressed("weapon_fire")
func _physics_process(_delta):
if not camera:
camera = get_viewport().get_camera_3d()
# Aim
var mouse_pos = get_viewport().get_mouse_position()
var ray_origin = camera.project_ray_origin(mouse_pos)
var ray_normal = camera.project_ray_normal(mouse_pos)
var ray_length = 128
var space = camera.get_world_3d().direct_space_state
var hit = space.intersect_ray(PhysicsRayQueryParameters3D.create(
ray_origin, ray_origin + ray_normal * ray_length
))
if not hit.is_empty():
# Aim at raycast hit
_aim_target = hit.position
_has_aim = true
else:
# Project to player's height
var height_diff = _player.global_position.y - ray_origin.y
_projected_target = ray_origin + ray_normal * (height_diff / ray_normal.y)
_has_aim = false
@@ -0,0 +1 @@
uid://bb3yqyxdf56jx
@@ -0,0 +1,96 @@
extends Node
class_name BrawlerSpawner
@export var player_scene: PackedScene
@export var spawn_root: Node
@export var camera: FollowingCamera
@export var joining_screen: Control
@export var name_input: LineEdit
var spawn_host_avatar: bool = true
var avatars: Dictionary = {}
func _ready():
NetworkEvents.on_client_start.connect(_handle_connected)
NetworkEvents.on_server_start.connect(_handle_host)
NetworkEvents.on_peer_join.connect(_handle_new_peer)
NetworkEvents.on_peer_leave.connect(_handle_leave)
NetworkEvents.on_client_stop.connect(_handle_stop)
NetworkEvents.on_server_stop.connect(_handle_stop)
func _handle_connected(id: int):
if joining_screen:
joining_screen.visible = true
# Spawn an avatar for us
_spawn(id)
if joining_screen:
await NetworkTime.after_sync
joining_screen.visible = false
func _handle_host():
if spawn_host_avatar:
# Spawn own avatar on host machine
_spawn(1)
func _handle_new_peer(id: int):
# Spawn an avatar for new player
var avatar = _spawn(id)
# Hide avatar until player syncs time
avatar.visible = false
while not NetworkTime.is_client_synced(id):
await NetworkTime.after_client_sync
avatar.visible = true
func _handle_leave(id: int):
if not avatars.has(id):
return
var avatar = avatars[id] as Node
avatar.queue_free()
avatars.erase(id)
func _handle_stop():
# Remove all avatars on game end
for avatar in avatars.values():
avatar.queue_free()
avatars.clear()
func _spawn(id: int) -> BrawlerController:
var avatar = player_scene.instantiate() as BrawlerController
avatars[id] = avatar
avatar.name += " #%d" % id
avatar.player_id = id
spawn_root.add_child(avatar)
# Avatar is always owned by server
avatar.set_multiplayer_authority(1)
print("Spawned avatar %s at %s" % [avatar.name, multiplayer.get_unique_id()])
# Avatar's input object is owned by player
var input = avatar.find_child("Input")
if input != null:
input.set_multiplayer_authority(id)
print("Set input(%s) ownership to %s" % [input.name, id])
if id == multiplayer.get_unique_id():
# If avatar is own, assign it as camera follow target and emit event
camera.target = avatar
GameEvents.on_own_brawler_spawn.emit(avatar)
# Submit name
var player_name = name_input.text
print("Submitting player name " + player_name)
_submit_name.rpc(player_name)
return avatar
@rpc("any_peer", "reliable", "call_local")
func _submit_name(player_name: String):
var pid = multiplayer.get_remote_sender_id()
var avatar = avatars[pid]
avatar.player_name = player_name
print("Setting player name for #%s to %s" % [pid, player_name])
@@ -0,0 +1 @@
uid://uqw0ew8y4tqf
@@ -0,0 +1,44 @@
extends NetworkWeapon3D
class_name BrawlerWeapon
@export var projectile: PackedScene
@export var fire_cooldown: float = 0.15
@onready var input: BrawlerInput = $"../Input"
@onready var sound: AudioStreamPlayer3D = $AudioStreamPlayer3D
var last_fire: int = -1
static var _logger := NetfoxLogger.new("fb", "BrawlerWeapon")
func _ready():
NetworkTime.on_tick.connect(_tick)
func _can_fire() -> bool:
return NetworkTime.seconds_between(last_fire, NetworkTime.tick) >= fire_cooldown
func _can_peer_use(peer_id: int) -> bool:
return peer_id == input.get_multiplayer_authority()
func _after_fire(projectile: Node3D):
var bomb := projectile as BombProjectile
last_fire = get_fired_tick()
sound.play()
_logger.trace("[%s] Ticking new bomb %d -> %d", [bomb.name, get_fired_tick(), NetworkTime.tick])
for t in range(get_fired_tick(), NetworkTime.tick):
if bomb.is_queued_for_deletion():
break
bomb._tick(NetworkTime.ticktime, t)
func _spawn() -> Node3D:
var bomb_projectile: BombProjectile = projectile.instantiate() as BombProjectile
get_tree().root.add_child(bomb_projectile, true)
bomb_projectile.global_transform = global_transform
bomb_projectile.fired_by = get_parent()
return bomb_projectile
func _tick(_delta: float, _t: int):
if input.is_firing:
fire()
@@ -0,0 +1 @@
uid://b08u2o8edupqh
@@ -0,0 +1,65 @@
extends Area3D
@export var clouds: Array[PackedScene] = []
@export var count: int = 16
@export var float_direction: Vector3 = Vector3.RIGHT
@export var float_speed_min: float = 4.0
@export var float_speed_max: float = 8.0
var _clouds: Array[Node3D] = []
var _speeds: Array[float] = []
var _aabb: AABB
func _ready():
_aabb = _find_aabb()
if not _aabb.has_volume():
push_error("CloudArea required a box shape!")
queue_free()
return
for i in range(count):
var cloud = _spawn_cloud()
cloud.position = _aabb.position + _aabb.size * Vector3(randf(), randf(), randf())
_clouds.push_back(cloud)
_speeds.push_back(randf_range(float_speed_min, float_speed_max))
func _process(delta):
for i in range(count):
var cloud = _clouds[i]
var speed = _speeds[i]
cloud.position += float_direction * speed * delta
if not _aabb.has_point(cloud.position):
cloud.queue_free()
cloud = _spawn_cloud()
cloud.position = _aabb.position + _aabb.size * \
Vector3(randf(), randf(), randf()) * \
(Vector3.ONE * 0.5 - float_direction * 0.5)
_clouds[i] = cloud
_speeds[i] = randf_range(float_speed_min, float_speed_max)
func _spawn_cloud() -> Node3D:
var cloud_template = clouds.pick_random() as PackedScene
var cloud = cloud_template.instantiate() as Node3D
add_child(cloud)
cloud.owner = self
return cloud
func _find_aabb() -> AABB:
var shape_owners = get_shape_owners()
for shape_owner in shape_owners:
for i in range(shape_owner_get_shape_count(shape_owner)):
var shape = shape_owner_get_shape(shape_owner, i)
if shape is BoxShape3D:
var pos = shape_owner_get_transform(shape_owner).origin
var size = shape.size
return AABB(pos - size / 2, size)
return AABB(Vector3.ZERO, Vector3.ZERO)
@@ -0,0 +1 @@
uid://bgsb1gf87lh85
@@ -0,0 +1,74 @@
extends Node3D
@export var duration: float = 0.5
@export var strength: float = 1.0
@export var shape: Shape3D = SphereShape3D.new()
var birth_tick: int
var death_tick: int
var despawn_tick: int
var fired_by: Node
var _logger := NetfoxLogger.new("fb", "Displacer")
func _ready():
birth_tick = NetworkTime.tick
death_tick = birth_tick + NetworkTime.seconds_to_ticks(duration)
despawn_tick = death_tick + NetworkRollback.history_limit
NetworkRollback.on_process_tick.connect(_rollback_tick)
NetworkTime.on_tick.connect(_tick)
# Run from birth tick on next loop
NetworkRollback.notify_resimulation_start(birth_tick)
(func():
_logger.debug("Created explosion at %s@%d", [global_position, birth_tick])
).call_deferred()
func _rollback_tick(tick: int):
if tick < birth_tick or tick > death_tick:
# Tick outside of range
return
var strength_factor := inverse_lerp(death_tick, birth_tick, tick)
strength_factor = clampf(strength_factor, 0., 1.)
strength_factor = pow(strength_factor, 2)
for brawler in _get_overlapping_brawlers():
var diff := brawler.global_position - global_position
var f := clampf(1.0 / (1.0 + diff.length_squared()), 0.0, 1.0)
var offset := Vector3(diff.x, max(0, diff.y), diff.z).normalized()
offset *= strength_factor * strength * f * NetworkTime.ticktime
brawler.shove(offset)
NetworkRollback.mutate(brawler)
if brawler != fired_by:
brawler.register_hit(fired_by)
func _tick(_delta, tick):
if tick >= death_tick:
visible = false
if tick > despawn_tick:
queue_free()
func _get_overlapping_brawlers() -> Array[BrawlerController]:
var result: Array[BrawlerController] = []
var state := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.shape = shape
query.transform = global_transform
# TODO: Move map geo and brawlers to separate layers, so map doesn't clog up
# the 32 max_results - this would enable bigger collision shapes
var hits := state.intersect_shape(query)
for hit in hits:
var hit_object = hit["collider"]
if hit_object is BrawlerController:
result.push_back(hit_object)
return result
@@ -0,0 +1 @@
uid://cxnxh3ffpwn2s
@@ -0,0 +1,55 @@
extends Node3D
class_name Effect
@export var duration: float = 8.0
@export var winddown_time: float = 2.0
@onready var animation_player: AnimationPlayer = $AnimationPlayer as AnimationPlayer
var _apply_tick: int = 0
var _cease_tick: int = 0
var _destroy_tick: int = 0
func _ready():
if not get_parent() is BrawlerController:
push_error("Powerup effect added to non-player!")
queue_free()
return
_apply_tick = NetworkTime.tick + 1
_cease_tick = _apply_tick + NetworkTime.seconds_to_ticks(duration)
_destroy_tick = max(
_cease_tick + NetworkTime.seconds_to_ticks(winddown_time),
_cease_tick + NetworkRollback.history_limit
)
# Resim from apply tick on the next loop
NetworkRollback.before_loop.connect(func(): NetworkRollback.notify_resimulation_start(_apply_tick), CONNECT_ONE_SHOT)
NetworkRollback.on_process_tick.connect(_rollback_tick)
NetworkTime.on_tick.connect(_tick)
func _rollback_tick(tick):
if tick == _apply_tick:
_apply()
if tick == _cease_tick:
_cease()
func _tick(_delta, tick):
if tick == _cease_tick:
animation_player.play("death")
if tick >= _destroy_tick:
queue_free()
func _apply():
pass
func _cease():
pass
func get_target() -> BrawlerController:
return get_parent_node_3d() as BrawlerController
func is_active() -> bool:
var tick = NetworkRollback.tick if NetworkRollback.is_rollback() else NetworkTime.tick
return tick >= _apply_tick and tick < _cease_tick
@@ -0,0 +1 @@
uid://rbj18ues4b75
@@ -0,0 +1,11 @@
extends Effect
@export var bonus_mass: float = 1.0
func _apply():
get_target().mass += bonus_mass
NetworkRollback.mutate(get_target())
func _cease():
get_target().mass -= bonus_mass
NetworkRollback.mutate(get_target())
@@ -0,0 +1 @@
uid://ku0h0bido33i
@@ -0,0 +1,25 @@
extends Effect
@export var area: Area3D
@export var strength: float = 4.0
func _rollback_tick(tick):
super._rollback_tick(tick)
if not is_active():
return
for body in area.get_overlapping_bodies():
if not body is BrawlerController or body == get_parent_node_3d():
continue
var brawler := body as BrawlerController
var diff: Vector3 = brawler.global_position - global_position
var f = clampf(1.0 / (1.0 + diff.length_squared()), 0.0, 1.0)
f = clampf(1. - diff.length_squared() / 16., 0., 1.)
diff.y = max(0, diff.y)
var motion = diff.normalized() * strength * f * NetworkTime.ticktime
brawler.shove(motion)
brawler.register_hit(get_parent_node_3d())
NetworkRollback.mutate(brawler)
@@ -0,0 +1 @@
uid://bbyjqcch0uq2b
@@ -0,0 +1,11 @@
extends Effect
@export var bonus: float = 0.2
func _apply():
get_target().speed *= 1 + bonus
NetworkRollback.mutate(get_target())
func _cease():
get_target().speed /= 1 + bonus
NetworkRollback.mutate(get_target())
@@ -0,0 +1 @@
uid://byyjui57c0qjx
@@ -0,0 +1,8 @@
extends CPUParticles3D
func _ready():
one_shot = true
func _process(_delta):
if not emitting:
queue_free()
@@ -0,0 +1 @@
uid://clfqawybja64i
@@ -0,0 +1,22 @@
extends Camera3D
class_name FollowingCamera
@export var distance: float = 4.0
@export var approach_time: float = 0.125
@export var target: Node3D
func _ready():
NetworkTime.on_tick.connect(_tick)
func _tick(delta: float, _t: int):
if not target:
return
var desired_pos = target.global_position
desired_pos += transform.basis.z * distance
var diff = desired_pos - global_position
var dst = diff.length()
diff = diff.normalized()
global_position += diff * minf(dst / approach_time * delta, dst)
@@ -0,0 +1 @@
uid://bqrt0ucoqar5v
@@ -0,0 +1,10 @@
extends Node
# Only used for Forest Brawl example
signal on_brawler_spawn(brawler: BrawlerController)
signal on_own_brawler_spawn(brawler: BrawlerController)
signal on_brawler_fall(brawler: BrawlerController)
signal on_brawler_respawn(brawler: BrawlerController)
signal on_brawler_despawn(brawler: BrawlerController)
signal on_scores_updated(scores: Dictionary)
@@ -0,0 +1 @@
uid://wpjf12noxvp5
@@ -0,0 +1,27 @@
extends AnimatableBody3D
class_name MovingPlatform
@export var speed: float = 2.
@onready var _origin: Vector3 = global_position
@onready var _target: Vector3 = $Target.global_position
@onready var _distance: float = _origin.distance_to(_target)
var _velocity: Vector3 = Vector3.ZERO
func get_velocity() -> Vector3:
return _velocity
func _ready():
NetworkRollback.on_prepare_tick.connect(_apply_tick)
func _apply_tick(tick: int):
var previous_position = _get_position_for_tick(tick - 1)
global_position = _get_position_for_tick(tick)
_velocity = (global_position - previous_position) / NetworkTime.ticktime
func _get_position_for_tick(tick: int):
var distance_moved = NetworkTime.ticks_to_seconds(tick) * speed
var progress = distance_moved / _distance
progress = pingpong(progress, 1)
return _origin.lerp(_target, progress)
@@ -0,0 +1 @@
uid://4ynvkeku823s
@@ -0,0 +1,336 @@
extends Object
class_name NameProvider
static var _adjectives: PackedStringArray
static var _animals: PackedStringArray
static func _pick_random(from: PackedStringArray) -> String:
return from[randi_range(0, from.size()-1)]
static func name():
return ("%s %s" % [
NameProvider._pick_random(NameProvider._adjectives),
NameProvider._pick_random(NameProvider._animals)
]).capitalize()
static func _static_init():
# Source for adjectives: https://gist.github.com/hugsy/8910dc78d208e40de42deb29e62df913
# Source for animals: https://gist.github.com/atduskgreg/3cf8ef48cb0d29cf151bedad81553a54
_adjectives = PackedStringArray([
"abandoned", "able", "absolute", "adorable", "adventurous", "academic",
"acceptable", "acclaimed", "accomplished", "accurate", "aching", "acidic",
"acrobatic", "active", "actual", "adept", "admirable", "admired",
"adolescent", "adorable", "adored", "advanced", "afraid", "affectionate",
"aged", "aggravating", "aggressive", "agile", "agitated", "agonizing",
"agreeable", "ajar", "alarmed", "alarming", "alert", "alienated",
"alive", "all", "altruistic", "amazing", "ambitious", "ample",
"amused", "amusing", "anchored", "ancient", "angelic", "angry",
"anguished", "animated", "annual", "another", "antique", "anxious",
"any", "apprehensive", "appropriate", "apt", "arctic", "arid",
"aromatic", "artistic", "ashamed", "assured", "astonishing", "athletic",
"attached", "attentive", "attractive", "austere", "authentic", "authorized",
"automatic", "avaricious", "average", "aware", "awesome", "awful",
"awkward", "babyish", "bad", "back", "baggy", "bare",
"barren", "basic", "beautiful", "belated", "beloved", "beneficial",
"better", "best", "bewitched", "big", "big-hearted", "biodegradable",
"bite-sized", "bitter", "black", "black-and-white", "bland", "blank",
"blaring", "bleak", "blind", "blissful", "blond", "blue",
"blushing", "bogus", "boiling", "bold", "bony", "boring",
"bossy", "both", "bouncy", "bountiful", "bowed", "brave",
"breakable", "brief", "bright", "brilliant", "brisk", "broken",
"bronze", "brown", "bruised", "bubbly", "bulky", "bumpy",
"buoyant", "burdensome", "burly", "bustling", "busy", "buttery",
"buzzing", "calculating", "calm", "candid", "canine", "capital",
"carefree", "careful", "careless", "caring", "cautious", "cavernous",
"celebrated", "charming", "cheap", "cheerful", "cheery", "chief",
"chilly", "chubby", "circular", "classic", "clean", "clear",
"clear-cut", "clever", "close", "closed", "cloudy", "clueless",
"clumsy", "cluttered", "coarse", "cold", "colorful", "colorless",
"colossal", "comfortable", "common", "compassionate", "competent", "complete",
"complex", "complicated", "composed", "concerned", "concrete", "confused",
"conscious", "considerate", "constant", "content", "conventional", "cooked",
"cool", "cooperative", "coordinated", "corny", "corrupt", "costly",
"courageous", "courteous", "crafty", "crazy", "creamy", "creative",
"creepy", "criminal", "crisp", "critical", "crooked", "crowded",
"cruel", "crushing", "cuddly", "cultivated", "cultured", "cumbersome",
"curly", "curvy", "cute", "cylindrical", "damaged", "damp",
"dangerous", "dapper", "daring", "darling", "dark", "dazzling",
"dead", "deadly", "deafening", "dear", "dearest", "decent",
"decimal", "decisive", "deep", "defenseless", "defensive", "defiant",
"deficient", "definite", "definitive", "delayed", "delectable", "delicious",
"delightful", "delirious", "demanding", "dense", "dental", "dependable",
"dependent", "descriptive", "deserted", "detailed", "determined", "devoted",
"different", "difficult", "digital", "diligent", "dim", "dimpled",
"dimwitted", "direct", "disastrous", "discrete", "disfigured", "disgusting",
"disloyal", "dismal", "distant", "downright", "dreary", "dirty",
"disguised", "dishonest", "dismal", "distant", "distinct", "distorted",
"dizzy", "dopey", "doting", "double", "downright", "drab",
"drafty", "dramatic", "dreary", "droopy", "dry", "dual",
"dull", "dutiful", "each", "eager", "earnest", "early",
"easy", "easy-going", "ecstatic", "edible", "educated", "elaborate",
"elastic", "elated", "elderly", "electric", "elegant", "elementary",
"elliptical", "embarrassed", "embellished", "eminent", "emotional", "empty",
"enchanted", "enchanting", "energetic", "enlightened", "enormous", "enraged",
"entire", "envious", "equal", "equatorial", "essential", "esteemed",
"ethical", "euphoric", "even", "evergreen", "everlasting", "every",
"evil", "exalted", "excellent", "exemplary", "exhausted", "excitable",
"excited", "exciting", "exotic", "expensive", "experienced", "expert",
"extraneous", "extroverted", "extra-large", "extra-small", "fabulous", "failing",
"faint", "fair", "faithful", "fake", "false", "familiar",
"famous", "fancy", "fantastic", "far", "faraway", "far-flung",
"far-off", "fast", "fat", "fatal", "fatherly", "favorable",
"favorite", "fearful", "fearless", "feisty", "feline", "female",
"feminine", "few", "fickle", "filthy", "fine", "finished",
"firm", "first", "firsthand", "fitting", "fixed", "flaky",
"flamboyant", "flashy", "flat", "flawed", "flawless", "flickering",
"flimsy", "flippant", "flowery", "fluffy", "fluid", "flustered",
"focused", "fond", "foolhardy", "foolish", "forceful", "forked",
"formal", "forsaken", "forthright", "fortunate", "fragrant", "frail",
"frank", "frayed", "free", "French", "fresh", "frequent",
"friendly", "frightened", "frightening", "frigid", "frilly", "frizzy",
"frivolous", "front", "frosty", "frozen", "frugal", "fruitful",
"full", "fumbling", "functional", "funny", "fussy", "fuzzy",
"gargantuan", "gaseous", "general", "generous", "gentle", "genuine",
"giant", "giddy", "gigantic", "gifted", "giving", "glamorous",
"glaring", "glass", "gleaming", "gleeful", "glistening", "glittering",
"gloomy", "glorious", "glossy", "glum", "golden", "good",
"good-natured", "gorgeous", "graceful", "gracious", "grand", "grandiose",
"granular", "grateful", "grave", "gray", "great", "greedy",
"green", "gregarious", "grim", "grimy", "gripping", "grizzled",
"gross", "grotesque", "grouchy", "grounded", "growing", "growling",
"grown", "grubby", "gruesome", "grumpy", "guilty", "gullible",
"gummy", "hairy", "half", "handmade", "handsome", "handy",
"happy", "happy-go-lucky", "hard", "hard-to-find", "harmful", "harmless",
"harmonious", "harsh", "hasty", "hateful", "haunting", "healthy",
"heartfelt", "hearty", "heavenly", "heavy", "hefty", "helpful",
"helpless", "hidden", "hideous", "high", "high-level", "hilarious",
"hoarse", "hollow", "homely", "honest", "honorable", "honored",
"hopeful", "horrible", "hospitable", "hot", "huge", "humble",
"humiliating", "humming", "humongous", "hungry", "hurtful", "husky",
"icky", "icy", "ideal", "idealistic", "identical", "idle",
"idiotic", "idolized", "ignorant", "ill", "illegal", "ill-fated",
"ill-informed", "illiterate", "illustrious", "imaginary", "imaginative", "immaculate",
"immaterial", "immediate", "immense", "impassioned", "impeccable", "impartial",
"imperfect", "imperturbable", "impish", "impolite", "important", "impossible",
"impractical", "impressionable", "impressive", "improbable", "impure", "inborn",
"incomparable", "incompatible", "incomplete", "inconsequential", "incredible", "indelible",
"inexperienced", "indolent", "infamous", "infantile", "infatuated", "inferior",
"infinite", "informal", "innocent", "insecure", "insidious", "insignificant",
"insistent", "instructive", "insubstantial", "intelligent", "intent", "intentional",
"interesting", "internal", "international", "intrepid", "ironclad", "irresponsible",
"irritating", "itchy", "jaded", "jagged", "jam-packed", "jaunty",
"jealous", "jittery", "joint", "jolly", "jovial", "joyful",
"joyous", "jubilant", "judicious", "juicy", "jumbo", "junior",
"jumpy", "juvenile", "kaleidoscopic", "keen", "key", "kind",
"kindhearted", "kindly", "klutzy", "knobby", "knotty", "knowledgeable",
"knowing", "known", "kooky", "kosher", "lame", "lanky",
"large", "last", "lasting", "late", "lavish", "lawful",
"lazy", "leading", "lean", "leafy", "left", "legal",
"legitimate", "light", "lighthearted", "likable", "likely", "limited",
"limp", "limping", "linear", "lined", "liquid", "little",
"live", "lively", "livid", "loathsome", "lone", "lonely",
"long", "long-term", "loose", "lopsided", "lost", "loud",
"lovable", "lovely", "loving", "low", "loyal", "lucky",
"lumbering", "luminous", "lumpy", "lustrous", "luxurious", "mad",
"made-up", "magnificent", "majestic", "major", "male", "mammoth",
"married", "marvelous", "masculine", "massive", "mature", "meager",
"mealy", "mean", "measly", "meaty", "medical", "mediocre",
"medium", "meek", "mellow", "melodic", "memorable", "menacing",
"merry", "messy", "metallic", "mild", "milky", "mindless",
"miniature", "minor", "minty", "miserable", "miserly", "misguided",
"misty", "mixed", "modern", "modest", "moist", "monstrous",
"monthly", "monumental", "moral", "mortified", "motherly", "motionless",
"mountainous", "muddy", "muffled", "multicolored", "mundane", "murky",
"mushy", "musty", "muted", "mysterious", "naive", "narrow",
"nasty", "natural", "naughty", "nautical", "near", "neat",
"necessary", "needy", "negative", "neglected", "negligible", "neighboring",
"nervous", "new", "next", "nice", "nifty", "nimble",
"nippy", "nocturnal", "noisy", "nonstop", "normal", "notable",
"noted", "noteworthy", "novel", "noxious", "numb", "nutritious",
"nutty", "obedient", "obese", "oblong", "oily", "oblong",
"obvious", "occasional", "odd", "oddball", "offbeat", "offensive",
"official", "old", "old-fashioned", "only", "open", "optimal",
"optimistic", "opulent", "orange", "orderly", "organic", "ornate",
"ornery", "ordinary", "original", "other", "our", "outlying",
"outgoing", "outlandish", "outrageous", "outstanding", "oval", "overcooked",
"overdue", "overjoyed", "overlooked", "palatable", "pale", "paltry",
"parallel", "parched", "partial", "passionate", "past", "pastel",
"peaceful", "peppery", "perfect", "perfumed", "periodic", "perky",
"personal", "pertinent", "pesky", "pessimistic", "petty", "phony",
"physical", "piercing", "pink", "pitiful", "plain", "plaintive",
"plastic", "playful", "pleasant", "pleased", "pleasing", "plump",
"plush", "polished", "polite", "political", "pointed", "pointless",
"poised", "poor", "popular", "portly", "posh", "positive",
"possible", "potable", "powerful", "powerless", "practical", "precious",
"present", "prestigious", "pretty", "precious", "previous", "pricey",
"prickly", "primary", "prime", "pristine", "private", "prize",
"probable", "productive", "profitable", "profuse", "proper", "proud",
"prudent", "punctual", "pungent", "puny", "pure", "purple",
"pushy", "putrid", "puzzled", "puzzling", "quaint", "qualified",
"quarrelsome", "quarterly", "queasy", "querulous", "questionable", "quick",
"quick-witted", "quiet", "quintessential", "quirky", "quixotic", "quizzical",
"radiant", "ragged", "rapid", "rare", "rash", "raw",
"recent", "reckless", "rectangular", "ready", "real", "realistic",
"reasonable", "red", "reflecting", "regal", "regular", "reliable",
"relieved", "remarkable", "remorseful", "remote", "repentant", "required",
"respectful", "responsible", "repulsive", "revolving", "rewarding", "rich",
"rigid", "right", "ringed", "ripe", "roasted", "robust",
"rosy", "rotating", "rotten", "rough", "round", "rowdy",
"royal", "rubbery", "rundown", "ruddy", "rude", "runny",
"rural", "rusty", "sad", "safe", "salty", "same",
"sandy", "sane", "sarcastic", "sardonic", "satisfied", "scaly",
"scarce", "scared", "scary", "scented", "scholarly", "scientific",
"scornful", "scratchy", "scrawny", "second", "secondary", "second-hand",
"secret", "self-assured", "self-reliant", "selfish", "sentimental", "separate",
"serene", "serious", "serpentine", "several", "severe", "shabby",
"shadowy", "shady", "shallow", "shameful", "shameless", "sharp",
"shimmering", "shiny", "shocked", "shocking", "shoddy", "short",
"short-term", "showy", "shrill", "shy", "sick", "silent",
"silky", "silly", "silver", "similar", "simple", "simplistic",
"sinful", "single", "sizzling", "skeletal", "skinny", "sleepy",
"slight", "slim", "slimy", "slippery", "slow", "slushy",
"small", "smart", "smoggy", "smooth", "smug", "snappy",
"snarling", "sneaky", "sniveling", "snoopy", "sociable", "soft",
"soggy", "solid", "somber", "some", "spherical", "sophisticated",
"sore", "sorrowful", "soulful", "soupy", "sour", "Spanish",
"sparkling", "sparse", "specific", "spectacular", "speedy", "spicy",
"spiffy", "spirited", "spiteful", "splendid", "spotless", "spotted",
"spry", "square", "squeaky", "squiggly", "stable", "staid",
"stained", "stale", "standard", "starchy", "stark", "starry",
"steep", "sticky", "stiff", "stimulating", "stingy", "stormy",
"straight", "strange", "steel", "strict", "strident", "striking",
"striped", "strong", "studious", "stunning", "stupendous", "stupid",
"sturdy", "stylish", "subdued", "submissive", "substantial", "subtle",
"suburban", "sudden", "sugary", "sunny", "super", "superb",
"superficial", "superior", "supportive", "sure-footed", "surprised", "suspicious",
"svelte", "sweaty", "sweet", "sweltering", "swift", "sympathetic",
"tall", "talkative", "tame", "tan", "tangible", "tart",
"tasty", "tattered", "taut", "tedious", "teeming", "tempting",
"tender", "tense", "tepid", "terrible", "terrific", "testy",
"thankful", "that", "these", "thick", "thin", "third",
"thirsty", "this", "thorough", "thorny", "those", "thoughtful",
"threadbare", "thrifty", "thunderous", "tidy", "tight", "timely",
"tinted", "tiny", "tired", "torn", "total", "tough",
"traumatic", "treasured", "tremendous", "tragic", "trained", "tremendous",
"triangular", "tricky", "trifling", "trim", "trivial", "troubled",
"true", "trusting", "trustworthy", "trusty", "truthful", "tubby",
"turbulent", "twin", "ugly", "ultimate", "unacceptable", "unaware",
"uncomfortable", "uncommon", "unconscious", "understated", "unequaled", "uneven",
"unfinished", "unfit", "unfolded", "unfortunate", "unhappy", "unhealthy",
"uniform", "unimportant", "unique", "united", "unkempt", "unknown",
"unlawful", "unlined", "unlucky", "unnatural", "unpleasant", "unrealistic",
"unripe", "unruly", "unselfish", "unsightly", "unsteady", "unsung",
"untidy", "untimely", "untried", "untrue", "unused", "unusual",
"unwelcome", "unwieldy", "unwilling", "unwitting", "unwritten", "upbeat",
"upright", "upset", "urban", "usable", "used", "useful",
"useless", "utilized", "utter", "vacant", "vague", "vain",
"valid", "valuable", "vapid", "variable", "vast", "velvety",
"venerated", "vengeful", "verifiable", "vibrant", "vicious", "victorious",
"vigilant", "vigorous", "villainous", "violet", "violent", "virtual",
"virtuous", "visible", "vital", "vivacious", "vivid", "voluminous",
"wan", "warlike", "warm", "warmhearted", "warped", "wary",
"wasteful", "watchful", "waterlogged", "watery", "wavy", "wealthy",
"weak", "weary", "webbed", "wee", "weekly", "weepy",
"weighty", "weird", "welcome", "well-documented", "well-groomed", "well-informed",
"well-lit", "well-made", "well-off", "well-to-do", "well-worn", "wet",
"which", "whimsical", "whirlwind", "whispered", "white", "whole",
"whopping", "wicked", "wide", "wide-eyed", "wiggly", "wild",
"willing", "wilted", "winding", "windy", "winged", "wiry",
"wise", "witty", "wobbly", "woeful", "wonderful", "wooden",
"woozy", "wordy", "worldly", "worn", "worried", "worrisome",
"worse", "worst", "worthless", "worthwhile", "worthy", "wrathful",
"wretched", "writhing", "wrong", "wry", "yawning", "yearly",
"yellow", "yellowish", "young", "youthful", "yummy", "zany",
"zealous", "zesty", "zigzag",
])
_animals = PackedStringArray([
"canidae", "felidae", "cat", "cattle", "dog", "donkey",
"goat", "guinea pig", "horse", "pig", "rabbit", "fancy rat varieties",
"laboratory rat strains", "sheep breeds", "water buffalo breeds", "chicken breeds", "duck breeds", "goose breeds",
"pigeon breeds", "turkey breeds", "aardvark", "aardwolf", "african buffalo", "african elephant",
"african leopard", "albatross", "alligator", "alpaca", "american buffalo (bison)", "american robin",
"amphibian", "list", "anaconda", "angelfish", "anglerfish", "ant",
"anteater", "antelope", "antlion", "ape", "aphid", "arabian leopard",
"arctic fox", "arctic wolf", "armadillo", "arrow crab", "asp", "ass (donkey)",
"baboon", "badger", "bald eagle", "bandicoot", "barnacle", "barracuda",
"basilisk", "bass", "bat", "beaked whale", "bear", "list",
"beaver", "bedbug", "bee", "beetle", "bird", "list",
"bison", "blackbird", "black panther", "black widow spider", "blue bird", "blue jay",
"blue whale", "boa", "boar", "bobcat", "bobolink", "bonobo",
"booby", "box jellyfish", "bovid", "buffalo, african", "buffalo, american (bison)", "bug",
"butterfly", "buzzard", "camel", "canid", "cape buffalo", "capybara",
"cardinal", "caribou", "carp", "cat", "list", "catshark",
"caterpillar", "catfish", "cattle", "list", "centipede", "cephalopod",
"chameleon", "cheetah", "chickadee", "chicken", "list", "chimpanzee",
"chinchilla", "chipmunk", "clam", "clownfish", "cobra", "cockroach",
"cod", "condor", "constrictor", "coral", "cougar", "cow",
"coyote", "crab", "crane", "crane fly", "crawdad", "crayfish",
"cricket", "crocodile", "crow", "cuckoo", "cicada", "damselfly",
"deer", "dingo", "dinosaur", "list", "dog", "list",
"dolphin", "donkey", "list", "dormouse", "dove", "dragonfly",
"dragon", "duck", "list", "dung beetle", "eagle", "earthworm",
"earwig", "echidna", "eel", "egret", "elephant", "elephant seal",
"elk", "emu", "english pointer", "ermine", "falcon", "ferret",
"finch", "firefly", "fish", "flamingo", "flea", "fly",
"flyingfish", "fowl", "fox", "frog", "fruit bat", "gamefowl",
"list", "galliform", "list", "gazelle", "gecko", "gerbil",
"giant panda", "giant squid", "gibbon", "gila monster", "giraffe", "goat",
"list", "goldfish", "goose", "list", "gopher", "gorilla",
"grasshopper", "great blue heron", "great white shark", "grizzly bear", "ground shark", "ground sloth",
"grouse", "guan", "list", "guanaco", "guineafowl", "list",
"guinea pig", "list", "gull", "guppy", "haddock", "halibut",
"hammerhead shark", "hamster", "hare", "harrier", "hawk", "hedgehog",
"hermit crab", "heron", "herring", "hippopotamus", "hookworm", "hornet",
"horse", "list", "hoverfly", "hummingbird", "humpback whale", "hyena",
"iguana", "impala", "irukandji jellyfish", "jackal", "jaguar", "jay",
"jellyfish", "junglefowl", "kangaroo", "kangaroo mouse", "kangaroo rat", "kingfisher",
"kite", "kiwi", "koala", "koi", "komodo dragon", "krill",
"ladybug", "lamprey", "landfowl", "land snail", "lark", "leech",
"lemming", "lemur", "leopard", "leopon", "limpet", "lion",
"lizard", "llama", "lobster", "locust", "loon", "louse",
"lungfish", "lynx", "macaw", "mackerel", "magpie", "mammal",
"manatee", "mandrill", "manta ray", "marlin", "marmoset", "marmot",
"marsupial", "marten", "mastodon", "meadowlark", "meerkat", "mink",
"minnow", "mite", "mockingbird", "mole", "mollusk", "mongoose",
"monitor lizard", "monkey", "moose", "mosquito", "moth", "mountain goat",
"mouse", "mule", "muskox", "narwhal", "newt", "new world quail",
"nightingale", "ocelot", "octopus", "old world quail", "opossum", "orangutan",
"orca", "ostrich", "otter", "owl", "ox", "panda",
"panther", "panthera hybrid", "parakeet", "parrot", "parrotfish", "partridge",
"peacock", "peafowl", "pelican", "penguin", "perch", "peregrine falcon",
"pheasant", "pig", "pigeon", "list", "pike", "pilot whale",
"pinniped", "piranha", "planarian", "platypus", "polar bear", "pony",
"porcupine", "porpoise", "portuguese man o' war", "possum", "prairie dog", "prawn",
"praying mantis", "primate", "ptarmigan", "puffin", "puma", "python",
"quail", "quelea", "quokka", "rabbit", "list", "raccoon",
"rainbow trout", "rat", "rattlesnake", "raven", "ray (batoidea)", "ray (rajiformes)",
"red panda", "reindeer", "reptile", "rhinoceros", "right whale", "roadrunner",
"rodent", "rook", "rooster", "roundworm", "saber-toothed cat", "sailfish",
"salamander", "salmon", "sawfish", "scale insect", "scallop", "scorpion",
"seahorse", "sea lion", "sea slug", "sea snail", "shark", "list",
"sheep", "list", "shrew", "shrimp", "silkworm", "silverfish",
"skink", "skunk", "sloth", "slug", "smelt", "snail",
"snake", "list", "snipe", "snow leopard", "sockeye salmon", "sole",
"sparrow", "sperm whale", "spider", "spider monkey", "spoonbill", "squid",
"squirrel", "starfish", "star-nosed mole", "steelhead trout", "stingray", "stoat",
"stork", "sturgeon", "sugar glider", "swallow", "swan", "swift",
"swordfish", "swordtail", "tahr", "takin", "tapir", "tarantula",
"tarsier", "tasmanian devil", "termite", "tern", "thrush", "tick",
"tiger", "tiger shark", "tiglon", "toad", "tortoise", "toucan",
"trapdoor spider", "tree frog", "trout", "tuna", "turkey", "list",
"turtle", "tyrannosaurus", "urial", "vampire bat", "vampire squid", "vicuna",
"viper", "vole", "vulture", "wallaby", "walrus", "wasp",
"warbler", "water boa", "water buffalo", "weasel", "whale", "whippet",
"whitefish", "whooping crane", "wildcat", "wildebeest", "wildfowl", "wolf",
"wolverine", "wombat", "woodpecker", "worm", "wren", "xerinae",
"x-ray fish", "yak", "yellow perch", "zebra", "zebra finch", "animals by number of neurons",
"animals by size", "common household pests", "common names of poisonous animals", "alpaca", "bali cattle", "cat",
"cattle", "chicken", "dog", "domestic bactrian camel", "domestic canary", "domestic dromedary camel",
"domestic duck", "domestic goat", "domestic goose", "domestic guineafowl", "domestic hedgehog", "domestic pig",
"domestic pigeon", "domestic rabbit", "domestic silkmoth", "domestic silver fox", "domestic turkey", "donkey",
"fancy mouse", "fancy rat", "lab rat", "ferret", "gayal", "goldfish",
"guinea pig", "guppy", "horse", "koi", "llama", "ringneck dove",
"sheep", "siamese fighting fish", "society finch", "yak", "water buffalo"
])
@@ -0,0 +1 @@
uid://b4n5txktpn8ow
@@ -0,0 +1,14 @@
extends AudioStreamPlayer3D
class_name PlayRandomStream3D
@export var sounds: Array[AudioStream] = []
static var idx = 0
func _ready():
if autoplay:
play_random()
func play_random():
stream = sounds.pick_random()
play()
@@ -0,0 +1 @@
uid://b5g6tmcwyaell
@@ -0,0 +1,16 @@
extends Control
@export var score_label: Label
@export var effect_label: Label
@export var score_manager: ScoreManager
var brawler: BrawlerController
func _ready():
GameEvents.on_own_brawler_spawn.connect(func(b): brawler = b)
func _process(_delta):
if not brawler:
visible = false
else:
visible = true
score_label.text = str(score_manager.get_score(brawler.player_id))
@@ -0,0 +1 @@
uid://iyihxxg5xrrr
+51
View File
@@ -0,0 +1,51 @@
extends Area3D
@export var effects: Array[PackedScene] = []
@export var cooldown: float = 30.0
var is_active: bool = true
var fade_speed: float = 8.0
var respawn_tick: int = 0
func _ready():
NetworkTime.after_tick.connect(_tick)
set_multiplayer_authority(1)
func _tick(delta, tick):
if is_active:
scale = scale.lerp(Vector3.ONE, fade_speed * delta)
for body in get_overlapping_bodies():
if body.is_in_group("Brawlers") and not _has_powerup(body):
_take() # Predict
if is_multiplayer_authority():
_spawn_effect.rpc(randi_range(0, effects.size() - 1), body.get_path())
_take.rpc()
else:
scale = scale.lerp(Vector3.ONE * 0.0005, fade_speed * delta)
if tick == respawn_tick:
_respawn() # Predict
if is_multiplayer_authority():
_respawn.rpc()
func _has_powerup(target: Node) -> bool:
return target.get_children()\
.filter(func(child): return child is Effect)\
.any(func(effect: Effect): return effect.is_active())
@rpc("authority", "reliable", "call_local")
func _spawn_effect(effect_idx: int, target_path: NodePath):
var effect = effects[effect_idx]
var target = get_tree().get_root().get_node(target_path)
var spawn = effect.instantiate()
target.add_child(spawn)
@rpc("authority", "reliable")
func _take():
respawn_tick = NetworkTime.tick + NetworkTime.seconds_to_ticks(cooldown)
is_active = false
@rpc("authority", "reliable")
func _respawn():
is_active = true
@@ -0,0 +1 @@
uid://bv78115x5p0r2
@@ -0,0 +1,4 @@
extends LineEdit
func _ready():
text = NameProvider.name()
@@ -0,0 +1 @@
uid://bt0i7f3f8t5n2
@@ -0,0 +1,69 @@
extends Node
class_name ScoreManager
@export var scorescreen: ScoreScreen
@export var hit_threshold_time: float = 8.0
var _scores = {}
var _brawlers = {}
func get_score(player: int) -> int:
return _scores.get(player, 0)
func _ready():
GameEvents.on_brawler_spawn.connect(_handle_spawn)
GameEvents.on_brawler_fall.connect(_handle_fall)
GameEvents.on_brawler_respawn.connect(_handle_respawn)
GameEvents.on_brawler_despawn.connect(_handle_despawn)
set_multiplayer_authority(1)
func _handle_spawn(brawler: BrawlerController):
var id = brawler.player_id
_scores[id] = _scores.get(id, 0)
_brawlers[id] = brawler
func _handle_fall(brawler: BrawlerController):
var id = brawler.player_id
# Update scores
if is_multiplayer_authority():
if NetworkTime.seconds_between(brawler.last_hit_tick, NetworkTime.tick) < hit_threshold_time \
and brawler.last_hit_player:
var hit_id = brawler.last_hit_player.player_id
_scores[hit_id] = _scores.get(hit_id, 0) + 1
else:
_scores[id] = _scores.get(id, 0) - 1
GameEvents.on_scores_updated.emit(_scores)
_submit_scores.rpc(_scores)
# Display scoreboard
if id == multiplayer.get_unique_id():
scorescreen.render(_render_scores())
scorescreen.active = true
func _handle_respawn(brawler: BrawlerController):
# Hide scoreboard
if brawler.player_id == multiplayer.get_unique_id():
scorescreen.active = false
func _handle_despawn(brawler: BrawlerController):
_scores.erase(brawler.player_id)
_brawlers.erase(brawler.player_id)
@rpc("authority", "reliable", "call_remote")
func _submit_scores(scores: Dictionary):
print("Received new scores, updating %s -> %s" % [_scores, scores])
_scores = scores
GameEvents.on_scores_updated.emit(_scores)
# Re-render scoreboard if visible
if scorescreen.active:
scorescreen.render(_render_scores())
func _render_scores() -> Dictionary:
var render_scores = {}
for pid in _scores:
var brawler = _brawlers[pid]
var score = _scores[pid]
render_scores[brawler.player_name] = score
return render_scores
@@ -0,0 +1 @@
uid://cfe08o3gvubst
@@ -0,0 +1,56 @@
extends Control
class_name ScoreScreen
@export var messages: PackedStringArray
@export var message_label: Label
@export var names_column: Control
@export var scores_column: Control
@export var fade_time: float = 0.2
var active: bool = false
func render(scores: Dictionary):
clear()
if not visible:
message_label.text = messages[randi() % messages.size()]
for player_name in _sort_scores(scores):
var points = scores[player_name]
names_column.add_child(_make_label(str(player_name)))
scores_column.add_child(_make_label(str(points)))
func clear():
for child in names_column.get_children() + scores_column.get_children():
child.queue_free()
func _ready():
modulate.a = 0
visible = false
func _process(delta):
var current = modulate.a
var target = 1.0 if active else 0.0
modulate.a = move_toward(current, target, delta / fade_time)
visible = false if modulate.a < 0.05 else true
func _make_label(text: String) -> Label:
var label = Label.new()
label.text = text
label.size_flags_horizontal = SIZE_FILL
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
return label
func _sort_scores(scores: Dictionary) -> Array[String]:
var result: Array[String] = []
var points = scores.values()
points.sort()
points.reverse()
for score in points:
for player_name in scores:
if not result.has(player_name) and scores[player_name] == score:
result.push_back(player_name)
return result
@@ -0,0 +1 @@
uid://baylk3qu5fh2t
@@ -0,0 +1,7 @@
extends CheckButton
func _toggled(toggle):
if toggle:
DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_CONFINED)
else:
DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_VISIBLE)
@@ -0,0 +1 @@
uid://nvr3jr0bviin
@@ -0,0 +1,7 @@
extends CheckButton
func _toggled(toggle):
if toggle:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
@@ -0,0 +1 @@
uid://doidsx4hyb4gd
@@ -0,0 +1,9 @@
extends HSlider
func _value_changed(new_value):
var f = new_value / 100.0
var volume = lerp(-60, 0, f)
var mute = f < 0.01
AudioServer.set_bus_volume_db(0, volume)
AudioServer.set_bus_mute(0, mute)
@@ -0,0 +1 @@
uid://ghfyw0kak8vn
@@ -0,0 +1,7 @@
extends CheckButton
func _toggled(toggle):
if toggle:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE)
else:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
@@ -0,0 +1 @@
uid://gyiplrcda6mg