Skip to content

Extensibility

Every optimization the app does is modelled as an axis in the combination space. An axis represents “for this slot/item/scope, here are N options to try.”

interface OptimizationAxis {
id: string; // e.g. "slot:trinket1", "enchant:finger1"
label: string; // human-readable for UI
options: OptimizationOption[];
}
interface OptimizationOption {
id: string;
label: string;
simcLines: string[]; // SimC profile lines this option produces
}

The combination generator takes OptimizationAxis[] and returns the cartesian product. It doesn’t know or care whether an axis is a gear slot, a gem, an enchant, or a future feature.

Adding a new optimization type = defining new OptimizationAxis instances. Nothing else in the system needs to change.

┌─────────────────────────────────────────────────┐
│ LAYER 4: UI Components │
│ GearSlotCard, GemSelector, EnchantSelector │
│ Each produces OptimizationAxis[] for its domain │
├─────────────────────────────────────────────────┤
│ LAYER 3: Optimization Assembler │
│ Collects all axes, computes combo count │
├─────────────────────────────────────────────────┤
│ LAYER 2: Combination Generator (combinator.ts) │
│ Pure: OptimizationAxis[] → combinations[] │
├─────────────────────────────────────────────────┤
│ LAYER 1: ProfileSet Builder │
│ combinations[] + base profile → .simc file │
├─────────────────────────────────────────────────┤
│ LAYER 0: SimC Runner (Rust / Tauri command) │
│ Runs SimC, returns SimResult[] │
└─────────────────────────────────────────────────┘

Adding a new optimization type only touches Layer 4 (new UI component) and possibly Layer 3 (registering the new axis in the assembler).

Example: adding embellishment comparison.

src/components/optimizations/EmbellishmentSelector.tsx

This component lets the user select embellishments and exports:

function EmbellishmentSelector({ profile }: Props): {
axes: OptimizationAxis[];
ui: JSX.Element;
};

Step 2 — Register in the Optimization Assembler

Section titled “Step 2 — Register in the Optimization Assembler”

In optimization-assembler.ts, add the new component’s axes to the list passed to the combination generator.

If the feature has a known set of options, add a preset file:

src/lib/presets/embellishment-presets.ts
src/lib/features.ts
export const FEATURES = {
// ...
EMBELLISHMENT_COMPARISON: false, // set true when ready
} as const;

Presets export typed arrays for UI dropdowns:

src/lib/presets/
├── season-config.ts ← all season data
├── gem-presets.ts ← re-exports from season-config
├── enchant-presets.ts ← re-exports from season-config
└── (future)
├── embellishment-presets.ts
└── consumable-presets.ts
src-tauri/src/commands/my_feature.rs
  1. Define: #[tauri::command] pub async fn my_feature(...) -> Result<MyOutput, String>
  2. Register in main.rs: tauri::generate_handler![..., my_feature]
  3. Call from TS: invoke<MyOutput>('my_feature', { args })

Never put business logic in main.rs — it’s only a wiring file.

A feature is easy to add if:

  • It requires changes to at most 3 files
  • It does not modify the combination generator or ProfileSet builder
  • It does not modify any existing component (only adds new ones)
  • It can be feature-flagged with a single boolean

Every new module in src/lib/ must have a corresponding .test.ts file.

The combinator must have tests covering:

  • Single axis (no multiplication)
  • Multiple axes (cartesian product)
  • Cap enforcement (1000 max)
  • Empty axis (no options) → locked/pinned