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,6 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
import { robotFace } from '@/config/assets.js';
|
||||
/** @typedef {{nodes:object,actions:object}} BriefingPanelProps */
|
||||
/** Presents the BriefingPanel view using plain models and semantic native elements.
|
||||
* @param {BriefingPanelProps} props - Interface snapshots and callbacks.
|
||||
@@ -14,14 +16,14 @@ export function BriefingPanel({ nodes, actions }) {
|
||||
<div className="brief-panel">
|
||||
<img
|
||||
className="brief-robot"
|
||||
src="./robot/front.webp"
|
||||
alt="روبات راهنمای روبات مارکت"
|
||||
src={robotFace('front')}
|
||||
alt={strings.briefing.robotAlt}
|
||||
width="136"
|
||||
height="223"
|
||||
/>
|
||||
<small className="tag">{'عملیات ۰۱ · خراسان رضوی'}</small>
|
||||
<h1>{'عملیات مشهد'}</h1>
|
||||
<p className="sub">{'روبات مارکت · شکار دستگاههای وندینگ'}</p>
|
||||
<small className="tag">{strings.briefing.tag}</small>
|
||||
<h1>{strings.briefing.title}</h1>
|
||||
<p className="sub">{strings.briefing.subtitle}</p>
|
||||
<GameNode
|
||||
as="pre"
|
||||
id="brief-text"
|
||||
@@ -35,7 +37,7 @@ export function BriefingPanel({ nodes, actions }) {
|
||||
model={nodes['start-mission']}
|
||||
onAction={() => actions.emit('start-mission')}
|
||||
>
|
||||
{'شروع عملیات'}
|
||||
{strings.briefing.start}
|
||||
</GameNode>
|
||||
<div className="brief-actions">
|
||||
<GameNode
|
||||
@@ -46,7 +48,7 @@ export function BriefingPanel({ nodes, actions }) {
|
||||
model={nodes['real-mode']}
|
||||
onAction={() => actions.emit('real-mode')}
|
||||
>
|
||||
{'📍 بازی در دنیای واقعی (GPS)'}
|
||||
{strings.briefing.realWorld}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -57,14 +59,10 @@ export function BriefingPanel({ nodes, actions }) {
|
||||
model={nodes['install']}
|
||||
onAction={() => actions.emit('install')}
|
||||
>
|
||||
{'⬇ نصب بازی روی گوشی'}
|
||||
{strings.briefing.install}
|
||||
</GameNode>
|
||||
</div>
|
||||
<p className="keys">
|
||||
{
|
||||
'حرکت: W A S D · دویدن: Shift · دوربین: ماوس · سوار و پیاده شدن: F · خرید: E · نقشه: M'
|
||||
}
|
||||
</p>
|
||||
<p className="keys">{strings.briefing.keys}</p>
|
||||
</div>
|
||||
</GameNode>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { ViewLists } from './ViewLists.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} EndDialogProps */
|
||||
/** Presents the EndDialog view using plain models and semantic native elements.
|
||||
* @param {EndDialogProps} props - Interface snapshots and callbacks.
|
||||
@@ -10,7 +11,7 @@ export function EndDialog({ nodes, actions }) {
|
||||
<>
|
||||
<GameNode as="dialog" id="end" model={nodes['end']} onClose={() => actions.close('end')}>
|
||||
<GameNode as="small" id="end-tag" model={nodes['end-tag']}>
|
||||
{'پایان عملیات'}
|
||||
{strings.end.title}
|
||||
</GameNode>
|
||||
<GameNode as="h2" id="end-title" model={nodes['end-title']}></GameNode>
|
||||
<GameNode as="p" id="end-text" model={nodes['end-text']}></GameNode>
|
||||
@@ -29,7 +30,7 @@ export function EndDialog({ nodes, actions }) {
|
||||
model={nodes['again']}
|
||||
onAction={() => actions.emit('again')}
|
||||
>
|
||||
{'عملیات دوباره'}
|
||||
{strings.end.again}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -39,7 +40,7 @@ export function EndDialog({ nodes, actions }) {
|
||||
model={nodes['continue']}
|
||||
onAction={() => actions.emit('continue')}
|
||||
>
|
||||
{'ادامهٔ گشت در شهر'}
|
||||
{strings.end.keepExploring}
|
||||
</GameNode>
|
||||
</div>
|
||||
</GameNode>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GameToolbar } from './GameToolbar.jsx';
|
||||
import { TouchControls } from './TouchControls.jsx';
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { ViewLists } from './ViewLists.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} GameHudProps */
|
||||
/** Presents the GameHud view using plain models and semantic native elements.
|
||||
* @param {GameHudProps} props - Interface snapshots and callbacks.
|
||||
@@ -20,12 +21,12 @@ export function GameHud({ nodes, actions }) {
|
||||
as="button"
|
||||
id="minimap-button"
|
||||
type="button"
|
||||
aria-label="باز کردن نقشهٔ شهر (M)"
|
||||
aria-label={strings.hud.openMap}
|
||||
model={nodes['minimap-button']}
|
||||
onAction={() => actions.emit('minimap-button')}
|
||||
>
|
||||
<GameNode as="canvas" id="minimap" width="220" height="220" model={nodes['minimap']} />
|
||||
<span className="map-hint">{'نقشه · M'}</span>
|
||||
<span className="map-hint">{strings.hud.mapHint}</span>
|
||||
</GameNode>
|
||||
<LocationStatus nodes={nodes} actions={actions} />
|
||||
<MissionList nodes={nodes} actions={actions} />
|
||||
@@ -39,7 +40,7 @@ export function GameHud({ nodes, actions }) {
|
||||
id="prompt"
|
||||
hidden
|
||||
type="button"
|
||||
aria-label="تعامل با بازی"
|
||||
aria-label={strings.hud.interact}
|
||||
model={nodes['prompt']}
|
||||
onAction={() => actions.emit('prompt')}
|
||||
></GameNode>
|
||||
@@ -68,7 +69,7 @@ export function GameHud({ nodes, actions }) {
|
||||
<TouchControls nodes={nodes} actions={actions} />
|
||||
<GameNode as="div" id="crosshair" model={nodes['crosshair']}></GameNode>
|
||||
<GameNode as="small" id="attribution" model={nodes['attribution']}>
|
||||
{'نقشه: © مشارکتکنندگان OpenStreetMap'}
|
||||
{strings.app.osmCredit}
|
||||
</GameNode>
|
||||
</GameNode>
|
||||
</>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { GameSurfaces } from './GameSurfaces.jsx';
|
||||
import { BriefingPanel } from './BriefingPanel.jsx';
|
||||
import { GameHud } from './GameHud.jsx';
|
||||
import { GameDialogs } from './GameDialogs.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
|
||||
/** Composes React views around the isolated Three.js runtime. */
|
||||
export function GamePage() {
|
||||
@@ -49,9 +50,9 @@ export function GamePage() {
|
||||
role="alert"
|
||||
className="fixed inset-0 z-[100] grid place-content-center bg-surface p-8 text-hud"
|
||||
>
|
||||
<p>بازی اجرا نشد: {error}</p>
|
||||
<p>{strings.app.failed(error)}</p>
|
||||
<button type="button" onClick={() => location.reload()}>
|
||||
تلاش دوباره
|
||||
{strings.app.retry}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} GameSurfacesProps */
|
||||
/** Presents the GameSurfaces view using plain models and semantic native elements.
|
||||
* @param {GameSurfacesProps} props - Interface snapshots and callbacks.
|
||||
@@ -18,7 +19,7 @@ export function GameSurfaces({ nodes }) {
|
||||
></GameNode>
|
||||
<GameNode as="canvas" id="world" model={nodes['world']} />
|
||||
<GameNode as="div" id="loading" model={nodes['loading']}>
|
||||
{'در حال ساختن شهر مشهد…'}
|
||||
{strings.app.buildingCity}
|
||||
</GameNode>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} GameToolbarProps */
|
||||
/** Presents the GameToolbar using the current interface snapshot. */
|
||||
export function GameToolbar({ nodes, actions }) {
|
||||
@@ -12,7 +13,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['showroom-open']}
|
||||
onAction={() => actions.emit('showroom-open')}
|
||||
>
|
||||
مدلهای کارخانه · Q
|
||||
{strings.toolbar.showroom}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -21,7 +22,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['gps-toggle']}
|
||||
onAction={() => actions.emit('gps-toggle')}
|
||||
>
|
||||
{'📍 دنیای واقعی'}
|
||||
{strings.toolbar.realWorld}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -31,7 +32,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['ar-toggle']}
|
||||
onAction={() => actions.emit('ar-toggle')}
|
||||
>
|
||||
{'📷 دوربین'}
|
||||
{strings.toolbar.camera}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -42,7 +43,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['stable-toggle']}
|
||||
onAction={() => actions.emit('stable-toggle')}
|
||||
>
|
||||
{'🎯 پایدار: روشن'}
|
||||
{strings.toolbar.stableOn}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -52,7 +53,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['sound']}
|
||||
onAction={() => actions.emit('sound')}
|
||||
>
|
||||
{'صدا: خاموش'}
|
||||
{strings.toolbar.soundOff}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -61,7 +62,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['quality']}
|
||||
onAction={() => actions.emit('quality')}
|
||||
>
|
||||
{'گرافیک'}
|
||||
{strings.toolbar.graphics}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -70,7 +71,7 @@ export function GameToolbar({ nodes, actions }) {
|
||||
model={nodes['restart']}
|
||||
onAction={() => actions.emit('restart')}
|
||||
>
|
||||
{'شروع دوباره'}
|
||||
{strings.toolbar.restart}
|
||||
</GameNode>
|
||||
</GameNode>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} InstallDialogProps */
|
||||
/** Presents the InstallDialog view using plain models and semantic native elements.
|
||||
* @param {InstallDialogProps} props - Interface snapshots and callbacks.
|
||||
@@ -13,21 +14,19 @@ export function InstallDialog({ nodes, actions }) {
|
||||
model={nodes['ios-install']}
|
||||
onClose={() => actions.close('ios-install')}
|
||||
>
|
||||
<h2>{'نصب روی آیفون'}</h2>
|
||||
<h2>{strings.install.title}</h2>
|
||||
<ol>
|
||||
<li>
|
||||
{'در Safari دکمهٔ '}
|
||||
{strings.install.shareBefore}
|
||||
<b>{'Share'}</b>
|
||||
{' (مربع با فلش رو به بالا) را بزنید.'}
|
||||
{strings.install.shareAfter}
|
||||
</li>
|
||||
<li>
|
||||
{'گزینهٔ '}
|
||||
{strings.install.addBefore}
|
||||
<b>{'Add to Home Screen'}</b>
|
||||
{' را انتخاب کنید.'}
|
||||
</li>
|
||||
<li>
|
||||
{'روبات مارکت مثل یک اپ روی صفحهٔ اصلی گوشی شما قرار میگیرد و آفلاین هم اجرا میشود.'}
|
||||
{strings.install.addAfter}
|
||||
</li>
|
||||
<li>{strings.install.note}</li>
|
||||
</ol>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -36,7 +35,7 @@ export function InstallDialog({ nodes, actions }) {
|
||||
model={nodes['ios-close']}
|
||||
onAction={() => actions.emit('ios-close')}
|
||||
>
|
||||
{'متوجه شدم'}
|
||||
{strings.common.gotIt}
|
||||
</GameNode>
|
||||
</GameNode>
|
||||
</>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} LocationStatusProps */
|
||||
/** Presents the LocationStatus using the current interface snapshot. */
|
||||
export function LocationStatus({ nodes }) {
|
||||
return (
|
||||
<GameNode as="div" id="location" model={nodes['location']}>
|
||||
<GameNode as="span" id="street" model={nodes['street']}>
|
||||
{'مشهد'}
|
||||
{strings.common.mashhad}
|
||||
</GameNode>
|
||||
<GameNode as="small" id="mode-label" model={nodes['mode-label']}>
|
||||
{'پیاده'}
|
||||
{strings.common.walking}
|
||||
</GameNode>
|
||||
<GameNode as="small" id="gps-status" hidden model={nodes['gps-status']}></GameNode>
|
||||
</GameNode>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} MachineDetailsProps */
|
||||
/** Presents the MachineDetails using the current interface snapshot. */
|
||||
export function MachineDetails({ nodes, actions }) {
|
||||
@@ -15,7 +16,7 @@ export function MachineDetails({ nodes, actions }) {
|
||||
model={nodes['route-button']}
|
||||
onAction={() => actions.emit('route-button')}
|
||||
>
|
||||
{'مسیریابی تا اینجا'}
|
||||
{strings.map.route}
|
||||
</GameNode>
|
||||
</GameNode>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MachineDetails } from './MachineDetails.jsx';
|
||||
import { MachineList } from './MachineList.jsx';
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} MapDialogProps */
|
||||
/** Presents the MapDialog view using plain models and semantic native elements.
|
||||
* @param {MapDialogProps} props - Interface snapshots and callbacks.
|
||||
@@ -27,7 +28,7 @@ export function MapDialog({ nodes, actions }) {
|
||||
model={nodes['tab-city']}
|
||||
onAction={() => actions.emit('tab-city')}
|
||||
>
|
||||
{'مشهد'}
|
||||
{strings.common.mashhad}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -39,7 +40,7 @@ export function MapDialog({ nodes, actions }) {
|
||||
model={nodes['tab-iran']}
|
||||
onAction={() => actions.emit('tab-iran')}
|
||||
>
|
||||
{'ایران'}
|
||||
{strings.common.iran}
|
||||
</GameNode>
|
||||
</div>
|
||||
<GameNode
|
||||
@@ -47,7 +48,7 @@ export function MapDialog({ nodes, actions }) {
|
||||
type="button"
|
||||
className="close"
|
||||
id="map-close"
|
||||
aria-label="بستن"
|
||||
aria-label={strings.map.close}
|
||||
model={nodes['map-close']}
|
||||
onAction={() => actions.emit('map-close')}
|
||||
>
|
||||
@@ -58,12 +59,10 @@ export function MapDialog({ nodes, actions }) {
|
||||
<div className="map-view">
|
||||
<GameNode as="canvas" id="fullmap" model={nodes['fullmap']} />
|
||||
<GameNode as="canvas" id="iran-map" hidden model={nodes['iran-map']} />
|
||||
<p className="map-help">
|
||||
{'کشیدن: جابهجایی · دو انگشت یا چرخ ماوس: زوم · لمس یا کلیک روی دستگاه: انتخاب'}
|
||||
</p>
|
||||
<p className="map-help">{strings.map.help}</p>
|
||||
</div>
|
||||
<aside className="map-side">
|
||||
<h3>{'دستگاههای روبات مارکت'}</h3>
|
||||
<h3>{strings.map.machines}</h3>
|
||||
<MachineDetails nodes={nodes} actions={actions} />
|
||||
<MachineList nodes={nodes} actions={actions} />
|
||||
</aside>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { ViewLists } from './ViewLists.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} MissionListProps */
|
||||
/** Presents the MissionList using the current interface snapshot. */
|
||||
export function MissionList({ nodes, actions }) {
|
||||
return (
|
||||
<GameNode as="aside" id="objectives" model={nodes['objectives']}>
|
||||
<h2>{'مأموریتها'}</h2>
|
||||
<h2>{strings.status.missions}</h2>
|
||||
<GameNode as="ol" id="mission-list" model={nodes['mission-list']}>
|
||||
<ViewLists
|
||||
id="mission-list"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} PaymentPanelProps */
|
||||
/** Presents the PaymentPanel view using plain models and semantic native elements.
|
||||
* @param {PaymentPanelProps} props - Interface snapshots and callbacks.
|
||||
@@ -8,7 +9,7 @@ export function PaymentPanel({ nodes, actions }) {
|
||||
return (
|
||||
<>
|
||||
<GameNode as="section" id="payment" hidden model={nodes['payment']}>
|
||||
<small>{'کارتخوان روبات مارکت'}</small>
|
||||
<small>{strings.payment.reader}</small>
|
||||
<GameNode as="h2" id="payment-title" model={nodes['payment-title']}></GameNode>
|
||||
<GameNode as="p" id="payment-step" model={nodes['payment-step']}></GameNode>
|
||||
<div className="actions">
|
||||
@@ -20,7 +21,7 @@ export function PaymentPanel({ nodes, actions }) {
|
||||
model={nodes['swipe']}
|
||||
onAction={() => actions.emit('swipe')}
|
||||
>
|
||||
{'کشیدن کارت · C'}
|
||||
{strings.payment.swipe}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -30,7 +31,7 @@ export function PaymentPanel({ nodes, actions }) {
|
||||
model={nodes['cancel-payment']}
|
||||
onAction={() => actions.emit('cancel-payment')}
|
||||
>
|
||||
{'انصراف'}
|
||||
{strings.common.cancel}
|
||||
</GameNode>
|
||||
</div>
|
||||
</GameNode>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} RankProgressProps */
|
||||
/** Presents the RankProgress using the current interface snapshot. */
|
||||
export function RankProgress({ nodes }) {
|
||||
return (
|
||||
<GameNode as="div" id="rank" model={nodes['rank']}>
|
||||
<GameNode as="b" id="rank-name" model={nodes['rank-name']}>
|
||||
{'تازهوارد'}
|
||||
{strings.status.rankStart}
|
||||
</GameNode>
|
||||
<GameNode as="span" id="xp" model={nodes['xp']}>
|
||||
{'۰ XP'}
|
||||
{strings.status.xpStart}
|
||||
</GameNode>
|
||||
<div className="bar">
|
||||
<GameNode as="i" id="xp-bar" model={nodes['xp-bar']}></GameNode>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
import { robotFace } from '@/config/assets.js';
|
||||
/** @typedef {{nodes:object,actions:object}} RealIntroDialogProps */
|
||||
/** Presents the RealIntroDialog view using plain models and semantic native elements.
|
||||
* @param {RealIntroDialogProps} props - Interface snapshots and callbacks.
|
||||
@@ -13,18 +15,13 @@ export function RealIntroDialog({ nodes, actions }) {
|
||||
model={nodes['real-intro']}
|
||||
onClose={() => actions.close('real-intro')}
|
||||
>
|
||||
<img src="./robot/serious.webp" alt="" width="72" height="68" />
|
||||
<h2>{'بازی در دنیای واقعی'}</h2>
|
||||
<p>
|
||||
{
|
||||
'با روشن کردن این حالت، موقعیت گوشی شما (GPS) روی نقشهٔ بازی نمایش داده میشود. وقتی واقعاً به محل یک دستگاه روبات مارکت برسید، میتوانید خرید کنید و ۲۰۰ امتیاز اضافه بگیرید.'
|
||||
}
|
||||
</p>
|
||||
<img src={robotFace('serious')} alt="" width="72" height="68" />
|
||||
<h2>{strings.realIntro.title}</h2>
|
||||
<p>{strings.realIntro.body}</p>
|
||||
<ul>
|
||||
<li>{'هنگام راه رفتن به خیابان و ماشینها نگاه کنید، نه به صفحه.'}</li>
|
||||
<li>{'بچهها فقط همراه یک بزرگتر بازی کنند.'}</li>
|
||||
<li>{'هنگام رانندگی از این حالت استفاده نکنید؛ در سرعت بالا خرید غیرفعال میشود.'}</li>
|
||||
<li>{'موقعیت شما فقط روی همین گوشی میماند و جایی ارسال نمیشود.'}</li>
|
||||
{strings.realIntro.safety.map((line) => (
|
||||
<li key={line}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="actions">
|
||||
<GameNode
|
||||
@@ -34,7 +31,7 @@ export function RealIntroDialog({ nodes, actions }) {
|
||||
model={nodes['real-go']}
|
||||
onAction={() => actions.emit('real-go')}
|
||||
>
|
||||
{'قبول دارم، شروع کن'}
|
||||
{strings.realIntro.accept}
|
||||
</GameNode>
|
||||
<GameNode
|
||||
as="button"
|
||||
@@ -44,7 +41,7 @@ export function RealIntroDialog({ nodes, actions }) {
|
||||
model={nodes['real-cancel']}
|
||||
onAction={() => actions.emit('real-cancel')}
|
||||
>
|
||||
{'انصراف'}
|
||||
{strings.common.cancel}
|
||||
</GameNode>
|
||||
</div>
|
||||
</GameNode>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { ViewLists } from './ViewLists.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} ShopDialogProps */
|
||||
/** Presents the ShopDialog view using plain models and semantic native elements.
|
||||
* @param {ShopDialogProps} props - Interface snapshots and callbacks.
|
||||
@@ -20,7 +21,7 @@ export function ShopDialog({ nodes, actions }) {
|
||||
type="button"
|
||||
className="close"
|
||||
id="shop-close"
|
||||
aria-label="بستن"
|
||||
aria-label={strings.shop.close}
|
||||
model={nodes['shop-close']}
|
||||
onAction={() => actions.emit('shop-close')}
|
||||
>
|
||||
@@ -35,9 +36,7 @@ export function ShopDialog({ nodes, actions }) {
|
||||
onAction={(name, value) => actions.emit(name, value)}
|
||||
/>
|
||||
</GameNode>
|
||||
<p className="rate">
|
||||
{'۱۰ هتومان = ۱۰۰٬۰۰۰ ریال · مبالغ نمایشیاند و پرداخت واقعی انجام نمیشود.'}
|
||||
</p>
|
||||
<p className="rate">{strings.shop.note}</p>
|
||||
</GameNode>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { SHOWROOM_CATALOG } from '@/constants/showroomCatalog.js';
|
||||
import { robotFace } from '@/config/assets.js';
|
||||
import { showroomModelById, formatRial } from '@/utils/showroomAnswers.js';
|
||||
import { ShowroomImage } from './ShowroomImage.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
|
||||
/** @typedef {{nodes:object,actions:object}} ShowroomDialogProps */
|
||||
/** Presents the factory sample catalog and questions answered by the robot host.
|
||||
@@ -22,17 +24,17 @@ export function ShowroomDialog({ nodes, actions }) {
|
||||
>
|
||||
<header className="mb-5 flex items-center justify-between gap-4">
|
||||
<h2 id="showroom-title" className="text-xl font-bold">
|
||||
نمایشگاه کارخانه · {model.name}
|
||||
{strings.showroom.heading(model.name)}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="بستن معرفی دستگاه"
|
||||
aria-label={strings.showroom.close}
|
||||
onClick={() => actions.close('showroom')}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
<nav aria-label="مدل دستگاه" className="mb-5 flex flex-wrap gap-3">
|
||||
<nav aria-label={strings.showroom.models} className="mb-5 flex flex-wrap gap-3">
|
||||
{SHOWROOM_CATALOG.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
@@ -49,16 +51,14 @@ export function ShowroomDialog({ nodes, actions }) {
|
||||
<ShowroomImage key={model.id} model={model} />
|
||||
<section>
|
||||
<h3 className="mb-3 text-lg font-semibold">{model.name}</h3>
|
||||
<p className="mb-3 text-gold">قیمت خود دستگاه: {formatRial(model.priceNumeric)}</p>
|
||||
<p className="mb-3 text-gold">{strings.showroom.price(formatRial(model.priceNumeric))}</p>
|
||||
<p>{model.description}</p>
|
||||
<ul className="my-4 list-inside list-disc">
|
||||
{model.highlights.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sm opacity-75">
|
||||
این بخش معرفی محصول است؛ خرید واقعی انجام نمیشود و بودجهٔ هتومان تغییر نمیکند.
|
||||
</p>
|
||||
<p className="text-sm opacity-75">{strings.showroom.displayOnly}</p>
|
||||
</section>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
@@ -72,7 +72,7 @@ export function ShowroomDialog({ nodes, actions }) {
|
||||
})
|
||||
}
|
||||
>
|
||||
روبات، قیمتش چنده؟
|
||||
{strings.showroom.askPrice}
|
||||
</button>
|
||||
<button
|
||||
className="rounded border border-teal px-4 py-3"
|
||||
@@ -84,19 +84,16 @@ export function ShowroomDialog({ nodes, actions }) {
|
||||
})
|
||||
}
|
||||
>
|
||||
روبات، امکاناتش چیه؟
|
||||
{strings.showroom.askFeatures}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 flex items-start gap-3 border-s-2 border-gold ps-4">
|
||||
<img
|
||||
className="h-16 w-16 shrink-0 object-contain"
|
||||
src="./robot/happy.webp"
|
||||
alt="روبات راهنمای نمایشگاه"
|
||||
src={robotFace('happy')}
|
||||
alt={strings.showroom.robotAlt}
|
||||
/>
|
||||
<p role="status">
|
||||
{nodes['showroom-response']?.text ??
|
||||
'روبات: سلام! دربارهٔ قیمت و امکانات این مدل از من بپرس.'}
|
||||
</p>
|
||||
<p role="status">{nodes['showroom-response']?.text ?? strings.showroom.greeting}</p>
|
||||
</div>
|
||||
</GameNode>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
|
||||
/** @typedef {{model:object}} ShowroomImageProps */
|
||||
/** Shows supplied catalog assets, with an explicit fallback while files are unavailable.
|
||||
@@ -10,7 +11,7 @@ export function ShowroomImage({ model }) {
|
||||
const [failed, setFailed] = useState({});
|
||||
const path = model[variant];
|
||||
const unavailable = failed[path];
|
||||
const src = new URL(`.${path}`, document.baseURI).href;
|
||||
const src = new URL(path, document.baseURI).href;
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
{unavailable ? (
|
||||
@@ -18,13 +19,13 @@ export function ShowroomImage({ model }) {
|
||||
className="grid min-h-40 place-content-center rounded border border-white/20 p-4 text-center text-hud"
|
||||
role="status"
|
||||
>
|
||||
نمای سهبعدی این مدل داخل کارخانه موجود است؛ فایل تصویر هنوز اضافه نشده.
|
||||
{strings.showroom.noImage}
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
className="mx-auto max-h-64 max-w-full object-contain"
|
||||
src={src}
|
||||
alt={`${model.name} · ${variant === 'imageAlt' ? 'رنگ سفید' : variant === 'gif' ? 'نمای متحرک' : 'نمای اصلی'}`}
|
||||
alt={`${model.name} · ${strings.showroom.views[variant]}`}
|
||||
onError={() =>
|
||||
setFailed((previous) => ({
|
||||
...previous,
|
||||
@@ -34,11 +35,7 @@ export function ShowroomImage({ model }) {
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
['image', 'نمای اصلی'],
|
||||
['imageAlt', 'رنگ سفید'],
|
||||
['gif', 'نمای متحرک'],
|
||||
].map(([key, label]) => (
|
||||
{Object.entries(strings.showroom.views).map(([key, label]) => (
|
||||
<button
|
||||
className="rounded border border-white/20 px-3 py-2"
|
||||
key={key}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} StatusBarProps */
|
||||
/** Presents the StatusBar using the current interface snapshot. */
|
||||
export function StatusBar({ nodes }) {
|
||||
return (
|
||||
<GameNode as="div" id="status" model={nodes['status']}>
|
||||
<div className="stat">
|
||||
<small>{'بودجه'}</small>
|
||||
<small>{strings.status.budget}</small>
|
||||
<GameNode as="strong" id="balance" model={nodes['balance']}>
|
||||
{'۵۰۰'}
|
||||
{strings.status.budgetStart}
|
||||
</GameNode>
|
||||
<span>{'هتومان'}</span>
|
||||
<span>{strings.units.hetomanName}</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<small>{'ساعت'}</small>
|
||||
<small>{strings.status.clock}</small>
|
||||
<GameNode as="strong" id="clock" model={nodes['clock']}>
|
||||
{'۰۸:۳۰'}
|
||||
{strings.status.clockStart}
|
||||
</GameNode>
|
||||
</div>
|
||||
<GameNode as="div" className="stat" id="speed-stat" hidden model={nodes['speed-stat']}>
|
||||
<small>{'سرعت'}</small>
|
||||
<small>{strings.status.speed}</small>
|
||||
<GameNode as="strong" id="speed" model={nodes['speed']}>
|
||||
{'۰'}
|
||||
{strings.status.speedStart}
|
||||
</GameNode>
|
||||
<span>{'km/h'}</span>
|
||||
</GameNode>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} TouchControlsProps */
|
||||
/** Presents the TouchControls using the current interface snapshot. */
|
||||
export function TouchControls({ nodes, actions }) {
|
||||
@@ -36,7 +37,7 @@ export function TouchControls({ nodes, actions }) {
|
||||
model={nodes['touch-shift']}
|
||||
onAction={() => actions.emit('touch-shift')}
|
||||
>
|
||||
{'دویدن'}
|
||||
{strings.status.run}
|
||||
</GameNode>
|
||||
</div>
|
||||
</GameNode>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GameNode } from '@/components/shared/GameNode.jsx';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
/** @typedef {{nodes:object,actions:object}} UpdateNoticeProps */
|
||||
/** Presents the UpdateNotice view using plain models and semantic native elements.
|
||||
* @param {UpdateNoticeProps} props - Interface snapshots and callbacks.
|
||||
@@ -8,7 +9,7 @@ export function UpdateNotice({ nodes, actions }) {
|
||||
return (
|
||||
<>
|
||||
<GameNode as="div" id="update-toast" hidden model={nodes['update-toast']}>
|
||||
{'نسخهٔ جدید بازی آماده است '}
|
||||
{strings.app.updateReady}
|
||||
<GameNode
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -16,7 +17,7 @@ export function UpdateNotice({ nodes, actions }) {
|
||||
model={nodes['update-reload']}
|
||||
onAction={() => actions.emit('update-reload')}
|
||||
>
|
||||
{'بهروزرسانی'}
|
||||
{strings.app.update}
|
||||
</GameNode>
|
||||
</GameNode>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatHetoman } from '@/game/economy/gameState.js';
|
||||
import { strings } from '@/i18n/fa.js';
|
||||
|
||||
/** @typedef {{id:string,model?:object,onAction:function}} ViewListsProps */
|
||||
/** Renders structured game lists as JSX; strings are never interpreted as HTML.
|
||||
@@ -20,8 +21,8 @@ export function ViewLists({ id, model = {}, onAction }) {
|
||||
<span className="price">{formatHetoman(item.price)}</span>
|
||||
{item.price !== item.cost && <s>{formatHetoman(item.cost)}</s>}
|
||||
<small>
|
||||
{item.type === 'drink' ? 'نوشیدنی' : 'خوراکی'}
|
||||
{item.blocked === 'funds' ? ' · موجودی کافی نیست' : ''}
|
||||
{item.type === 'drink' ? strings.lists.drink : strings.lists.snack}
|
||||
{item.blocked === 'funds' ? strings.lists.noFunds : ''}
|
||||
</small>
|
||||
</button>
|
||||
));
|
||||
@@ -52,7 +53,7 @@ export function ViewLists({ id, model = {}, onAction }) {
|
||||
{item.name}
|
||||
{item.visited ? ' ✓' : ''}
|
||||
<span className="meta">
|
||||
{item.locked ? item.note : `${item.category} · ${item.open ? 'باز' : 'بسته'}`}
|
||||
{item.locked ? item.note : strings.lists.machineState(item.category, item.open)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="dist">{item.distance}</span>
|
||||
|
||||
Reference in New Issue
Block a user