Files
tactical-shooter/examples/multiplayer-fps/scripts/ui/buy_menu.gd
T
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

230 lines
6.8 KiB
GDScript

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")