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:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user