4a5264c5b0
- 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.
238 lines
9.4 KiB
GDScript
238 lines
9.4 KiB
GDScript
## 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")
|