Files
tactical-shooter/server/scripts/economy/economy_manager.gd
T
shawn 4a5264c5b0 Fix headless class_name dependencies
- Replaced TeamData.Team type hints with int in all scripts
- Added explicit preloads for headless mode class resolution
- Created stub LagCompensation and DamageProcessor scripts
- Fixed PluginManager, SpawnManager, EconomyManager, BuyZone,
  RoundManager, BuyMenuHandler, BombObjective class_name references
- Updated server config to match ServerConfig.gd format

Gray screen root cause: server scripts failed to parse in headless
mode due to Godot 4 class_name loading order (Resource after Node),
leaving server_main.gd non-functional — accepted connections but
never spawned players.
2026-07-02 10:09:28 -04:00

238 lines
9.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## EconomyManager — Per-player money tracking and earnings.
##
## Tracks each player's money balance, awards income for kills/rounds/objectives,
## and validates affordability when spending. Supports escalating loss bonuses
## so losing teams catch up over consecutive round losses.
##
## Usage (add as child of GameServer):
## var em = EconomyManager.new()
## add_child(em)
##
## # Register/unregister as players join/leave
## em.register_player(player_id)
##
## # Award income
## em.award_kill_reward(player_id)
## em.award_round_win(team, team_player_ids)
## em.award_round_loss(team, team_player_ids)
##
## # Spend
## if em.spend_money(player_id, cost):
## give_weapon(player_id, weapon_id)
##
## Signals:
## money_changed(player_id, old_amount, new_amount, reason)
##
extends Node
class_name EconomyManager
const _td_eco = preload("res://scripts/teams/team_data.gd")
# ---------------------------------------------------------------------------
# Weapon costs
# ---------------------------------------------------------------------------
## Static mapping of weapon_id → purchase cost in dollars.
## Used by both EconomyManager and BuyMenuHandler.
const WEAPON_COSTS: Dictionary = {
"pistol": 500,
"smg": 1200,
"rifle": 2700,
"shotgun": 1900,
}
# ---------------------------------------------------------------------------
# Economy constants
# ---------------------------------------------------------------------------
## Starting money for every player at the beginning of a match or after reset.
const STARTING_MONEY: int = 800
## Per-player bonus when their team wins a round.
const ROUND_WIN_BONUS: int = 3250
## Base per-player bonus when their team loses a round.
const ROUND_LOSS_BASE: int = 1900
## Additional per-consecutive-loss bonus (additive per streak).
const LOSS_STREAK_BONUS: int = 500
## Maximum additional loss bonus (cap on loss_streak_bonus × streak).
## Total loss payout = ROUND_LOSS_BASE + min(LOSS_STREAK_BONUS * streak, MAX_LOSS_BONUS_ADDITIONAL)
const MAX_LOSS_BONUS_ADDITIONAL: int = 1000 # max total = 1900 + 1000 = 2900
## Kill reward (awarded to the shooter).
const KILL_REWARD: int = 300
## Bomb plant reward (T-side).
const BOMB_PLANT_REWARD: int = 800
## Bomb defuse reward (CT-side).
const BOMB_DEFUSE_REWARD: int = 800
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
## Emitted whenever a player's money balance changes.
## reason: String describing the cause ("kill", "round_win", "round_loss",
## "bomb_plant", "bomb_defuse", "spend", "reset", "register", "admin")
signal money_changed(player_id: int, old_amount: int, new_amount: int, reason: String)
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
## Per-player money balance indexed by player_id (int).
var _money: Dictionary = {} # player_id (int) → balance (int)
## Consecutive round losses per team, used to escalate the loss bonus.
## Key: int (int), Value: consecutive losses (int)
var _consecutive_losses: Dictionary = {}
## Total money ever earned per player (for stats). Not reset on round restart.
var _total_earned: Dictionary = {} # player_id (int) → total (int)
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _ready() -> void:
_consecutive_losses[_td_eco.Team.COUNTER_TERRORIST] = 0
_consecutive_losses[_td_eco.Team.TERRORIST] = 0
_consecutive_losses[_td_eco.Team.SPECTATOR] = 0
# ---------------------------------------------------------------------------
# Public API — Money queries
# ---------------------------------------------------------------------------
## Return the player's current money balance, or 0 if not tracked.
func get_money(player_id: int) -> int:
return _money.get(player_id, 0)
## Set a player's money directly (admin override / system init).
## Emits money_changed with the given reason.
func set_money(player_id: int, amount: int, reason: String = "admin") -> void:
var clamped: int = maxi(0, amount)
var old: int = _money.get(player_id, 0)
_money[player_id] = clamped
money_changed.emit(player_id, old, clamped, reason)
## Register a new player with starting money.
func register_player(player_id: int) -> void:
var old: int = _money.get(player_id, 0)
_money[player_id] = STARTING_MONEY
if not _total_earned.has(player_id):
_total_earned[player_id] = 0
money_changed.emit(player_id, old, STARTING_MONEY, "register")
## Remove a player from money tracking (on disconnect).
func unregister_player(player_id: int) -> void:
_money.erase(player_id)
## Add money to a player's balance.
func add_money(player_id: int, amount: int, reason: String) -> void:
if amount <= 0:
return
var old: int = _money.get(player_id, 0)
var new_balance: int = old + amount
_money[player_id] = new_balance
_total_earned[player_id] = _total_earned.get(player_id, 0) + amount
money_changed.emit(player_id, old, new_balance, reason)
## Deduct money from a player. Returns true if the player could afford it.
## The purchase is only deducted if the balance is sufficient.
func spend_money(player_id: int, amount: int) -> bool:
var current: int = _money.get(player_id, 0)
if current < amount:
return false
var old: int = current
_money[player_id] = current - amount
money_changed.emit(player_id, old, _money[player_id], "spend")
return true
## Returns true if the player has at least `amount` money.
func can_afford(player_id: int, amount: int) -> bool:
return _money.get(player_id, 0) >= amount
# ---------------------------------------------------------------------------
# Static helpers — weapon costs
# ---------------------------------------------------------------------------
## Return the cost of a weapon by weapon_id. Returns -1 if not purchasable.
static func get_weapon_cost(weapon_id: String) -> int:
return WEAPON_COSTS.get(weapon_id, -1)
## Return a dictionary of weapon_id → cost for all purchasable weapons.
static func get_purchasable_weapons() -> Dictionary:
return WEAPON_COSTS.duplicate()
# ---------------------------------------------------------------------------
# Public API — Earnings
# ---------------------------------------------------------------------------
## Award the kill reward to a player.
func award_kill_reward(player_id: int) -> void:
add_money(player_id, KILL_REWARD, "kill")
## Award the round-win bonus to every player on the winning team.
## Resets the loss streak for that team.
func award_round_win(team: int, player_ids: Array[int]) -> void:
for pid in player_ids:
add_money(pid, ROUND_WIN_BONUS, "round_win")
_consecutive_losses[team] = 0
## Award the round-loss bonus to every player on the losing team.
## Escalates based on consecutive losses (loss streak).
func award_round_loss(team: int, player_ids: Array[int]) -> void:
var streak: int = _consecutive_losses.get(team, 0)
var bonus: int = mini(streak * LOSS_STREAK_BONUS, MAX_LOSS_BONUS_ADDITIONAL)
var total_bonus: int = ROUND_LOSS_BASE + bonus
_consecutive_losses[team] = streak + 1
for pid in player_ids:
add_money(pid, total_bonus, "round_loss")
## Award the bomb-plant reward to the planter.
func award_bomb_plant(player_id: int) -> void:
add_money(player_id, BOMB_PLANT_REWARD, "bomb_plant")
## Award the bomb-defuse reward to the defuser.
func award_bomb_defuse(player_id: int) -> void:
add_money(player_id, BOMB_DEFUSE_REWARD, "bomb_defuse")
## Return the total loss bonus the given team would receive on their
## next round loss, accounting for the current loss streak.
func get_loss_bonus_for_team(team: int) -> int:
var streak: int = _consecutive_losses.get(team, 0)
var bonus: int = mini(streak * LOSS_STREAK_BONUS, MAX_LOSS_BONUS_ADDITIONAL)
return ROUND_LOSS_BASE + bonus
# ---------------------------------------------------------------------------
# Public API — Stats
# ---------------------------------------------------------------------------
## Return the total amount of money a player has ever earned.
func get_total_earned(player_id: int) -> int:
return _total_earned.get(player_id, 0)
# ---------------------------------------------------------------------------
# Public API — Reset
# ---------------------------------------------------------------------------
## Reset all tracked players' money to STARTING_MONEY.
## If player_ids is provided, only resets those players; otherwise resets all.
func reset_economy(player_ids: Array[int] = []) -> void:
if player_ids.is_empty():
for pid in _money.keys():
var old: int = _money[pid]
_money[pid] = STARTING_MONEY
money_changed.emit(pid, old, STARTING_MONEY, "reset")
else:
for pid in player_ids:
var old: int = _money.get(pid, 0)
_money[pid] = STARTING_MONEY
money_changed.emit(pid, old, STARTING_MONEY, "reset")
_consecutive_losses[_td_eco.Team.COUNTER_TERRORIST] = 0
_consecutive_losses[_td_eco.Team.TERRORIST] = 0
## Reset exactly one player's money to STARTING_MONEY.
func reset_player_money(player_id: int) -> void:
var old: int = _money.get(player_id, 0)
_money[player_id] = STARTING_MONEY
money_changed.emit(player_id, old, STARTING_MONEY, "reset")