shrike/balance.py¶
Part of SHRIKE.
1"""Every tuning number in SHRIKE, as named constants and small specs.
2
3This module is the single source of truth for balance. Gameplay modules import
4from here and never hard-code a rate, price, HP value or timer of their own.
5It is data only: no engine imports, no behaviour beyond tiny pure helpers.
6
7Units, unless stated otherwise: seconds for time, hull points for damage,
8scrap for prices, world units for distance on the flight plane. Conversion
9rates share one unit, Cores per 10 scrap.
10"""
11
12from dataclasses import dataclass, field
13
14# ============================================================================
15# Run structure and acts
16# ============================================================================
17
18RUN_SECTOR_VISITS_MIN = 7
19RUN_SECTOR_VISITS_MAX = 9
20CHART_NODES_MIN = 14
21CHART_NODES_MAX = 18
22CHART_ACTS = 3
23
24#: First sector index (1-based) of each act. Act 1 covers sectors 1 to 2,
25#: Act 2 covers 3 to 6, Act 3 covers 7 and beyond.
26ACT_2_FIRST_SECTOR = 3
27ACT_3_FIRST_SECTOR = 7
28
29#: Destination choices offered per chart node.
30CHART_DESTINATIONS_MIN = 2
31CHART_DESTINATIONS_MAX = 4
32
33#: The Wake front consumes one chart column per this many jumps.
34WAKE_COLUMNS_PER_JUMP = 1 / 2
35#: Above this notoriety the Wake accelerates to one column per 1.5 jumps.
36WAKE_ACCEL_NOTORIETY = 60
37WAKE_ACCEL_COLUMNS_PER_JUMP = 1 / 1.5
38#: The chart screen always shows the front's next advances this far ahead.
39WAKE_PREVIEW_ADVANCES = 2
40
41#: Drift depots appear on roughly half the chart's between-node edges.
42DEPOT_EDGE_FRACTION = 0.5
43
44#: Every dealt chart guarantees an oxygen refill (Ice or Vent Field) reachable
45#: inside this many columns of the start, so a seed can never starve the run of
46#: air before the first detour decision exists.
47CHART_O2_GUARANTEE_COLUMNS = 3
48
49
50def act_for_sector(sector_index: int) -> int:
51 """The act (1 to 3) a 1-based sector index belongs to."""
52 if sector_index >= ACT_3_FIRST_SECTOR:
53 return 3
54 if sector_index >= ACT_2_FIRST_SECTOR:
55 return 2
56 return 1
57
58
59# ============================================================================
60# Fuel (strategic; tank 100, start full)
61# ============================================================================
62
63FUEL_MAX = 100.0
64WARP_FUEL_BASE = 15.0
65WARP_FUEL_PER_COLUMN_SKIPPED = 5.0
66EMERGENCY_WARP_FUEL_MULT = 1.5
67DEEP_GATE_TOLL = 60.0
68GENERATOR_FUEL_PER_S = 0.1
69
70FUEL_COMET_MIN = 10.0
71FUEL_COMET_MAX = 15.0
72WRECK_TANK_FUEL = 5.0
73DEPOT_FUEL_PRICE_SCRAP = 25
74DEPOT_FUEL_AMOUNT = 20.0
75
76# ============================================================================
77# Life support (strategic; 100 O2)
78# ============================================================================
79
80O2_MAX = 100.0
81O2_DRAIN_PER_S = 0.25
82O2_DRAIN_PER_BREACH_PER_S = 0.15
83#: A breach opens each time hull crosses another 25 percent threshold.
84BREACH_HULL_FRACTION_STEP = 0.25
85BREACH_PATCH_SCRAP = 5
86BREACH_PATCH_CHANNEL_S = 4.0
87ICE_CHUNK_O2 = 10.0
88DEPOT_O2_CANISTER_AMOUNT = 40.0
89DEPOT_O2_CANISTER_PRICE_SCRAP = 25
90DEPOT_O2_CANISTERS_PER_VISIT = 1
91#: Below this many seconds of remaining O2 the mix low-passes and breathing enters.
92O2_LOW_WARNING_SECONDS = 60.0
93
94# ============================================================================
95# Energy (tactical; capacitor)
96# ============================================================================
97
98CAPACITOR_MAX = 100.0
99CAPACITOR_MAX_UPGRADED = 160.0
100
101#: The ship's bus keeps this trickle flowing into the capacitor even with the
102#: wings folded and the generator cold, so a dark ship is dim, never bricked.
103POWER_EMERGENCY_TRICKLE_PER_S = 2.0
104#: Weapons fire down to this floor, spending whatever charge is left; only
105#: below it does the trigger go dead. Non-weapon consumers stay all-or-nothing.
106WEAPONS_ENERGY_FLOOR = 5.0
107
108ENERGY_PULSE_BLASTER_PER_SHOT = 2.0
109ENERGY_ARC_BEAM_PER_S = 8.0
110ENERGY_MINING_BEAM_PER_S = 3.0
111ENERGY_SHIELD_REGEN_PER_S = 4.0
112ENERGY_AFTERBURNER_PER_S = 10.0
113ENERGY_TRACTOR_SCOOP_PER_S = 3.0
114ENERGY_AUTO_TURRET_PER_S = 1.5
115ENERGY_REFINERY_PER_S = 5.0
116
117# Solar wings
118SOLAR_OUTPUT_PER_S = 6.0
119SOLAR_OUTPUT_SOLAR_SHALLOWS_PER_S = 12.0
120SOLAR_OUTPUT_NEBULA_PER_S = 0.0
121SOLAR_WING_HP = 30.0
122SOLAR_HITBOX_WIDEN_FRACTION = 0.25
123SOLAR_DEPLOY_S = 2.0
124SOLAR_RETRACT_S = 2.0
125
126# Generator
127GENERATOR_OUTPUT_PER_S = 14.0
128GENERATOR_SIGNATURE_PER_S = 0.5
129
130# RTG (meta unlock, then a depot module). Kept strictly above
131# POWER_EMERGENCY_TRICKLE_PER_S: the module a player paid for must out-pay the
132# bus every bare hull already carries, or fitting it reads as nothing changing.
133RTG_OUTPUT_PER_S = 3.0
134RTG_PRICE_SCRAP = 60
135
136# Silent running
137SILENT_SIGNATURE_GAIN_MULT = 0.25
138SILENT_SIGNATURE_DECAY_PER_S = 1.5
139
140# ============================================================================
141# Ammo (tactical; per-weapon magazines)
142# ============================================================================
143
144AMMO_BOX_ROUNDS = 60
145MISSILES_PER_RACK = 8
146AMMO_BOX_PRICE_SCRAP = 20
147FABRICATOR_SCRAP_PER_BOX = 10
148#: Ballistic weapons hit this much harder per shot than energy weapons.
149BALLISTIC_DAMAGE_PREMIUM_MIN = 0.40
150BALLISTIC_DAMAGE_PREMIUM_MAX = 0.60
151
152# ============================================================================
153# Signature (per-sector, 0 to 100)
154# ============================================================================
155
156SIGNATURE_MAX = 100.0
157SIGNATURE_BASELINE_PER_S = {1: 0.1, 2: 0.4, 3: 0.8}
158#: Silent-running decay never drops the meter below the act floor.
159SIGNATURE_FLOOR = {1: 0.0, 2: 20.0, 3: 40.0}
160
161SIGNATURE_RICH_NODE_TAP = 2.0
162SIGNATURE_PER_KILL = 1.0
163SIGNATURE_REFINERY_BATCH = 12.0
164SIGNATURE_VAULT_HACK = 15.0
165SIGNATURE_SCREAMER_SURVIVED = 10.0
166
167# ============================================================================
168# The conversion bet (all rates in Cores per 10 scrap)
169# ============================================================================
170
171CONVERT_SCRAP_UNIT = 10.0
172CONVERT_RATE_REFINERY = 1.3
173CONVERT_RATE_TRADING_POST = 1.0
174CONVERT_RATE_EXTRACTION = 1.0
175CONVERT_RATE_DEATH = 0.4
176#: A cheap early doctrine node raises the death rate to this.
177CONVERT_RATE_DEATH_UPGRADED = 0.6
178
179REFINERY_BATCH_CHANNEL_S = 6.0
180
181# Last Stand (the collapse rule)
182LAST_STAND_CONVERT_RATE = 1.0
183LAST_STAND_CORES_CAP = 40
184LAST_STAND_SHRIKE_ARRIVES_WITHIN_S = 60.0
185
186
187def cores_from_scrap(scrap: float, rate: float) -> float:
188 """Cores produced by converting *scrap* at *rate* Cores per 10 scrap."""
189 return scrap / CONVERT_SCRAP_UNIT * rate
190
191
192# ============================================================================
193# Weapons (10, split down the two economies)
194# ============================================================================
195
196
197@dataclass(frozen=True)
198class WeaponSpec:
199 """One weapon's anchor numbers. ``family`` is "energy", "ballistic" or "utility".
200
201 ``energy_per_shot`` / ``energy_per_s`` are None where the design leaves the
202 cost unset; the owner sets those during balance, implementers must not
203 invent values here.
204 """
205
206 id: str
207 family: str
208 dps: float
209 energy_per_shot: float | None = None
210 energy_per_s: float | None = None
211 rounds_per_box: int | None = None
212 note: str = ""
213
214
215WEAPONS: dict[str, WeaponSpec] = {
216 "pulse_blaster": WeaponSpec("pulse_blaster", "energy", 20.0, energy_per_shot=ENERGY_PULSE_BLASTER_PER_SHOT),
217 "scatter_coil": WeaponSpec("scatter_coil", "energy", 35.0, note="DPS at point blank"),
218 "arc_beam": WeaponSpec("arc_beam", "energy", 30.0, energy_per_s=ENERGY_ARC_BEAM_PER_S, note="sustained"),
219 "mining_laser": WeaponSpec(
220 "mining_laser", "energy", 12.0, energy_per_s=ENERGY_MINING_BEAM_PER_S, note="triple DPS against deposits"
221 ),
222 "nova_mortar": WeaponSpec("nova_mortar", "energy", 70.0, note="area"),
223 "rail_lance": WeaponSpec("rail_lance", "ballistic", 45.0, rounds_per_box=AMMO_BOX_ROUNDS),
224 "flak_cannon": WeaponSpec("flak_cannon", "ballistic", 55.0, rounds_per_box=AMMO_BOX_ROUNDS, note="against shoals"),
225 "autocannon": WeaponSpec("autocannon", "ballistic", 40.0, rounds_per_box=AMMO_BOX_ROUNDS),
226 "missile_rack": WeaponSpec("missile_rack", "ballistic", 60.0, rounds_per_box=MISSILES_PER_RACK, note="burst"),
227 "grav_hook": WeaponSpec("grav_hook", "utility", 0.0, note="yanks enemies and salvage"),
228}
229
230MINING_LASER_DEPOSIT_MULT = 3.0
231
232# ============================================================================
233# Player defence and hull
234# ============================================================================
235
236HULL_MAX_STARTER = 100.0
237
238SHIELD_ARC_DEGREES = 120.0
239SHIELD_ABSORB_MAX = 60.0
240SHIELD_BREAK_LOCKOUT_S = 3.0
241#: Stacking emitters widens the arc toward a bubble at this energy multiplier.
242SHIELD_BUBBLE_ENERGY_MULT = 3.0
243#: Gamepad shield nudge step, degrees per bumper tap.
244SHIELD_PAD_NUDGE_DEGREES = 60.0
245
246AUTO_TURRETS_MAX = 4
247TURRET_PRICE_SCRAP_MIN = 40
248TURRET_PRICE_SCRAP_MAX = 80
249
250HARDPOINTS_STARTER = 2
251HARDPOINTS_MAX = 4
252
253# ============================================================================
254# Flight feel (drift model and juice anchors)
255# ============================================================================
256
257INERTIAL_DAMPENING = 0.90
258#: Camera leads the aim by this fraction of screen width.
259CAMERA_AIM_LEAD_FRACTION = 0.12
260AFTERBURNER_IGNITION_RAMP_S = 0.2
261#: Releasing all keys drifts the ship this many ship lengths before stopping.
262RELEASE_DRIFT_SHIP_LENGTHS = 1.5
263#: The hull's length in world units; the unit RELEASE_DRIFT_SHIP_LENGTHS is
264#: quoted in, and the mesh height ``ship.py`` builds the cone at.
265SHIP_LENGTH_UNITS = 1.8
266RAILGUN_RECOIL_PX = 40.0
267HIT_STOP_MIN_MS = 30.0
268HIT_STOP_MAX_MS = 60.0
269KILL_COMBO_WINDOW_S = 2.0
270#: Gamepad aim-assist magnetism cone, degrees.
271AIM_ASSIST_CONE_DEGREES = 12.0
272
273# ============================================================================
274# Enemies (9 archetypes plus elites and set-pieces)
275# ============================================================================
276
277
278@dataclass(frozen=True)
279class EnemySpec:
280 """One archetype's anchors: threat-budget cost, HP, damage per hit."""
281
282 id: str
283 threat: int
284 hp: float
285 damage: float
286 note: str = ""
287
288
289ENEMIES: dict[str, EnemySpec] = {
290 # A mite's hp sits exactly on one pulse-blaster round (dps 20 / rate 5), so
291 # the starter kit pops a splinter per hit; at the design's first-pass 5.0
292 # every round left a sliver and the whole shoal read as spongy.
293 "mite": EnemySpec("mite", 1, 4.0, 2.0, note="shoals of 8 to 20 on one flow field"),
294 "skimmer": EnemySpec("skimmer", 2, 25.0, 0.0, note="steals floating salvage, never shoots"),
295 "lancer": EnemySpec("lancer", 3, 60.0, 12.0, note="dash telegraph line drawn 0.5 s early"),
296 "mag_mine": EnemySpec("mag_mine", 2, 10.0, 20.0, note="lunges once the ship is inside 8 units"),
297 "welder": EnemySpec("welder", 4, 80.0, 0.0, note="repair umbilical, broken by flying through it"),
298 "bombardier": EnemySpec("bombardier", 5, 90.0, 18.0, note="stand-off mortars, painted reticles"),
299 "screamer": EnemySpec("screamer", 2, 20.0, 0.0, note="+10 signature if alive 8 s after aggro"),
300 "husk_turret": EnemySpec("husk_turret", 3, 150.0, 8.0, note="damage is per beam tick"),
301 "herald": EnemySpec("herald", 4, 70.0, 10.0, note="pours in at T-30, drops Fletchings"),
302}
303
304WARDEN = EnemySpec("warden", 15, 1600.0, 15.0, note="stationary mini-boss with a rotating shield arc")
305MAGISTRATE = EnemySpec("magistrate", 0, 900.0, 14.0, note="named bounty hunter, jumps in at notoriety 40+")
306MAGISTRATE_COUNT = 3
307MAGISTRATE_NOTORIETY_THRESHOLD = 40
308MAGISTRATE_SCRAP_DROP_MIN = 60
309MAGISTRATE_SCRAP_DROP_MAX = 120
310
311MITE_SHOAL_MIN = 8
312MITE_SHOAL_MAX = 20
313SCREAMER_AGGRO_TIMER_S = 8.0
314LANCER_TELEGRAPH_S = 0.5
315ENEMY_FIRE_TELEGRAPH_S = 0.3
316SPAWN_EDGE_CHEVRON_S = 1.0
317
318#: Hard cap on concurrent enemies (multimesh-instanced).
319ENEMY_HARD_CAP = 120
320
321# ============================================================================
322# Enemy speeds and engagement ranges
323#
324# Two rules govern this table, and both were bought with a playtest.
325#
326# **Speed.** Every hostile speed is quoted as a multiple of the player's cruise
327# speed, because that is the only frame in which "can I disengage?" has an
328# answer. The Shrike alone is allowed past the afterburner ceiling; a hostile
329# that outruns the burner has taken the decision away from the player, and the
330# first five minutes stop being a game. The one licensed exception is a burst:
331# a Lancer's dash beats the burner for the two thirds of a second it lasts,
332# which is a dodge rather than a chase.
333#
334# **Range.** Every engagement range is quoted against what the camera actually
335# shows. A telegraph fired from off-screen is not a telegraph, so a trigger
336# range wider than the visible field is a bug however good the wind-up looks.
337# ============================================================================
338
339#: The player's cruise top speed, world units per second (16.2): the speed at
340#: which the dampened drift model coasts RELEASE_DRIFT_SHIP_LENGTHS hull
341#: lengths after release at the 60 Hz fixed step. ``ship.CRUISE_SPEED`` binds
342#: to this, so the table below and the flight model cannot drift apart.
343PLAYER_CRUISE_SPEED = RELEASE_DRIFT_SHIP_LENGTHS * SHIP_LENGTH_UNITS * 60.0 * (1.0 - INERTIAL_DAMPENING)
344#: The afterburner lifts both the acceleration and the ceiling by this much;
345#: ``ship.AFTERBURNER_SPEED_MULT`` binds to it.
346PLAYER_AFTERBURNER_SPEED_MULT = 1.8
347#: The ceiling no hostile but the Shrike may hold, world units per second.
348PLAYER_AFTERBURNER_SPEED = PLAYER_CRUISE_SPEED * PLAYER_AFTERBURNER_SPEED_MULT
349
350#: What the camera shows of the flight plane at the design's pitch and distance,
351#: world units. Engagement ranges are quoted against these two numbers.
352VISIBLE_FIELD_WIDTH = 77.0
353VISIBLE_FIELD_HEIGHT = 39.5
354
355
356def enemy_speed(cruise_multiple: float) -> float:
357 """A hostile speed, quoted as a multiple of the player's cruise speed."""
358 return PLAYER_CRUISE_SPEED * cruise_multiple
359
360
361# Mites: slower than a cruising ship, so a shoal is a thing you leave rather
362# than a thing you outrun, and the telegraphed surge is the only time it gains.
363MITE_SPEED = enemy_speed(0.70)
364MITE_SURGE_SPEED = enemy_speed(1.10)
365MITE_SURGE_RANGE = 24.0
366
367# Skimmers: exactly as fast as the ship while hunting, and faster while running
368# for the edge with your salvage, but never faster than the burner. Chasing one
369# down is a fuel decision, not an impossibility.
370SKIMMER_SPEED = enemy_speed(1.00)
371SKIMMER_FLEE_SPEED = enemy_speed(1.50)
372SKIMMER_IDLE_SPEED = enemy_speed(0.35)
373SKIMMER_GRAB_RADIUS = 3.0
374#: How far a loaded Skimmer runs before it is gone with the mote.
375SKIMMER_ESCAPE_DISTANCE = 60.0
376
377# Lancers: they close slightly slower than you cruise, hold at a distance that
378# fits on screen, and buy their damage with a dash the burner cannot match for
379# the two thirds of a second it lasts.
380LANCER_APPROACH_SPEED = enemy_speed(0.95)
381LANCER_DASH_SPEED = enemy_speed(2.20)
382LANCER_STANDOFF = 15.0
383LANCER_DASH_RANGE = 25.0
384LANCER_DASH_DISTANCE = 22.0
385
386# Mag-mines: they drift, and the lunge triggers close enough that the white
387# flash and the lunge both happen well inside the frame.
388MAG_MINE_DRIFT_SPEED = enemy_speed(0.20)
389MAG_MINE_LUNGE_SPEED = enemy_speed(1.60)
390MAG_MINE_LUNGE_RANGE = 8.0
391
392# Welders: slow support that has to be caught, and an umbilical short enough to
393# see both ends of.
394WELDER_SPEED = enemy_speed(0.70)
395WELDER_UMBILICAL_RANGE = 24.0
396WELDER_TETHER_DISTANCE = 12.0
397WELDER_LOITER_DISTANCE = 30.0
398#: How near the umbilical the ship must pass to shear it.
399WELDER_UMBILICAL_BREAK_RADIUS = 1.8
400
401# Bombardiers: the slowest hull in the roster, holding a stand-off that stays
402# inside the frame so the tube and its reticles are on screen together.
403BOMBARDIER_SPEED = enemy_speed(0.55)
404BOMBARDIER_STANDOFF_DISTANCE = 26.0
405BOMBARDIER_STANDOFF_BAND = 5.0
406BOMBARDIER_MAX_RANGE = 34.0
407BOMBARDIER_BLAST_RADIUS = 6.0
408#: Random scatter applied to each shell after the lead, in plane units.
409BOMBARDIER_SCATTER = 5.0
410
411# Screamers: they run, and slower than you, so the tax is always payable.
412SCREAMER_SPEED = enemy_speed(0.90)
413SCREAMER_AGGRO_RANGE = 22.0
414SCREAMER_FLEE_DISTANCE = 28.0
415
416# Husk turrets: terrain that shoots, with a beam that reaches most of the frame.
417HUSK_TURRET_SPEED = 0.0
418HUSK_TURRET_BEAM_RANGE = 26.0
419
420# Heralds: the hunter's language at a survivable scale, orbiting inside the
421# frame and never able to run the ship down.
422HERALD_SPEED = enemy_speed(0.90)
423HERALD_ORBIT_RADIUS = 18.0
424HERALD_LANCE_RANGE = 22.0
425#: Heralds tear in on a ring this far from the ship.
426HERALD_SPAWN_RADIUS = 30.0
427
428# ============================================================================
429# Contact
430#
431# Hull-to-hull damage is the only damage in the game with nothing flying
432# between the two bodies, so it is the only damage that has to read as a
433# collision. An attacker that lands one loses its drive briefly and both hulls
434# take a radial impulse apart; without that an enemy slides through the ship
435# and the hit registers as a number rather than as a crash.
436# ============================================================================
437
438#: How long a hostile's drive is dead after it lands a contact hit.
439CONTACT_STALL_S = 0.3
440#: Separation velocity the collision puts on the attacker, world units per second.
441CONTACT_SEPARATION_IMPULSE = 18.0
442#: The share of that impulse the body being hit takes. The ship is the heavier
443#: of the two, so it is shoved less than the thing that ran into it.
444CONTACT_TARGET_IMPULSE_FRACTION = 0.5
445#: How fast a separation impulse bleeds off, as an exponential rate per second.
446CONTACT_RECOIL_DAMPING = 6.0
447
448#: Elite modifiers appear past this threat budget.
449ELITE_THREAT_THRESHOLD = 25
450ELITE_MODIFIERS = ("armoured", "splitting", "emp_laced")
451
452#: Target time-to-kill bands, the tuning contract.
453TTK_BANDS_S = {"mite": 0.5, "lancer": 2.0, "warden": 45.0, "shrike_fin": 15.0}
454
455# ============================================================================
456# Waves
457# ============================================================================
458
459WAVE_BUDGET_BASE = 10
460WAVE_BUDGET_PER_SECTOR = 3
461WAVE_BUDGET_NOTORIETY_DIVISOR = 10
462WAVE_INTERVAL_MIN_S = 35.0
463WAVE_INTERVAL_MAX_S = 55.0
464WAVE_RECIPES_TARGET = 40
465
466
467def wave_threat_budget(sector_index: int, notoriety: float) -> float:
468 """T = 10 + 3 x sector index + notoriety / 10."""
469 return WAVE_BUDGET_BASE + WAVE_BUDGET_PER_SECTOR * sector_index + notoriety / WAVE_BUDGET_NOTORIETY_DIVISOR
470
471
472# ============================================================================
473# The Shrike
474# ============================================================================
475
476# Telegraph ladder (seconds before arrival)
477SHRIKE_TELEGRAPH_FIRST_S = 60.0
478SHRIKE_TELEGRAPH_KLAXON_S = 30.0
479#: Per-arrival compression: second arrival warns at T-45, third and later at T-30.
480SHRIKE_TELEGRAPH_SECOND_ARRIVAL_S = 45.0
481SHRIKE_TELEGRAPH_LATER_ARRIVALS_S = 30.0
482#: In the Nebula the T-60 stage is muffled to this.
483SHRIKE_TELEGRAPH_NEBULA_S = 20.0
484
485SHRIKE_LOCKOUT_S = 10.0
486WARP_SPOOL_S = 5.0
487WARP_SPOOL_PENALTY_PER_HIT_S = 0.5
488#: Hits a single spool channel absorbs before it breaks outright. The design
489#: prices each hit but names no cap; without one the ring never resolves under
490#: sustained fire, so the channel fails loudly past this many interruptions.
491WARP_SPOOL_MAX_INTERRUPTIONS = 6
492
493#: Jettisoned scrap buys about this much escape time, seconds per scrap dumped.
494SHRIKE_FEED_SECONDS_PER_SCRAP = 8.0 / 40.0
495
496SHRIKE_FIN_HP = 250.0
497#: Fin plating trims incoming DPS to roughly a third.
498SHRIKE_FIN_DAMAGE_TAKEN_MULT = 1 / 3
499#: Damage output ramps this much every ramp interval while present.
500SHRIKE_RAMP_FRACTION = 0.20
501SHRIKE_RAMP_INTERVAL_S = 10.0
502#: Third and later arrivals start the ramp already at +20 percent.
503SHRIKE_RAMP_START_LATER_ARRIVALS = 0.20
504
505SHRIKE_CRUISE_SPEED_MULT = 1.15
506SHRIKE_LANTERN_HULL_DAMAGE = 35.0
507SHRIKE_LANTERN_CAPACITOR_BLACKOUT_S = 1.0
508SHRIKE_LANTERN_PREBURN_WARNING_S = 1.0
509#: The ship is this many times the player ship's length.
510SHRIKE_LENGTH_MULT = 8.0
511
512#: Seconds after arrival when every vault is shorn open and every core split.
513SHRIKE_CRACK_OPEN_S = 15.0
514
515# ============================================================================
516# Quills, Fletchings and the Roost
517# ============================================================================
518
519QUILLS_TO_OPEN_ROOST = 5
520QUILLS_KEPT_ON_DEATH = 1
521FLETCHINGS_PER_QUILL = 10
522#: Herald drop rates rise this much per run since your last quill.
523FLETCHING_RATE_RISE_PER_RUN = 0.05
524ROOST_RETRY_QUILL_COST = 1
525ROOST_KILL_CORES = 150
526ROOST_FUEL_REQUIRED = 40.0
527ROOST_PROVOCATION_NOTORIETY = 60
528#: The assembled Lure counts as this much notoriety worth of noise by itself.
529LURE_NOTORIETY_EQUIVALENT = 60
530LURE_ASSEMBLY_SCRAP = 200
531LURE_FRAGMENT_BIOME_TYPES = 3
532MIRROR_LANTERN_BLIND_S = 3.0
533
534# ============================================================================
535# Notoriety (run-level, 0 to 100)
536# ============================================================================
537
538NOTORIETY_MAX = 100
539NOTORIETY_VAULT_CRACKED = 5
540NOTORIETY_REFINERY_BATCH = 4
541NOTORIETY_ELITE_KILL = 3
542NOTORIETY_ARRIVAL_SURVIVED = 8
543NOTORIETY_QUIET_SECTOR_DECAY = 5
544
545BROKER_NOTORIETY_THRESHOLD = 50
546BOUNTY_DOUBLE_NOTORIETY_THRESHOLD = 60
547
548# ============================================================================
549# Scrap economy
550# ============================================================================
551
552SCRAP_ORE_CHUNK_MIN = 3
553SCRAP_ORE_CHUNK_MAX = 8
554SCRAP_WRECK_MIN = 15
555SCRAP_WRECK_MAX = 40
556SCRAP_VAULT_MIN = 60
557SCRAP_VAULT_MAX = 150
558SCRAP_KILL_MIN = 1
559SCRAP_KILL_MAX = 4
560SCRAP_BOUNTY_MIN = 60
561SCRAP_BOUNTY_MAX = 120
562
563VAULT_HACK_CHANNEL_S = 20.0
564#: Deposits are back-loaded: the surface fraction cracks fast, the core pays double.
565DEPOSIT_SURFACE_FRACTION = 0.70
566DEPOSIT_CORE_FRACTION = 0.30
567DEPOSIT_CORE_PAYOUT_MULT = 2.0
568
569TYPICAL_RUN_SCRAP_TIMID = 450
570TYPICAL_RUN_SCRAP_GREEDY = 900
571TYPICAL_RUN_CORES_MIN = 40
572TYPICAL_RUN_CORES_MAX = 110
573
574# Depot stock and prices
575DEPOT_STOCK_ITEMS = 6
576DEPOT_STOCK_WEAPONS = 2
577DEPOT_STOCK_MODULES = 3
578DEPOT_STOCK_CONSUMABLES = 1
579DEPOT_PITY_SLOTS = 1
580DEPOT_REROLL_BASE_SCRAP = 10
581DEPOT_REROLL_PRICE_MULT = 2.0
582#: How many times one visit may restock the same consumable shelf entry. O2
583#: canisters ignore this and honour DEPOT_O2_CANISTERS_PER_VISIT instead.
584DEPOT_CONSUMABLE_RESTOCK_LIMIT = 9
585#: Fuel cells one visit will sell. Air is rationed to a single canister so a
586#: full purse cannot buy its way out of the life-support clock; fuel is
587#: rationed far more loosely, because a tank too empty to reach the next node
588#: is the one dead end the shop exists to answer. Four cells is five jumps.
589DEPOT_FUEL_CELLS_PER_VISIT = 4
590
591MODULE_PRICE_TIER1_MIN = 35
592MODULE_PRICE_TIER1_MAX = 60
593MODULE_PRICE_TIER2_MIN = 90
594MODULE_PRICE_TIER2_MAX = 140
595MODULE_PRICE_TIER3_MIN = 180
596MODULE_PRICE_TIER3_MAX = 260
597
598# ============================================================================
599# Per-act drop tables
600# ============================================================================
601
602
603@dataclass(frozen=True)
604class ActDrops:
605 """Loot density anchors for one act."""
606
607 scrap_per_sector: tuple[int, int]
608 module_tiers_in_wrecks: tuple[int, int]
609 fletchings_per_herald_wave: tuple[int, int]
610 vaults_per_sector: tuple[int, int]
611
612
613ACT_DROPS: dict[int, ActDrops] = {
614 1: ActDrops((40, 60), (1, 1), (0, 0), (0, 1)),
615 2: ActDrops((80, 140), (1, 2), (1, 2), (1, 1)),
616 3: ActDrops((140, 220), (2, 3), (2, 4), (1, 2)),
617}
618
619NODE_SCRAP_ACT1_MIN = 40
620NODE_SCRAP_ACT1_MAX = 60
621
622# ============================================================================
623# Biomes (7 plus the Roost)
624# ============================================================================
625
626
627@dataclass(frozen=True)
628class BiomeSpec:
629 """One biome's resource-pressure inversion, as multipliers on the baseline."""
630
631 id: str
632 solar_mult: float = 1.0
633 scrap_mult: float = 1.0
634 #: Override for the first telegraph stage, seconds; None keeps the ladder.
635 telegraph_override_s: float | None = None
636 note: str = ""
637
638
639BIOMES: dict[str, BiomeSpec] = {
640 "debris_field": BiomeSpec("debris_field", note="baseline; wrecks, ammo boxes"),
641 "solar_shallows": BiomeSpec("solar_shallows", solar_mult=2.0, note="deployed panels are prime flak targets"),
642 "ice_field": BiomeSpec("ice_field", scrap_mult=0.5, note="O2 chunks refill life support; slow enemies"),
643 "nebula": BiomeSpec(
644 "nebula",
645 solar_mult=0.0,
646 scrap_mult=1.5,
647 telegraph_override_s=SHRIKE_TELEGRAPH_NEBULA_S,
648 note="spawn directions fogged",
649 ),
650 "wreck_graveyard": BiomeSpec("wreck_graveyard", note="huge scrap; husk turrets; Skimmers steal salvage"),
651 "vent_field": BiomeSpec("vent_field", note="fuel and O2 refills; mag-mine mazes"),
652 "broker_claim": BiomeSpec("broker_claim", note="black market barge, docks at notoriety 50+; Warden guarded"),
653 "roost": BiomeSpec("roost", note="unique Act 3 node, appears only with the assembled Lure aboard"),
654}
655
656#: Ice Field scrap is "poor" in the design; 0.5 is the working multiplier and
657#: the owner tunes it at the balance pass.
658
659# ============================================================================
660# Modules and sockets
661# ============================================================================
662
663MODULES_AT_LAUNCH = 30
664BROKER_EXCLUSIVE_MODULES = 8
665POOL_EXPANSION_PAIRS = 8
666SOCKETS_STARTER_HULL = 6
667
668WAKE_DAMPER_SIGNATURE_MULT = 0.85
669WAKE_DAMPER_MINING_SPEED_MULT = 0.85
670SENSOR_MAST_CHART_TAG_JUMPS = 2
671
672TYPICAL_RUN_PURCHASES_MIN = 5
673TYPICAL_RUN_PURCHASES_MAX = 7
674
675# ============================================================================
676# Doctrine tree and meta progression
677# ============================================================================
678
679DOCTRINE_NODES = 60
680DOCTRINE_BRANCHES = 5
681DOCTRINE_NODES_PER_BRANCH = 12
682DOCTRINE_MINORS_PER_BRANCH = 7
683DOCTRINE_NOTABLES_PER_BRANCH = 4
684DOCTRINE_KEYSTONES_PER_BRANCH = 1
685DOCTRINE_COST_MINOR = 15
686DOCTRINE_COST_NOTABLE = 40
687DOCTRINE_COST_KEYSTONE = 90
688DOCTRINE_FULL_TREE_CORES = 1775
689DOCTRINE_MAX_ACTIVE_KEYSTONES = 2
690
691DOCTRINE_BRANCHES_IDS = ("gunnery", "engineering", "silent_running", "salvage", "predation")
692DOCTRINE_KEYSTONES = {
693 "gunnery": "dead_reckoning",
694 "engineering": "overload_discharge",
695 "silent_running": "cold_start",
696 "salvage": "magpie_protocol",
697 "predation": "quillborn",
698}
699
700COLD_START_INVISIBLE_S = 20.0
701MAGPIE_SALVAGE_FRACTION = 0.50
702DEEP_INSERTION_START_SECTOR = 3
703DEEP_INSERTION_START_SIGNATURE = 15.0
704
705# Milestone bounties (Cores), the first-five-runs velocity ramp.
706MILESTONE_FIRST_EXTRACTION = 30
707MILESTONE_FIRST_VAULT = 20
708MILESTONE_FIRST_ELITE_KILL = 20
709MILESTONE_FIRST_ACT3_ENTRY = 25
710MILESTONE_FIRST_ARRIVAL_SURVIVED = 30
711
712# Run score: scrap earned + Cores banked x 10 + quills x 100 + a depth bonus.
713SCORE_CORES_WEIGHT = 10
714SCORE_QUILLS_WEIGHT = 100
715
716# Hulls (feat-gated trophies)
717HULL_SOCKETS = {"vagrant": 6, "barge": 8, "dart": 4, "hive": 6}
718#: Gun mounts per hull, between HARDPOINTS_STARTER and HARDPOINTS_MAX. The Barge
719#: is the fortress and takes all four; the Hive spends its third mount on the
720#: drone bays it is built around; the Dart buys its speed by staying at two.
721HULL_HARDPOINTS = {
722 "vagrant": HARDPOINTS_STARTER,
723 "barge": HARDPOINTS_MAX,
724 "dart": HARDPOINTS_STARTER,
725 "hive": 3,
726}
727BARGE_UNLOCK_SCRAP_CARRIED = 200
728DART_SIGNATURE_FILL_MULT = 0.80
729HIVE_UNLOCK_HUNT_RANK = 2
730
731# Hunt Ranks (8 ascension tiers; 1 to 4 at launch)
732HUNT_RANKS_AT_LAUNCH = 4
733HUNT_RANKS_TOTAL = 8
734HUNT_RANK_CORE_INCOME_BONUS = 0.10
735HUNT_RANK1_SIGNATURE_FILL_MULT = 1.15
736HUNT_RANK2_DEPOT_STOCK_ITEMS = 5
737HUNT_RANK3_BREACH_BLEED_MULT = 2.0
738HUNT_RANK5_MUFFLE_STRENGTH = 0.5
739HUNT_RANK6_DEEP_GATE_TOLL = 90.0
740HUNT_RANK7_QUILLS_TO_OPEN_ROOST = 6
741
742# Doctrine kits (the free first-depot pick, one of three)
743KIT_ARSENAL_FLAK_ROUNDS = 120
744#: What each kit delivers, by catalogue id, per design section 4. The Solar
745#: Kit keeps the design's composition (solar wings + signature dampener) even
746#: though every hull already boots with the first power source it can carry:
747#: the grant tops up rather than stacks, so a part whose module is already
748#: socketed is skipped and the kit's visible value on a wings-carrying hull is
749#: the dampener. A second wing array was never priced and never intended.
750KIT_CONTENTS = {
751 "solar": ("solar_wings", "signature_dampener"),
752 "forge": ("generator", "sentry_turret"),
753 "arsenal": ("flak_cannon",),
754}
755
756# ============================================================================
757# Signal events
758# ============================================================================
759
760SIGNAL_EVENT_DECK_SIZE = 12
761LOG_FRAGMENTS_TOTAL = 30
762
763# ============================================================================
764# Steady Wake assist panel (accessibility; marked on the ledger, locks nothing)
765# ============================================================================
766
767ASSIST_SIGNATURE_FILL_MULT = 0.80
768ASSIST_DAMAGE_TAKEN_STEPS = (1.0, 0.8, 0.6)
769ASSIST_LANTERN_SWEEP_MULT = 0.75
770
771
772@dataclass(frozen=True)
773class AssistSettings:
774 """The Steady Wake panel's live state; defaults are everything off."""
775
776 slow_signature: bool = False
777 damage_taken_mult: float = 1.0
778 slow_lantern: bool = False
779 spool_immunity: bool = False
780 extra: dict = field(default_factory=dict)