refactor: centralize brand, company info, assets and Persian strings
Phase 1 of turning the game into the company landing page. - Add src/config/company.js with name, tagline, contact, about and the product line, plus a brand palette built around #00a693 (contrast checked); placeholders are marked TODO(company). - Add src/config/assets.js listing every loaded file with its expected format; placeholders are marked TODO(asset). - Move all Persian interface text, dialogue, missions and scenery text into src/i18n/fa.js; components and game code read from it. - Expose the palette as CSS variables (--color-primary and shades) and Tailwind colours, and use it for 3D materials and map drawings. - Fix the arrival line that still called the manager's car a white pickup. - Add tests that block hard-coded Persian text and low-contrast colours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import * as THREE from 'three';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
|
||||
// Location-based AR: the phone's rear camera is the background and the 3D camera stands where the
|
||||
// phone is (GPS) and points where the phone points (orientation sensors), so machines and the
|
||||
@@ -119,8 +120,7 @@ export function createArView(video) {
|
||||
// Opens the rear camera. Resolves to null on success or to a message explaining the failure.
|
||||
async start() {
|
||||
if (stream) return null;
|
||||
if (!navigator.mediaDevices?.getUserMedia)
|
||||
return 'این مرورگر به دوربین دسترسی نمیدهد (HTTPS لازم است).';
|
||||
if (!navigator.mediaDevices?.getUserMedia) return strings.real.cameraNeedsHttps;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
@@ -139,8 +139,8 @@ export function createArView(video) {
|
||||
} catch (error) {
|
||||
stream = null;
|
||||
return error?.name === 'NotAllowedError'
|
||||
? 'اجازهٔ دوربین داده نشد؛ از تنظیمات مرورگر اجازه بدهید.'
|
||||
: 'دوربین باز نشد؛ برنامهٔ دیگری از آن استفاده نمیکند؟';
|
||||
? strings.real.cameraDenied
|
||||
: strings.real.cameraBusy;
|
||||
}
|
||||
video.srcObject = stream;
|
||||
video.hidden = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toLocal } from '../../utils/geo.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
|
||||
// Real-world mode: the family follows the phone's GPS position. Phone GPS in a city is off by
|
||||
// 5–20 m and jumps around, so fixes are blended by their accuracy and arrival uses a wide radius.
|
||||
@@ -124,11 +125,11 @@ export function startTracking({
|
||||
stabilizer = createStabilizer(),
|
||||
}) {
|
||||
if (!('geolocation' in navigator)) {
|
||||
onError('این دستگاه موقعیتیابی (GPS) ندارد.');
|
||||
onError(strings.real.noGps);
|
||||
return () => {};
|
||||
}
|
||||
if (!window.isSecureContext) {
|
||||
onError('موقعیتیابی فقط روی HTTPS کار میکند؛ بازی را از نشانی https:// باز کنید.');
|
||||
onError(strings.real.needsHttps);
|
||||
return () => {};
|
||||
}
|
||||
let fixes = 0;
|
||||
@@ -160,17 +161,10 @@ export function startTracking({
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
const fail = (error) => {
|
||||
if (error.code === error.PERMISSION_DENIED)
|
||||
onError(
|
||||
'اجازهٔ دسترسی به موقعیت داده نشد. در تنظیمات مرورگر و گوشی، Location را برای این سایت مجاز کنید. (کد ۱)',
|
||||
);
|
||||
else if (error.code === error.POSITION_UNAVAILABLE)
|
||||
onError(
|
||||
'موقعیت در دسترس نیست. Location (GPS) گوشی را روشن کنید؛ روی کامپیوتر معمولاً موقعیتیابی کار نمیکند. (کد ۲)',
|
||||
);
|
||||
if (error.code === error.PERMISSION_DENIED) onError(strings.real.denied);
|
||||
else if (error.code === error.POSITION_UNAVAILABLE) onError(strings.real.unavailable);
|
||||
// A timeout only means no fix yet; the watch keeps trying, so it is a status, not an error.
|
||||
else if (!fixes)
|
||||
onStatus('هنوز سیگنال GPS نرسیده… در فضای باز و کنار پنجره سریعتر پیدا میشود. (کد ۳)');
|
||||
else if (!fixes) onStatus(strings.real.timeout);
|
||||
};
|
||||
// A quick coarse fix from Wi-Fi or the mobile network shows the player at once; GPS refines it.
|
||||
navigator.geolocation.getCurrentPosition(handle, () => {}, {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
// Money is counted in whole hetoman: 10 hetoman = 100,000 rial.
|
||||
export const INITIAL_BALANCE = 500;
|
||||
export const START_MINUTES = 8 * 60 + 30;
|
||||
@@ -12,7 +13,7 @@ export const fa = (n) => n.toLocaleString('fa-IR');
|
||||
* @param {*} value - value.
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
export const formatHetoman = (value) => `${fa(value)} هتومان`;
|
||||
export const formatHetoman = (value) => strings.units.hetoman(fa(value));
|
||||
/**
|
||||
* Runs the hetoman to rial operation for this subsystem.
|
||||
* @param {*} value - value.
|
||||
@@ -31,63 +32,63 @@ export const formatClock = (minutes) => {
|
||||
export const catalog = {
|
||||
juice: {
|
||||
id: 'juice',
|
||||
name: 'آبمیوه',
|
||||
name: strings.snacks.juice,
|
||||
icon: '🧃',
|
||||
cost: 35,
|
||||
type: 'drink',
|
||||
},
|
||||
water: {
|
||||
id: 'water',
|
||||
name: 'آب معدنی',
|
||||
name: strings.snacks.water,
|
||||
icon: '💧',
|
||||
cost: 15,
|
||||
type: 'drink',
|
||||
},
|
||||
milk: {
|
||||
id: 'milk',
|
||||
name: 'شیر کاکائو',
|
||||
name: strings.snacks.milk,
|
||||
icon: '🥛',
|
||||
cost: 40,
|
||||
type: 'drink',
|
||||
},
|
||||
tea: {
|
||||
id: 'tea',
|
||||
name: 'چای سرد',
|
||||
name: strings.snacks.tea,
|
||||
icon: '🧋',
|
||||
cost: 45,
|
||||
type: 'drink',
|
||||
},
|
||||
lemonade: {
|
||||
id: 'lemonade',
|
||||
name: 'لیموناد',
|
||||
name: strings.snacks.lemonade,
|
||||
icon: '🍋',
|
||||
cost: 40,
|
||||
type: 'drink',
|
||||
},
|
||||
biscuit: {
|
||||
id: 'biscuit',
|
||||
name: 'بیسکویت',
|
||||
name: strings.snacks.biscuit,
|
||||
icon: '🍪',
|
||||
cost: 25,
|
||||
type: 'snack',
|
||||
},
|
||||
chocolate: {
|
||||
id: 'chocolate',
|
||||
name: 'شکلات',
|
||||
name: strings.snacks.chocolate,
|
||||
icon: '🍫',
|
||||
cost: 30,
|
||||
type: 'snack',
|
||||
},
|
||||
nuts: {
|
||||
id: 'nuts',
|
||||
name: 'آجیل',
|
||||
name: strings.snacks.nuts,
|
||||
icon: '🥜',
|
||||
cost: 65,
|
||||
type: 'snack',
|
||||
},
|
||||
cake: {
|
||||
id: 'cake',
|
||||
name: 'کیک',
|
||||
name: strings.snacks.cake,
|
||||
icon: '🧁',
|
||||
cost: 35,
|
||||
type: 'snack',
|
||||
|
||||
+18
-17
@@ -1,4 +1,5 @@
|
||||
import { CATEGORIES } from '../../constants/locations.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
|
||||
// The bonus for visiting every kind of place counts city locations, not the factory.
|
||||
const CITY_CATEGORIES = Object.keys(CATEGORIES).filter((c) => c !== 'factory' && c !== 'test');
|
||||
@@ -10,27 +11,27 @@ export const POINTS = {
|
||||
};
|
||||
export const RANKS = [
|
||||
{
|
||||
name: 'تازهوارد',
|
||||
name: strings.ranks[0],
|
||||
xp: 0,
|
||||
},
|
||||
{
|
||||
name: 'کاوشگر',
|
||||
name: strings.ranks[1],
|
||||
xp: 600,
|
||||
},
|
||||
{
|
||||
name: 'راهبلد',
|
||||
name: strings.ranks[2],
|
||||
xp: 1400,
|
||||
},
|
||||
{
|
||||
name: 'شکارچی دستگاه',
|
||||
name: strings.ranks[3],
|
||||
xp: 2600,
|
||||
},
|
||||
{
|
||||
name: 'استاد نقشه',
|
||||
name: strings.ranks[4],
|
||||
xp: 4200,
|
||||
},
|
||||
{
|
||||
name: 'افسانهٔ مشهد',
|
||||
name: strings.ranks[5],
|
||||
xp: 6500,
|
||||
},
|
||||
];
|
||||
@@ -45,21 +46,21 @@ export const RANKS = [
|
||||
export function awardsFor(previous, record) {
|
||||
const awards = [
|
||||
{
|
||||
label: 'خرید موفق',
|
||||
label: strings.bonuses.purchase,
|
||||
points: POINTS.purchase,
|
||||
},
|
||||
];
|
||||
const here = previous.filter((p) => p.machineId === record.machineId);
|
||||
if (!here.length)
|
||||
awards.push({
|
||||
label: 'کشف مکان جدید',
|
||||
label: strings.bonuses.newPlace,
|
||||
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: 'ترکیب نوشیدنی و خوراکی',
|
||||
label: strings.bonuses.combo,
|
||||
points: POINTS.combo,
|
||||
});
|
||||
const categories = new Set(
|
||||
@@ -71,7 +72,7 @@ export function awardsFor(previous, record) {
|
||||
categories.size === CITY_CATEGORIES.length - 1
|
||||
)
|
||||
awards.push({
|
||||
label: 'همهٔ انواع مکان',
|
||||
label: strings.bonuses.allCategories,
|
||||
points: POINTS.allCategories,
|
||||
});
|
||||
return awards;
|
||||
@@ -102,7 +103,7 @@ const placesVisited = (purchases) => new Set(purchases.map((p) => p.machineId)).
|
||||
export const MISSIONS = [
|
||||
{
|
||||
id: 'factory',
|
||||
title: 'در دفتر فنی مهندسی کارخانه از دستگاه وندینگ خرید کن',
|
||||
title: strings.missions.factory,
|
||||
target: 'factory',
|
||||
xp: 100,
|
||||
hetoman: 20,
|
||||
@@ -110,7 +111,7 @@ export const MISSIONS = [
|
||||
},
|
||||
{
|
||||
id: 'first',
|
||||
title: 'با ماشین تا بوستان ملت برو و اولین خرید شهری را انجام بده',
|
||||
title: strings.missions.first,
|
||||
target: 'mellat-park',
|
||||
xp: 150,
|
||||
hetoman: 30,
|
||||
@@ -118,7 +119,7 @@ export const MISSIONS = [
|
||||
},
|
||||
{
|
||||
id: 'school',
|
||||
title: 'قبل از تعطیلی مدرسه (۱۳:۳۰) از دستگاه مدرسه خرید کن',
|
||||
title: strings.missions.school,
|
||||
target: 'school-rahil',
|
||||
xp: 200,
|
||||
hetoman: 40,
|
||||
@@ -126,7 +127,7 @@ export const MISSIONS = [
|
||||
},
|
||||
{
|
||||
id: 'campus',
|
||||
title: 'به دانشگاه فردوسی برو و یک خوراکی بخر',
|
||||
title: strings.missions.campus,
|
||||
target: 'ferdowsi-university',
|
||||
xp: 200,
|
||||
hetoman: 40,
|
||||
@@ -134,7 +135,7 @@ export const MISSIONS = [
|
||||
},
|
||||
{
|
||||
id: 'metro',
|
||||
title: 'از دستگاه ایستگاه مترو آزادی خرید کن',
|
||||
title: strings.missions.metro,
|
||||
target: 'azadi-metro',
|
||||
xp: 150,
|
||||
hetoman: 30,
|
||||
@@ -142,7 +143,7 @@ export const MISSIONS = [
|
||||
},
|
||||
{
|
||||
id: 'mall',
|
||||
title: 'با ماشین تا مشهد مال برو و خرید کن',
|
||||
title: strings.missions.mall,
|
||||
target: 'mashhad-mall',
|
||||
xp: 250,
|
||||
hetoman: 50,
|
||||
@@ -150,7 +151,7 @@ export const MISSIONS = [
|
||||
},
|
||||
{
|
||||
id: 'explorer',
|
||||
title: 'از ۵ مکان متفاوت شهر خرید کن',
|
||||
title: strings.missions.explorer,
|
||||
target: null,
|
||||
xp: 400,
|
||||
hetoman: 80,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import * as THREE from 'three';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
|
||||
// Detailed vehicles for the family and the factory, plus the shared helpers luxuryCars.js builds
|
||||
// on. All face local +X and sit on y = 0, like createCarModel in traffic.js; `wheels` lists rear wheels first, then the two front (steering) ones.
|
||||
@@ -337,7 +341,7 @@ export function createTruck(color = '#e9eef0') {
|
||||
const black = mat('#1b1d1f', {
|
||||
roughness: 0.7,
|
||||
}),
|
||||
teal = mat('#0b8f88', {
|
||||
teal = mat(BRAND.primary, {
|
||||
metalness: 0.3,
|
||||
roughness: 0.4,
|
||||
});
|
||||
|
||||
+65
-78
@@ -6,6 +6,11 @@ import { MANAGER_CAR_BRAND, MOTORCYCLE_COUNT, VEHICLE_BLACK } from '../../consta
|
||||
import { createPerson, createCrew, nameTag } from './staff.js';
|
||||
import { FACTORY } from '../../constants/locations.js';
|
||||
import { textTexture } from './textures.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
/**
|
||||
* Runs the stripes operation for this subsystem.
|
||||
* @param {*} c - c.
|
||||
@@ -32,9 +37,9 @@ function stripes(c, w, h, base, line, every) {
|
||||
*/
|
||||
const sign = (text, sub = '') =>
|
||||
textTexture(512, 160, (c, w, h) => {
|
||||
c.fillStyle = '#0d2f2c';
|
||||
c.fillStyle = BRAND.primaryDarker;
|
||||
c.fillRect(0, 0, w, h);
|
||||
c.fillStyle = '#3ee0c9';
|
||||
c.fillStyle = BRAND.primary;
|
||||
c.fillRect(0, h - 12, w, 12);
|
||||
c.textAlign = 'center';
|
||||
c.direction = 'rtl';
|
||||
@@ -42,7 +47,7 @@ const sign = (text, sub = '') =>
|
||||
c.font = `bold ${sub ? 58 : 66}px Vazirmatn, Tahoma, sans-serif`;
|
||||
c.fillText(text, w / 2, sub ? 72 : 100);
|
||||
if (sub) {
|
||||
c.fillStyle = '#9fe9da';
|
||||
c.fillStyle = BRAND.primaryLighter;
|
||||
c.font = '36px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText(sub, w / 2, 128);
|
||||
}
|
||||
@@ -113,25 +118,20 @@ const CODE = {
|
||||
),
|
||||
sheet: screen(
|
||||
[
|
||||
['فاکتور دستگاه مبلغ', '#1d3b2f'],
|
||||
['۱۰۴۲ بوستان ملت ۴۸۰', '#333'],
|
||||
['۱۰۴۳ دانشگاه فردوسی ۳۲۰', '#333'],
|
||||
['۱۰۴۴ مترو آزادی ۲۹۰', '#333'],
|
||||
['۱۰۴۵ مشهد مال ۶۱۰', '#333'],
|
||||
['۱۰۴۶ مدرسه راحیل ۱۸۰', '#333'],
|
||||
['جمع ۱۸۸۰', '#0b8f88'],
|
||||
// Header, five invoice rows, then the total in the brand colour.
|
||||
...strings.factory.decor.invoices.map((line, i, all) => [
|
||||
line,
|
||||
['#1d3b2f', ...Array(all.length - 2).fill('#333'), COMPANY.colors.primaryDark][i],
|
||||
]),
|
||||
],
|
||||
'#f4f7f2',
|
||||
),
|
||||
dashboard: screen(
|
||||
[
|
||||
['فروش امروز: ۱۲۸ دستگاه', '#3ee0c9'],
|
||||
['تولید هفته: ۴۶ دستگاه', '#f2c94c'],
|
||||
['ارسال شده: ۳۸', '#eeffff'],
|
||||
['', '#fff'],
|
||||
['▇▇▇▇▇▇▇▇▇▇▇ خم', '#82aaff'],
|
||||
['▇▇▇▇▇▇▇▇▇ برش', '#82aaff'],
|
||||
['▇▇▇▇▇▇▇ رنگ', '#82aaff'],
|
||||
...strings.factory.decor.dashboard.map((line, i) => [
|
||||
line,
|
||||
[COMPANY.colors.primary, '#f2c94c', '#eeffff', '#fff'][i] ?? '#82aaff',
|
||||
]),
|
||||
],
|
||||
'#101820',
|
||||
),
|
||||
@@ -507,7 +507,7 @@ function factoryBuildFlat(build) {
|
||||
build.steel = build.mat('#59646b', {
|
||||
metalness: 0.5,
|
||||
});
|
||||
build.teal = build.mat('#0b8f88', {
|
||||
build.teal = build.mat(BRAND.primary, {
|
||||
metalness: 0.3,
|
||||
roughness: 0.4,
|
||||
});
|
||||
@@ -563,16 +563,16 @@ function factoryBuildAnimated(build) {
|
||||
*/
|
||||
function factoryBuildNameSign(build) {
|
||||
build.nameSign = textTexture(1024, 320, (c, w, h) => {
|
||||
c.fillStyle = '#0d2f2c';
|
||||
c.fillStyle = BRAND.primaryDarker;
|
||||
c.fillRect(0, 0, w, h);
|
||||
c.fillStyle = '#3ee0c9';
|
||||
c.fillStyle = BRAND.primary;
|
||||
c.fillRect(0, h - 18, w, 18);
|
||||
c.textAlign = 'center';
|
||||
c.direction = 'rtl';
|
||||
c.fillStyle = '#ffffff';
|
||||
c.font = 'bold 66px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText(FACTORY.name, w / 2, 120);
|
||||
c.fillStyle = '#9fe9da';
|
||||
c.fillStyle = BRAND.primaryLighter;
|
||||
c.font = '44px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText(FACTORY.tagline, w / 2, 210);
|
||||
c.font = 'bold 34px sans-serif';
|
||||
@@ -716,12 +716,8 @@ function factoryBuildGeometry3(build) {
|
||||
for (const du of [-2.2, 2.2])
|
||||
build.box(u + du - 0.03, u + du + 0.03, 20.97, 21.03, 7.45, build.H, build.steel, false);
|
||||
};
|
||||
build.zone('خمکاری', 'ورقکاری بدنهٔ دستگاه', -93);
|
||||
build.zone('برش لیزر', 'برش ورق و قطعات', -71);
|
||||
build.zone('رنگکاری', 'کابین رنگ پودری', -52);
|
||||
build.zone('یخچالسازی', 'کمپرسور و عایقکاری', -31);
|
||||
build.zone('مونتاژ', 'خط مونتاژ نهایی', -13);
|
||||
build.zone('بستهبندی', 'ارسال به شهر', -3);
|
||||
// Work areas along the hall, from the press brakes to packing (u of each sign).
|
||||
[-93, -71, -52, -31, -13, -3].forEach((u, i) => build.zone(...strings.factory.zones[i], u));
|
||||
|
||||
// Bending: two press brakes with a moving ram, sheet stacks behind.
|
||||
}
|
||||
@@ -942,7 +938,7 @@ function factoryBuildGeometry6(build) {
|
||||
*/
|
||||
function factoryBuildFrames(build) {
|
||||
build.frames = [];
|
||||
build.frameMaterial = build.mat('#0b8f88', {
|
||||
build.frameMaterial = build.mat(BRAND.primary, {
|
||||
wireframe: true,
|
||||
});
|
||||
for (let i = 0; i < 4; i++) {
|
||||
@@ -990,7 +986,7 @@ function factoryBuildGeometry7(build) {
|
||||
function factoryBuildGeometry8(build) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const finished = createVendingMachine({
|
||||
name: 'آمادهٔ نصب',
|
||||
name: strings.factory.readyToInstall,
|
||||
products: ['water', 'juice', 'biscuit', 'chocolate'],
|
||||
});
|
||||
finished.beacon.visible = finished.ring.visible = false;
|
||||
@@ -1014,7 +1010,7 @@ function factoryBuildCarton(build) {
|
||||
c.fillRect(0, 0, w, h);
|
||||
c.fillStyle = 'rgba(255,255,255,.18)';
|
||||
for (let y = 0; y < h; y += 40) c.fillRect(0, y, w, 14);
|
||||
c.fillStyle = '#0b8f88';
|
||||
c.fillStyle = BRAND.primary;
|
||||
c.fillRect(0, h * 0.42, w, 34);
|
||||
c.fillStyle = '#1d2a2a';
|
||||
c.textAlign = 'center';
|
||||
@@ -1294,9 +1290,9 @@ function factoryBuildGeometry9(build) {
|
||||
])
|
||||
build.box(u0, u1, -36, -34, build.OH - 0.08, build.OH - 0.02, build.lamp, false).castShadow =
|
||||
false;
|
||||
build.panel(sign('دفتر فنی مهندسی', 'Engineering'), 3.6, 1.1, 15.5, 3.25, build.F + 0.05);
|
||||
build.panel(sign('واحد حسابداری', 'Accounting'), 3.6, 1.1, 32.5, 3.25, build.F + 0.05);
|
||||
build.panel(sign('مدیریت', 'آقای محمدی'), 3.6, 1.1, 45, 3.25, build.F + 0.05);
|
||||
build.panel(sign(...strings.factory.signs.engineering), 3.6, 1.1, 15.5, 3.25, build.F + 0.05);
|
||||
build.panel(sign(...strings.factory.signs.accounting), 3.6, 1.1, 32.5, 3.25, build.F + 0.05);
|
||||
build.panel(sign(...strings.factory.signs.management), 3.6, 1.1, 45, 3.25, build.F + 0.05);
|
||||
build.wood = build.mat('#c8a477', {
|
||||
roughness: 0.6,
|
||||
});
|
||||
@@ -1322,7 +1318,7 @@ function factoryBuildFabric(build) {
|
||||
function factoryBuildDevelopers(build) {
|
||||
build.developers = [
|
||||
{
|
||||
name: 'حامد',
|
||||
name: strings.factory.staff.hamed.name,
|
||||
role: 'React',
|
||||
u: 11,
|
||||
v: -31.6,
|
||||
@@ -1333,13 +1329,10 @@ function factoryBuildDevelopers(build) {
|
||||
},
|
||||
code: CODE.react,
|
||||
speaker: 'hamed',
|
||||
lines: [
|
||||
'سلام! من حامدم. رابط کاربری صفحهٔ لمسی دستگاهها رو با React میسازم.',
|
||||
'هر چی روی صفحهٔ دستگاه میبینین، از همین مانیتور اومده!',
|
||||
],
|
||||
lines: strings.factory.staff.hamed.lines,
|
||||
},
|
||||
{
|
||||
name: 'رایان',
|
||||
name: strings.factory.staff.ryan.name,
|
||||
role: 'Go',
|
||||
u: 16,
|
||||
v: -31.6,
|
||||
@@ -1351,10 +1344,10 @@ function factoryBuildDevelopers(build) {
|
||||
},
|
||||
code: CODE.go,
|
||||
speaker: 'ryan',
|
||||
lines: ['سلام! من رایانم. سرور پرداخت و گزارش فروش دستگاهها رو با Go مینویسم.'],
|
||||
lines: strings.factory.staff.ryan.lines,
|
||||
},
|
||||
{
|
||||
name: 'علیرضا',
|
||||
name: strings.factory.staff.alireza.name,
|
||||
role: 'C++',
|
||||
u: 21,
|
||||
v: -31.6,
|
||||
@@ -1366,12 +1359,10 @@ function factoryBuildDevelopers(build) {
|
||||
},
|
||||
code: CODE.cpp,
|
||||
speaker: 'alireza',
|
||||
lines: [
|
||||
'سلام! علیرضام. فریمور بُرد کنترل دستگاه با C++ ـه؛ الان دارم موتورهای تحویل کالا رو تنظیم میکنم.',
|
||||
],
|
||||
lines: strings.factory.staff.alireza.lines,
|
||||
},
|
||||
{
|
||||
name: 'رضا',
|
||||
name: strings.factory.staff.reza.name,
|
||||
role: 'C++',
|
||||
u: 13.5,
|
||||
v: -38.6,
|
||||
@@ -1384,13 +1375,10 @@ function factoryBuildDevelopers(build) {
|
||||
},
|
||||
code: CODE.cpp,
|
||||
speaker: 'reza',
|
||||
lines: [
|
||||
'سلام، خوش اومدین. من رضام؛ کد C++ سنسورها و دمای یخچال دستگاه با منه.',
|
||||
'دستگاه وندینگ همین دفتر کنار دره؛ از اون خرید کنین!',
|
||||
],
|
||||
lines: strings.factory.staff.reza.lines,
|
||||
},
|
||||
{
|
||||
name: 'محمد',
|
||||
name: strings.factory.staff.mohammad.name,
|
||||
role: '',
|
||||
u: 18.5,
|
||||
v: -38.6,
|
||||
@@ -1402,7 +1390,7 @@ function factoryBuildDevelopers(build) {
|
||||
},
|
||||
code: CODE.terminal,
|
||||
speaker: 'mohammad',
|
||||
lines: ['سلام! من محمدم. به دفتر فنی مهندسی روبات مارکت خوش اومدین!'],
|
||||
lines: strings.factory.staff.mohammad.lines,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1444,12 +1432,10 @@ function factoryBuildBoard(build) {
|
||||
c.direction = 'rtl';
|
||||
c.fillStyle = '#1d4f91';
|
||||
c.font = 'bold 40px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText('اسپرینت ۱۴: دستگاه نسل جدید', w / 2, 56);
|
||||
c.fillText(strings.factory.decor.sprintTitle, w / 2, 56);
|
||||
c.font = '28px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillStyle = '#333';
|
||||
['صفحهٔ لمسی جدید ← React', 'سرویس پرداخت ← Go', 'فریمور موتور و سنسور ← C++'].forEach((t, i) =>
|
||||
c.fillText(t, w / 2, 110 + i * 44),
|
||||
);
|
||||
strings.factory.decor.sprintItems.forEach((t, i) => c.fillText(t, w / 2, 110 + i * 44));
|
||||
});
|
||||
build.panel(build.board, 4, 2, 16, 1.9, build.B + 0.05);
|
||||
build.officeMachine = {
|
||||
@@ -1538,10 +1524,10 @@ function factoryBuildGeometry11(build) {
|
||||
);
|
||||
build.talks.push({
|
||||
speaker: 'accountant',
|
||||
label: 'واحد حسابداری',
|
||||
label: strings.factory.accounting.label,
|
||||
radius: 4,
|
||||
pos: () => build.toWorld(33, -29),
|
||||
lines: ['سلام! اینجا فروش همهٔ دستگاهها ثبت میشه. هر خریدی بکنین، فاکتورش اینجا میاد!'],
|
||||
lines: strings.factory.accounting.lines,
|
||||
});
|
||||
|
||||
// Management: the manager's desk facing the door, guest chairs, bookshelf and flag.
|
||||
@@ -1576,7 +1562,15 @@ function factoryBuildGeometry12(build) {
|
||||
build.leaves = new THREE.Mesh(new THREE.SphereGeometry(0.45, 10, 8), build.mat('#3e7a3e'));
|
||||
build.leaves.position.set(49.25, 1.0, -26.75);
|
||||
build.group.add(build.leaves);
|
||||
build.nameplate = build.panel(sign('محمدی', 'مدیر شرکت'), 0.6, 0.19, 45, 0.87, -39.2, 1);
|
||||
build.nameplate = build.panel(
|
||||
sign(...strings.factory.signs.nameplate),
|
||||
0.6,
|
||||
0.19,
|
||||
45,
|
||||
0.87,
|
||||
-39.2,
|
||||
1,
|
||||
);
|
||||
build.nameplate.rotation.x = -0.3;
|
||||
|
||||
// ---------- Staff car park ----------
|
||||
@@ -1618,7 +1612,7 @@ function factoryBuildManager(build) {
|
||||
glasses: true,
|
||||
shoes: '#111',
|
||||
});
|
||||
build.managerTag = nameTag('آقای محمدی', 'مدیر شرکت');
|
||||
build.managerTag = nameTag(strings.factory.manager.name, strings.factory.manager.role);
|
||||
build.managerTag.position.y = 2.15;
|
||||
build.manager.root.add(build.managerTag);
|
||||
build.group.add(build.manager.root);
|
||||
@@ -1652,15 +1646,11 @@ function factoryBuildArrival(build) {
|
||||
};
|
||||
build.talks.push({
|
||||
speaker: 'mohammadi',
|
||||
label: 'آقای محمدی',
|
||||
label: strings.factory.manager.name,
|
||||
radius: 3.4,
|
||||
pos: () => build.toWorld(build.manager.root.position.x, build.manager.root.position.z),
|
||||
ready: () => build.arrival.stage === 'seated',
|
||||
lines: [
|
||||
'سلام، خوش اومدین! من محمدیام، مدیر روبات مارکت.',
|
||||
'امروز چهل نفر توی سالن تولید کار میکنن: خمکاری، برش، رنگ، یخچالسازی و مونتاژ.',
|
||||
'یه سر به دفتر فنی بزنین و از دستگاهش خرید کنین؛ بعد برین سراغ دستگاههای شهر!',
|
||||
],
|
||||
lines: strings.factory.managerLines,
|
||||
});
|
||||
|
||||
// Position and direction a distance `s` along a polyline of [u, v] points.
|
||||
@@ -1751,7 +1741,7 @@ function factoryBuildDelivery(build) {
|
||||
radius: 9,
|
||||
pos: () => build.toWorld(4, 31),
|
||||
once: true,
|
||||
lines: ['بابا ببین! کامیون داره دستگاههای بستهبندیشده رو بار میزنه که ببره شهر!'],
|
||||
lines: [strings.factory.truck],
|
||||
});
|
||||
// The family comments on each work area the first time they walk into it.
|
||||
}
|
||||
@@ -1761,23 +1751,20 @@ function factoryBuildDelivery(build) {
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function factoryBuildGeometry14(build) {
|
||||
for (const [u, text, speaker] of [
|
||||
[-93, 'اینجا خمکاریه؛ ورقهای فلزی بدنهٔ دستگاه رو با پرس خم میکنن.', 'father'],
|
||||
[-71, 'دستگاه برش لیزر رو ببین! ورق رو مثل کاغذ میبُره.', 'child'],
|
||||
[-52, 'این کابین رنگه؛ بدنهها اینجا رنگ فیروزهای روبات مارکت میگیرن.', 'father'],
|
||||
[-31, 'یخچالسازی! برای همینه که نوشیدنیهای دستگاه همیشه خنکن.', 'child'],
|
||||
[
|
||||
-13,
|
||||
'خط مونتاژ: روباتها و کارگرها قطعهها رو کنار هم میذارن و دستگاه کامل میشه.',
|
||||
'father',
|
||||
],
|
||||
])
|
||||
// u of each production area and who comments on it; the lines come from the strings file.
|
||||
for (const [i, [u, speaker]] of [
|
||||
[-93, 'father'],
|
||||
[-71, 'child'],
|
||||
[-52, 'father'],
|
||||
[-31, 'child'],
|
||||
[-13, 'father'],
|
||||
].entries())
|
||||
build.talks.push({
|
||||
speaker,
|
||||
radius: 8,
|
||||
pos: () => build.toWorld(u, 25),
|
||||
once: true,
|
||||
lines: [text],
|
||||
lines: [strings.factory.areaTalks[i]],
|
||||
});
|
||||
|
||||
// ---------- Key positions ----------
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import * as THREE from 'three';
|
||||
import { textTexture } from './textures.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
|
||||
// Factory staff. createPerson() builds one articulated office person (hoodie, suit, or manteau with
|
||||
// a headscarf) that can stand, walk, sit and type. createCrew() draws the production-line workers
|
||||
@@ -264,7 +268,7 @@ export function nameTag(name, role = '') {
|
||||
c.beginPath();
|
||||
c.roundRect(6, 6, w - 12, h - 12, 26);
|
||||
c.fill();
|
||||
c.fillStyle = '#3ee0c9';
|
||||
c.fillStyle = BRAND.primary;
|
||||
c.fillRect(6, h - 18, w - 12, 8);
|
||||
c.textAlign = 'center';
|
||||
c.direction = 'rtl';
|
||||
@@ -272,7 +276,7 @@ export function nameTag(name, role = '') {
|
||||
c.font = `bold ${role ? 58 : 66}px Vazirmatn, Tahoma, sans-serif`;
|
||||
c.fillText(name, w / 2, role ? 72 : 100);
|
||||
if (role) {
|
||||
c.fillStyle = '#9fe9da';
|
||||
c.fillStyle = BRAND.primaryLighter;
|
||||
c.font = '40px Vazirmatn, Tahoma, sans-serif';
|
||||
c.direction = 'ltr';
|
||||
c.fillText(role, w / 2, 126);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import * as THREE from 'three';
|
||||
import { catalog } from '../economy/gameState.js';
|
||||
import { textTexture } from './textures.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
const PRODUCT_COLORS = {
|
||||
juice: '#e5ae47',
|
||||
water: '#9fd3e6',
|
||||
@@ -24,14 +28,14 @@ const label = (text, sub) =>
|
||||
textTexture(512, 160, (c, width, height) => {
|
||||
c.fillStyle = '#0f2a2a';
|
||||
c.fillRect(0, 0, width, height);
|
||||
c.fillStyle = '#3ee0c9';
|
||||
c.fillStyle = BRAND.primary;
|
||||
c.fillRect(0, height - 10, width, 10);
|
||||
c.textAlign = 'center';
|
||||
c.direction = 'rtl';
|
||||
c.fillStyle = '#e9fff8';
|
||||
c.font = 'bold 56px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText(text, width / 2, 72);
|
||||
c.fillStyle = '#9fe9da';
|
||||
c.fillStyle = BRAND.primaryLighter;
|
||||
c.font = '34px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText(sub, width / 2, 126);
|
||||
});
|
||||
@@ -75,10 +79,10 @@ export function createVendingMachine(def) {
|
||||
parent.add(mesh);
|
||||
return mesh;
|
||||
};
|
||||
const shell = mat('#0b8f88', {
|
||||
const shell = mat(BRAND.primary, {
|
||||
metalness: 0.45,
|
||||
roughness: 0.3,
|
||||
emissive: '#0b8f88',
|
||||
emissive: BRAND.primary,
|
||||
emissiveIntensity: 0.15,
|
||||
}),
|
||||
white = mat('#eef5f3', {
|
||||
@@ -190,7 +194,7 @@ export function createVendingMachine(def) {
|
||||
const sign = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(1.86, 0.3),
|
||||
new THREE.MeshBasicMaterial({
|
||||
map: label('روبات مارکت', def.name),
|
||||
map: label(COMPANY.shortName, def.name),
|
||||
}),
|
||||
);
|
||||
sign.position.set(0, 2.62, 0.575);
|
||||
@@ -213,7 +217,7 @@ export function createVendingMachine(def) {
|
||||
|
||||
// Objective beacon: a tall column of light visible across the city, like a mission marker.
|
||||
const beaconMaterial = new THREE.MeshBasicMaterial({
|
||||
color: '#3ee0c9',
|
||||
color: BRAND.primary,
|
||||
transparent: true,
|
||||
opacity: 0.22,
|
||||
depthWrite: false,
|
||||
@@ -230,7 +234,7 @@ export function createVendingMachine(def) {
|
||||
const ring = new THREE.Mesh(
|
||||
new THREE.RingGeometry(1.6, 1.9, 40),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: '#3ee0c9',
|
||||
color: BRAND.primary,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
depthWrite: false,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CATEGORIES, lockedPlaces } from '../../constants/locations.js';
|
||||
import { catalog, priceAt, isOpen, fa } from '../economy/gameState.js';
|
||||
import { drawFullMap, drawIran } from '../world/mapRender.js';
|
||||
import { formatDistance } from '../../utils/geo.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
/**
|
||||
* Runs the size canvas operation for this subsystem.
|
||||
* @param {*} context - context.
|
||||
@@ -104,7 +105,7 @@ export function selectMachine(context, id, center = false) {
|
||||
text: `📍 ${def.address}`,
|
||||
});
|
||||
context.ui.patch('detail-meta', {
|
||||
text: `🕒 ${context.hoursText(def)} · ${isOpen(def, context.state.minutes) ? 'الان باز است' : 'الان بسته است'} · ${def.note}`,
|
||||
text: strings.map.details(context.hoursText(def), isOpen(def, context.state.minutes), def.note),
|
||||
});
|
||||
context.ui.patch('detail-products', {
|
||||
text: def.products
|
||||
@@ -112,7 +113,7 @@ export function selectMachine(context, id, center = false) {
|
||||
.join(' · '),
|
||||
});
|
||||
context.ui.patch('route-button', {
|
||||
text: context.waypoint === id ? 'مسیر فعال است ✓' : 'مسیریابی تا اینجا',
|
||||
text: context.waypoint === id ? strings.map.routeActive : strings.map.route,
|
||||
});
|
||||
context.renderMachineList();
|
||||
context.renderMap();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { resolveCircle } from '../world/collision.js';
|
||||
import { stepCar } from '../systems/vehicle.js';
|
||||
import { EXIT_MAX_SPEED } from '../../constants/vehicles.js';
|
||||
import { collideDriving, nearestRide, takeRide, walkAroundVehicles } from './garage.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
/**
|
||||
* Runs the look operation for this subsystem.
|
||||
* @param {*} context - context.
|
||||
@@ -39,7 +40,7 @@ export // ---------- Car ----------
|
||||
function toggleCar(context) {
|
||||
if (context.payment.phase !== 'idle' || context.state.busy) return;
|
||||
if (context.real.on) {
|
||||
context.say('father', 'توی حالت دنیای واقعی پیاده راه میریم؛ ماشین مجازی لازم نیست.');
|
||||
context.say('father', strings.real.noVirtualCar);
|
||||
return;
|
||||
}
|
||||
if (context.mode === 'walk') getIn(context);
|
||||
@@ -58,10 +59,7 @@ function getIn(context) {
|
||||
context.mode = 'drive';
|
||||
context.cam.distance = Math.max(context.cam.distance, kind === 'bike' ? 6 : 8);
|
||||
context.tone(320, 0.2, 0.05, 'square');
|
||||
const line =
|
||||
kind === 'bike'
|
||||
? 'کلاه ایمنی سرت باشه! با موتور میشه از لای ماشینها رد شد.'
|
||||
: 'کمربندها رو ببندیم! مسیر طلایی روی زمین راه رو نشون میده.';
|
||||
const line = kind === 'bike' ? strings.dialogue.bikeOn : strings.dialogue.carOn;
|
||||
context.say('father', line);
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ function getIn(context) {
|
||||
function getOut(context) {
|
||||
const { state, spec } = context.vehicle;
|
||||
if (Math.abs(state.speed) > EXIT_MAX_SPEED) {
|
||||
context.say('father', 'اول نگه دار، بعد پیاده میشویم.');
|
||||
context.say('father', strings.dialogue.stopFirst);
|
||||
return;
|
||||
}
|
||||
const left = { x: -Math.sin(state.heading), z: -Math.cos(state.heading) };
|
||||
|
||||
@@ -8,6 +8,11 @@ import { kmh } from '../systems/vehicle.js';
|
||||
import { nearestRide } from './garage.js';
|
||||
import { drawMinimap } from '../world/mapRender.js';
|
||||
import { bearing, formatDistance } from '../../utils/geo.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
/**
|
||||
* Runs the rebuild route mesh operation for this subsystem.
|
||||
* @param {*} context - context.
|
||||
@@ -125,12 +130,7 @@ export function drawCompass(context, headingDeg, markers) {
|
||||
context.compass.fillRect(0, 10, w, 24);
|
||||
context.compass.textAlign = 'center';
|
||||
context.compass.textBaseline = 'middle';
|
||||
const labels = {
|
||||
0: 'شمال',
|
||||
90: 'شرق',
|
||||
180: 'جنوب',
|
||||
270: 'غرب',
|
||||
};
|
||||
const labels = strings.hud.compass;
|
||||
for (let d = -100; d <= 100; d += 5) {
|
||||
const deg = (((Math.round(headingDeg / 5) * 5 + d) % 360) + 360) % 360,
|
||||
x = w / 2 + ((deg - headingDeg + 540) % 360) * 2.6 - 180 * 2.6;
|
||||
@@ -204,9 +204,9 @@ export function refreshHUD(context) {
|
||||
* @returns {string} Persian label.
|
||||
*/
|
||||
function modeLabel(context) {
|
||||
if (context.mode !== 'drive') return 'پیاده';
|
||||
if (context.vehicle.kind === 'bike') return 'روی موتور · F برای پیاده شدن';
|
||||
return `در ${context.vehicle.label} · F برای پیاده شدن`;
|
||||
if (context.mode !== 'drive') return strings.common.walking;
|
||||
if (context.vehicle.kind === 'bike') return strings.travel.onBike;
|
||||
return strings.travel.inVehicle(context.vehicle.label);
|
||||
}
|
||||
/**
|
||||
* Updates prompt from the active simulation.
|
||||
@@ -217,7 +217,7 @@ export function updatePrompt(context) {
|
||||
const sample = context.nearShowroom();
|
||||
if (sample && !context.state.busy && context.payment.phase === 'idle') {
|
||||
context.ui.patch('prompt', {
|
||||
text: `Q · معرفی ${sample.model.name} و پرسیدن قیمت`,
|
||||
text: strings.prompts.showroom(sample.model.name),
|
||||
hidden: false,
|
||||
});
|
||||
return;
|
||||
@@ -228,9 +228,9 @@ export function updatePrompt(context) {
|
||||
if (!context.state.busy && context.payment.phase === 'idle') {
|
||||
if (station)
|
||||
text = isOpen(station.def, context.state.minutes)
|
||||
? `E · خرید از روبات مارکت · ${station.def.name}`
|
||||
: `دستگاه بسته است · ${context.hoursText(station.def)}`;
|
||||
else if (ride) text = `F · سوار ${ride.label} شو`;
|
||||
? strings.prompts.buy(station.def.name)
|
||||
: strings.prompts.closed(context.hoursText(station.def));
|
||||
else if (ride) text = strings.prompts.getIn(ride.label);
|
||||
else if (
|
||||
context.mode === 'drive' &&
|
||||
Math.abs(context.car.speed) < 1 &&
|
||||
@@ -238,7 +238,7 @@ export function updatePrompt(context) {
|
||||
(s) => Math.hypot(s.front.x - context.car.x, s.front.z - context.car.z) < 25,
|
||||
)
|
||||
)
|
||||
text = 'F · پیاده شو؛ دستگاه نزدیک است';
|
||||
text = strings.prompts.getOutNearMachine;
|
||||
}
|
||||
context.ui.patch('prompt', {
|
||||
text,
|
||||
@@ -334,7 +334,7 @@ export function updateHUD(context, dt) {
|
||||
});
|
||||
for (const s of context.stations) {
|
||||
const isObjective = s === objective;
|
||||
s.model.beacon.material.color.set(isObjective ? '#f2c94c' : '#3ee0c9');
|
||||
s.model.beacon.material.color.set(isObjective ? '#f2c94c' : BRAND.primary);
|
||||
s.model.beacon.material.opacity =
|
||||
(isObjective ? 0.32 : 0.14) * Math.min(1, Math.hypot(s.x - pos.x, s.z - pos.z) / 40);
|
||||
}
|
||||
@@ -346,10 +346,10 @@ export function updateHUD(context, dt) {
|
||||
const inFactory = Math.hypot(pos.x - FACTORY.center.x, pos.z - FACTORY.center.z) < 130;
|
||||
context.ui.patch('street', {
|
||||
text: inFactory
|
||||
? `کارخانهٔ ${FACTORY.name}`
|
||||
? strings.hud.factory(FACTORY.name)
|
||||
: street
|
||||
? `مشهد · ${data.names[street.name]}`
|
||||
: 'حومهٔ مشهد',
|
||||
? strings.hud.street(data.names[street.name])
|
||||
: strings.hud.outskirts,
|
||||
});
|
||||
}
|
||||
context.updatePrompt();
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
activeMission,
|
||||
MISSIONS,
|
||||
} from '../economy/scoring.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
/**
|
||||
* Runs the open showroom operation for this subsystem.
|
||||
* @param {*} context - context.
|
||||
@@ -40,7 +41,7 @@ export function openShowroom(context) {
|
||||
modelId: sample.model.id,
|
||||
});
|
||||
context.ui.patch('showroom-response', {
|
||||
text: 'روبات: سلام! قیمت و امکانات هر سه مدل را میتوانم توضیح بدهم.',
|
||||
text: strings.showroom.allModels,
|
||||
});
|
||||
context.keys.clear();
|
||||
document.exitPointerLock?.();
|
||||
@@ -55,13 +56,13 @@ export function interact(context) {
|
||||
const station = context.nearStation();
|
||||
if (!station || context.state.busy || context.payment.phase !== 'idle' || context.ended) return;
|
||||
if (context.real.on && (context.real.fix?.speed ?? 0) > MAX_WALKING_SPEED) {
|
||||
context.say('father', 'داریم سریع حرکت میکنیم؛ اول بایستیم، بعد خرید کنیم.');
|
||||
context.say('father', strings.dialogue.tooFastToBuy);
|
||||
return;
|
||||
}
|
||||
if (!isOpen(station.def, context.state.minutes)) {
|
||||
context.say(
|
||||
context.guideOut() ? 'robot' : 'father',
|
||||
`این دستگاه الان بسته است. ساعت کاری: ${context.hoursText(station.def)}.`,
|
||||
strings.dialogue.machineClosed(context.hoursText(station.def)),
|
||||
'serious',
|
||||
);
|
||||
return;
|
||||
@@ -81,16 +82,20 @@ export function interact(context) {
|
||||
export function renderShop(context) {
|
||||
const def = context.shopStation.def;
|
||||
context.ui.patch('shop-category', {
|
||||
text: `${CATEGORIES[def.category].icon} ${CATEGORIES[def.category].label} · ساعت کاری ${context.hoursText(def)}`,
|
||||
text: strings.shop.category(
|
||||
CATEGORIES[def.category].icon,
|
||||
CATEGORIES[def.category].label,
|
||||
context.hoursText(def),
|
||||
),
|
||||
});
|
||||
context.ui.patch('shop-name', {
|
||||
text: `روبات مارکت · ${def.name}`,
|
||||
text: strings.shop.title(def.name),
|
||||
});
|
||||
context.ui.patch('shop-address', {
|
||||
text: def.address,
|
||||
});
|
||||
context.ui.patch('shop-note', {
|
||||
text: `${def.note} · موجودی شما: ${formatHetoman(context.state.balance)}`,
|
||||
text: strings.shop.balance(def.note, formatHetoman(context.state.balance)),
|
||||
});
|
||||
context.ui.patch('products', {
|
||||
items: def.products.map((id) => ({
|
||||
@@ -120,7 +125,7 @@ export function startPayment(context, productId) {
|
||||
text: `${p.icon} ${p.name} · ${formatHetoman(priceAt(context.shopStation.def, productId))}`,
|
||||
});
|
||||
context.ui.patch('payment-step', {
|
||||
text: 'در حال نزدیک شدن به کارتخوان…',
|
||||
text: strings.payment.approaching,
|
||||
});
|
||||
context.ui.patch('swipe', {
|
||||
disabled: true,
|
||||
@@ -128,7 +133,7 @@ export function startPayment(context, productId) {
|
||||
context.ui.patch('cancel-payment', {
|
||||
hidden: false,
|
||||
});
|
||||
context.say('child', 'بابا، کارت رو بده من بکشم!');
|
||||
context.say('child', strings.dialogue.childSwipes);
|
||||
}
|
||||
/**
|
||||
* Runs the swipe operation for this subsystem.
|
||||
@@ -146,7 +151,7 @@ export function swipe(context) {
|
||||
hidden: true,
|
||||
});
|
||||
context.ui.patch('payment-step', {
|
||||
text: 'پرداخت موفق ✓ محصول در حال تحویل است',
|
||||
text: strings.payment.success,
|
||||
});
|
||||
context.tone(1100, 0.3);
|
||||
context.refreshHUD();
|
||||
@@ -178,7 +183,7 @@ export function updatePayment(context, dt) {
|
||||
disabled: false,
|
||||
});
|
||||
context.ui.patch('payment-step', {
|
||||
text: 'کارت را بکشید: دکمهٔ «کشیدن کارت» یا کلید C',
|
||||
text: strings.payment.swipeHint,
|
||||
});
|
||||
}
|
||||
if (context.payment.phase === 'approved') {
|
||||
@@ -243,7 +248,7 @@ export function reward(context, record, product) {
|
||||
rankBefore = rankFor(context.state.xp).index;
|
||||
if (context.real.on)
|
||||
awards.push({
|
||||
label: 'خرید در دنیای واقعی',
|
||||
label: strings.payment.realPurchase,
|
||||
points: 200,
|
||||
});
|
||||
const total = awards.reduce((s, a) => s + a.points, 0);
|
||||
@@ -260,13 +265,13 @@ export function reward(context, record, product) {
|
||||
}),
|
||||
3200,
|
||||
);
|
||||
context.say('child', `${product.icon} ${product.name} گرفتم! مرسی بابا.`);
|
||||
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,
|
||||
`+${fa(m.xp)} XP · +${formatHetoman(m.hetoman)} پاداش`,
|
||||
strings.rewards.missionReward(fa(m.xp), formatHetoman(m.hetoman)),
|
||||
);
|
||||
context.tone(660, 0.5);
|
||||
}
|
||||
@@ -274,9 +279,9 @@ export function reward(context, record, product) {
|
||||
context.setTimeout(
|
||||
() =>
|
||||
context.showBanner(
|
||||
'ارتقای درجه',
|
||||
strings.rewards.rankUp,
|
||||
rankFor(context.state.xp).name,
|
||||
'دستگاههای بیشتری پیدا کن',
|
||||
strings.rewards.findMore,
|
||||
),
|
||||
done.length ? 3400 : 0,
|
||||
);
|
||||
@@ -286,7 +291,11 @@ export function reward(context, record, product) {
|
||||
context.route = null;
|
||||
context.setTimeout(
|
||||
() =>
|
||||
context.say(context.guideOut() ? 'robot' : 'father', `مأموریت بعدی: ${next.title}`, 'wink'),
|
||||
context.say(
|
||||
context.guideOut() ? 'robot' : 'father',
|
||||
strings.dialogue.nextMission(next.title),
|
||||
'wink',
|
||||
),
|
||||
1500,
|
||||
);
|
||||
}
|
||||
@@ -328,25 +337,26 @@ export function checkEnd(context) {
|
||||
context.ended = true;
|
||||
document.exitPointerLock?.();
|
||||
context.ui.patch('end-tag', {
|
||||
text: allDone ? 'عملیات موفق' : 'پایان بودجه',
|
||||
text: allDone ? strings.end.success : strings.end.outOfBudget,
|
||||
});
|
||||
context.ui.patch('end-title', {
|
||||
text: allDone ? 'عملیات مشهد کامل شد!' : 'بودجهٔ امروز تمام شد',
|
||||
text: allDone ? strings.end.successHeading : strings.end.budgetHeading,
|
||||
});
|
||||
context.ui.patch('end-text', {
|
||||
text: allDone
|
||||
? 'همهٔ مأموریتهای محدودهٔ بوستان ملت انجام شد. مناطق دیگر مشهد بهزودی باز میشوند.'
|
||||
: 'دیگر نمیتوانید از دستگاهها خرید کنید. امتیاز امروز:',
|
||||
text: allDone ? strings.end.successText : strings.end.budgetText,
|
||||
});
|
||||
const places = new Set(context.state.purchases.map((p) => p.machineId)).size;
|
||||
context.ui.patch('end-stats', {
|
||||
items: [
|
||||
['امتیاز', `${fa(context.state.xp)} XP`],
|
||||
['درجه', rankFor(context.state.xp).name],
|
||||
['خریدها', fa(context.state.purchases.length)],
|
||||
['مکانهای کشفشده', `${fa(places)} از ${fa(machines.length)}`],
|
||||
['مأموریتها', `${fa(context.state.missionsDone.length)} از ${fa(MISSIONS.length)}`],
|
||||
['باقیمانده', formatHetoman(context.state.balance)],
|
||||
[strings.end.score, `${fa(context.state.xp)} XP`],
|
||||
[strings.end.rank, rankFor(context.state.xp).name],
|
||||
[strings.end.purchases, fa(context.state.purchases.length)],
|
||||
[strings.end.places, strings.end.outOf(fa(places), fa(machines.length))],
|
||||
[
|
||||
strings.end.missions,
|
||||
strings.end.outOf(fa(context.state.missionsDone.length), fa(MISSIONS.length)),
|
||||
],
|
||||
[strings.end.remaining, formatHetoman(context.state.balance)],
|
||||
],
|
||||
});
|
||||
context.ui.patch('continue', {
|
||||
|
||||
@@ -9,6 +9,7 @@ import data from '../../data/mellat.json';
|
||||
import { resolveCircle } from '../world/collision.js';
|
||||
import { fa } from '../economy/gameState.js';
|
||||
import { formatDistance } from '../../utils/geo.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
/**
|
||||
* Runs the set real mode operation for this subsystem.
|
||||
* @param {*} context - context.
|
||||
@@ -59,10 +60,7 @@ export function setRealMode(context, on) {
|
||||
stabilizer: (context.real.stabilizer = createStabilizer()),
|
||||
});
|
||||
context.real.stabilizer.stable = context.stableMode;
|
||||
context.say(
|
||||
'father',
|
||||
'حالت دنیای واقعی روشن شد؛ از چشم خودمون میبینیم و هر جا راه بریم، توی بازی هم همونجاییم.',
|
||||
);
|
||||
context.say('father', strings.real.on);
|
||||
context.setArMode(true);
|
||||
}
|
||||
|
||||
@@ -80,21 +78,17 @@ export async function setArMode(context, on) {
|
||||
if (on && !context.arMode) {
|
||||
const problem = await context.arView.start();
|
||||
if (problem) {
|
||||
context.say('father', `${problem} فعلاً شهر سهبعدی رو از چشم خودمون میبینیم.`);
|
||||
context.say('father', strings.real.cameraFailed(problem));
|
||||
return;
|
||||
}
|
||||
if (!context.real.on) {
|
||||
context.arView.stop();
|
||||
return;
|
||||
}
|
||||
context.say(
|
||||
'robot',
|
||||
'دوربین روشن شد! گوشی رو صاف جلوی خودت بگیر؛ ستون نورانی طلایی جای دستگاهه.',
|
||||
'excited',
|
||||
);
|
||||
context.say('robot', strings.real.cameraOn, 'excited');
|
||||
context.setTimeout(() => {
|
||||
if (context.real.on && !context.arView.hasOrientation)
|
||||
context.say('father', 'حسگر جهت گوشی جواب نمیده؛ برای چرخیدن، صفحه رو با انگشت بکش.');
|
||||
context.say('father', strings.real.noOrientation);
|
||||
}, 3000);
|
||||
} else if (!on && context.arMode) context.arView.stop();
|
||||
else return;
|
||||
@@ -133,16 +127,19 @@ function askRealMode(context, thenStart) {
|
||||
export function updateReal(context, dt) {
|
||||
let uiStatus;
|
||||
if (context.real.error) uiStatus = `⚠ ${context.real.error}`;
|
||||
else if (!context.real.fix) uiStatus = `📍 ${context.real.waiting ?? 'در حال یافتن موقعیت…'}`;
|
||||
else if (!context.real.fix) uiStatus = `📍 ${context.real.waiting ?? strings.real.locating}`;
|
||||
else if (!insideWorld(data.bounds, context.real.fix.x, context.real.fix.z)) {
|
||||
const nearest = Math.min(
|
||||
...context
|
||||
.activeStations()
|
||||
.map((s) => Math.hypot(s.x - context.real.fix.x, s.z - context.real.fix.z)),
|
||||
);
|
||||
uiStatus = `📍 خارج از محدودهٔ بازی · ${formatDistance(nearest)} تا نزدیکترین دستگاه`;
|
||||
uiStatus = strings.real.outOfArea(formatDistance(nearest));
|
||||
} else
|
||||
uiStatus = `📍 GPS ±${fa(Math.round(context.real.fix.accuracy))} متر${context.real.fix.speed > MAX_WALKING_SPEED ? ' · حرکت سریع' : ''}`;
|
||||
uiStatus = strings.real.accuracy(
|
||||
fa(Math.round(context.real.fix.accuracy)),
|
||||
context.real.fix.speed > MAX_WALKING_SPEED,
|
||||
);
|
||||
context.ui.patch('gps-status', {
|
||||
text: uiStatus,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,11 @@ import { createState } from '../economy/gameState.js';
|
||||
import { createPayment } from '../economy/payment.js';
|
||||
import { createVendingMachine } from '../models/vending.js';
|
||||
import { resetGarage } from './garage.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
/**
|
||||
* Runs the place station operation for this subsystem.
|
||||
* @param {*} context - context.
|
||||
@@ -53,7 +58,7 @@ export function updateReachCircles(context) {
|
||||
s.reach.visible = context.real.on;
|
||||
if (!context.real.on) continue;
|
||||
s.reach.scale.setScalar(radius);
|
||||
s.reach.material.color.set(s === objective ? '#f2c94c' : '#3ee0c9');
|
||||
s.reach.material.color.set(s === objective ? '#f2c94c' : BRAND.primary);
|
||||
}
|
||||
}
|
||||
// GPS test points only take part while real-world mode is on.
|
||||
@@ -130,18 +135,12 @@ export function startMission(context) {
|
||||
context.ui.patch('hud', {
|
||||
hidden: false,
|
||||
});
|
||||
context.say(
|
||||
'robot',
|
||||
'سه نمونهٔ ۳۵، ۴۸ و ۶۰ کانال توی حیاط کنار ماشین هستند. دکمهٔ مدلهای کارخانه یا Q را بزن تا قیمت و امکاناتشان را بگویم.',
|
||||
'wink',
|
||||
);
|
||||
context.say('child', 'بابا، اینجا همون کارخونهایه که دستگاههای روبات مارکت رو میسازه؟');
|
||||
context.say(
|
||||
'father',
|
||||
'آره! سالن تولید اونوره؛ دستگاه وندینگ هم توی دفتر فنی مهندسیه. بریم ببینیم!',
|
||||
);
|
||||
context.say('child', 'اون وانت سفیده کیه داره میاد تو؟');
|
||||
context.say('father', 'آقای محمدیه، مدیر شرکت. بعداً بریم اتاقش سلام کنیم.');
|
||||
const { dialogue } = strings;
|
||||
context.say('robot', dialogue.showroomIntro, 'wink');
|
||||
context.say('child', dialogue.childAsksFactory);
|
||||
context.say('father', dialogue.fatherAnswersFactory);
|
||||
context.say('child', dialogue.childAsksArrival);
|
||||
context.say('father', dialogue.fatherAnswersArrival);
|
||||
if (!context.isTouch && !context.real.on)
|
||||
context.canvas.requestPointerLock?.()?.catch?.(() => {});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ import { createTraffic } from '../systems/traffic.js';
|
||||
import { createGarage } from './garage.js';
|
||||
import { createMapAssets } from '../world/mapRender.js';
|
||||
import { formatDistance, toLocal } from '../../utils/geo.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
|
||||
// City map zoom limits in metres per screen pixel (closest and furthest).
|
||||
const MAP_ZOOM_MIN = 0.35;
|
||||
@@ -73,7 +78,7 @@ export async function initializeRenderer(context) {
|
||||
});
|
||||
} catch {
|
||||
context.ui.patch('loading', {
|
||||
text: 'مرورگر شما گرافیک سهبعدی (WebGL) را اجرا نمیکند. شتابدهی گرافیکی را فعال کنید.',
|
||||
text: strings.app.noWebgl,
|
||||
});
|
||||
throw new Error('WebGL unavailable');
|
||||
}
|
||||
@@ -281,7 +286,7 @@ export async function initializeStationById(context) {
|
||||
s.reach = new THREE.Mesh(
|
||||
new THREE.RingGeometry(0.96, 1, 96),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: '#3ee0c9',
|
||||
color: BRAND.primary,
|
||||
transparent: true,
|
||||
opacity: 0.55,
|
||||
depthWrite: false,
|
||||
@@ -326,7 +331,7 @@ export async function initializeClosestStation(context) {
|
||||
});
|
||||
if (context.imagery.available)
|
||||
context.ui.patch('attribution', {
|
||||
text: `نقشه: © مشارکتکنندگان OpenStreetMap · تصویر ماهوارهای: ${context.imageryIndex.attribution}`,
|
||||
text: strings.app.osmImageryCredit(context.imageryIndex.attribution),
|
||||
});
|
||||
context.traffic = createTraffic(context.scene, data, 28);
|
||||
context.mapAssets = createMapAssets(data, [context.factory.label]);
|
||||
@@ -357,7 +362,7 @@ export async function buildParkGate(context) {
|
||||
c.fillStyle = '#f4efd9';
|
||||
c.textAlign = 'center';
|
||||
c.font = 'bold 64px Vazirmatn, Tahoma, sans-serif';
|
||||
c.fillText('بوستان ملت', 256, 86);
|
||||
c.fillText(strings.places.parkSign, 256, 86);
|
||||
const texture = new THREE.CanvasTexture(sign);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const gate = context.parkGate,
|
||||
@@ -598,11 +603,7 @@ export async function initializeAxis(context) {
|
||||
context.axis = (pos, neg) =>
|
||||
Number(pos.some((k) => context.keys.has(k))) - Number(neg.some((k) => context.keys.has(k)));
|
||||
// ---------- Subtitles ----------
|
||||
context.SPEAKERS = {
|
||||
robot: 'روبات',
|
||||
father: 'پدر',
|
||||
child: 'بچه',
|
||||
};
|
||||
context.SPEAKERS = { ...strings.speakers };
|
||||
// Factory staff speak under their own names.
|
||||
for (const t of context.factory.talks) if (t.label) context.SPEAKERS[t.speaker] = t.label;
|
||||
context.lines = [];
|
||||
@@ -637,7 +638,7 @@ export async function registerSoundControl(context) {
|
||||
};
|
||||
}
|
||||
context.ui.patch('sound', {
|
||||
text: context.soundOn ? 'صدا: روشن' : 'صدا: خاموش',
|
||||
text: context.soundOn ? strings.toolbar.soundOn : strings.toolbar.soundOff,
|
||||
});
|
||||
context.ui.patch('sound', {
|
||||
pressed: String(context.soundOn),
|
||||
@@ -645,7 +646,7 @@ export async function registerSoundControl(context) {
|
||||
context.tone(700);
|
||||
} catch {
|
||||
context.ui.patch('sound', {
|
||||
text: 'صدا در دسترس نیست',
|
||||
text: strings.toolbar.soundUnavailable,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -668,8 +669,8 @@ export async function initializeReachRadius(context) {
|
||||
: null;
|
||||
context.hoursText = (m) =>
|
||||
m.hours[1] - m.hours[0] >= 24 * 60
|
||||
? 'شبانهروزی'
|
||||
: `${formatClock(m.hours[0])} تا ${formatClock(m.hours[1])}`;
|
||||
? strings.common.allDay
|
||||
: strings.common.hours(formatClock(m.hours[0]), formatClock(m.hours[1]));
|
||||
context.shopStation = null;
|
||||
context.nearShowroom = () =>
|
||||
!context.real.on && context.mode === 'walk' ? context.showroom.nearest(context.player) : null;
|
||||
@@ -679,7 +680,7 @@ export async function initializeReachRadius(context) {
|
||||
modelId: id,
|
||||
});
|
||||
context.ui.patch('showroom-response', {
|
||||
text: 'روبات: دربارهٔ این مدل چی میخواهی بدانی؟',
|
||||
text: strings.showroom.whatToKnow,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -699,7 +700,7 @@ export async function registerPurchaseControls(context) {
|
||||
const answer = showroomAnswer(question);
|
||||
if (!answer) return;
|
||||
context.ui.patch('showroom-response', {
|
||||
text: `روبات: ${answer}`,
|
||||
text: strings.showroom.robotSays(answer),
|
||||
});
|
||||
context.say('robot', answer, question.topic === 'price' ? 'happy' : 'wink');
|
||||
});
|
||||
@@ -717,7 +718,7 @@ export async function registerPurchaseControls(context) {
|
||||
context.ui.on('cancel-payment', context.cancel);
|
||||
context.ui.on('shop-close', () => context.ui.close('shop'));
|
||||
context.ui.patch('quality', {
|
||||
text: context.quality === 'low' ? 'گرافیک: سبک' : 'گرافیک: کامل',
|
||||
text: context.quality === 'low' ? strings.toolbar.graphicsLow : strings.toolbar.graphicsHigh,
|
||||
});
|
||||
}
|
||||
/**
|
||||
@@ -825,7 +826,10 @@ export async function registerRouteSelection(context) {
|
||||
const s = context.stationById(context.selected);
|
||||
context.say(
|
||||
'father',
|
||||
`مسیر تا ${s.def.name} رو گذاشتم؛ ${context.route ? formatDistance(context.route.length) : ''}. دنبال خط طلایی بریم.`,
|
||||
strings.dialogue.routeSet(
|
||||
s.def.name,
|
||||
context.route ? formatDistance(context.route.length) : '',
|
||||
),
|
||||
);
|
||||
context.selectMachine(context.selected);
|
||||
});
|
||||
@@ -978,63 +982,63 @@ export async function registerMapSelectionAndZoom(context) {
|
||||
export async function initializeCities(context) {
|
||||
context.CITIES = [
|
||||
{
|
||||
name: 'مشهد',
|
||||
name: strings.cities.mashhad,
|
||||
lat: 36.3,
|
||||
lon: 59.6,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: 'تهران',
|
||||
name: strings.cities.tehran,
|
||||
lat: 35.69,
|
||||
lon: 51.39,
|
||||
},
|
||||
{
|
||||
name: 'اصفهان',
|
||||
name: strings.cities.isfahan,
|
||||
lat: 32.65,
|
||||
lon: 51.67,
|
||||
},
|
||||
{
|
||||
name: 'شیراز',
|
||||
name: strings.cities.shiraz,
|
||||
lat: 29.59,
|
||||
lon: 52.58,
|
||||
},
|
||||
{
|
||||
name: 'تبریز',
|
||||
name: strings.cities.tabriz,
|
||||
lat: 38.08,
|
||||
lon: 46.29,
|
||||
},
|
||||
{
|
||||
name: 'اهواز',
|
||||
name: strings.cities.ahvaz,
|
||||
lat: 31.32,
|
||||
lon: 48.67,
|
||||
},
|
||||
{
|
||||
name: 'کرمان',
|
||||
name: strings.cities.kerman,
|
||||
lat: 30.28,
|
||||
lon: 57.08,
|
||||
},
|
||||
{
|
||||
name: 'رشت',
|
||||
name: strings.cities.rasht,
|
||||
lat: 37.28,
|
||||
lon: 49.58,
|
||||
},
|
||||
{
|
||||
name: 'یزد',
|
||||
name: strings.cities.yazd,
|
||||
lat: 31.9,
|
||||
lon: 54.37,
|
||||
},
|
||||
{
|
||||
name: 'زاهدان',
|
||||
name: strings.cities.zahedan,
|
||||
lat: 29.5,
|
||||
lon: 60.86,
|
||||
},
|
||||
{
|
||||
name: 'کرمانشاه',
|
||||
name: strings.cities.kermanshah,
|
||||
lat: 34.31,
|
||||
lon: 47.07,
|
||||
},
|
||||
{
|
||||
name: 'بندرعباس',
|
||||
name: strings.cities.bandarAbbas,
|
||||
lat: 27.18,
|
||||
lon: 56.27,
|
||||
},
|
||||
@@ -1046,14 +1050,7 @@ export async function initializeCities(context) {
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
export async function initializeBriefingAndTracking(context) {
|
||||
context.BRIEF = [
|
||||
`نقطهٔ شروع: ${FACTORY.name}، بزرگراه مشهد–چناران`,
|
||||
'تیم: پدر و فرزند · روبات راهنما در دستگاهها منتظر شماست',
|
||||
'هدف: از کارخانه تا بوستان ملت و دانشگاه فردوسی (حدود ۲۰ کیلومتر) بروید و دستگاههای روبات مارکت را در پارک، مدرسه، دانشگاه، ایستگاه مترو و مرکز خرید پیدا کنید.',
|
||||
'هر خرید امتیاز دارد؛ کشف مکان تازه و ترکیب نوشیدنی و خوراکی امتیاز بیشتری دارد.',
|
||||
'بودجه: ۵۰۰ هتومان · مأموریتها پاداش نقدی دارند.',
|
||||
'رفتوآمد: پیاده یا با ماشین. نقشهٔ گوشهٔ صفحه را بزنید تا آدرس دستگاهها را ببینید.',
|
||||
].join('\n');
|
||||
context.BRIEF = strings.briefing.lines.join('\n');
|
||||
context.briefChars = 0;
|
||||
context.ui.on('start-mission', context.startMission);
|
||||
|
||||
@@ -1099,7 +1096,7 @@ export async function initializeArMode(context) {
|
||||
context.real.stabilizer.stable = context.stableMode;
|
||||
context.ui.classState('stable-toggle', 'on', context.stableMode);
|
||||
context.ui.patch('stable-toggle', {
|
||||
text: context.stableMode ? '🎯 پایدار: روشن' : '🎯 پایدار: خاموش',
|
||||
text: context.stableMode ? strings.toolbar.stableOn : strings.toolbar.stableOff,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as THREE from 'three';
|
||||
import { FACTORY } from '../../constants/locations.js';
|
||||
import { isOpen } from '../economy/gameState.js';
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
/**
|
||||
* Updates talks from the active simulation.
|
||||
* @param {*} context - context.
|
||||
@@ -33,36 +34,20 @@ export function greet(context, station) {
|
||||
first = !context.guideState.greeted.has(def.id);
|
||||
context.guideState.greeted.add(def.id);
|
||||
if (def.id === 'factory' && first) {
|
||||
context.say(
|
||||
'robot',
|
||||
'سلام! من روبات راهنمای روبات مارکتم؛ تو دستگاه دفتر فنی زندگی میکنم!',
|
||||
'excited',
|
||||
);
|
||||
context.say(
|
||||
'robot',
|
||||
`به ${FACTORY.name} خوش اومدین. بچههای همین دفتر نرمافزار دستگاهها رو مینویسن.`,
|
||||
'happy',
|
||||
);
|
||||
context.say('robot', 'اول از همین دستگاه یه چیزی بخرین؛ کلید E رو بزنین.', 'wink');
|
||||
context.say(
|
||||
'robot',
|
||||
'ماشینتون جلوی همین ساختمونه. با F سوار شین و برین بوستان ملت؛ من توی همهٔ دستگاهها منتظرتونم!',
|
||||
'love',
|
||||
);
|
||||
context.say('robot', strings.dialogue.robotHello, 'excited');
|
||||
context.say('robot', strings.dialogue.robotWelcome(FACTORY.name), 'happy');
|
||||
context.say('robot', strings.dialogue.robotFirstBuy, 'wink');
|
||||
context.say('robot', strings.dialogue.robotGoToPark, 'love');
|
||||
return;
|
||||
}
|
||||
if (!isOpen(def, context.state.minutes)) {
|
||||
context.say(
|
||||
'robot',
|
||||
`سلام! متأسفم، این دستگاه الان بسته است. ساعت کاری: ${context.hoursText(def)}.`,
|
||||
'serious',
|
||||
);
|
||||
context.say('robot', strings.dialogue.robotClosed(context.hoursText(def)), 'serious');
|
||||
return;
|
||||
}
|
||||
if (first) {
|
||||
context.say('robot', `سلام دوباره! به دستگاه ${def.name} خوش اومدین.`, 'excited');
|
||||
context.say('robot', `${def.note}. برای خرید E رو بزنین.`, 'happy');
|
||||
} else context.say('robot', 'باز هم خوش اومدین! برای خرید E رو بزنین.', 'wink');
|
||||
context.say('robot', strings.dialogue.robotWelcomeBack(def.name), 'excited');
|
||||
context.say('robot', strings.dialogue.robotNote(def.note), 'happy');
|
||||
} else context.say('robot', strings.dialogue.robotAgain, 'wink');
|
||||
}
|
||||
/**
|
||||
* Updates guide from the active simulation.
|
||||
@@ -95,7 +80,7 @@ export function updateGuide(context, dt) {
|
||||
context.tone(990, 0.15);
|
||||
context.tone(1320, 0.2);
|
||||
}
|
||||
if (wasOut && near !== s) context.say('robot', 'خداحافظ! مسیر طلایی رو دنبال کنین.', 'happy');
|
||||
if (wasOut && near !== s) context.say('robot', strings.dialogue.robotBye, 'happy');
|
||||
if (!context.guideState.t) {
|
||||
context.guideState.station = null;
|
||||
context.guide.group.visible = false;
|
||||
@@ -149,7 +134,7 @@ export function updateHints(context) {
|
||||
context.hints.closed = true;
|
||||
context.say(
|
||||
'robot',
|
||||
`${target.def.name} الان بسته است (${context.hoursText(target.def)}). از نقشه مقصد دیگری انتخاب کن.`,
|
||||
strings.dialogue.targetClosed(target.def.name, context.hoursText(target.def)),
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -159,6 +144,6 @@ export function updateHints(context) {
|
||||
!context.hints.car
|
||||
) {
|
||||
context.hints.car = true;
|
||||
context.say('father', 'راه طولانیه؛ بهتره با ماشین بریم.');
|
||||
context.say('father', strings.dialogue.longWay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { strings } from '../../i18n/fa.js';
|
||||
import { COMPANY } from '../../config/company.js';
|
||||
|
||||
// Brand colours for materials and canvas drawings.
|
||||
const BRAND = COMPANY.colors;
|
||||
// Tactical 2D maps drawn from the same OSM data as the 3D city. The region is too large for one
|
||||
// raster, so detail is drawn in lazily rendered, cached tiles and an overview covers zoomed-out views.
|
||||
const SCALE = 0.75; // tile pixels per metre
|
||||
@@ -271,7 +276,7 @@ function machineIcon(c, x, y, { objective, visited, open }) {
|
||||
c.fillRect(-7, -7, 14, 14);
|
||||
c.strokeRect(-7, -7, 14, 14);
|
||||
} else {
|
||||
c.fillStyle = open ? (visited ? '#7fb8ad' : '#3ee0c9') : '#7b817f';
|
||||
c.fillStyle = open ? (visited ? '#7fb8ad' : BRAND.primary) : '#7b817f';
|
||||
c.strokeStyle = '#08201c';
|
||||
c.lineWidth = 2;
|
||||
c.beginPath();
|
||||
@@ -367,7 +372,7 @@ export function drawMinimap(ctx, assets, view) {
|
||||
ctx.font = 'bold 13px Vazirmatn, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('ش', (nx / nl) * (r - 12), (ny / nl) * (r - 12));
|
||||
ctx.fillText(strings.hud.minimapNorth, (nx / nl) * (r - 12), (ny / nl) * (r - 12));
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(230,240,235,.55)';
|
||||
ctx.lineWidth = 2;
|
||||
@@ -461,11 +466,11 @@ export function drawFullMap(ctx, assets, view, state) {
|
||||
metro: '#ff8a80',
|
||||
mall: '#f5b8d9',
|
||||
industrial: '#a9b2ad',
|
||||
factory: '#3ee0c9',
|
||||
factory: BRAND.primary,
|
||||
}[l.kind];
|
||||
if (l.kind === 'factory') ctx.font = 'bold 14px Vazirmatn, sans-serif';
|
||||
ctx.fillText(
|
||||
l.kind === 'metro' ? `Ⓜ ${l.name.replace('ایستگاه مترو ', '')}` : l.name,
|
||||
l.kind === 'metro' ? `Ⓜ ${l.name.replace(strings.hud.metroPrefix, '')}` : l.name,
|
||||
sx,
|
||||
sy + 14,
|
||||
);
|
||||
@@ -546,9 +551,9 @@ export function drawIran(
|
||||
for (const province of iran.provinces) {
|
||||
const active = province.id === highlight;
|
||||
ctx.fillStyle = active
|
||||
? `rgba(62,224,201,${0.35 + Math.sin(t * 3) * 0.12})`
|
||||
? `rgba(0,166,147,${0.35 + Math.sin(t * 3) * 0.12})`
|
||||
: 'rgba(40,56,50,.85)';
|
||||
ctx.strokeStyle = active ? '#3ee0c9' : 'rgba(150,190,175,.35)';
|
||||
ctx.strokeStyle = active ? BRAND.primary : 'rgba(150,190,175,.35)';
|
||||
ctx.lineWidth = active ? 2 : 1;
|
||||
for (const ring of province.rings) {
|
||||
ctx.beginPath();
|
||||
|
||||
Reference in New Issue
Block a user