class_name PlayerStats extends RefCounted ## A player's combat numbers, computed from the upgrades they hold. ## ## Derived, never stored: the character record keeps the list of upgrade ids and ## this is rebuilt from it. One source of truth means a saved stat can never ## disagree with the upgrades that produced it -- the same reasoning that makes ## [Progression] derive level from experience rather than storing both. ## The damage formula, settled with the user: ## ## base x (1 + sum of additive) x product of multiplicative ## ## The +5% every upgrade carries, Spread's −10%, Doubleshot's −50% and Glass ## Cannon's +100% all pool into the additive term. Sniper's 2x multiplies the ## result, which is why it stays worth taking however many percentages have ## already piled up -- and why two Snipers is 4x rather than +200%. var damage: int = SimConfig.PLAYER_BULLET_DAMAGE var fire_cooldown: int = SimConfig.PLAYER_FIRE_COOLDOWN var bullet_speed: float = SimConfig.PLAYER_BULLET_SPEED ## Multiplies the level's maximum health. See SimPlayer.recompute_max_hp(). var max_hp_mult: float = 1.0 var side_shots: int = 0 var parallel_shots: int = 0 var split_charges: int = 0 var poison_fraction: float = 0.0 var erase_chance: float = 0.0 ## How many upgrades produced these numbers, duplicates included. Only used for ## display, but it is the number a player counts. var upgrade_count: int = 0 ## True when the bullet-speed ceiling actually bit. Surfaced so the choice ## screen can say a second Sniper buys no more speed instead of silently ## selling one. var speed_capped: bool = false static func build(ids: Array[StringName]) -> PlayerStats: var s := PlayerStats.new() var additive := 0.0 var multiplicative := 1.0 var fire_rate := 1.0 var speed := 1.0 var hp := 1.0 for id in ids: var def := Upgrades.get_def(id) if def == null: continue # an upgrade this build no longer has: ignored, not fatal s.upgrade_count += 1 # Every upgrade carries this, whatever else it does. additive += SimConfig.UPGRADE_DAMAGE_BONUS additive += def.damage_add multiplicative *= def.damage_mult fire_rate *= def.fire_rate_mult speed *= def.bullet_speed_mult hp *= def.max_hp_mult s.side_shots += def.side_shots s.parallel_shots += def.parallel_shots s.split_charges += def.split_charges s.poison_fraction += def.poison_fraction s.erase_chance += def.erase_chance # Floored at 1 rather than allowed to reach zero. Stacking Doubleshot and # Spread can in principle drive the additive pool below −100%, and a shot # that deals nothing is indistinguishable from a bug. s.damage = maxi(1, roundi(float(SimConfig.PLAYER_BULLET_DAMAGE) * (1.0 + additive) * multiplicative)) # A division, so "half the fire rate" is half the shots per second rather # than half the cooldown. s.fire_cooldown = maxi(1, roundi(float(SimConfig.PLAYER_FIRE_COOLDOWN) / maxf(fire_rate, 0.01))) var wanted := SimConfig.PLAYER_BULLET_SPEED * speed s.bullet_speed = minf(wanted, SimConfig.MAX_BULLET_SPEED) s.speed_capped = wanted > SimConfig.MAX_BULLET_SPEED s.max_hp_mult = maxf(hp, 0.01) s.erase_chance = clampf(s.erase_chance, 0.0, 1.0) return s