Files
shawn b0c83af092 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>
2026-07-02 20:55:20 -04:00

313 lines
10 KiB
GDScript

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)