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.
| Field | Description |
|---|---|
id | Unique 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. |
name | Short display label (UPPERCASE with leading +). E.g. +DMG, +HP, +SHIELD. Repeats across templates that share a stat theme are allowed. |
size | Bounding-box { w, h }. Both w and h must satisfy 1 ≤ n ≤ 4. |
cells | Row-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 key | Unit | Notes |
|---|---|---|
hpMax | flat HP | Adds to hull max HP. |
shieldMax | flat shield | Adds to shield pool max. |
shieldRegenRate | flat units/s | Shield regen-rate additive. |
damageReduction | flat | Damage reduction additive. |
maxSpeed | flat units/s | Speed cap additive. |
thrust | flat | Thrust additive — feeds ship-velocity integration. |
turnSpeed | radians/frame at 60 fps | Turn-rate additive. |
weaponDamagePct | percentage | Weapon damage bonus. 0 = no bonus. |
fireRatePct | percentage | Fire-rate bonus. 0 = no bonus. |
magnetRange | flat world units | Pickup magnet range additive. |
luck | flat | Affects 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:
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Must be unique across MOD_TEMPLATES. |
name | string | yes | Display label. |
size | ModSize | yes | { w, h }, each 1..4. |
cells | ReadonlyArray<ReadonlyArray<boolean>> | yes | Row-major mask. Use rectCells or shape. |
stats | ModStatBlock | yes | Base stats at T1 Common. |
effects | EffectDef[] | no | Optional triggered effects via the unified effect engine. Omit for pure stat mods (the default and recommended path). |
Existing 9 templates, for reference:
id | name | size | stats |
|---|---|---|---|
mod_chip_targeting | +DMG | 1×1 | weaponDamagePct: 3 |
mod_plate_hull | +HP | 2×1 | hpMax: 20 |
mod_core_reactor | +SHIELD | 2×2 | shieldMax: 25, shieldRegenRate: 2 |
mod_strut_thruster | +SPEED | 1×2 | maxSpeed: 12, thrust: 8 |
mod_array_magnet | +MAGNET | 3×1 | magnetRange: 80, luck: 4 |
mod_block_armor | +DEFENSE | 2×3 | hpMax: 60, damageReduction: 4 |
mod_bank_capacitor | +DMG | 2×4 | fireRatePct: 10, weaponDamagePct: 5 |
mod_frame_titan | +DEFENSE | 3×3 | hpMax: 100, shieldMax: 20, damageReduction: 3 |
mod_spire_gyro | +HANDLING | 1×3 | turnSpeed: 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.
| Tier | Rarity | Multiplier | Source |
|---|---|---|---|
| T1 | common | 1× | every post-mission drop starts here |
| T2 | uncommon | 2× | merge 3 commons |
| T3 | rare | 4× | merge 3 uncommons |
| T4 | epic | 8× | merge 3 rares |
| T5 | legendary | 16× | 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:
- Add a named import alongside the existing 9 (e.g.
import { mod_my_new } from './my-new';). - Append
mod_my_newto theMOD_TEMPLATESarray. - Update the
seen.size !== 9assert 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.
| Roll | Distribution |
|---|---|
| Template | 1/N per template, where N = MOD_TEMPLATES.length |
| Rarity | 70% 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:
| Check | Where |
|---|---|
| Appears in post-mission drops | Run a mission; the template should roll occasionally based on uniform 1/N. |
| Footprint snaps to the grid | Place the mod in the Mod Grid UI; it should occupy exactly the filled cells of the mask, respecting rotation. |
| Rarity tier shows correct stat values | Merge 3 commons; the resulting uncommon’s tooltip should show base stats × 2. |
| No module-load errors | The 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.