@idosgames/mcp 0.1.44 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1597 -1542
- package/package.json +1 -1
- package/registry/host.json +3 -3
- package/registry/index.json +172 -125
- package/registry/modules/ai.json +5 -5
- package/registry/modules/board-game.json +6 -6
- package/registry/modules/boosts.json +5 -5
- package/registry/modules/character.json +9 -9
- package/registry/modules/chat.json +3 -3
- package/registry/modules/collections.json +5 -5
- package/registry/modules/coop-events.json +6 -6
- package/registry/modules/craft.json +5 -5
- package/registry/modules/deals.json +6 -6
- package/registry/modules/events.json +3 -3
- package/registry/modules/friends.json +3 -3
- package/registry/modules/hero-run.json +15 -234
- package/registry/modules/idle-rpg.json +8 -8
- package/registry/modules/inventory.json +3 -3
- package/registry/modules/invite.json +3 -3
- package/registry/modules/kit-game.json +171 -0
- package/registry/modules/leaderboard.json +3 -3
- package/registry/modules/lootboxes.json +5 -5
- package/registry/modules/mailbox.json +3 -3
- package/registry/modules/marketplace.json +3 -3
- package/registry/modules/mini-game.json +3 -3
- package/registry/modules/multiplayer.json +3 -3
- package/registry/modules/premium.json +5 -5
- package/registry/modules/profile.json +3 -3
- package/registry/modules/pvp.json +3 -3
- package/registry/modules/quests.json +3 -3
- package/registry/modules/rewards.json +3 -3
- package/registry/modules/season.json +3 -3
- package/registry/modules/store.json +6 -6
- package/registry/modules/tutorials.json +3 -3
- package/registry/modules/video-studio.json +9 -9
- package/registry/modules/voxelcraft.json +6 -6
- package/registry/modules/workshop.json +6 -6
- package/registry/skills/currency-system.json +1 -1
- package/registry/skills/idosgames-compose-modules.json +1 -1
- package/registry/skills/idosgames-getting-started.json +1 -1
- package/registry/skills/idosgames-ui-kit.json +1 -1
- package/registry/skills/store-system.json +1 -1
- package/registry/video.json +1 -1
|
@@ -45,17 +45,17 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@idosgames/core": "0.
|
|
49
|
-
"@idosgames/module-sdk": "0.13.
|
|
50
|
-
"@idosgames/react": "0.
|
|
51
|
-
"@idosgames/wallet": "0.6.
|
|
48
|
+
"@idosgames/core": "0.26.1",
|
|
49
|
+
"@idosgames/module-sdk": "0.13.3",
|
|
50
|
+
"@idosgames/react": "0.7.1",
|
|
51
|
+
"@idosgames/wallet": "0.6.10",
|
|
52
52
|
"@tanstack/react-query": "5.101.2",
|
|
53
53
|
"react": "19.2.7",
|
|
54
54
|
"three": "0.185.1",
|
|
55
55
|
"viem": "2.55.2",
|
|
56
56
|
"wagmi": "3.7.2"
|
|
57
57
|
},
|
|
58
|
-
"contentHash": "
|
|
58
|
+
"contentHash": "a93a6822f166b908586371e556127149c5d275ee8044bf0c1c0c5384c345d266",
|
|
59
59
|
"files": [
|
|
60
60
|
{
|
|
61
61
|
"path": "chrome.ts",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"path": "components/SpecialPanel.tsx",
|
|
114
|
-
"content": "import {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport type { BoardPendingInteraction } from \"@idosgames/core\";\nimport {\n Icon,\n formatAmount,\n grantLines,\n useCatalog,\n useSound,\n useToast,\n useUiKit,\n type ResourceLine,\n} from \"@idosgames/react/ui\";\nimport { useIDosGamesClient } from \"../react/context\";\nimport { setPanelBusy } from \"../react/boardBusy\";\nimport { useBoardState } from \"../react/hooks\";\nimport { resolveSpecialChoices } from \"../data/boardConfig\";\nimport type { SpecialChoice } from \"../data/interactions\";\nimport { ChestArt } from \"./modal/art\";\nimport {\n Stage,\n at,\n easeOutBack,\n outlinedText,\n tween,\n type StageHandle,\n} from \"./modal/stage\";\n\n// The special tile (the reference's SpecialPanel, BoardSurvival BoardScene): the board goes dark,\n// a plum ribbon names the mode, one big green card offers the long run (PLAY) and \"Skip\" below takes\n// the instant prize. The web board has no run of its own, so a Timed choice whose payout is measured\n// by run statistics is not offered here — the card then takes the instant prize. After a Timed choice\n// the victory screen (Play_Result_Victory) shows the rewards with Claim and Claim ×3.\n\nconst FALLBACK: SpecialChoice[] = [\n { ChoiceID: \"Instant\", Mode: \"Instant\" },\n { ChoiceID: \"Timed\", Mode: \"Timed\" },\n];\n\nexport function SpecialPanel({\n pending,\n onDone,\n}: {\n pending: BoardPendingInteraction;\n onDone: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const kit = useUiKit();\n const play = useSound();\n const toast = useToast();\n const catalog = useCatalog();\n const stage = useRef<StageHandle>(null);\n const card = useRef<HTMLDivElement>(null);\n const [busy, setBusy] = useState(false);\n\n useEffect(() => {\n setPanelBusy(true);\n return () => setPanelBusy(false);\n }, []);\n\n const special = pending.Special;\n const timedChosen =\n special?.ChosenMode === \"Timed\" || special?.ChosenMode === 1;\n const board = useBoardState();\n const stashed = special?.Choices as SpecialChoice[] | null | undefined;\n const configured = resolveSpecialChoices(\n client,\n board?.StageLevel ?? 1,\n special?.ModeID,\n ) as SpecialChoice[];\n const choices = stashed?.length\n ? stashed\n : configured.length\n ? configured\n : FALLBACK;\n const instant = choices.find((c) => c.Mode === \"Instant\");\n const timedRaw = choices.find((c) => c.Mode === \"Timed\");\n // A run-stat payout needs the reference's survival run — not on the web board.\n const timed =\n timedRaw && !(timedRaw as { RunStats?: unknown }).RunStats\n ? timedRaw\n : undefined;\n const [phase, setPhase] = useState<\"choose\" | \"victory\">(\n timedChosen ? \"victory\" : \"choose\",\n );\n\n const linesOf = (c: SpecialChoice | undefined): ResourceLine[] =>\n grantLines(\n c?.Reward?.Operation?.Grant as Parameters<typeof grantLines>[0],\n ).filter((l) => l.amount > 0);\n\n const close = async (\n lines: ResourceLine[],\n from: Element | null,\n ): Promise<void> => {\n const rect = from?.getBoundingClientRect() ?? null;\n kit.fx.reserve(lines);\n const fade = stage.current?.fadeOut();\n kit.fx.fly(lines, () => rect);\n await fade;\n onDone();\n };\n\n const choose = async (choice: SpecialChoice | undefined): Promise<void> => {\n if (!choice || busy) return;\n setBusy(true);\n play(\"click\");\n const res = await client.gameLoop.boardSpecialChoose(\n choice.ChoiceID ?? choice.Mode ?? \"Instant\",\n );\n if (!res.ok) {\n // The reference's FailAndClose: say why and close — the pending, if alive, comes back on ROLL.\n toast(res.error, \"error\");\n await stage.current?.fadeOut();\n onDone();\n return;\n }\n if (choice.Mode === \"Timed\") {\n setBusy(false);\n setPhase(\"victory\");\n return;\n }\n play(\"reward\");\n const lines = grantLines(res.data.Operation?.Grant).filter(\n (l) => l.amount > 0,\n );\n await close(lines.length ? lines : linesOf(choice), card.current);\n };\n\n const claim = async (withAd: boolean): Promise<void> => {\n if (busy) return;\n setBusy(true);\n play(\"claim\");\n if (withAd) {\n const applied = await client.gameLoop.boardSpecialApplyMultiplier();\n if (!applied.ok) {\n setBusy(false);\n toast(applied.error, \"error\");\n return;\n }\n }\n const res = await client.gameLoop.boardSpecialClaim();\n if (!res.ok) {\n toast(res.error, \"error\");\n await stage.current?.fadeOut();\n onDone();\n return;\n }\n const lines = grantLines(\n (res.data as { Operation?: { Grant?: Parameters<typeof grantLines>[0] } })\n .Operation?.Grant,\n ).filter((l) => l.amount > 0);\n await close(lines, document.querySelector(\"[data-board-victory-rewards]\"));\n };\n\n // The card: Timed (PLAY) when the web board can play it, else the instant prize.\n const cardChoice = timed ?? instant;\n const cardIsTimed = cardChoice === timed && timed !== undefined;\n\n return (\n <Stage ref={stage} dim=\"rgba(0,0,0,.72)\">\n {phase === \"choose\" ? (\n <>\n <Ribbon\n y={500}\n w={900}\n h={150}\n tone=\"plum\"\n text={\n special?.ModeID && special.ModeID !== \"default\"\n ? special.ModeID\n : \"SPECIAL\"\n }\n size={65}\n />\n <div\n ref={card}\n onClick={() => void choose(cardChoice)}\n style={{\n ...at(0, 0, 483, 630),\n cursor: busy ? \"default\" : \"pointer\",\n animation:\n \"idos-board-card-in .45s cubic-bezier(.34,1.56,.64,1) both, idos-board-breathe 2.4s ease-in-out .5s infinite\",\n }}\n >\n <GreenCard>\n <div style={{ ...abs(0, 85 - 192, 384, 384, 483, 630) }}>\n <ChestArt />\n </div>\n <div\n style={{\n ...abs(0, -130 - 55, 423, 110, 483, 630),\n display: \"grid\",\n placeItems: \"center\",\n textAlign: \"center\",\n ...outlinedText(42),\n }}\n >\n {cardIsTimed\n ? \"Bigger reward — play to multiply it.\"\n : \"Claim your reward instantly.\"}\n </div>\n <div\n style={{\n ...abs(0, -245 - 62, 412, 124, 483, 630),\n display: \"grid\",\n placeItems: \"center\",\n ...outlinedText(70, \"#fff36a\", \"#145a2c\"),\n }}\n >\n {cardIsTimed ? \"PLAY\" : \"CLAIM\"}\n </div>\n </GreenCard>\n </div>\n {cardIsTimed && instant ? (\n <div\n onClick={() => void choose(instant)}\n style={{\n ...at(0, -500, 300, 100),\n display: \"grid\",\n placeItems: \"center\",\n cursor: \"pointer\",\n ...outlinedText(58),\n }}\n >\n Skip\n </div>\n ) : null}\n </>\n ) : (\n <Victory\n lines={linesOf(timedRaw)}\n busy={busy}\n onClaim={() => void claim(false)}\n onClaimX3={() => void claim(true)}\n iconOf={catalog.iconOf}\n />\n )}\n </Stage>\n );\n}\n\n/** Position inside a parent box of `pw × ph` the Unity way (centre anchor, y up). */\nfunction abs(\n x: number,\n y: number,\n w: number,\n h: number,\n pw: number,\n ph: number,\n): CSSProperties {\n return {\n position: \"absolute\",\n left: pw / 2 + x - w / 2,\n top: ph / 2 - y - h,\n width: w,\n height: h,\n };\n}\n\n/** CardFrame03 (green): outer frame, lighter inner panel shifted up, a soft top light. */\nfunction GreenCard({ children }: { children: ReactNode }): ReactNode {\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n borderRadius: 46,\n background:\n \"linear-gradient(180deg,#34d77c 0%,#2ac870 60%,#20a95b 100%)\",\n border: \"8px solid #17193a\",\n boxShadow: \"inset 0 8px 0 rgba(255,255,255,.45)\",\n boxSizing: \"border-box\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: 26,\n right: 26,\n top: 26,\n bottom: 120,\n borderRadius: 30,\n background:\n \"radial-gradient(circle at 50% 35%, #6af0a4 0%, #38e283 60%, #2fc673 100%)\",\n boxShadow: \"inset 0 -6px 0 rgba(0,0,0,.12)\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/** Title_Ribbon01 (plum / sky): a raised middle plate with folded ends. */\nfunction Ribbon({\n y,\n w,\n h,\n tone,\n text,\n size,\n}: {\n y: number;\n w: number;\n h: number;\n tone: \"plum\" | \"sky\";\n text: string;\n size: number;\n}): ReactNode {\n const [face, fold, light] =\n tone === \"plum\"\n ? [\"#8a1fff\", \"#5a10b8\", \"#c27cff\"]\n : [\"#2fa8ff\", \"#1b6fc4\", \"#8fd4ff\"];\n return (\n <div\n style={{\n ...at(0, y, w, h),\n animation: \"idos-board-card-in .4s cubic-bezier(.34,1.56,.64,1) both\",\n }}\n >\n <svg\n viewBox=\"0 0 300 150\"\n preserveAspectRatio=\"none\"\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n }}\n aria-hidden\n >\n <g stroke=\"#17193a\" strokeWidth=\"6\" strokeLinejoin=\"round\">\n <path d=\"M4 40 L70 40 L70 120 L4 120 L28 80 Z\" fill={face} />\n <path d=\"M296 40 L230 40 L230 120 L296 120 L272 80 Z\" fill={face} />\n <path\n d=\"M60 50 L80 50 L80 130 L60 120 Z M240 50 L220 50 L220 130 L240 120 Z\"\n fill={fold}\n />\n <rect x=\"70\" y=\"14\" width=\"160\" height=\"112\" rx=\"14\" fill={face} />\n </g>\n <rect x=\"80\" y=\"22\" width=\"140\" height=\"16\" rx=\"8\" fill={light} />\n </svg>\n <div\n style={{\n position: \"absolute\",\n inset: \"0 15% 12% 15%\",\n display: \"grid\",\n placeItems: \"center\",\n ...outlinedText(size),\n whiteSpace: \"nowrap\",\n }}\n >\n {text}\n </div>\n </div>\n );\n}\n\n/** Play_Result_Victory: rotating rays, the winged badge, a sky ribbon, REWARDS, item frames, Claim / Claim ×3. */\nfunction Victory({\n lines,\n busy,\n onClaim,\n onClaimX3,\n iconOf,\n}: {\n lines: ResourceLine[];\n busy: boolean;\n onClaim: () => void;\n onClaimX3: () => void;\n iconOf: (line: ResourceLine) => string;\n}): ReactNode {\n const badge = useRef<HTMLDivElement>(null);\n useEffect(() => {\n const b = badge.current;\n void tween(0.5, (t) => {\n if (b) b.style.transform = `scale(${easeOutBack(t)})`;\n });\n }, []);\n return (\n <>\n <div\n style={{\n ...at(0, 437, 620, 620),\n animation: \"idos-board-spin 6s linear infinite\",\n pointerEvents: \"none\",\n }}\n >\n <svg\n viewBox=\"0 0 200 200\"\n style={{ width: \"100%\", height: \"100%\" }}\n aria-hidden\n >\n {Array.from({ length: 12 }, (_, i) => (\n <path\n key={i}\n d=\"M100 100 L92 0 L108 0 Z\"\n fill=\"rgba(255,240,150,.45)\"\n transform={`rotate(${i * 30} 100 100)`}\n />\n ))}\n </svg>\n </div>\n <div\n ref={badge}\n style={{ ...at(0, 533.8, 536, 449), transform: \"scale(0)\" }}\n >\n <svg\n viewBox=\"0 0 536 449\"\n style={{ width: \"100%\", height: \"100%\" }}\n aria-hidden\n >\n <g stroke=\"#17193a\" strokeWidth=\"9\" strokeLinejoin=\"round\">\n <path\n d=\"M268 360 L60 250 Q20 160 70 90 Q150 160 268 170 Z\"\n fill=\"#ffd23a\"\n />\n <path\n d=\"M268 360 L476 250 Q516 160 466 90 Q386 160 268 170 Z\"\n fill=\"#ffd23a\"\n />\n <path\n d=\"M268 30 L380 90 L370 280 L268 400 L166 280 L156 90 Z\"\n fill=\"#2fa8ff\"\n />\n <path\n d=\"M268 70 L340 112 L334 262 L268 346 L202 262 L196 112 Z\"\n fill=\"#8fd4ff\"\n />\n <path d=\"M268 110 L282 250 L268 300 L254 250 Z\" fill=\"#fff\" />\n <rect\n x=\"226\"\n y=\"246\"\n width=\"84\"\n height=\"18\"\n rx=\"6\"\n fill=\"#ffd23a\"\n />\n </g>\n </svg>\n </div>\n <Ribbon y={291} w={690} h={143} tone=\"sky\" text=\"VICTORY!\" size={67} />\n <div\n style={{\n ...at(0, 11, 650, 67),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: 24,\n }}\n >\n <div\n style={{\n width: 174,\n height: 10,\n borderRadius: 5,\n background: \"linear-gradient(90deg, transparent, #ffe066)\",\n }}\n />\n <div style={outlinedText(40)}>REWARDS</div>\n <div\n style={{\n width: 174,\n height: 10,\n borderRadius: 5,\n background: \"linear-gradient(90deg, #ffe066, transparent)\",\n }}\n />\n </div>\n <div\n data-board-victory-rewards\n style={{\n ...at(0, -150, 915, 190),\n display: \"flex\",\n justifyContent: \"center\",\n gap: 24,\n }}\n >\n {(lines.length ? lines : []).map((l, i) => (\n <div\n key={i}\n style={{\n width: 190,\n height: 190,\n borderRadius: 34,\n background:\n \"radial-gradient(circle at 50% 40%, #35fbff 0%, #0b69d6 75%)\",\n border: \"7px solid #1d3fb1\",\n boxShadow: \"0 0 0 5px #17193a\",\n position: \"relative\",\n display: \"grid\",\n placeItems: \"center\",\n animation: `idos-board-card-in .4s cubic-bezier(.34,1.56,.64,1) ${0.3 + i * 0.1}s both`,\n }}\n >\n <Icon glyph={iconOf(l)} size={130} />\n <div\n style={{ position: \"absolute\", bottom: 6, ...outlinedText(38) }}\n >\n {formatAmount(l.amount)}\n </div>\n </div>\n ))}\n </div>\n <GameButton\n y={-644}\n w={350}\n h={130}\n tone=\"sky\"\n disabled={busy}\n onClick={onClaim}\n >\n Claim\n </GameButton>\n <GameButton\n y={-810}\n w={370}\n h={160}\n tone=\"green\"\n disabled={busy}\n onClick={onClaimX3}\n >\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 14 }}>\n <svg viewBox=\"0 0 40 40\" width={70} height={70} aria-hidden>\n <rect\n x=\"3\"\n y=\"7\"\n width=\"34\"\n height=\"26\"\n rx=\"6\"\n fill=\"#ff4d4d\"\n stroke=\"#17193a\"\n strokeWidth=\"3\"\n />\n <path d=\"M16 13 L27 20 L16 27 Z\" fill=\"#fff\" />\n </svg>\n Claim x3\n </span>\n </GameButton>\n </>\n );\n}\n\n/** Button01 (sky / green): glossy pill with a dark outline and a bottom lip. */\nfunction GameButton({\n y,\n w,\n h,\n tone,\n disabled,\n onClick,\n children,\n}: {\n y: number;\n w: number;\n h: number;\n tone: \"sky\" | \"green\";\n disabled?: boolean;\n onClick: () => void;\n children: ReactNode;\n}): ReactNode {\n const [top, bottom] =\n tone === \"sky\"\n ? [\"#5cc8ff\", \"#1c8fe6\", \"#0d5aa8\"]\n : [\"#7be34a\", \"#33b31d\", \"#1f7a10\"];\n return (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={onClick}\n style={{\n ...at(0, y, w, h),\n borderRadius: h / 2.6,\n border: \"7px solid #17193a\",\n background: `linear-gradient(180deg, ${top} 0%, ${bottom} 100%)`,\n boxShadow: `inset 0 8px 0 rgba(255,255,255,.4)`,\n cursor: disabled ? \"default\" : \"pointer\",\n opacity: disabled ? 0.7 : 1,\n fontFamily: \"inherit\",\n ...outlinedText(48),\n }}\n >\n {children}\n </button>\n );\n}\n"
|
|
114
|
+
"content": "import {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport type { BoardPendingInteraction } from \"@idosgames/core\";\nimport {\n Icon,\n formatAmount,\n grantLines,\n useCatalog,\n useSound,\n useToast,\n useUiKit,\n type ResourceLine,\n payWithCredits,\n errorText,\n} from \"@idosgames/react/ui\";\nimport { useIDosGamesClient } from \"../react/context\";\nimport { setPanelBusy } from \"../react/boardBusy\";\nimport { useBoardState } from \"../react/hooks\";\nimport { resolveSpecialChoices } from \"../data/boardConfig\";\nimport type { SpecialChoice } from \"../data/interactions\";\nimport { ChestArt } from \"./modal/art\";\nimport {\n Stage,\n at,\n easeOutBack,\n outlinedText,\n tween,\n type StageHandle,\n} from \"./modal/stage\";\n\n// The special tile (the reference's SpecialPanel, BoardSurvival BoardScene): the board goes dark,\n// a plum ribbon names the mode, one big green card offers the long run (PLAY) and \"Skip\" below takes\n// the instant prize. The web board has no run of its own, so a Timed choice whose payout is measured\n// by run statistics is not offered here — the card then takes the instant prize. After a Timed choice\n// the victory screen (Play_Result_Victory) shows the rewards with Claim and Claim ×3.\n\nconst FALLBACK: SpecialChoice[] = [\n { ChoiceID: \"Instant\", Mode: \"Instant\" },\n { ChoiceID: \"Timed\", Mode: \"Timed\" },\n];\n\nexport function SpecialPanel({\n pending,\n onDone,\n}: {\n pending: BoardPendingInteraction;\n onDone: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const kit = useUiKit();\n const play = useSound();\n const toast = useToast();\n const catalog = useCatalog();\n const stage = useRef<StageHandle>(null);\n const card = useRef<HTMLDivElement>(null);\n const [busy, setBusy] = useState(false);\n\n useEffect(() => {\n setPanelBusy(true);\n return () => setPanelBusy(false);\n }, []);\n\n const special = pending.Special;\n const timedChosen =\n special?.ChosenMode === \"Timed\" || special?.ChosenMode === 1;\n const board = useBoardState();\n const stashed = special?.Choices as SpecialChoice[] | null | undefined;\n const configured = resolveSpecialChoices(\n client,\n board?.StageLevel ?? 1,\n special?.ModeID,\n ) as SpecialChoice[];\n const choices = stashed?.length\n ? stashed\n : configured.length\n ? configured\n : FALLBACK;\n const instant = choices.find((c) => c.Mode === \"Instant\");\n const timedRaw = choices.find((c) => c.Mode === \"Timed\");\n // A run-stat payout needs the reference's survival run — not on the web board.\n const timed =\n timedRaw && !(timedRaw as { RunStats?: unknown }).RunStats\n ? timedRaw\n : undefined;\n const [phase, setPhase] = useState<\"choose\" | \"victory\">(\n timedChosen ? \"victory\" : \"choose\",\n );\n\n const linesOf = (c: SpecialChoice | undefined): ResourceLine[] =>\n grantLines(\n c?.Reward?.Operation?.Grant as Parameters<typeof grantLines>[0],\n ).filter((l) => l.amount > 0);\n\n const close = async (\n lines: ResourceLine[],\n from: Element | null,\n ): Promise<void> => {\n const rect = from?.getBoundingClientRect() ?? null;\n kit.fx.reserve(lines);\n const fade = stage.current?.fadeOut();\n kit.fx.fly(lines, () => rect);\n await fade;\n onDone();\n };\n\n const choose = async (choice: SpecialChoice | undefined): Promise<void> => {\n if (!choice || busy) return;\n setBusy(true);\n play(\"click\");\n // A price in Aspis: the first call asks the player to confirm on idosgames.com (the kit shows it).\n const res = await payWithCredits(client, (creditsConfirmationID) =>\n client.gameLoop.boardSpecialChoose(\n choice.ChoiceID ?? choice.Mode ?? \"Instant\",\n undefined,\n creditsConfirmationID,\n ),\n );\n if (!res.ok) {\n // The reference's FailAndClose: say why and close — the pending, if alive, comes back on ROLL.\n toast(errorText(res.error), \"error\");\n await stage.current?.fadeOut();\n onDone();\n return;\n }\n if (choice.Mode === \"Timed\") {\n setBusy(false);\n setPhase(\"victory\");\n return;\n }\n play(\"reward\");\n const lines = grantLines(res.data.Operation?.Grant).filter(\n (l) => l.amount > 0,\n );\n await close(lines.length ? lines : linesOf(choice), card.current);\n };\n\n const claim = async (withAd: boolean): Promise<void> => {\n if (busy) return;\n setBusy(true);\n play(\"claim\");\n if (withAd) {\n const applied = await client.gameLoop.boardSpecialApplyMultiplier();\n if (!applied.ok) {\n setBusy(false);\n toast(applied.error, \"error\");\n return;\n }\n }\n const res = await client.gameLoop.boardSpecialClaim();\n if (!res.ok) {\n toast(res.error, \"error\");\n await stage.current?.fadeOut();\n onDone();\n return;\n }\n const lines = grantLines(\n (res.data as { Operation?: { Grant?: Parameters<typeof grantLines>[0] } })\n .Operation?.Grant,\n ).filter((l) => l.amount > 0);\n await close(lines, document.querySelector(\"[data-board-victory-rewards]\"));\n };\n\n // The card: Timed (PLAY) when the web board can play it, else the instant prize.\n const cardChoice = timed ?? instant;\n const cardIsTimed = cardChoice === timed && timed !== undefined;\n\n return (\n <Stage ref={stage} dim=\"rgba(0,0,0,.72)\">\n {phase === \"choose\" ? (\n <>\n <Ribbon\n y={500}\n w={900}\n h={150}\n tone=\"plum\"\n text={\n special?.ModeID && special.ModeID !== \"default\"\n ? special.ModeID\n : \"SPECIAL\"\n }\n size={65}\n />\n <div\n ref={card}\n onClick={() => void choose(cardChoice)}\n style={{\n ...at(0, 0, 483, 630),\n cursor: busy ? \"default\" : \"pointer\",\n animation:\n \"idos-board-card-in .45s cubic-bezier(.34,1.56,.64,1) both, idos-board-breathe 2.4s ease-in-out .5s infinite\",\n }}\n >\n <GreenCard>\n <div style={{ ...abs(0, 85 - 192, 384, 384, 483, 630) }}>\n <ChestArt />\n </div>\n <div\n style={{\n ...abs(0, -130 - 55, 423, 110, 483, 630),\n display: \"grid\",\n placeItems: \"center\",\n textAlign: \"center\",\n ...outlinedText(42),\n }}\n >\n {cardIsTimed\n ? \"Bigger reward — play to multiply it.\"\n : \"Claim your reward instantly.\"}\n </div>\n <div\n style={{\n ...abs(0, -245 - 62, 412, 124, 483, 630),\n display: \"grid\",\n placeItems: \"center\",\n ...outlinedText(70, \"#fff36a\", \"#145a2c\"),\n }}\n >\n {cardIsTimed ? \"PLAY\" : \"CLAIM\"}\n </div>\n </GreenCard>\n </div>\n {cardIsTimed && instant ? (\n <div\n onClick={() => void choose(instant)}\n style={{\n ...at(0, -500, 300, 100),\n display: \"grid\",\n placeItems: \"center\",\n cursor: \"pointer\",\n ...outlinedText(58),\n }}\n >\n Skip\n </div>\n ) : null}\n </>\n ) : (\n <Victory\n lines={linesOf(timedRaw)}\n busy={busy}\n onClaim={() => void claim(false)}\n onClaimX3={() => void claim(true)}\n iconOf={catalog.iconOf}\n />\n )}\n </Stage>\n );\n}\n\n/** Position inside a parent box of `pw × ph` the Unity way (centre anchor, y up). */\nfunction abs(\n x: number,\n y: number,\n w: number,\n h: number,\n pw: number,\n ph: number,\n): CSSProperties {\n return {\n position: \"absolute\",\n left: pw / 2 + x - w / 2,\n top: ph / 2 - y - h,\n width: w,\n height: h,\n };\n}\n\n/** CardFrame03 (green): outer frame, lighter inner panel shifted up, a soft top light. */\nfunction GreenCard({ children }: { children: ReactNode }): ReactNode {\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n borderRadius: 46,\n background:\n \"linear-gradient(180deg,#34d77c 0%,#2ac870 60%,#20a95b 100%)\",\n border: \"8px solid #17193a\",\n boxShadow: \"inset 0 8px 0 rgba(255,255,255,.45)\",\n boxSizing: \"border-box\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: 26,\n right: 26,\n top: 26,\n bottom: 120,\n borderRadius: 30,\n background:\n \"radial-gradient(circle at 50% 35%, #6af0a4 0%, #38e283 60%, #2fc673 100%)\",\n boxShadow: \"inset 0 -6px 0 rgba(0,0,0,.12)\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/** Title_Ribbon01 (plum / sky): a raised middle plate with folded ends. */\nfunction Ribbon({\n y,\n w,\n h,\n tone,\n text,\n size,\n}: {\n y: number;\n w: number;\n h: number;\n tone: \"plum\" | \"sky\";\n text: string;\n size: number;\n}): ReactNode {\n const [face, fold, light] =\n tone === \"plum\"\n ? [\"#8a1fff\", \"#5a10b8\", \"#c27cff\"]\n : [\"#2fa8ff\", \"#1b6fc4\", \"#8fd4ff\"];\n return (\n <div\n style={{\n ...at(0, y, w, h),\n animation: \"idos-board-card-in .4s cubic-bezier(.34,1.56,.64,1) both\",\n }}\n >\n <svg\n viewBox=\"0 0 300 150\"\n preserveAspectRatio=\"none\"\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n }}\n aria-hidden\n >\n <g stroke=\"#17193a\" strokeWidth=\"6\" strokeLinejoin=\"round\">\n <path d=\"M4 40 L70 40 L70 120 L4 120 L28 80 Z\" fill={face} />\n <path d=\"M296 40 L230 40 L230 120 L296 120 L272 80 Z\" fill={face} />\n <path\n d=\"M60 50 L80 50 L80 130 L60 120 Z M240 50 L220 50 L220 130 L240 120 Z\"\n fill={fold}\n />\n <rect x=\"70\" y=\"14\" width=\"160\" height=\"112\" rx=\"14\" fill={face} />\n </g>\n <rect x=\"80\" y=\"22\" width=\"140\" height=\"16\" rx=\"8\" fill={light} />\n </svg>\n <div\n style={{\n position: \"absolute\",\n inset: \"0 15% 12% 15%\",\n display: \"grid\",\n placeItems: \"center\",\n ...outlinedText(size),\n whiteSpace: \"nowrap\",\n }}\n >\n {text}\n </div>\n </div>\n );\n}\n\n/** Play_Result_Victory: rotating rays, the winged badge, a sky ribbon, REWARDS, item frames, Claim / Claim ×3. */\nfunction Victory({\n lines,\n busy,\n onClaim,\n onClaimX3,\n iconOf,\n}: {\n lines: ResourceLine[];\n busy: boolean;\n onClaim: () => void;\n onClaimX3: () => void;\n iconOf: (line: ResourceLine) => string;\n}): ReactNode {\n const badge = useRef<HTMLDivElement>(null);\n useEffect(() => {\n const b = badge.current;\n void tween(0.5, (t) => {\n if (b) b.style.transform = `scale(${easeOutBack(t)})`;\n });\n }, []);\n return (\n <>\n <div\n style={{\n ...at(0, 437, 620, 620),\n animation: \"idos-board-spin 6s linear infinite\",\n pointerEvents: \"none\",\n }}\n >\n <svg\n viewBox=\"0 0 200 200\"\n style={{ width: \"100%\", height: \"100%\" }}\n aria-hidden\n >\n {Array.from({ length: 12 }, (_, i) => (\n <path\n key={i}\n d=\"M100 100 L92 0 L108 0 Z\"\n fill=\"rgba(255,240,150,.45)\"\n transform={`rotate(${i * 30} 100 100)`}\n />\n ))}\n </svg>\n </div>\n <div\n ref={badge}\n style={{ ...at(0, 533.8, 536, 449), transform: \"scale(0)\" }}\n >\n <svg\n viewBox=\"0 0 536 449\"\n style={{ width: \"100%\", height: \"100%\" }}\n aria-hidden\n >\n <g stroke=\"#17193a\" strokeWidth=\"9\" strokeLinejoin=\"round\">\n <path\n d=\"M268 360 L60 250 Q20 160 70 90 Q150 160 268 170 Z\"\n fill=\"#ffd23a\"\n />\n <path\n d=\"M268 360 L476 250 Q516 160 466 90 Q386 160 268 170 Z\"\n fill=\"#ffd23a\"\n />\n <path\n d=\"M268 30 L380 90 L370 280 L268 400 L166 280 L156 90 Z\"\n fill=\"#2fa8ff\"\n />\n <path\n d=\"M268 70 L340 112 L334 262 L268 346 L202 262 L196 112 Z\"\n fill=\"#8fd4ff\"\n />\n <path d=\"M268 110 L282 250 L268 300 L254 250 Z\" fill=\"#fff\" />\n <rect\n x=\"226\"\n y=\"246\"\n width=\"84\"\n height=\"18\"\n rx=\"6\"\n fill=\"#ffd23a\"\n />\n </g>\n </svg>\n </div>\n <Ribbon y={291} w={690} h={143} tone=\"sky\" text=\"VICTORY!\" size={67} />\n <div\n style={{\n ...at(0, 11, 650, 67),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: 24,\n }}\n >\n <div\n style={{\n width: 174,\n height: 10,\n borderRadius: 5,\n background: \"linear-gradient(90deg, transparent, #ffe066)\",\n }}\n />\n <div style={outlinedText(40)}>REWARDS</div>\n <div\n style={{\n width: 174,\n height: 10,\n borderRadius: 5,\n background: \"linear-gradient(90deg, #ffe066, transparent)\",\n }}\n />\n </div>\n <div\n data-board-victory-rewards\n style={{\n ...at(0, -150, 915, 190),\n display: \"flex\",\n justifyContent: \"center\",\n gap: 24,\n }}\n >\n {(lines.length ? lines : []).map((l, i) => (\n <div\n key={i}\n style={{\n width: 190,\n height: 190,\n borderRadius: 34,\n background:\n \"radial-gradient(circle at 50% 40%, #35fbff 0%, #0b69d6 75%)\",\n border: \"7px solid #1d3fb1\",\n boxShadow: \"0 0 0 5px #17193a\",\n position: \"relative\",\n display: \"grid\",\n placeItems: \"center\",\n animation: `idos-board-card-in .4s cubic-bezier(.34,1.56,.64,1) ${0.3 + i * 0.1}s both`,\n }}\n >\n <Icon glyph={iconOf(l)} size={130} />\n <div\n style={{ position: \"absolute\", bottom: 6, ...outlinedText(38) }}\n >\n {formatAmount(l.amount)}\n </div>\n </div>\n ))}\n </div>\n <GameButton\n y={-644}\n w={350}\n h={130}\n tone=\"sky\"\n disabled={busy}\n onClick={onClaim}\n >\n Claim\n </GameButton>\n <GameButton\n y={-810}\n w={370}\n h={160}\n tone=\"green\"\n disabled={busy}\n onClick={onClaimX3}\n >\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 14 }}>\n <svg viewBox=\"0 0 40 40\" width={70} height={70} aria-hidden>\n <rect\n x=\"3\"\n y=\"7\"\n width=\"34\"\n height=\"26\"\n rx=\"6\"\n fill=\"#ff4d4d\"\n stroke=\"#17193a\"\n strokeWidth=\"3\"\n />\n <path d=\"M16 13 L27 20 L16 27 Z\" fill=\"#fff\" />\n </svg>\n Claim x3\n </span>\n </GameButton>\n </>\n );\n}\n\n/** Button01 (sky / green): glossy pill with a dark outline and a bottom lip. */\nfunction GameButton({\n y,\n w,\n h,\n tone,\n disabled,\n onClick,\n children,\n}: {\n y: number;\n w: number;\n h: number;\n tone: \"sky\" | \"green\";\n disabled?: boolean;\n onClick: () => void;\n children: ReactNode;\n}): ReactNode {\n const [top, bottom] =\n tone === \"sky\"\n ? [\"#5cc8ff\", \"#1c8fe6\", \"#0d5aa8\"]\n : [\"#7be34a\", \"#33b31d\", \"#1f7a10\"];\n return (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={onClick}\n style={{\n ...at(0, y, w, h),\n borderRadius: h / 2.6,\n border: \"7px solid #17193a\",\n background: `linear-gradient(180deg, ${top} 0%, ${bottom} 100%)`,\n boxShadow: `inset 0 8px 0 rgba(255,255,255,.4)`,\n cursor: disabled ? \"default\" : \"pointer\",\n opacity: disabled ? 0.7 : 1,\n fontFamily: \"inherit\",\n ...outlinedText(48),\n }}\n >\n {children}\n </button>\n );\n}\n"
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"path": "components/StatusBar.tsx",
|
|
@@ -31,16 +31,16 @@
|
|
|
31
31
|
"version": "0.1.1"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@idosgames/core": "0.
|
|
35
|
-
"@idosgames/module-sdk": "0.13.
|
|
36
|
-
"@idosgames/react": "0.
|
|
34
|
+
"@idosgames/core": "0.26.1",
|
|
35
|
+
"@idosgames/module-sdk": "0.13.3",
|
|
36
|
+
"@idosgames/react": "0.7.1",
|
|
37
37
|
"react": "19.2.7"
|
|
38
38
|
},
|
|
39
|
-
"contentHash": "
|
|
39
|
+
"contentHash": "d79e71eb438f32329a34d0aeb4b9dcf7e7a8c6dac168224951a85a50f7bf84b0",
|
|
40
40
|
"files": [
|
|
41
41
|
{
|
|
42
42
|
"path": "BoostsScreen.tsx",
|
|
43
|
-
"content": "import {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport type { ActiveBoostWindowInfo } from \"@idosgames/core\";\nimport type { FeatureScreenProps } from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n Card,\n EmptyState,\n Icon,\n ResourceList,\n SectionTitle,\n Timer,\n canAfford,\n configSection,\n errorText,\n outlined,\n useCatalog,\n useNow,\n useStagger,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n boostRows,\n durationParts,\n effectText,\n runningBoosts,\n type ActiveBoostView,\n type BoostEffectView,\n type BoostRow,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// Boosts (engine TimedBoost): what runs now — the player's own boosts with their timers or charges,\n// and the title's global windows (a happy hour, a phase of a boost chain) — then the boosts the\n// player can activate, free or for currency. The server applies every boost; the screen only shows.\n\nexport function BoostsScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const catalog = useCatalog();\n const now = useNow();\n const stagger = useStagger();\n const [windows, setWindows] = useState<ActiveBoostWindowInfo[]>([]);\n const section = configSection(client, \"TimedBoost\");\n const rows = boostRows(section);\n const running = runningBoosts(\n (state?.TimedBoost as { Active?: Record<string, ActiveBoostView> } | null)\n ?.Active,\n now,\n );\n const liveWindows = windows.filter(\n (w) => !w.EndUtc || new Date(w.EndUtc).getTime() > now,\n );\n\n // Fresh on open: expired boosts drop out, global windows are resolved by the server.\n useEffect(() => {\n void client.timedBoost.getActive();\n void client.timedBoost\n .getActiveWindows()\n .then((r) => r.ok && setWindows(r.data.Windows ?? []));\n }, [client]);\n\n if (rows.length === 0 && running.length === 0 && liveWindows.length === 0)\n return <EmptyState glyph=\"energy\" text={t(\"noBoosts\")} />;\n\n const nameOf = (boostID: string | undefined) =>\n catalog.localize(\n rows.find((r) => r.boostID === boostID)?.name ?? boostID ?? \"\",\n );\n\n return (\n <div style={{ display: \"grid\", gap: 14 }}>\n {running.length + liveWindows.length > 0 ? (\n <div style={{ display: \"grid\", gap: 8 }}>\n <SectionTitle>{t(\"running\")}</SectionTitle>\n {liveWindows.map((w, i) => (\n <RunningCard\n key={`w:${w.SourceID}:${w.PhaseID ?? \"\"}`}\n name={catalog.localize(w.DisplayName ?? w.SourceID ?? \"\")}\n effects={w.Effects ?? []}\n until={w.EndUtc ?? null}\n charges={null}\n global\n style={stagger(i)}\n />\n ))}\n {running.map((b, i) => (\n <RunningCard\n key={b.InstanceID}\n name={nameOf(b.BoostID)}\n effects={b.EffectSnapshot ? [b.EffectSnapshot] : []}\n until={b.ExpiresAtUtc ?? null}\n charges={b.RemainingCharges ?? null}\n style={stagger(liveWindows.length + i)}\n />\n ))}\n </div>\n ) : null}\n {rows.length > 0 ? (\n <div style={{ display: \"grid\", gap: 8 }}>\n <SectionTitle>{t(\"boosts\")}</SectionTitle>\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(220px, 1fr))\",\n gap: 10,\n }}\n >\n {rows.map((row, i) => (\n <BoostCard key={row.boostID} row={row} style={stagger(i)} />\n ))}\n </div>\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction EffectLine({ effect }: { effect: BoostEffectView }): ReactNode {\n const e = effectText(effect);\n return (\n <span style={{ ...outlined, fontSize: 14, color: v.gold }}>\n {e.size} {e.target === \"custom\" ? \"\" : t(`target_${e.target}`)}\n </span>\n );\n}\n\nfunction RunningCard({\n name,\n effects,\n until,\n charges,\n global,\n style,\n}: {\n name: string;\n effects: BoostEffectView[];\n until: string | null;\n charges: number | null;\n global?: boolean;\n style: CSSProperties;\n}): ReactNode {\n const now = useNow();\n return (\n <Card\n highlight\n style={{\n display: \"grid\",\n gridTemplateColumns: \"auto 1fr\",\n alignItems: \"center\",\n gap: 12,\n ...style,\n }}\n >\n <div className=\"idos-bounce\">\n <Icon glyph={global ? \"clock\" : \"energy\"} size={40} />\n </div>\n <div style={{ display: \"grid\", gap: 4, minWidth: 0 }}>\n <div style={{ ...outlined, fontSize: 15 }}>\n {name}\n {global ? (\n <span style={{ color: v.textDim, fontSize: 12 }}>\n {\" \"}\n · {t(\"forEveryone\")}\n </span>\n ) : null}\n </div>\n <div style={{ display: \"flex\", gap: 10, flexWrap: \"wrap\" }}>\n {effects.map((e, i) => (\n <EffectLine key={i} effect={e} />\n ))}\n </div>\n {until ? <Timer at={until} now={now} label={t(\"endsIn\")} /> : null}\n {charges != null ? (\n <div style={{ ...outlined, fontSize: 13, color: v.textDim }}>\n {t(\"charges\")}: {charges}\n </div>\n ) : null}\n </div>\n </Card>\n );\n}\n\nfunction BoostCard({\n row,\n style,\n}: {\n row: BoostRow;\n style: CSSProperties;\n}): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const catalog = useCatalog();\n const toast = useToast();\n const kit = useUiKit();\n const ref = useRef<HTMLDivElement>(null);\n const [busy, setBusy] = useState<string | null>(null);\n const balances = state?.InventoryV2?.VirtualCurrencies as\n Record<string, { Amount?: number }> | undefined;\n const { h, m } = durationParts(row.durationSeconds);\n\n const activate = async (optionID: string | undefined) => {\n setBusy(optionID ?? \"free\");\n const res = await client.timedBoost.activate(
|
|
43
|
+
"content": "import {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport type { ActiveBoostWindowInfo } from \"@idosgames/core\";\nimport type { FeatureScreenProps } from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n Card,\n EmptyState,\n Icon,\n ResourceList,\n SectionTitle,\n Timer,\n canAfford,\n configSection,\n errorText,\n outlined,\n useCatalog,\n useNow,\n useStagger,\n useToast,\n useUiKit,\n v,\n payWithCredits,\n} from \"@idosgames/react/ui\";\nimport {\n boostRows,\n durationParts,\n effectText,\n runningBoosts,\n type ActiveBoostView,\n type BoostEffectView,\n type BoostRow,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// Boosts (engine TimedBoost): what runs now — the player's own boosts with their timers or charges,\n// and the title's global windows (a happy hour, a phase of a boost chain) — then the boosts the\n// player can activate, free or for currency. The server applies every boost; the screen only shows.\n\nexport function BoostsScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const catalog = useCatalog();\n const now = useNow();\n const stagger = useStagger();\n const [windows, setWindows] = useState<ActiveBoostWindowInfo[]>([]);\n const section = configSection(client, \"TimedBoost\");\n const rows = boostRows(section);\n const running = runningBoosts(\n (state?.TimedBoost as { Active?: Record<string, ActiveBoostView> } | null)\n ?.Active,\n now,\n );\n const liveWindows = windows.filter(\n (w) => !w.EndUtc || new Date(w.EndUtc).getTime() > now,\n );\n\n // Fresh on open: expired boosts drop out, global windows are resolved by the server.\n useEffect(() => {\n void client.timedBoost.getActive();\n void client.timedBoost\n .getActiveWindows()\n .then((r) => r.ok && setWindows(r.data.Windows ?? []));\n }, [client]);\n\n if (rows.length === 0 && running.length === 0 && liveWindows.length === 0)\n return <EmptyState glyph=\"energy\" text={t(\"noBoosts\")} />;\n\n const nameOf = (boostID: string | undefined) =>\n catalog.localize(\n rows.find((r) => r.boostID === boostID)?.name ?? boostID ?? \"\",\n );\n\n return (\n <div style={{ display: \"grid\", gap: 14 }}>\n {running.length + liveWindows.length > 0 ? (\n <div style={{ display: \"grid\", gap: 8 }}>\n <SectionTitle>{t(\"running\")}</SectionTitle>\n {liveWindows.map((w, i) => (\n <RunningCard\n key={`w:${w.SourceID}:${w.PhaseID ?? \"\"}`}\n name={catalog.localize(w.DisplayName ?? w.SourceID ?? \"\")}\n effects={w.Effects ?? []}\n until={w.EndUtc ?? null}\n charges={null}\n global\n style={stagger(i)}\n />\n ))}\n {running.map((b, i) => (\n <RunningCard\n key={b.InstanceID}\n name={nameOf(b.BoostID)}\n effects={b.EffectSnapshot ? [b.EffectSnapshot] : []}\n until={b.ExpiresAtUtc ?? null}\n charges={b.RemainingCharges ?? null}\n style={stagger(liveWindows.length + i)}\n />\n ))}\n </div>\n ) : null}\n {rows.length > 0 ? (\n <div style={{ display: \"grid\", gap: 8 }}>\n <SectionTitle>{t(\"boosts\")}</SectionTitle>\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(220px, 1fr))\",\n gap: 10,\n }}\n >\n {rows.map((row, i) => (\n <BoostCard key={row.boostID} row={row} style={stagger(i)} />\n ))}\n </div>\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction EffectLine({ effect }: { effect: BoostEffectView }): ReactNode {\n const e = effectText(effect);\n return (\n <span style={{ ...outlined, fontSize: 14, color: v.gold }}>\n {e.size} {e.target === \"custom\" ? \"\" : t(`target_${e.target}`)}\n </span>\n );\n}\n\nfunction RunningCard({\n name,\n effects,\n until,\n charges,\n global,\n style,\n}: {\n name: string;\n effects: BoostEffectView[];\n until: string | null;\n charges: number | null;\n global?: boolean;\n style: CSSProperties;\n}): ReactNode {\n const now = useNow();\n return (\n <Card\n highlight\n style={{\n display: \"grid\",\n gridTemplateColumns: \"auto 1fr\",\n alignItems: \"center\",\n gap: 12,\n ...style,\n }}\n >\n <div className=\"idos-bounce\">\n <Icon glyph={global ? \"clock\" : \"energy\"} size={40} />\n </div>\n <div style={{ display: \"grid\", gap: 4, minWidth: 0 }}>\n <div style={{ ...outlined, fontSize: 15 }}>\n {name}\n {global ? (\n <span style={{ color: v.textDim, fontSize: 12 }}>\n {\" \"}\n · {t(\"forEveryone\")}\n </span>\n ) : null}\n </div>\n <div style={{ display: \"flex\", gap: 10, flexWrap: \"wrap\" }}>\n {effects.map((e, i) => (\n <EffectLine key={i} effect={e} />\n ))}\n </div>\n {until ? <Timer at={until} now={now} label={t(\"endsIn\")} /> : null}\n {charges != null ? (\n <div style={{ ...outlined, fontSize: 13, color: v.textDim }}>\n {t(\"charges\")}: {charges}\n </div>\n ) : null}\n </div>\n </Card>\n );\n}\n\nfunction BoostCard({\n row,\n style,\n}: {\n row: BoostRow;\n style: CSSProperties;\n}): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const catalog = useCatalog();\n const toast = useToast();\n const kit = useUiKit();\n const ref = useRef<HTMLDivElement>(null);\n const [busy, setBusy] = useState<string | null>(null);\n const balances = state?.InventoryV2?.VirtualCurrencies as\n Record<string, { Amount?: number }> | undefined;\n const { h, m } = durationParts(row.durationSeconds);\n\n const activate = async (optionID: string | undefined) => {\n setBusy(optionID ?? \"free\");\n // A price in Aspis: the first call asks the player to confirm on idosgames.com (the kit shows it).\n const res = await payWithCredits(client, (creditsConfirmationID) =>\n client.timedBoost.activate(row.boostID, {\n selectedOptionID: optionID,\n creditsConfirmationID,\n }),\n );\n setBusy(null);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n kit.play(\"claim\");\n kit.fx.burst(ref.current);\n toast(t(\"activated\"), \"success\");\n };\n\n return (\n <div ref={ref} style={style}>\n <Card style={{ display: \"grid\", gap: 8, justifyItems: \"center\" }}>\n <Icon glyph={row.icon} size={52} />\n <div style={{ ...outlined, fontSize: 15, textAlign: \"center\" }}>\n {catalog.localize(row.name)}\n </div>\n {row.effect ? <EffectLine effect={row.effect} /> : null}\n {row.description ? (\n <div\n style={{\n ...outlined,\n fontWeight: 600,\n fontSize: 12,\n color: v.textDim,\n textAlign: \"center\",\n }}\n >\n {catalog.localize(row.description)}\n </div>\n ) : null}\n <div style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {row.durationSeconds > 0\n ? `${h > 0 ? `${h} ${t(\"hours\")} ` : \"\"}${m > 0 ? `${m} ${t(\"minutes\")}` : \"\"}`\n : null}\n {row.charges > 0 ? ` · ${t(\"charges\")}: ${row.charges}` : null}\n </div>\n {/* A free option (often a one-time welcome boost) next to the paid ones, not instead of\n them: once it is used up the player still has a way to buy the boost. */}\n {row.free ? (\n <Button\n tone=\"green\"\n attract\n busy={busy === (row.freeOptionID ?? \"free\")}\n onClick={() => void activate(row.freeOptionID)}\n style={{ width: \"100%\" }}\n data-tutorial-anchor={`boost:${row.boostID}`}\n >\n {t(\"activate\")}\n </Button>\n ) : null}\n {row.options.map((o) => {\n const affordable = canAfford(o.price, balances);\n return (\n <Button\n key={o.id}\n tone=\"gold\"\n disabled={!affordable}\n busy={busy === o.id}\n onClick={() => void activate(o.id)}\n style={{ width: \"100%\" }}\n data-tutorial-anchor={\n row.free ? undefined : `boost:${row.boostID}`\n }\n >\n <ResourceList\n lines={o.price}\n size={18}\n dimIf={() => !affordable}\n />\n </Button>\n );\n })}\n </Card>\n </div>\n );\n}\n"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "i18n.ts",
|