Skip to content

Minigame Catalog

All 12 minigame types. Each entry covers: activity mapping, gameplay loop, Phaser 3 implementation primitives, scoring formula, event hash data, and duration bounds.

Engine: Phaser 3 (free, MIT-compatible, browser-native).


1. Rhythm — type: rhythm

Used for: Combat expeditions, boss encounters, elite encounters, group departure slots.

Gameplay

A horizontal track scrolls from right to left. Markers (beats) travel along the track. A hit zone is fixed on the left. Player presses Space or taps when a marker enters the zone.

  • Each beat has a tolerance window (ms)
  • Hit within window: perfect (1.0 pts), good (0.7 pts), ok (0.4 pts)
  • Miss: 0 pts
  • Total score = sum(hit pts) / (beat_count * 1.0)

Track length: seeded beat count = 12–24 beats at 120 BPM from seed. Higher skill band = wider tolerance window.

Phaser 3 Implementation

// Scene: RhythmScene extends Phaser.Scene
// Assets: 1 rectangle sprite per beat marker; 1 hit zone rectangle

// Setup
this.track = this.add.graphics();
this.hitZone = this.add.rectangle(80, 300, 20, 80, 0xffffff);
this.beats = this.physics.add.group();

// Beat spawner (seeded timing via Phaser.Math.RND seeded with session.seed)
this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
this.beatIntervals = generateBeatTimings(this.rnd, beatCount);

// Each beat
const marker = this.add.rectangle(800, 300, 30, 70, 0xff4444);
this.tweens.add({ targets: marker, x: 0, duration: 2000, onComplete: () => scoreMiss() });

// Input
this.input.keyboard.on('keydown-SPACE', () => checkHit());
this.input.on('pointerdown', () => checkHit()); // mobile tap

// Hit check
function checkHit() {
  const nearest = findNearestBeat(); // O(n) scan, small n
  const offset = Math.abs(nearest.x - hitZone.x);
  if (offset < PERFECT_WINDOW) recordHit('perfect');
  else if (offset < GOOD_WINDOW) recordHit('good');
  else recordHit('ok');
}

Event hash data: array of { beat_index, hit_type } sorted by beat_index.


2. Whack — type: whack

Used for: Mining, Quarrying, Logging, Salvage Recovery.

Gameplay

A 4×4 grid of stone/wood tiles. Highlighted tiles pop up one at a time (multiple may be active simultaneously at higher difficulty). Player clicks/taps active tiles before they fade.

  • 30 total spawns over 20 seconds
  • Each hit = 1 pt; each miss (tile fades without hit) = 0 pt
  • Score = hits / 30

Tile positions and spawn timing are seeded. Higher skill band = tiles stay visible longer.

Phaser 3 Implementation

// Scene: WhackScene extends Phaser.Scene
// Grid: 4x4 = 16 GameObjects.Rectangle tiles

const grid = [];
for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) {
  grid.push(this.add.rectangle(100 + c*100, 100 + r*100, 80, 80, 0x444444).setInteractive());
}

// Seeded spawn sequence
this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const spawnSeq = Array.from({ length: 30 }, () => this.rnd.integerInRange(0, 15));

this.time.addEvent({
  delay: 650,
  repeat: 29,
  callback: () => activateTile(spawnSeq[spawnIdx++]),
});

function activateTile(idx) {
  const tile = grid[idx];
  tile.setFillStyle(0xffaa00);
  tile.once('pointerdown', () => { recordHit(idx); tile.setFillStyle(0x444444); });
  this.time.delayedCall(visibleMs, () => {
    if (tile.fillColor === 0xffaa00) { recordMiss(idx); tile.setFillStyle(0x444444); }
  });
}

Event hash data: array of { tile_index, hit: true|false } in spawn order.


3. Sequence — type: sequence

Used for: Smithing, Carpentry, Leatherworking, Clothworking, Tinkering, Artifice, Masonry.

Gameplay

Simon Says variant. A 4-button color grid flashes a sequence. Player must reproduce it. Each successful round adds one step. Starts at 3 steps; target is 8 rounds.

  • Complete round = 1.0 / target_rounds pts
  • Wrong input = round failed, no points for that round
  • Score = correct_rounds / target_rounds

Sequence generated from seed.

Phaser 3 Implementation

// Scene: SequenceScene extends Phaser.Scene
// 4 buttons: top-left, top-right, bottom-left, bottom-right

const buttons = ['red','blue','green','yellow'].map((col, i) => {
  const x = 150 + (i % 2) * 200, y = 200 + Math.floor(i / 2) * 200;
  return this.add.rectangle(x, y, 160, 160, COLORS[col]).setInteractive();
});

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const masterSeq = Array.from({ length: 20 }, () => this.rnd.integerInRange(0, 3));

function playSequence(len) {
  let i = 0;
  this.time.addEvent({ delay: 600, repeat: len - 1, callback: () => {
    flashButton(buttons[masterSeq[i++]]);
  }});
}

function flashButton(btn) {
  btn.setAlpha(0.3);
  this.time.delayedCall(400, () => btn.setAlpha(1.0));
}

// After sequence plays, enable input
buttons.forEach((btn, i) => btn.on('pointerdown', () => checkInput(i)));

Event hash data: array of { round, button_index } for each player button press, in order.


4. Fishing — type: fishing

Used for: Fishing expeditions, all fishing board orders.

Gameplay

A bobber floats on an animated water surface. Periodically it dips (bite event). Player must:

  1. Hold mouse/touch when the bobber dips (pull phase)
  2. Hold for the correct duration (seeded 0.6–1.4s per catch)
  3. Release before the line snaps

Too early = miss. Too late = snap. Perfect timing window = hit.

  • 5 catch opportunities per session (seeded)
  • Each catch = 1 pt; miss/snap = 0 pt
  • Score = caught / 5

Phaser 3 Implementation

// Scene: FishingScene extends Phaser.Scene

this.bobber = this.add.circle(400, 300, 12, 0xff4444);
this.waterTween = this.tweens.add({
  targets: this.bobber, y: '+=8', yoyo: true, repeat: -1, duration: 800
});

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const biteDelays = Array.from({ length: 5 }, () => this.rnd.integerInRange(3000, 8000));
const holdWindows = Array.from({ length: 5 }, () => this.rnd.realInRange(0.6, 1.4) * 1000);

function scheduleBite(catchIdx) {
  this.time.delayedCall(biteDelays[catchIdx], () => {
    // Bobber dips fast
    this.tweens.add({ targets: this.bobber, y: '+= 30', duration: 200, yoyo: true });
    biteActive = true;
    biteStart = this.time.now;
  });
}

this.input.on('pointerdown', () => { if (biteActive) holdStart = this.time.now; });
this.input.on('pointerup', () => {
  const held = this.time.now - holdStart;
  const target = holdWindows[currentCatch];
  if (Math.abs(held - target) < tolerance) recordCatch();
  else recordMiss();
});

Event hash data: array of { catch_index, held_ms } for each attempt.


5. Recipe — type: recipe

Used for: Cooking, Preservation, Alchemy, high-complexity crafting sequences.

Gameplay

A recipe card shows 4–6 ingredient icons in a required order. Ingredient icons scroll past a conveyor belt. Player clicks them in the correct recipe order within the time window.

  • Correct ingredient in order = 1 pt
  • Wrong ingredient = 0 pt + slight penalty
  • Score = correct_placements / total_required

Ingredient types and scroll timing seeded.

Phaser 3 Implementation

// Scene: RecipeScene extends Phaser.Scene

const INGREDIENTS = ['herb','salt','oil','water','ash','crystal'];
this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);

// Generate recipe: 4–6 ingredients in seeded order
const recipe = Array.from({ length: this.rnd.integerInRange(4,6) }, () =>
  INGREDIENTS[this.rnd.integerInRange(0, INGREDIENTS.length - 1)]);

// Conveyor items: recipe items + 3 decoys, shuffled
const conveyor = [...recipe, ...decoys].sort(() => this.rnd.frac() - 0.5);

// Display recipe card (top of screen)
recipe.forEach((ing, i) => this.add.text(50 + i * 80, 30, ing, { fontSize: '14px' }));

// Spawn conveyor items as interactive sprites
conveyor.forEach((ing, i) => {
  const sprite = this.add.text(800 + i * 120, 300, ing, { fontSize: '20px' }).setInteractive();
  this.tweens.add({ targets: sprite, x: -100, duration: 6000 });
  sprite.on('pointerdown', () => checkIngredient(ing));
});

Event hash data: array of { step, ingredient_id, correct } in click order.


6. Spot — type: spot

Used for: Scouting, Surveying, Cartography, Weather Sense.

Gameplay

A grid of tiles (6×5) representing terrain. One tile has a hidden anomaly (unusual pattern, faint color variation — seeded). Player has 15 seconds to click where they think the anomaly is.

  • Score = 1.0 - (distance_to_correct / max_possible_distance) — clamped 0.0–1.0
  • Exact hit: 1.0; adjacent: ~0.85; far: grades down

Higher skill band = anomaly is slightly more visible.

Phaser 3 Implementation

// Scene: SpotScene extends Phaser.Scene

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const correctRow = this.rnd.integerInRange(0, 4);
const correctCol = this.rnd.integerInRange(0, 5);

// Draw grid
const tiles = [];
for (let r = 0; r < 5; r++) for (let c = 0; c < 6; c++) {
  const baseColor = 0x4a6741; // terrain green
  const isAnomaly = r === correctRow && c === correctCol;
  // Anomaly: slight hue shift seeded from rnd
  const color = isAnomaly
    ? Phaser.Display.Color.ValueToColor(baseColor).brighten(session.anomalyIntensity).color
    : baseColor;
  tiles.push(this.add.rectangle(80 + c*90, 80 + r*90, 80, 80, color).setInteractive());
}

this.input.once('pointerdown', (pointer) => {
  const clickCol = Math.floor((pointer.x - 40) / 90);
  const clickRow = Math.floor((pointer.y - 40) / 90);
  const dist = Math.sqrt((clickCol - correctCol)**2 + (clickRow - correctRow)**2);
  const maxDist = Math.sqrt(25 + 36); // diagonal of grid
  recordScore(1.0 - dist / maxDist);
});

// 15s timer
this.time.delayedCall(15000, () => forceEndIfNotClicked());

Event hash data: { clicked_row, clicked_col, correct_row, correct_col }.


7. Harvest — type: harvest

Used for: Herbalism, gathering (plants), Farming, quick-gather board orders.

Gameplay

Plant/herb icons appear across the screen and fade out over 1–2 seconds. Player taps/clicks as many as possible in 15 seconds.

  • 20 total spawns (seeded positions and timing)
  • Each click on active icon = 1 pt
  • Score = hits / 20

Phaser 3 Implementation

// Scene: HarvestScene extends Phaser.Scene

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const spawns = Array.from({ length: 20 }, () => ({
  x: this.rnd.integerInRange(50, 750),
  y: this.rnd.integerInRange(50, 550),
  delay: this.rnd.integerInRange(0, 12000),
  visible_ms: this.rnd.integerInRange(1000, 2500),
}));

spawns.forEach((s, i) => {
  this.time.delayedCall(s.delay, () => {
    const icon = this.add.circle(s.x, s.y, 22, 0x44cc44).setInteractive().setAlpha(1.0);
    this.tweens.add({ targets: icon, alpha: 0, duration: s.visible_ms,
      onComplete: () => { if (icon.active) recordMiss(i); icon.destroy(); }
    });
    icon.once('pointerdown', () => { recordHit(i); icon.destroy(); });
  });
});

Event hash data: array of { spawn_index, hit: true|false }.


8. Maze — type: maze

Used for: Routefinding, Navigation, Waterfinding, Campcraft (route to campsite).

Gameplay

A 10×10 grid maze. Start top-left, exit bottom-right. Player drags/taps cells to trace a path. Must find a valid route (no wall crossings) within the time limit.

  • Valid route within time = 1.0 base
  • Score = route_quality * time_bonus
  • route_quality = 1.0 if valid route found; 0.0 if not
  • time_bonus = 1.0 + (remaining_ms / total_ms) * 0.0 (score is binary success/fail; time adds nothing — kept simple)
  • Shortcut bonus: if route length < optimal * 1.2 → score = 1.0; otherwise 0.6

Maze generated deterministically from seed using recursive backtracker.

Phaser 3 Implementation

// Scene: MazeScene extends Phaser.Scene

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);

// Generate maze (recursive backtracker)
const maze = generateMaze(10, 10, this.rnd);

// Draw walls as Graphics rectangles
const g = this.add.graphics();
maze.walls.forEach(w => g.fillStyle(0x333333).fillRect(w.x, w.y, w.w, w.h));

// Player traces cells by pointerdown + pointermove
const path = [];
this.input.on('pointermove', (ptr) => {
  if (!ptr.isDown) return;
  const col = Math.floor(ptr.x / CELL_SIZE);
  const row = Math.floor(ptr.y / CELL_SIZE);
  if (isValidMove(path[path.length-1], {col,row}, maze)) {
    path.push({col,row});
    g.fillStyle(0x88ff88, 0.4).fillRect(col*CELL_SIZE, row*CELL_SIZE, CELL_SIZE, CELL_SIZE);
  }
});

// Check arrival at exit
// Submit button or auto-check on reaching (9,9)

Event hash data: { path_length, reached_exit: true|false, elapsed_ms }.


9. Steady — type: steady

Used for: Field Medicine, Healing, surgery-type craft actions (Alchemy, fine instrument work).

Gameplay

A circle appears on screen and shrinks toward a smaller target circle. Player must click when the outer circle's size matches the inner target.

  • 8 circles in sequence (seeded sizes and shrink rates)
  • Each: 1.0 - (abs(outer_radius - target_radius) / max_error) — clamped 0.0–1.0
  • Score = sum of per-circle scores / 8

Phaser 3 Implementation

// Scene: SteadyScene extends Phaser.Scene

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const targets = Array.from({ length: 8 }, () => ({
  targetRadius: this.rnd.integerInRange(20, 60),
  startRadius: 120,
  shrinkDuration: this.rnd.integerInRange(1500, 3000),
}));

function showCircle(idx) {
  const t = targets[idx];
  const outer = this.add.circle(400, 300, t.startRadius, 0x3399ff, 0.3);
  const inner = this.add.circle(400, 300, t.targetRadius, 0xffffff, 0.8);
  this.tweens.add({
    targets: outer, radius: 0, duration: t.shrinkDuration,
    onComplete: () => { recordMiss(idx); next(); },
    onUpdate: () => outer.setRadius(outer.radius) // radius prop driven by tween
  });
  this.input.once('pointerdown', () => {
    const err = Math.abs(outer.radius - t.targetRadius);
    recordScore(idx, 1.0 - Math.min(err / MAX_ERROR, 1.0));
    outer.destroy(); inner.destroy(); next();
  });
}

Event hash data: array of { circle_index, click_radius, target_radius }.


10. Sigil — type: sigil

Status: Currently reserved — not assigned to any active activity. Arcana skills are now passive (see Activity → Minigame Map). Kept in catalog for potential future use (puzzle boards, locked relics, arcane item unlock sequences, etc.).

Gameplay

A sigil board of 6 symbol positions flashes a sequence. Player must click the symbols in the same order within a time window.

  • 6-symbol sequence (seeded from symbol atlas of 8 symbols)
  • Each correct sequential click = 1 pt
  • Wrong symbol or timeout on position = 0 pt for remaining
  • Score = correct_in_order / 6

Phaser 3 Implementation

// Scene: SigilScene extends Phaser.Scene
// 8 symbols arranged in circle

const SYMBOLS = ['sun','moon','star','flame','wave','void','tree','stone'];
const positions = arrangeCircle(400, 300, 180, 8);

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const sequence = Array.from({ length: 6 }, () => this.rnd.integerInRange(0, 7));

const sprites = SYMBOLS.map((sym, i) => {
  return this.add.text(positions[i].x, positions[i].y, sym, { fontSize: '24px' })
    .setInteractive().setName(`${i}`);
});

// Flash sequence
let flashIdx = 0;
this.time.addEvent({ delay: 700, repeat: 5, callback: () => {
  sprites[sequence[flashIdx]].setStyle({ color: '#ffff00' });
  this.time.delayedCall(400, () => sprites[sequence[flashIdx]].setStyle({ color: '#ffffff' }));
  flashIdx++;
}});

// Player input phase (after sequence plays)
this.time.delayedCall(700 * 6 + 500, () => enableInput());
sprites.forEach(s => s.on('pointerdown', () => checkClick(parseInt(s.name))));

Event hash data: array of { position, symbol_index } in click order.


11. Price — type: price

Used for: Trade, Appraisal, Negotiation, market order actions.

Gameplay

A price needle swings like a pendulum across a value bar. A green "profit zone" is marked in the center. Player clicks when the needle is in the zone.

  • 5 swings (seeded speed and zone width)
  • Each: 1.0 if in zone, score by proximity otherwise
  • Score = sum / 5

Phaser 3 Implementation

// Scene: PriceScene extends Phaser.Scene

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);
const swingSpeeds = Array.from({ length: 5 }, () => this.rnd.realInRange(1500, 3500));
const zoneCentre = 400;
const zoneWidth = 80 + session.skillBand * 15; // wider with skill

// Needle pendulum
const needle = this.add.rectangle(400, 200, 8, 120, 0xffaa00);
let swingTween = this.tweens.add({
  targets: needle, x: 200, duration: swingSpeeds[0],
  yoyo: true, repeat: -1, ease: 'Sine.easeInOut'
});

// Zone indicator
const zone = this.add.rectangle(zoneCentre, 380, zoneWidth, 30, 0x44ff44, 0.4);

this.input.on('pointerdown', () => {
  const dist = Math.abs(needle.x - zoneCentre);
  const halfZone = zoneWidth / 2;
  const score = dist < halfZone ? 1.0 : Math.max(0, 1.0 - (dist - halfZone) / 150);
  recordSwing(score);
  if (swingsDone === totalSwings) endScene();
});

Event hash data: array of { swing_index, needle_x } at each click.


12. Patience — type: patience

Used for: Animal Handling, Beastbonding, Taming, Mount Husbandry.

Gameplay

An animal trust meter oscillates between 0 and 100 via a smooth sine wave with noise. Player must click when the meter is in a high-trust zone (top 30%). Multiple peaks over 30 seconds.

  • Score = peaks_hit / total_peaks (peaks = meter crosses above threshold)
  • Total peaks seeded (8–14 per session)

Phaser 3 Implementation

// Scene: PatienceScene extends Phaser.Scene

this.rnd = new Phaser.Math.RandomDataGenerator([session.seed]);

// Trust meter: composite sine + noise from seed
const phases = [
  { freq: 0.0015, amp: 40 },
  { freq: 0.0037, amp: 20 },
  { freq: 0.0071, amp: 10 },
];
const noiseOffset = this.rnd.frac() * 1000;

const bar = this.add.rectangle(100, 300, 20, 0, 0x44ff44).setOrigin(0.5, 1);
const threshold = 70; // trust % threshold

// Compute trust at time t
function trustAt(t) {
  const v = phases.reduce((sum, p) => sum + Math.sin((t + noiseOffset) * p.freq) * p.amp, 50);
  return Phaser.Math.Clamp(v, 0, 100);
}

// Draw bar each frame
this.time.addEvent({ delay: 16, loop: true, callback: () => {
  const t = this.time.now;
  const trust = trustAt(t);
  bar.height = (trust / 100) * 300;
  if (trust > threshold && !wasAbove) { wasAbove = true; peakStart = t; }
  if (trust <= threshold && wasAbove) { wasAbove = false; recordPeakEnd(peakStart, t); }
}});

this.input.on('pointerdown', () => {
  const trust = trustAt(this.time.now);
  if (trust > threshold) recordHit(); else recordMiss();
});

Event hash data: array of { click_index, trust_value_at_click }.