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
+4 -31
View File
@@ -17,14 +17,9 @@ import {
swipePayment,
cancelPayment,
} from '../economy/payment.js';
import {
awardsFor,
rankFor,
completeMissions,
activeMission,
MISSIONS,
} from '../economy/scoring.js';
import { awardsFor, rankFor } from '../economy/scoring.js';
import { strings } from '../../i18n/fa.js';
import { MISSIONS } from '../missions/missions.js';
/**
* Runs the open showroom operation for this subsystem.
* @param {*} context - context.
@@ -266,15 +261,7 @@ export function reward(context, record, product) {
3200,
);
context.say('child', strings.dialogue.childGotIt(product.icon, product.name));
const done = completeMissions(context.state);
for (const m of done) {
context.showBanner(
strings.rewards.missionDone,
m.title,
strings.rewards.missionReward(fa(m.xp), formatHetoman(m.hetoman)),
);
context.tone(660, 0.5);
}
const { mission } = context.reportMissionEvent({ type: 'buy', machineId: record.machineId });
if (rankFor(context.state.xp).index > rankBefore)
context.setTimeout(
() =>
@@ -283,22 +270,8 @@ export function reward(context, record, product) {
rankFor(context.state.xp).name,
strings.rewards.findMore,
),
done.length ? 3400 : 0,
mission ? 3400 : 0,
);
const next = activeMission(context.state);
if (next) {
context.waypoint = null;
context.route = null;
context.setTimeout(
() =>
context.say(
context.guideOut() ? 'robot' : 'father',
strings.dialogue.nextMission(next.title),
'wink',
),
1500,
);
}
context.refreshHUD();
context.checkEnd();
}