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,113 @@
extends Control
class_name BombHUD
var bomb: Bomb
var _progress_bar: ProgressBar
var _timer_label: Label
var _status_label: Label
const COLOR_T := Color("#FFCC00")
const COLOR_CT := Color("#99CCFF")
const COLOR_DANGER := Color("#FF4444")
func _ready():
var scene_root := get_tree().current_scene
bomb = scene_root.get_node_or_null("Network/Bomb")
mouse_filter = MOUSE_FILTER_IGNORE
_build_ui()
func _build_ui():
# Progress bar for plant/defuse
_progress_bar = ProgressBar.new()
_progress_bar.name = "BombProgress"
_progress_bar.min_value = 0.0
_progress_bar.max_value = 1.0
_progress_bar.step = 0.01
_progress_bar.show_percentage = false
_progress_bar.mouse_filter = MOUSE_FILTER_IGNORE
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = Color(0.1, 0.1, 0.1, 0.7)
_progress_bar.add_theme_stylebox_override("background", bg_style)
var fill_style := StyleBoxFlat.new()
fill_style.bg_color = COLOR_T
_progress_bar.add_theme_stylebox_override("fill", fill_style)
add_child(_progress_bar)
_progress_bar.hide()
# Bomb timer label
_timer_label = Label.new()
_timer_label.name = "BombTimer"
_timer_label.add_theme_font_size_override("font_size", 24)
_timer_label.add_theme_color_override("font_color", COLOR_DANGER)
_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_timer_label.mouse_filter = MOUSE_FILTER_IGNORE
add_child(_timer_label)
_timer_label.hide()
# Status label (BOMB PLANTED, DEFUSING, etc.)
_status_label = Label.new()
_status_label.name = "BombStatus"
_status_label.add_theme_font_size_override("font_size", 16)
_status_label.add_theme_color_override("font_color", COLOR_DANGER)
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_status_label.mouse_filter = MOUSE_FILTER_IGNORE
add_child(_status_label)
_status_label.hide()
func _process(_delta: float):
if bomb == null:
return
var vp := get_viewport_rect().size
match bomb.state:
Bomb.BombState.PLANTING:
_progress_bar.value = bomb.get_plant_progress()
_progress_bar.size = Vector2(200, 12)
_progress_bar.position = Vector2(vp.x / 2.0 - 100, vp.y * 0.65)
_progress_bar.show()
_status_label.text = "PLANTING..."
_status_label.add_theme_color_override("font_color", COLOR_T)
_status_label.size = Vector2(200, 24)
_status_label.position = Vector2(vp.x / 2.0 - 100, vp.y * 0.65 - 28)
_status_label.show()
_timer_label.hide()
Bomb.BombState.PLANTED:
_progress_bar.hide()
var remaining := bomb.get_bomb_time_remaining()
_timer_label.text = "%.1f" % remaining
_timer_label.add_theme_color_override("font_color", COLOR_DANGER if remaining < 10.0 else COLOR_T)
_timer_label.size = Vector2(100, 32)
_timer_label.position = Vector2(vp.x / 2.0 - 50, vp.y * 0.35)
_timer_label.show()
_status_label.text = "BOMB PLANTED"
_status_label.add_theme_color_override("font_color", COLOR_DANGER)
_status_label.size = Vector2(200, 24)
_status_label.position = Vector2(vp.x / 2.0 - 100, vp.y * 0.35 - 28)
_status_label.show()
Bomb.BombState.DEFUSING:
_progress_bar.value = bomb.get_defuse_progress()
_progress_bar.size = Vector2(200, 12)
_progress_bar.position = Vector2(vp.x / 2.0 - 100, vp.y * 0.65)
_progress_bar.show()
var remaining := bomb.get_bomb_time_remaining()
_timer_label.text = "%.1f" % remaining
_timer_label.add_theme_color_override("font_color", COLOR_DANGER if remaining < 10.0 else COLOR_T)
_timer_label.size = Vector2(100, 32)
_timer_label.position = Vector2(vp.x / 2.0 - 50, vp.y * 0.35)
_timer_label.show()
_status_label.text = "DEFUSING..."
_status_label.add_theme_color_override("font_color", COLOR_CT)
_status_label.size = Vector2(200, 24)
_status_label.position = Vector2(vp.x / 2.0 - 100, vp.y * 0.65 - 28)
_status_label.show()
_:
_progress_bar.hide()
_timer_label.hide()
_status_label.hide()
@@ -0,0 +1 @@
uid://ct2uyvqjqfe5n
@@ -0,0 +1,229 @@
extends Control
class_name BuyMenu
var economy_manager: EconomyManager
var team_manager: TeamManager
var round_manager: RoundManager
var _panel: PanelContainer
var _vbox: VBoxContainer
var _is_open := false
func _ready():
var scene_root := get_tree().current_scene
economy_manager = scene_root.get_node_or_null("Network/EconomyManager")
team_manager = scene_root.get_node_or_null("Network/TeamManager")
round_manager = scene_root.get_node_or_null("Network/RoundManager")
mouse_filter = MOUSE_FILTER_IGNORE
_build_panel()
hide()
func _build_panel():
_panel = PanelContainer.new()
_panel.name = "BuyPanel"
var style := StyleBoxFlat.new()
style.bg_color = Color(0.05, 0.08, 0.12, 0.92)
style.border_color = Color(0.4, 0.4, 0.4, 0.6)
style.set_border_width_all(1)
style.set_content_margin_all(16)
_panel.add_theme_stylebox_override("panel", style)
_vbox = VBoxContainer.new()
_vbox.name = "BuyVBox"
_panel.add_child(_vbox)
add_child(_panel)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("buy_menu"):
if _is_open:
_close()
elif _can_buy():
_open()
if _is_open and event.is_action_pressed("escape"):
_close()
# Number keys for quick buy
if _is_open and event is InputEventKey and event.pressed:
_handle_buy_key(event)
func _process(_delta: float):
if not _is_open:
return
# Close if no longer in freeze time
if not _can_buy():
_close()
return
# Position panel
var vp := get_viewport_rect().size
_panel.size = Vector2(340, 400)
_panel.position = Vector2(vp.x / 2.0 - 170, vp.y / 2.0 - 200)
func _can_buy() -> bool:
if round_manager == null:
return false
# Tick-based check only — deterministic, no state-transition race
if round_manager.freeze_end_tick < 0:
return false
if NetworkTime.tick >= round_manager.freeze_end_tick:
return false
# Must be alive (spectators/dead can't buy)
var local_id := multiplayer.get_unique_id()
if not round_manager.is_player_alive(local_id):
return false
return true
func _open():
_is_open = true
_rebuild_menu()
show()
func _close():
_is_open = false
hide()
func _rebuild_menu():
for child in _vbox.get_children():
child.queue_free()
var local_id := multiplayer.get_unique_id()
var money := economy_manager.get_money(local_id) if economy_manager else 0
var team := team_manager.get_team(local_id) if team_manager else TeamManager.Team.T
# Title
var title := Label.new()
title.text = "BUY MENU - $%d" % money
title.add_theme_font_size_override("font_size", 18)
title.add_theme_color_override("font_color", Color("#FFDD44"))
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_vbox.add_child(title)
var sep := HSeparator.new()
_vbox.add_child(sep)
# Pistols
_add_section("PISTOLS")
if team == TeamManager.Team.T:
_add_weapon_row("1", WeaponRegistry.GLOCK, money)
else:
_add_weapon_row("1", WeaponRegistry.USP, money)
# Rifles
_add_section("RIFLES")
if team == TeamManager.Team.T:
_add_weapon_row("2", WeaponRegistry.AK47, money)
else:
_add_weapon_row("2", WeaponRegistry.M4A1, money)
_add_weapon_row("3", WeaponRegistry.AWP, money)
# Grenades
_add_section("GRENADES")
_add_equipment_row("4", "Flashbang", EconomyManager.FLASH_PRICE, money)
_add_equipment_row("5", "Smoke", EconomyManager.SMOKE_PRICE, money)
# Equipment
_add_section("EQUIPMENT")
_add_equipment_row("6", "Kevlar", EconomyManager.KEVLAR_PRICE, money)
if team == TeamManager.Team.CT:
_add_equipment_row("7", "Defuse Kit", EconomyManager.DEFUSE_KIT_PRICE, money)
func _add_section(text: String):
var spacer := Control.new()
spacer.custom_minimum_size.y = 8
_vbox.add_child(spacer)
var lbl := Label.new()
lbl.text = text
lbl.add_theme_font_size_override("font_size", 12)
lbl.add_theme_color_override("font_color", Color("#AAAAAA"))
_vbox.add_child(lbl)
func _add_weapon_row(key: String, weapon_id: int, money: int):
var weapon := WeaponRegistry.get_weapon(weapon_id)
if weapon == null:
return
var hbox := HBoxContainer.new()
var can_afford := money >= weapon.price
var key_lbl := Label.new()
key_lbl.text = "[%s]" % key
key_lbl.add_theme_font_size_override("font_size", 14)
key_lbl.add_theme_color_override("font_color", Color.YELLOW if can_afford else Color.DARK_GRAY)
key_lbl.custom_minimum_size.x = 40
hbox.add_child(key_lbl)
var name_lbl := Label.new()
name_lbl.text = weapon.weapon_name
name_lbl.add_theme_font_size_override("font_size", 14)
name_lbl.add_theme_color_override("font_color", Color.WHITE if can_afford else Color.DARK_GRAY)
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(name_lbl)
var price_lbl := Label.new()
price_lbl.text = "$%d" % weapon.price
price_lbl.add_theme_font_size_override("font_size", 14)
price_lbl.add_theme_color_override("font_color", Color("#00FF00") if can_afford else Color.DARK_RED)
price_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
hbox.add_child(price_lbl)
_vbox.add_child(hbox)
func _add_equipment_row(key: String, item_name: String, price: int, money: int):
var hbox := HBoxContainer.new()
var can_afford := money >= price
var key_lbl := Label.new()
key_lbl.text = "[%s]" % key
key_lbl.add_theme_font_size_override("font_size", 14)
key_lbl.add_theme_color_override("font_color", Color.YELLOW if can_afford else Color.DARK_GRAY)
key_lbl.custom_minimum_size.x = 40
hbox.add_child(key_lbl)
var name_lbl := Label.new()
name_lbl.text = item_name
name_lbl.add_theme_font_size_override("font_size", 14)
name_lbl.add_theme_color_override("font_color", Color.WHITE if can_afford else Color.DARK_GRAY)
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(name_lbl)
var price_lbl := Label.new()
price_lbl.text = "$%d" % price
price_lbl.add_theme_font_size_override("font_size", 14)
price_lbl.add_theme_color_override("font_color", Color("#00FF00") if can_afford else Color.DARK_RED)
price_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
hbox.add_child(price_lbl)
_vbox.add_child(hbox)
func _handle_buy_key(event: InputEventKey):
if economy_manager == null:
return
var local_id := multiplayer.get_unique_id()
var team := team_manager.get_team(local_id) if team_manager else TeamManager.Team.T
match event.physical_keycode:
KEY_1:
var pistol_id := WeaponRegistry.GLOCK if team == TeamManager.Team.T else WeaponRegistry.USP
economy_manager.request_buy_weapon(pistol_id)
KEY_2:
var rifle_id := WeaponRegistry.AK47 if team == TeamManager.Team.T else WeaponRegistry.M4A1
economy_manager.request_buy_weapon(rifle_id)
KEY_3:
economy_manager.request_buy_weapon(WeaponRegistry.AWP)
KEY_4:
economy_manager.request_buy_grenade(0) # Flash
KEY_5:
economy_manager.request_buy_grenade(1) # Smoke
KEY_6:
economy_manager.request_buy_kevlar()
KEY_7:
if team == TeamManager.Team.CT:
economy_manager.request_buy_defuse_kit()
# Rebuild to update money display
call_deferred("_rebuild_menu")
@@ -0,0 +1 @@
uid://bui6lde7owfhl
@@ -0,0 +1,31 @@
@tool
class_name CrossHair extends Node2D
## Places a crosshair in the center of the screen
@export var crosshair_sprite: Texture2D
@export var crosshair_scale: Vector2 = Vector2(1, 1)
var sprite2d: Sprite2D = Sprite2D.new()
func _ready() -> void:
if Engine.is_editor_hint():
return
get_tree().get_root().size_changed.connect(func(): _on_window_resized(DisplayServer.window_get_size()))
_on_window_resized(DisplayServer.window_get_size())
func _enter_tree() -> void:
if has_node("Sprite2D"): return
sprite2d.name = "Sprite2D"
sprite2d.set_texture(crosshair_sprite)
sprite2d.scale = crosshair_scale;
add_child(sprite2d)
func _on_window_resized(new_size: Vector2) -> void:
if Engine.is_editor_hint():
return
sprite2d.position = new_size / 2
sprite2d.scale = crosshair_scale
@@ -0,0 +1 @@
uid://68ntxlb14fan
@@ -0,0 +1,6 @@
extends ProgressBar
@export var player: CharacterBody3D
func _process(delta: float):
value = player.health
@@ -0,0 +1 @@
uid://cf2qoxmc6v72y
@@ -0,0 +1,71 @@
@tool
class_name Projection3D extends Node3D
## Whatever 2D nodes are added to Projection3D are projected onto a plane
## Width of the projection
@export var projection_size: Vector2 = Vector2(1.0, 0.66)
## Resolution of the projection
@export var resolution_size: Vector2 = Vector2(640, 480)
## Billboard mode (always face the player)
@export var billboard: bool = true
## Render in front of everything else
@export var always_on_top: bool = false
## No lighting or shading
@export var disable_shading: bool = true
## Emitted when the Node is fully constructed
signal on_constructed
var plane_mesh = PlaneMesh.new()
var viewport = SubViewport.new()
var original_children: Array[Node] = []
## Set the size of the projection and resolution
func set_size(new_projection_size: Vector2, new_resolution_size: Vector2):
plane_mesh.size = new_projection_size
viewport.size = new_resolution_size
func _ready():
if Engine.is_editor_hint():
return
var children = get_children()
original_children = children
var mesh = MeshInstance3D.new()
plane_mesh.size = projection_size
plane_mesh.orientation = PlaneMesh.FACE_Z
mesh.gi_mode = GeometryInstance3D.GI_MODE_DISABLED
mesh.cast_shadow = false
mesh.mesh = plane_mesh
add_child(mesh)
var viewport_container = SubViewportContainer.new()
viewport_container.clip_children = CanvasItem.CLIP_CHILDREN_ONLY
viewport_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
viewport.size = resolution_size
# TODO: Figure out why the progressbar is slightly transparent.
# It may not be a material issue but a rendering one (cleared one frame but drawn the next)
viewport.transparent_bg = true
viewport.disable_3d = true
viewport.handle_input_locally = false
viewport_container.add_child(viewport)
add_child(viewport_container)
for child in children:
child.reparent(viewport)
var material = StandardMaterial3D.new()
material.albedo_texture = viewport.get_texture()
material.transparency = BaseMaterial3D.TRANSPARENCY_MAX
if billboard:
material.billboard_mode = BaseMaterial3D.BILLBOARD_FIXED_Y
material.billboard_keep_scale = true
if disable_shading:
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.disable_ambient_light = true
material.no_depth_test = always_on_top
plane_mesh.material = material
on_constructed.emit()
@@ -0,0 +1 @@
uid://dbrlsg5lrpgjb
@@ -0,0 +1,312 @@
extends Control
class_name RoundHUD
var round_manager: RoundManager
var team_manager: TeamManager
@onready var timer_label := $TimerLabel as Label
@onready var score_label := $ScoreLabel as Label
@onready var state_label := $StateLabel as Label
@onready var scoreboard_panel := $ScoreboardPanel as PanelContainer
var scoreboard_vbox: VBoxContainer
var _scoreboard_rebuild_timer := 0.0
# Ammo and money display
var ammo_label: Label
var money_label: Label
const COLOR_T := Color("#FFCC00")
const COLOR_CT := Color("#99CCFF")
const COLOR_DEAD := Color("#884444")
const COLOR_GREEN := Color("#00FF00")
const COLOR_HEADER := Color("#AAAAAA")
const COLOR_AMMO := Color("#FFDD44")
func _ready():
var scene_root := get_tree().current_scene
round_manager = scene_root.get_node_or_null("Network/RoundManager")
team_manager = scene_root.get_node_or_null("Network/TeamManager")
_setup_styles()
scoreboard_panel.hide()
state_label.hide()
if round_manager:
round_manager.round_state_changed.connect(_on_state_changed)
round_manager.round_ended.connect(_on_round_ended)
func _setup_styles():
mouse_filter = MOUSE_FILTER_IGNORE
# Timer
timer_label.add_theme_font_size_override("font_size", 20)
timer_label.add_theme_color_override("font_color", COLOR_GREEN)
timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
timer_label.mouse_filter = MOUSE_FILTER_IGNORE
# Score
score_label.add_theme_font_size_override("font_size", 16)
score_label.add_theme_color_override("font_color", Color.WHITE)
score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
score_label.mouse_filter = MOUSE_FILTER_IGNORE
# State
state_label.add_theme_font_size_override("font_size", 36)
state_label.add_theme_color_override("font_color", Color.YELLOW)
state_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
state_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
state_label.mouse_filter = MOUSE_FILTER_IGNORE
# Scoreboard panel
var panel_style := StyleBoxFlat.new()
panel_style.bg_color = Color(0.05, 0.05, 0.1, 0.88)
panel_style.border_color = Color(0.4, 0.4, 0.4, 0.6)
panel_style.set_border_width_all(1)
panel_style.set_corner_radius_all(0)
panel_style.set_content_margin_all(12)
scoreboard_panel.add_theme_stylebox_override("panel", panel_style)
var old_label := scoreboard_panel.get_node_or_null("Label")
if old_label:
old_label.queue_free()
scoreboard_vbox = VBoxContainer.new()
scoreboard_vbox.name = "ScoreboardVBox"
scoreboard_vbox.mouse_filter = MOUSE_FILTER_IGNORE
scoreboard_panel.add_child(scoreboard_vbox)
# Ammo label
ammo_label = Label.new()
ammo_label.name = "AmmoLabel"
ammo_label.add_theme_font_size_override("font_size", 22)
ammo_label.add_theme_color_override("font_color", COLOR_AMMO)
ammo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
ammo_label.mouse_filter = MOUSE_FILTER_IGNORE
add_child(ammo_label)
# Money label
money_label = Label.new()
money_label.name = "MoneyLabel"
money_label.add_theme_font_size_override("font_size", 18)
money_label.add_theme_color_override("font_color", Color("#00FF00"))
money_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
money_label.mouse_filter = MOUSE_FILTER_IGNORE
add_child(money_label)
func _process(_delta: float):
if round_manager == null:
return
var vp := get_viewport_rect().size
# Position elements
timer_label.size = Vector2(100, 28)
timer_label.position = Vector2(vp.x / 2.0 - 50, vp.y - 34)
score_label.size = Vector2(200, 24)
score_label.position = Vector2(vp.x / 2.0 - 100, 6)
state_label.size = Vector2(500, 60)
state_label.position = Vector2(vp.x / 2.0 - 250, vp.y / 2.0 - 30)
scoreboard_panel.size = Vector2(560, 420)
scoreboard_panel.position = Vector2(vp.x / 2.0 - 280, vp.y / 2.0 - 210)
# Ammo label — bottom right
ammo_label.size = Vector2(200, 30)
ammo_label.position = Vector2(vp.x - 220, vp.y - 40)
# Money label — bottom left
money_label.size = Vector2(200, 28)
money_label.position = Vector2(20, vp.y - 40)
# Timer content
var time_left: float = maxf(0.0, round_manager.timer)
var minutes := int(time_left) / 60
var seconds := int(time_left) % 60
timer_label.text = "%d:%02d" % [minutes, seconds]
if round_manager.state == RoundManager.RoundState.ACTIVE and time_left <= 10.0:
timer_label.add_theme_color_override("font_color", Color.RED)
else:
timer_label.add_theme_color_override("font_color", COLOR_GREEN)
# Score content
var t_score := int(round_manager.score.get(TeamManager.Team.T, 0))
var ct_score := int(round_manager.score.get(TeamManager.Team.CT, 0))
score_label.text = "T %d : %d CT" % [t_score, ct_score]
# Ammo + money display
_update_ammo_display()
_update_money_display()
# Scoreboard (Tab)
if Input.is_action_pressed("scoreboard"):
_scoreboard_rebuild_timer -= _delta
if _scoreboard_rebuild_timer <= 0:
_rebuild_scoreboard()
_scoreboard_rebuild_timer = 0.5
scoreboard_panel.show()
else:
scoreboard_panel.hide()
_scoreboard_rebuild_timer = 0.0
func _update_ammo_display():
var local_player: CharacterBody3D = _find_local_player()
if local_player == null or local_player.weapon_manager == null:
ammo_label.text = ""
return
var wm: WeaponManager = local_player.weapon_manager
var weapon := wm.get_active_weapon()
if weapon == null:
ammo_label.text = ""
return
if weapon.fire_mode == WeaponData.FireMode.MELEE:
ammo_label.text = weapon.weapon_name
else:
ammo_label.text = "%s %d / %d" % [weapon.weapon_name, wm.ammo[wm.active_slot], wm.reserve[wm.active_slot]]
func _update_money_display():
var economy := get_tree().current_scene.get_node_or_null("Network/EconomyManager") as EconomyManager
if economy == null:
money_label.text = ""
return
var local_id := multiplayer.get_unique_id()
var money := economy.get_money(local_id)
money_label.text = "$%d" % money
func _find_local_player() -> CharacterBody3D:
var players_container := get_tree().current_scene.get_node_or_null("Network/Player Spawner")
if players_container == null:
return null
for child in players_container.get_children():
if child is CharacterBody3D:
var input_node := child.get_node_or_null("Input")
if input_node and input_node.is_multiplayer_authority():
return child
return null
func _on_state_changed(new_state: RoundManager.RoundState):
match new_state:
RoundManager.RoundState.FREEZE_TIME:
state_label.text = "FREEZE TIME"
state_label.add_theme_color_override("font_color", Color.YELLOW)
state_label.show()
RoundManager.RoundState.ACTIVE:
state_label.hide()
RoundManager.RoundState.ROUND_END:
pass
func _on_round_ended(winner: TeamManager.Team):
var winner_name := "ROUND OVER"
var color := Color.YELLOW
if team_manager:
winner_name = team_manager.get_team_name(winner).to_upper() + " WIN"
color = COLOR_T if winner == TeamManager.Team.T else COLOR_CT
state_label.text = winner_name
state_label.add_theme_color_override("font_color", color)
state_label.show()
# --- Scoreboard building ---
func _rebuild_scoreboard():
if team_manager == null or scoreboard_vbox == null:
return
for child in scoreboard_vbox.get_children():
child.queue_free()
var t_members := team_manager.get_team_members(TeamManager.Team.T)
var ct_members := team_manager.get_team_members(TeamManager.Team.CT)
var t_score := int(round_manager.score.get(TeamManager.Team.T, 0))
var ct_score := int(round_manager.score.get(TeamManager.Team.CT, 0))
_add_label_centered("Counter-Strike", Color.WHITE, 20)
_add_label_centered("Round %d" % round_manager.round_number, Color.GRAY, 12)
_add_spacer(8)
_add_row(["Name", "Score", "Deaths", "Latency"], COLOR_HEADER, 13)
_add_separator()
_add_team_header("Terrorists", t_members.size(), t_score, COLOR_T)
for peer_id in t_members:
var alive := round_manager.is_player_alive(peer_id)
var c := COLOR_T if alive else COLOR_DEAD
var status := "ALIVE" if alive else "DEAD"
_add_row([" Player #%d" % peer_id, "0", "0", status], c, 13)
_add_spacer(4)
_add_separator()
_add_team_header("Counter-Terrorists", ct_members.size(), ct_score, COLOR_CT)
for peer_id in ct_members:
var alive := round_manager.is_player_alive(peer_id)
var c := COLOR_CT if alive else COLOR_DEAD
var status := "ALIVE" if alive else "DEAD"
_add_row([" Player #%d" % peer_id, "0", "0", status], c, 13)
func _add_label_centered(text: String, color: Color, font_size: int):
var lbl := Label.new()
lbl.text = text
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.add_theme_color_override("font_color", color)
lbl.add_theme_font_size_override("font_size", font_size)
lbl.mouse_filter = MOUSE_FILTER_IGNORE
scoreboard_vbox.add_child(lbl)
func _add_team_header(team_name: String, player_count: int, team_score: int, color: Color):
var hbox := HBoxContainer.new()
hbox.mouse_filter = MOUSE_FILTER_IGNORE
var name_lbl := Label.new()
name_lbl.text = "%s - %d players" % [team_name, player_count]
name_lbl.add_theme_color_override("font_color", color)
name_lbl.add_theme_font_size_override("font_size", 14)
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
name_lbl.mouse_filter = MOUSE_FILTER_IGNORE
hbox.add_child(name_lbl)
var score_lbl := Label.new()
score_lbl.text = str(team_score)
score_lbl.add_theme_color_override("font_color", Color.WHITE)
score_lbl.add_theme_font_size_override("font_size", 14)
score_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
score_lbl.mouse_filter = MOUSE_FILTER_IGNORE
hbox.add_child(score_lbl)
scoreboard_vbox.add_child(hbox)
func _add_row(columns: Array, color: Color, font_size: int):
var hbox := HBoxContainer.new()
hbox.mouse_filter = MOUSE_FILTER_IGNORE
var ratios := [5, 2, 2, 2]
for i in columns.size():
var lbl := Label.new()
lbl.text = columns[i]
lbl.add_theme_color_override("font_color", color)
lbl.add_theme_font_size_override("font_size", font_size)
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
lbl.size_flags_stretch_ratio = ratios[i] if i < ratios.size() else 1
lbl.mouse_filter = MOUSE_FILTER_IGNORE
if i > 0:
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hbox.add_child(lbl)
scoreboard_vbox.add_child(hbox)
func _add_separator():
var sep := HSeparator.new()
sep.mouse_filter = MOUSE_FILTER_IGNORE
sep.add_theme_color_override("separator", Color(0.4, 0.4, 0.4))
scoreboard_vbox.add_child(sep)
func _add_spacer(height: int):
var spacer := Control.new()
spacer.custom_minimum_size.y = height
spacer.mouse_filter = MOUSE_FILTER_IGNORE
scoreboard_vbox.add_child(spacer)
@@ -0,0 +1 @@
uid://1wur66am5ly8
@@ -0,0 +1,34 @@
@tool
class_name WindowSizeConnector extends Node
## Allows you to update node size to the window size
## The nodes to apply the new size to
@export var target_nodes: Array[Node] = []: set = _target_nodes_changed
## The percentage of the window size to fill (1.0 for full size)
@export var size_multiplier: float = 1.0
signal window_size_updated(new_size: Vector2)
func _ready() -> void:
if Engine.is_editor_hint():
return
get_tree().get_root().size_changed.connect(func(): _on_window_resized(DisplayServer.window_get_size()))
_on_window_resized(DisplayServer.window_get_size())
func _on_window_resized(new_size: Vector2) -> void:
for node in target_nodes:
if node and "size" in node:
node.set_deferred("size", new_size * size_multiplier)
window_size_updated.emit(new_size)
func _target_nodes_changed(new_value: Array[Node]):
target_nodes = new_value
if Engine.is_editor_hint():
return
_on_window_resized(Vector2(
ProjectSettings.get_setting(&"display/window/size/viewport_width"),
ProjectSettings.get_setting(&"display/window/size/viewport_height")
))
@@ -0,0 +1 @@
uid://bpp3g8xk4qngo