feat: step-based missions around park machines with 3D objective arrow

Phase 5 of turning the game into the company landing page.

- Replace purchase-only missions with a data-driven list of short step
  chains (goto, locate on map, inspect, order, buy, buy at N places)
  in src/game/missions, with pure progress logic on the game state.
- Six missions, most about machines installed in Park Mellat; add two
  sites there (lake and sports fields) and record the product model
  installed at every site.
- Inspecting a machine opens a machine card with the installed model,
  specs, price, an order request and sales contact.
- The objective chip shows the current step; the missions sheet lists
  missions with the current one's steps ticked; a brand-coloured arrow
  hovers over the target machine.
- Rewards and banners per finished mission, the next step is spoken.
- Tests for mission data, step logic, text and the machine card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 09:19:25 +03:00
co-authored by Claude Opus 5.5
parent 5a4dffc9a3
commit bced4dc6f6
23 changed files with 745 additions and 176 deletions
+2 -24
View File
@@ -1,13 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
awardsFor,
rankFor,
completeMissions,
activeMission,
POINTS,
RANKS,
} from '../src/game/economy/scoring.js';
import { awardsFor, rankFor, POINTS, RANKS } from '../src/game/economy/scoring.js';
import { createState, purchase } from '../src/game/economy/gameState.js';
import { machineById } from '../src/constants/locations.js';
@@ -33,23 +26,8 @@ test('Discovery, combo and all-category bonuses are paid once', () => {
assert.ok(last.some((a) => a.points === POINTS.allCategories));
});
test('Ranks and missions advance with progress', () => {
test('Ranks advance with experience', () => {
assert.equal(rankFor(0).name, RANKS[0].name);
assert.equal(rankFor(RANKS[1].xp).index, 1);
assert.equal(rankFor(1e6).progress, 1);
const state = createState();
assert.equal(activeMission(state).id, 'factory');
purchase(state, machineById('factory'), 'water');
assert.deepEqual(
completeMissions(state).map((m) => m.id),
['factory'],
);
assert.equal(state.balance, 500 - 8 + 20, 'factory sells at half price');
purchase(state, machineById('mellat-park'), 'water');
assert.deepEqual(
completeMissions(state).map((m) => m.id),
['first'],
);
assert.deepEqual(completeMissions(state), [], 'rewards are not repeated');
assert.equal(activeMission(state).id, 'school');
});