How to design a new passive
This guide walks through adding a new ship passive — a permanent, presence-only ability that ships in for the entire run as soon as the hull is selected.
Before you start
Passives are presence-only. They have no procs, no cooldowns, no trigger events, and no state. Each passive is a single stat modifier (percent or flat) applied at run start by applyPassives() in services/assembleRunService.ts.
Each ship hull has exactly one passive, referenced by passiveId on its ShipDef. The default baseline passiveId is jack_of_all. A passive’s magnitude scales with the ship’s rarity tier — the same passive on a Legendary hull is stronger than on a Common hull.
If your idea needs to react to events (on-hit, on-kill, on-low-HP, periodic ticks), it is not a passive. Use the authoring-new-artifact guide instead, or define an EffectDef and attach via the optional effects? field on PassiveDef (registered with EffectEngine at run start).
Step 1 — Identity
Decide the following fields:
| Field | Description |
|---|---|
id | Unique snake_case string. Matches the file name (kebab-case). E.g. void_siphon. |
name | Display name (Title Case). E.g. Void Siphon. |
description | Description template. The literal {value} is replaced with the tier’s number at resolve time. E.g. +{value} HP regen/sec. |
stat | The combat stat key being modified. Must match a key the run assembler knows about. |
isPercent | true if the value is a percentage modifier, false if it is a flat additive number. |
Existing stat keys in the 16 shipped passives:
| Stat key | Used by | Unit |
|---|---|---|
all | jack_of_all | % |
hpMax | heavy_plating | % |
maxSpeed | afterburner | % |
weaponDamagePct | predator_instinct | % (flat add) |
fireRatePct | payload_delivery | % (flat add) |
shieldMax | shield_capacitor | % |
thrust | phase_drive | % |
damageReduction | reinforced_hull | flat |
overheatCool | thermal_vents | % |
meleeMult | ram_prow | % |
shieldRegenRate | organic_regen | % |
turnSpeed | prismatic_agility | % |
magnetRange | tractor_array | % |
luck | fortune_core | flat |
currencyBonus | precision_salvage | % (flat add) |
hpRegen | void_siphon | flat |
Step 2 — Write the data file
One file per passive under src/starship-survivors/data/passives/. Kebab-case file name, snake_case export. The export must be typed PassiveDef and the named export must match the file name (e.g. void-siphon.ts exports void_siphon).
PassiveDef fields:
| Field | Type | Notes |
|---|---|---|
id | string | Unique. Used in passiveId references on ship defs. |
name | string | UI display name. |
description | string | Template containing {value}. |
stat | string | Combat stat key (see Step 1 table). |
isPercent | boolean | true for %, false for flat. |
values | readonly [number, number, number, number, number] | Tier values indexed by RARITY_INDEX: [common, uncommon, rare, epic, legendary]. |
effects? | EffectDef[] | Optional. Only for unified-effect-engine integration. Leave omitted for pure stat passives. |
Minimal example shape (matches jack-of-all.ts):
import type { PassiveDef } from './_types';
export const my_passive: PassiveDef = {
id: 'my_passive',
name: 'My Passive',
description: '+{value}% to something',
stat: 'someStatKey',
isPercent: true,
values: [5, 10, 16, 23, 32],
};Step 3 — Scaling curves
All 16 shipped passives use one of six fixed 5-tier curves. Pick the curve that matches the strength budget of your stat — do not invent a new curve unless tuning explicitly requires it.
| Curve | [C, U, R, E, L] | Used by | When to use |
|---|---|---|---|
| Standard % | [5, 10, 16, 23, 32] | heavy_plating, afterburner, shield_capacitor, phase_drive, organic_regen, prismatic_agility, thermal_vents | Most defensive/utility % stats. The default. |
| Damage / fire rate % | [4, 8, 13, 19, 28] | predator_instinct, payload_delivery | Direct DPS multipliers. Slightly tighter than standard. |
| Economy / luck flat | [3, 6, 10, 15, 22] | jack_of_all, fortune_core, precision_salvage | Multiplicative-feeling stats with broad reach. |
| Wide % | [8, 15, 24, 35, 50] | ram_prow, meleeMult; tractor_array, magnet range | Stats where large multipliers feel earned (niche or build-defining). |
| Flat DR | [1, 2, 3, 5, 7] | reinforced_hull | Flat damage reduction subtracted from incoming hits. |
| HP regen flat | [0.5, 1, 1.5, 2.5, 4] | void_siphon | Sub-1.0 fractional baseline; flat HP/sec regen. |
Step 4 — Register
Two edits in src/starship-survivors/data/passives/index.ts:
- Add an
import { my_passive } from './my-passive';line alongside the existing imports. - Add
my_passive,to thePASSIVESmap.
That is the entire registration surface. getPassiveValue, getPassiveDescription, and resolvePassive automatically pick up new entries from the map.
Step 5 — Ship assignment
Attach the passive to one or more hulls by setting passiveId: 'my_passive' on the relevant ship def in src/starship-survivors/data/ships.ts.
Notes on assignment:
- The default
BASELINE_STATS.passiveIdis'jack_of_all'. Any hull without an explicit override inherits this. - Per-hull
passiveIdoverrides live in theSHIP_STATS_S1[hull_class]records and are picked up bygetShipDef()at lookup time (thes1Over?.passiveIdbranch). - Multiple hulls can share the same
passiveId. The 5 ship rarities (Common → Legendary) automatically index the corresponding row in the passive’svaluesarray viaRARITY_INDEX, so a Common hull withvoid_siphongets0.5 HP/secand a Legendary hull with the same passive gets4 HP/sec.
Step 6 — Validate
After adding the file, registering it, and assigning it to a hull:
- The passive appears on the ship’s stat card in the meta UI with the correct name and tier value substituted into
{value}. - The effect magnitude scales with hull rarity — pick the same hull at Common vs. Legendary and confirm the value changes per the chosen curve.
- No TypeScript errors and no runtime errors at run start (
getPassiveValuethrows on unknown IDs, so a typo inpassiveIdwill crash immediately). - The
statkey resolves correctly insideapplyPassives()— if the stat key is unrecognized, the modifier silently does nothing, which is the one failure mode to watch for.
Custom-element structure rule
Passives are restricted to single-stat modifications. The system has no hook surface for procs, periodic ticks, or conditional triggers from inside PassiveDef.
If your design needs reactive behavior:
- Trigger on game events (on-hit, on-kill, on-damage-taken, periodic interval): use the authoring-new-artifact guide. Artifacts have full effect-engine access and event subscriptions.
- Stat change plus a small reactive bonus: attach an
EffectDef[]via the optionaleffects?field onPassiveDef. The stat modifier still applies viaapplyPassives(), and the effects register withEffectEngineat run start in parallel. - Multi-stat bundle: use
stat: 'all'likejack_of_all, which broadcasts the percentage to every stat. Do not stack multiplePassiveDefentries on a single hull — one passive per ship is a hard rule.