class_name Upgrades extends RefCounted ## Every upgrade in the game, in code, like the rest of `src/content/`. ## ## [constant ORDER] is a wire format: an upgrade's index is the byte that rides ## the upgrade-state message and the character save. Append, never reorder. const SPLIT_SHOT := &"split_shot" const GLASS_CANNON := &"glass_cannon" const SPREAD := &"spread" const SNIPER := &"sniper" const DOUBLESHOT := &"doubleshot" const POISON := &"poison" const ERASER := &"eraser" const ORDER: Array[StringName] = [ SPLIT_SHOT, GLASS_CANNON, SPREAD, SNIPER, DOUBLESHOT, POISON, ERASER, ] ## Draw weights by rarity. Relative, not percentages -- what matters is that a ## legendary is a story and a common is Tuesday. const WEIGHTS := { UpgradeDef.Rarity.COMMON: 100, UpgradeDef.Rarity.UNCOMMON: 45, UpgradeDef.Rarity.RARE: 18, UpgradeDef.Rarity.LEGENDARY: 4, } static func get_def(id: StringName) -> UpgradeDef: match id: SPLIT_SHOT: return split_shot() GLASS_CANNON: return glass_cannon() SPREAD: return spread() SNIPER: return sniper() DOUBLESHOT: return doubleshot() POISON: return poison() ERASER: return eraser() return null ## Wire value. 0 is reserved for "no upgrade", so an id's value is its position ## plus one -- same convention as [Items]. static func index_of(id: StringName) -> int: var at := ORDER.find(id) return at + 1 if at >= 0 else 0 static func by_index(index: int) -> StringName: if index <= 0 or index > ORDER.size(): return &"" return ORDER[index - 1] ## Pick [param count] distinct upgrades, weighted by rarity. ## ## Distinct within one offer only -- an upgrade you already hold can and should ## come up again, because taking Split Shot twice is how a shot splits twice. static func roll_offer(rng: RandomNumberGenerator, count: int) -> Array[StringName]: var pool := ORDER.duplicate() var picked: Array[StringName] = [] while picked.size() < count and not pool.is_empty(): var total := 0 for id in pool: total += _weight_of(id) var roll := rng.randi_range(1, maxi(total, 1)) for i in pool.size(): roll -= _weight_of(pool[i]) if roll <= 0: picked.append(pool[i]) pool.remove_at(i) break return picked static func _weight_of(id: StringName) -> int: var def := get_def(id) return int(WEIGHTS.get(def.rarity, 1)) if def != null else 1 # --- The upgrades ----------------------------------------------------------- static func split_shot() -> UpgradeDef: var u := UpgradeDef.new() u.id = SPLIT_SHOT u.display_name = "Split Shot" u.description = "Hitting an enemy spawns two more of the same shot, at 45° to either side, behind it. Take it again to split again." u.rarity = UpgradeDef.Rarity.COMMON u.split_charges = 1 return u static func glass_cannon() -> UpgradeDef: var u := UpgradeDef.new() u.id = GLASS_CANNON u.display_name = "Glass Cannon" u.description = "Double damage, half health. The health is half of whatever your level gives you, so it stays a real price." u.rarity = UpgradeDef.Rarity.COMMON u.damage_add = 1.0 u.max_hp_mult = 0.5 return u static func spread() -> UpgradeDef: var u := UpgradeDef.new() u.id = SPREAD u.display_name = "Spread" u.description = "Two more shots in a cone either side of your aim. −10% damage." u.rarity = UpgradeDef.Rarity.COMMON u.side_shots = 2 u.damage_add = -0.10 return u ## The only multiplicative damage source, and the only thing that touches bullet ## speed. Both are capped downstream: PlayerStats clamps the speed under the ## tunnelling threshold, because a bullet that moves more than a tile per tick ## walks straight through walls. static func sniper() -> UpgradeDef: var u := UpgradeDef.new() u.id = SNIPER u.display_name = "Sniper" u.description = "Double damage, multiplied on top of everything else. Half the fire rate, double the bullet speed." u.rarity = UpgradeDef.Rarity.COMMON u.damage_mult = 2.0 u.fire_rate_mult = 0.5 u.bullet_speed_mult = 2.0 return u static func doubleshot() -> UpgradeDef: var u := UpgradeDef.new() u.id = DOUBLESHOT u.display_name = "Doubleshot" u.description = "One more shot travelling parallel to the rest. −50% damage." u.rarity = UpgradeDef.Rarity.UNCOMMON u.parallel_shots = 1 u.damage_add = -0.50 return u static func poison() -> UpgradeDef: var u := UpgradeDef.new() u.id = POISON u.display_name = "Poison" u.description = "Every shot deals another 50% of its damage over the next 10 seconds. Stacks with itself — every hit adds another dose." u.rarity = UpgradeDef.Rarity.RARE u.poison_fraction = 0.5 return u static func eraser() -> UpgradeDef: var u := UpgradeDef.new() u.id = ERASER u.display_name = "Eraser" u.description = "Your shots have a 1% chance to delete an enemy projectile they pass through." u.rarity = UpgradeDef.Rarity.LEGENDARY u.erase_chance = 0.01 return u