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:
- Hold mouse/touch when the bobber dips (pull phase)
- Hold for the correct duration (seeded 0.6–1.4s per catch)
- 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 nottime_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 }.