How to design a new mod template

This guide walks through adding a new Tetris-grid mod template — a fixed-shape piece that occupies cells on the account-wide Mod Grid and grants flat additive stat bonuses scaled by rarity tier.

Before you start

Mods are Tetris-shaped pieces with rarity-tier stat boosts. Each template has a fixed footprint (a rectangle of cells, expressed as a row-major boolean mask) and a ModStatBlock of flat additive bonuses. A ModInstance (uid, templateId, rarity, rotation) is one specific mod the player owns; templates are the 9 archetypes those instances draw from.

Rarity multiplies the base stat block at run-assemble time. Templates do not have procs, triggers, on-hit hooks, or any per-frame logic — they are pure stat additions. If your idea needs to react to events, it is not a mod. Use how-to-design-a-new-artifact instead.

mod-templates.ts is a barrel re-export — all template types and data live under data/mods/. One template per .ts file.

Step 1 — Identity

Decide the template’s identity fields.

FieldDescription
idUnique snake_case string, prefixed mod_. Matches the file name (kebab-case minus the prefix). E.g. mod_array_magnet lives at data/mods/array-magnet.ts.
nameShort display label (UPPERCASE with leading +). E.g. +DMG, +HP, +SHIELD. Repeats across templates that share a stat theme are allowed.
sizeBounding-box { w, h }. Both w and h must satisfy 1 ≤ n ≤ 4.
cellsRow-major 2D boolean mask. cells[row][col]. cells.length === size.h; each row has size.w entries. Use rectCells(w, h) for solid rectangles or shape([...]) for Tetris L/T/S/Z masks. At least one cell must be true.

Module-load asserts in data/mods/index.ts enforce shape validity and id uniqueness; the file throws if MOD_TEMPLATES does not contain exactly 9 entries, so adding a 10th template also requires bumping that count assert.

Step 2 — Pick the stat slots

ModStatBlock is Partial<Pick<ShipCombatStats, ...>> over a fixed set of keys. Only these stats are modifiable by a mod template.

Stat keyUnitNotes
hpMaxflat HPAdds to hull max HP.
shieldMaxflat shieldAdds to shield pool max.
shieldRegenRateflat units/sShield regen-rate additive.
damageReductionflatDamage reduction additive.
maxSpeedflat units/sSpeed cap additive.
thrustflatThrust additive — feeds ship-velocity integration.
turnSpeedradians/frame at 60 fpsTurn-rate additive.
weaponDamagePctpercentageWeapon damage bonus. 0 = no bonus.
fireRatePctpercentageFire-rate bonus. 0 = no bonus.
magnetRangeflat world unitsPickup magnet range additive.
luckflatAffects drop quality.

Any stat omitted from the block is implicitly 0. Multiple stats per block are allowed (existing templates pair 2–3 stats — e.g. Core Reactor sets both shieldMax and shieldRegenRate).

Step 3 — Write the data file

Create src/starship-survivors/data/mods/<your-template>.ts. Required fields:

FieldTypeRequiredNotes
idstringyesMust be unique across MOD_TEMPLATES.
namestringyesDisplay label.
sizeModSizeyes{ w, h }, each 1..4.
cellsReadonlyArray<ReadonlyArray<boolean>>yesRow-major mask. Use rectCells or shape.
statsModStatBlockyesBase stats at T1 Common.
effectsEffectDef[]noOptional triggered effects via the unified effect engine. Omit for pure stat mods (the default and recommended path).

Existing 9 templates, for reference:

idnamesizestats
mod_chip_targeting+DMG1×1weaponDamagePct: 3
mod_plate_hull+HP2×1hpMax: 20
mod_core_reactor+SHIELD2×2shieldMax: 25, shieldRegenRate: 2
mod_strut_thruster+SPEED1×2maxSpeed: 12, thrust: 8
mod_array_magnet+MAGNET3×1magnetRange: 80, luck: 4
mod_block_armor+DEFENSE2×3hpMax: 60, damageReduction: 4
mod_bank_capacitor+DMG2×4fireRatePct: 10, weaponDamagePct: 5
mod_frame_titan+DEFENSE3×3hpMax: 100, shieldMax: 20, damageReduction: 3
mod_spire_gyro+HANDLING1×3turnSpeed: 0.08, maxSpeed: 6

Step 4 — Tier scaling

RARITY_MULTIPLIER in _types.ts multiplies the whole stats block at run-assemble time. Your T1 Common numbers double per tier.

TierRarityMultiplierSource
T1commonevery post-mission drop starts here
T2uncommonmerge 3 commons
T3raremerge 3 uncommons
T4epicmerge 3 rares
T5legendary16×merge 3 epics — never drops

81 commons compress into 1 legendary of the same template. Legendary is merge-only — drop rolls never produce it.

Step 5 — Register

Add to data/mods/index.ts:

  1. Add a named import alongside the existing 9 (e.g. import { mod_my_new } from './my-new';).
  2. Append mod_my_new to the MOD_TEMPLATES array.
  3. Update the seen.size !== 9 assert at the bottom of the file to the new count (e.g. !== 10).

Module-load asserts re-run on every import and will throw if shape or id constraints fail.

Step 6 — Drop rates

rollMissionDrops in services/mission-drops.ts rolls each drop independently. Template selection is uniform across MOD_TEMPLATES; rarity is rolled separately.

RollDistribution
Template1/N per template, where N = MOD_TEMPLATES.length
Rarity70% common, 30% uncommon

Adding a new template lowers the per-template chance for all templates (e.g. 9 → 10 templates moves each from 1/9 ≈ 11.1% to 1/10 = 10.0%). There is no per-template weight or rarity floor. The number of drops per mission is set by computeDropCount(tierReached, tierPB): 1 if the player fell short of tierPB - 1, 2 if equal to tierPB - 1 or first attempt (no PB), 3 if tierReached >= tierPB.

Step 7 — Validate

After registration, confirm:

CheckWhere
Appears in post-mission dropsRun a mission; the template should roll occasionally based on uniform 1/N.
Footprint snaps to the gridPlace the mod in the Mod Grid UI; it should occupy exactly the filled cells of the mask, respecting rotation.
Rarity tier shows correct stat valuesMerge 3 commons; the resulting uncommon’s tooltip should show base stats × 2.
No module-load errorsThe dev server starts cleanly. Shape-validation or id-uniqueness asserts throw at module load.

Custom-element structure rule

Mods are pure stat additions. No procs. No triggers. No on-hit, on-kill, or per-frame state. The effects? field exists for cases where the effect engine is the right fit, but the project’s design rule is: if a mod needs a trigger, it’s an artifact, not a mod. Use how-to-design-a-new-artifact for any concept that reacts to runtime events.