Turn the game into a third-person Mashhad city game
Replace the linear forest walk with an open area of real western Mashhad built from OpenStreetMap data, starting at Park Mellat. A father and child, guided by the Robot Market robot, find vending machines at a park, school, universities, a metro station and a mall, on foot or by car. - Iran province briefing map; only Mashhad is unlocked - Third-person camera, building collision, arcade car and ambient traffic - Minimap and full city map with addresses, opening hours and A* routing - Purchase scoring, missions with cash rewards, ranks and an end screen - Map data build script (OSM + Natural Earth) and new logic tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import {CATEGORIES} from './locations.js';
|
||||
|
||||
export const POINTS = {purchase: 100, discovery: 250, combo: 150, allCategories: 500};
|
||||
|
||||
export const RANKS = [
|
||||
{name: 'تازهوارد', xp: 0},
|
||||
{name: 'کاوشگر', xp: 600},
|
||||
{name: 'راهبلد', xp: 1400},
|
||||
{name: 'شکارچی دستگاه', xp: 2600},
|
||||
{name: 'استاد نقشه', xp: 4200},
|
||||
{name: 'افسانهٔ مشهد', xp: 6500}
|
||||
];
|
||||
|
||||
// Awards for `record`, given the purchases that happened before it.
|
||||
export function awardsFor(previous, record) {
|
||||
const awards = [{label: 'خرید موفق', points: POINTS.purchase}];
|
||||
const here = previous.filter(p => p.machineId === record.machineId);
|
||||
if (!here.length) awards.push({label: 'کشف مکان جدید', points: POINTS.discovery});
|
||||
// Paid once per machine: the first time it has sold both a drink and a snack.
|
||||
const typesHere = new Set(here.map(p => p.type));
|
||||
if (typesHere.size === 1 && !typesHere.has(record.type)) awards.push({label: 'ترکیب نوشیدنی و خوراکی', points: POINTS.combo});
|
||||
const categories = new Set(previous.map(p => p.category));
|
||||
if (!categories.has(record.category) && categories.size === Object.keys(CATEGORIES).length - 1)
|
||||
awards.push({label: 'همهٔ انواع مکان', points: POINTS.allCategories});
|
||||
return awards;
|
||||
}
|
||||
|
||||
export function rankFor(xp) {
|
||||
let index = 0;
|
||||
while (index + 1 < RANKS.length && xp >= RANKS[index + 1].xp) index++;
|
||||
const next = RANKS[index + 1] ?? null;
|
||||
return {index, name: RANKS[index].name, min: RANKS[index].xp, next: next?.xp ?? null, progress: next ? (xp - RANKS[index].xp) / (next.xp - RANKS[index].xp) : 1};
|
||||
}
|
||||
|
||||
const placesVisited = purchases => new Set(purchases.map(p => p.machineId)).size;
|
||||
|
||||
export const MISSIONS = [
|
||||
{
|
||||
id: 'first', title: 'اولین خرید را از دستگاه بوستان ملت انجام بده', target: 'mellat-park', xp: 150, hetoman: 30,
|
||||
done: ps => ps.some(p => p.machineId === 'mellat-park')
|
||||
},
|
||||
{
|
||||
id: 'school', title: 'قبل از تعطیلی مدرسه (۱۳:۳۰) از دستگاه مدرسه خرید کن', target: 'school-rahil', xp: 200, hetoman: 40,
|
||||
done: ps => ps.some(p => p.machineId === 'school-rahil')
|
||||
},
|
||||
{
|
||||
id: 'campus', title: 'به دانشگاه فردوسی برو و یک خوراکی بخر', target: 'ferdowsi-university', xp: 200, hetoman: 40,
|
||||
done: ps => ps.some(p => p.machineId === 'ferdowsi-university' && p.type === 'snack')
|
||||
},
|
||||
{
|
||||
id: 'metro', title: 'از دستگاه ایستگاه مترو آزادی خرید کن', target: 'azadi-metro', xp: 150, hetoman: 30,
|
||||
done: ps => ps.some(p => p.machineId === 'azadi-metro')
|
||||
},
|
||||
{
|
||||
id: 'mall', title: 'با ماشین تا مشهد مال برو و خرید کن', target: 'mashhad-mall', xp: 250, hetoman: 50,
|
||||
done: ps => ps.some(p => p.machineId === 'mashhad-mall')
|
||||
},
|
||||
{
|
||||
id: 'explorer', title: 'از ۵ مکان متفاوت خرید کن', target: null, xp: 400, hetoman: 80,
|
||||
done: ps => placesVisited(ps) >= 5
|
||||
}
|
||||
];
|
||||
|
||||
// Marks and returns missions that the current purchases complete for the first time.
|
||||
export function completeMissions(state) {
|
||||
const fresh = MISSIONS.filter(m => !state.missionsDone.includes(m.id) && m.done(state.purchases));
|
||||
for (const m of fresh) {
|
||||
state.missionsDone.push(m.id);
|
||||
state.xp += m.xp;
|
||||
state.balance += m.hetoman;
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
export const activeMission = state => MISSIONS.find(m => !state.missionsDone.includes(m.id)) ?? null;
|
||||
Reference in New Issue
Block a user