## 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) ## class_name EconomyManager extends Node # --------------------------------------------------------------------------- # 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: TeamData.Team (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[TeamData.Team.COUNTER_TERRORIST] = 0 _consecutive_losses[TeamData.Team.TERRORIST] = 0 _consecutive_losses[TeamData.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: TeamData.Team, 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: TeamData.Team, 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: TeamData.Team) -> 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[TeamData.Team.COUNTER_TERRORIST] = 0 _consecutive_losses[TeamData.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")