World map
Painted night scene, springing pins, and the objective dock.
1011
World 2 · Emberfall
4
Castle GatesBeat it with 3 ★ to earn a chest
"use client";
import {
Avatar,
Body,
Bolt,
BottomStack,
BundleCell,
BundleCells,
Button,
Chrome,
ChromeRow,
Coin,
CornerRibbon,
Counter,
Counters,
Dim,
Dock,
FloatChrome,
Gem,
GroupKicker,
Heart,
LevelBadge,
MapPin,
MapScene,
Meter,
Pack,
PackRow,
Panel,
PageDots,
Podium,
PodiumColumn,
ProfileChip,
RankRow,
Ribbon,
Screen,
ScreenName,
SettingsRow,
ShopRow,
StarBurst,
Strike,
Tab,
TabBar,
TimerPill,
Toggle,
TopRibbon,
Slider,
Version,
Well,
Wordmark,
YouBadge,
} from "@objectifthunes/royal-games-ui";
import { useState, type ReactNode } from "react";
const ignoreAction = () => undefined;
interface ResourceCountersProps {
interactive?: boolean;
onAddCoins?: () => void;
onAddGems?: () => void;
}
interface NavigationProps {
onNavigate?: (value: string) => void;
}
function ResourceCounters({
interactive = true,
onAddCoins = ignoreAction,
onAddGems = ignoreAction,
}: ResourceCountersProps) {
return (
<Counters aria-label="Player resources">
{interactive ? (
<>
<Counter graphic={<Coin />} onAdd={onAddCoins} addLabel="Add coins">
2,450
</Counter>
<Counter graphic={<Gem />} onAdd={onAddGems} addLabel="Add gems">
12
</Counter>
</>
) : (
<>
<Counter graphic={<Coin />}>2,450</Counter>
<Counter graphic={<Gem />}>12</Counter>
</>
)}
</Counters>
);
}
interface RoyalChromeProps {
name: ReactNode;
onAddCoins?: () => void;
onAddGems?: () => void;
onOpenSettings?: () => void;
}
function RoyalChrome({
name,
onAddCoins = ignoreAction,
onAddGems = ignoreAction,
onOpenSettings = ignoreAction,
}: RoyalChromeProps) {
return (
<Chrome>
<ChromeRow tone="velvet">
<ProfileChip
avatar={
<Avatar decorative size="small">
♛
</Avatar>
}
name="Max"
caption="LEVEL 12"
/>
<ResourceCounters onAddCoins={onAddCoins} onAddGems={onAddGems} />
</ChromeRow>
<ChromeRow tone="stone">
<ScreenName>{name}</ScreenName>
<Button
tone="primary"
size="small"
iconOnly
aria-label="Settings"
onClick={onOpenSettings}
>
⚙
</Button>
</ChromeRow>
</Chrome>
);
}
interface RoyalTabsProps extends NavigationProps {
initial: string;
}
function RoyalTabs({ initial, onNavigate = ignoreAction }: RoyalTabsProps) {
const [tab, setTab] = useState(initial);
const handleTabChange = (value: string) => {
setTab(value);
onNavigate(value);
};
return (
<TabBar
aria-label="Primary navigation"
value={tab}
onValueChange={handleTabChange}
>
<Tab value="shop" icon="🛒">
Shop
</Tab>
<Tab value="ranks" icon="♛">
Ranks
</Tab>
<Tab value="home" icon="⌂">
Home
</Tab>
<Tab value="map" icon="⚑">
Map
</Tab>
<Tab value="more" icon="⚙" dot>
More
</Tab>
</TabBar>
);
}
// SCREEN_RECIPE: MapScreen
export interface MapScreenProps extends NavigationProps {
onBack?: () => void;
onSelectStage?: (stage: number) => void;
}
/** Copy-ready authored-scene recipe with host-owned stage actions. */
export function MapScreen({
onBack = ignoreAction,
onNavigate = ignoreAction,
onSelectStage = ignoreAction,
}: MapScreenProps = {}) {
return (
<Screen aria-label="World map">
<MapScene aria-label="Emberfall route">
<MapPin
variant="button"
state="complete"
stars={2}
label="Stage seven complete"
onPress={() => onSelectStage(7)}
style={{
insetInlineStart: "calc(var(--rg-u) * 70)",
insetBlockStart: "calc(var(--rg-u) * 590)",
}}
>
7
</MapPin>
<MapPin
variant="button"
state="complete"
stars={3}
label="Stage eight complete"
onPress={() => onSelectStage(8)}
style={{
insetInlineStart: "calc(var(--rg-u) * 277)",
insetBlockStart: "calc(var(--rg-u) * 488)",
}}
>
8
</MapPin>
<MapPin
variant="button"
state="current"
label="Play stage nine"
onPress={() => onSelectStage(9)}
style={{
insetInlineStart: "calc(var(--rg-u) * 150)",
insetBlockStart: "calc(var(--rg-u) * 388)",
}}
>
9
</MapPin>
<MapPin
variant="static"
state="unavailable"
label="Stage ten unavailable"
style={{
insetInlineStart: "calc(var(--rg-u) * 120)",
insetBlockStart: "calc(var(--rg-u) * 272)",
}}
>
10
</MapPin>
<MapPin
variant="static"
state="unavailable"
label="Stage eleven unavailable"
style={{
insetInlineStart: "calc(var(--rg-u) * 260)",
insetBlockStart: "calc(var(--rg-u) * 170)",
}}
>
11
</MapPin>
</MapScene>
<FloatChrome variant="screen">
<Button
tone="primary"
size="small"
iconOnly
aria-label="Back"
onClick={onBack}
>
‹
</Button>
<Ribbon size="compact">World 2 · Emberfall</Ribbon>
<Counter graphic={<Heart />}>4</Counter>
</FloatChrome>
<BottomStack>
<Dock tone="stone" density="shallow">
<SettingsRow
variant="control"
icon={<LevelBadge level="9" label="LVL" aria-label="Level nine" />}
label="Castle Gates"
caption="Beat it with 3 ★ to earn a chest"
control={
<Button
tone="accent"
size="small"
onClick={() => onSelectStage(9)}
>
GO
</Button>
}
/>
<Meter value={12} max={20} label="Chapter 3" valueText="12 / 20" />
</Dock>
<RoyalTabs initial="map" onNavigate={onNavigate} />
</BottomStack>
</Screen>
);
}This is the exact component rendered above. Pin coordinates are authored in Screen design units, so the route remains aligned as the device changes size or orientation.