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