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

299 lines
7.8 KiB
GDScript

extends Node
class_name EconomyManager
const START_MONEY := 800
const MAX_MONEY := 16000
const WIN_REWARD := 3250
const LOSS_BASE := 1400
const LOSS_INCREMENT := 500
const LOSS_MAX := 3400
const PLANT_BONUS := 300
const KEVLAR_PRICE := 650
const DEFUSE_KIT_PRICE := 400
const FLASH_PRICE := 200
const FLASH_MAX := 2
const SMOKE_PRICE := 300
const SMOKE_MAX := 1
# peer_id -> money (server-authoritative)
var _money: Dictionary = {}
# peer_id -> loss streak count
var _loss_streak: Dictionary = {}
var team_manager: TeamManager
var round_manager: RoundManager
func _ready():
team_manager = $"../TeamManager"
round_manager = $"../RoundManager"
if round_manager:
round_manager.player_killed.connect(_on_player_killed)
round_manager.round_ended.connect(_on_round_ended)
var bomb_node := get_tree().current_scene.get_node_or_null("Network/Bomb") as Bomb
if bomb_node:
bomb_node.bomb_planted.connect(_on_bomb_planted.bind(bomb_node))
func init_player(peer_id: int):
_money[peer_id] = START_MONEY
_loss_streak[peer_id] = 0
if multiplayer.is_server():
_sync_money.rpc_id(peer_id, START_MONEY)
func get_money(peer_id: int) -> int:
return _money.get(peer_id, 0)
func add_money(peer_id: int, amount: int):
if not multiplayer.is_server():
return
_money[peer_id] = clampi(_money.get(peer_id, 0) + amount, 0, MAX_MONEY)
_sync_money.rpc_id(peer_id, _money[peer_id])
func try_buy_weapon(peer_id: int, weapon_id: int, avatar: CharacterBody3D) -> bool:
if not multiplayer.is_server():
return false
var weapon := WeaponRegistry.get_weapon(weapon_id)
if weapon == null:
return false
# Must be alive
if avatar.is_dead:
return false
# Must be in freeze time
if round_manager.state != RoundManager.RoundState.FREEZE_TIME:
return false
if NetworkTime.tick >= round_manager.freeze_end_tick:
return false
# Team restriction
if weapon.team >= 0:
var player_team := team_manager.get_team(peer_id)
if player_team != weapon.team:
return false
# Price check
var money := get_money(peer_id)
if money < weapon.price:
return false
# Already have same weapon = denied
var wm: WeaponManager = avatar.weapon_manager
if wm.inventory[weapon.slot] == weapon_id:
return false
# Buy: deduct money, give weapon
add_money(peer_id, -weapon.price)
wm.give_weapon(weapon_id)
return true
func try_buy_kevlar(peer_id: int, avatar: CharacterBody3D) -> bool:
if not multiplayer.is_server():
return false
if avatar.is_dead:
return false
if round_manager.state != RoundManager.RoundState.FREEZE_TIME:
return false
if NetworkTime.tick >= round_manager.freeze_end_tick:
return false
if avatar.armor >= 100:
return false
var money := get_money(peer_id)
if money < KEVLAR_PRICE:
return false
add_money(peer_id, -KEVLAR_PRICE)
avatar.armor = 100
return true
func try_buy_defuse_kit(peer_id: int, avatar: CharacterBody3D) -> bool:
if not multiplayer.is_server():
return false
if avatar.is_dead:
return false
if round_manager.state != RoundManager.RoundState.FREEZE_TIME:
return false
if NetworkTime.tick >= round_manager.freeze_end_tick:
return false
# Only CTs
if team_manager.get_team(peer_id) != TeamManager.Team.CT:
return false
if avatar.has_defuse_kit:
return false
var money := get_money(peer_id)
if money < DEFUSE_KIT_PRICE:
return false
add_money(peer_id, -DEFUSE_KIT_PRICE)
avatar.has_defuse_kit = true
return true
func try_buy_grenade(peer_id: int, grenade_type: int, avatar: CharacterBody3D) -> bool:
## grenade_type: 0=flash, 1=smoke
if not multiplayer.is_server():
return false
if avatar.is_dead:
return false
if round_manager.state != RoundManager.RoundState.FREEZE_TIME:
return false
if NetworkTime.tick >= round_manager.freeze_end_tick:
return false
var price := FLASH_PRICE if grenade_type == 0 else SMOKE_PRICE
var money := get_money(peer_id)
if money < price:
return false
var wm: WeaponManager = avatar.weapon_manager
if not wm.add_grenade(grenade_type):
return false # at max
add_money(peer_id, -price)
return true
func _on_bomb_planted(_site_name: String, bomb_node: Bomb):
if not multiplayer.is_server():
return
# Plant bonus: all Ts get $300
for peer_id in team_manager.get_team_members(TeamManager.Team.T):
add_money(peer_id, PLANT_BONUS)
func _on_player_killed(victim_id: int, killer_id: int, weapon_id: int, _kill_tick: int):
if not multiplayer.is_server():
return
if killer_id < 0:
return
var weapon := WeaponRegistry.get_weapon(weapon_id)
var reward := weapon.kill_reward if weapon else 300
add_money(killer_id, reward)
func _on_round_ended(winner: TeamManager.Team):
if not multiplayer.is_server():
return
for peer_id in team_manager.assignments:
var team := team_manager.get_team(peer_id)
if team == TeamManager.Team.NONE:
continue
if team == winner:
add_money(peer_id, WIN_REWARD)
_loss_streak[peer_id] = 0
else:
var streak: int = _loss_streak.get(peer_id, 0)
var loss_reward: int = mini(LOSS_BASE + streak * LOSS_INCREMENT, LOSS_MAX)
add_money(peer_id, loss_reward)
_loss_streak[peer_id] = streak + 1
func sync_to_peer(peer_id: int):
if not multiplayer.is_server():
return
var money: int = _money.get(peer_id, START_MONEY)
_sync_money.rpc_id(peer_id, money)
func remove_player(peer_id: int):
_money.erase(peer_id)
_loss_streak.erase(peer_id)
@rpc("authority", "reliable")
func _sync_money(amount: int) -> void:
var local_id := multiplayer.get_unique_id()
_money[local_id] = amount
# --- Public buy API (handles host-direct-call vs client-RPC) ---
func request_buy_weapon(weapon_id: int):
var peer_id := multiplayer.get_unique_id()
if multiplayer.is_server():
_do_buy_weapon(peer_id, weapon_id)
else:
_rpc_buy_weapon.rpc_id(get_multiplayer_authority(), weapon_id)
func request_buy_kevlar():
var peer_id := multiplayer.get_unique_id()
if multiplayer.is_server():
_do_buy_kevlar(peer_id)
else:
_rpc_buy_kevlar.rpc_id(get_multiplayer_authority())
func request_buy_grenade(grenade_type: int):
var peer_id := multiplayer.get_unique_id()
if multiplayer.is_server():
_do_buy_grenade(peer_id, grenade_type)
else:
_rpc_buy_grenade.rpc_id(get_multiplayer_authority(), grenade_type)
func request_buy_defuse_kit():
var peer_id := multiplayer.get_unique_id()
if multiplayer.is_server():
_do_buy_defuse_kit(peer_id)
else:
_rpc_buy_defuse_kit.rpc_id(get_multiplayer_authority())
# --- Internal: resolve peer_id to avatar and execute ---
func _get_avatar(peer_id: int) -> CharacterBody3D:
var spawner := get_tree().current_scene.get_node_or_null("Network/Player Spawner")
if spawner == null:
return null
return spawner.avatars.get(peer_id) as CharacterBody3D
func _do_buy_weapon(peer_id: int, weapon_id: int):
var avatar := _get_avatar(peer_id)
if avatar:
try_buy_weapon(peer_id, weapon_id, avatar)
func _do_buy_kevlar(peer_id: int):
var avatar := _get_avatar(peer_id)
if avatar:
try_buy_kevlar(peer_id, avatar)
func _do_buy_grenade(peer_id: int, grenade_type: int):
var avatar := _get_avatar(peer_id)
if avatar:
try_buy_grenade(peer_id, grenade_type, avatar)
func _do_buy_defuse_kit(peer_id: int):
var avatar := _get_avatar(peer_id)
if avatar:
try_buy_defuse_kit(peer_id, avatar)
# --- RPCs: client → server ---
@rpc("any_peer", "reliable")
func _rpc_buy_weapon(weapon_id: int) -> void:
if not multiplayer.is_server():
return
_do_buy_weapon(multiplayer.get_remote_sender_id(), weapon_id)
@rpc("any_peer", "reliable")
func _rpc_buy_kevlar() -> void:
if not multiplayer.is_server():
return
_do_buy_kevlar(multiplayer.get_remote_sender_id())
@rpc("any_peer", "reliable")
func _rpc_buy_grenade(grenade_type: int) -> void:
if not multiplayer.is_server():
return
_do_buy_grenade(multiplayer.get_remote_sender_id(), grenade_type)
@rpc("any_peer", "reliable")
func _rpc_buy_defuse_kit() -> void:
if not multiplayer.is_server():
return
_do_buy_defuse_kit(multiplayer.get_remote_sender_id())