import type { TDImageAsset } from "@tldraw/tldraw"; import { ColorStyle, TDAssetType, TDShapeType, Tldraw, type TldrawApp, } from "@tldraw/tldraw"; import randomInt from "just-random-integer"; import * as React from "react"; import { usePlannerBg } from "~/hooks/usePlannerBg"; import { useTranslation } from "~/hooks/useTranslation"; import type { LanguageCode } from "~/modules/i18n"; import type { ModeShort, StageId } from "~/modules/in-game-lists"; import { specialWeaponIds, subWeaponIds } from "~/modules/in-game-lists"; import { stageIds, weaponCategories } from "~/modules/in-game-lists"; import { modesShort } from "~/modules/in-game-lists/modes"; import { semiRandomId } from "~/utils/strings"; import { mainWeaponImageUrl, outlinedMainWeaponImageUrl, specialWeaponImageUrl, stageMinimapImageUrlWithEnding, subWeaponImageUrl, TLDRAW_URL, weaponCategoryUrl, } from "~/utils/urls"; import { Button } from "../../../components/Button"; import { Image } from "../../../components/Image"; import type { StageBackgroundStyle } from "../plans-types"; export default function Planner() { const { t } = useTranslation(["common"]); const { i18n } = useTranslation(); const plannerBgParams = usePlannerBg(); const [app, setApp] = React.useState(null); const handleMount = React.useCallback( (mountedApp: TldrawApp) => { setApp(mountedApp); mountedApp.setSetting( "language", ourLanguageToTldrawLanguage(i18n.language) ); mountedApp.style({ color: ColorStyle.Red }); }, [i18n] ); const handleAddImage = React.useCallback( ({ src, size, isLocked, point, cb, }: { src: string; size: number[]; isLocked: boolean; point: number[]; cb?: () => void; }) => { if (!app) return; const asset: TDImageAsset = { id: src, type: TDAssetType.Image, fileName: "img", src, size, }; // tldraw creator: // "So image shapes in tldraw work like this: we add an asset to the app.assets table, then we reference that asset in the shape object itself. // This lets us have multiple copies of an image on the canvas without having all of those take up memory individually" app.insertContent({ assets: [asset], shapes: [], }); app.createShapes({ id: semiRandomId(), type: TDShapeType.Image, assetId: src, size, isAspectRatioLocked: true, isLocked, point, }); cb?.(); }, [app] ); const handleAddWeapon = React.useCallback( (src: string) => { // Adjustable parameters for image spawning const imageSizePx = 45; const imageSpawnBoxSizeFactorX = 0.15; const imageSpawnBoxSizeFactorY = 0.3; const imageSpawnBoxOffsetFactorX = 0; const imageSpawnBoxOffsetFactorY = 0.2; // Get positions of the background rectangle const bgRectangleLeft = plannerBgParams.pointOffsetX; const bgRectangleTop = plannerBgParams.pointOffsetY; // Subtract the size of the image here to correct the image spawn location at the right-most & bottom-most boundaries const bgRectangleRight = bgRectangleLeft + plannerBgParams.bgWidth - imageSizePx; const bgRectangleBottom = plannerBgParams.pointOffsetY + plannerBgParams.bgHeight - imageSizePx; // Derived values for image spawn box const imageSpawnBoxLeft = bgRectangleLeft + plannerBgParams.bgWidth * imageSpawnBoxOffsetFactorX; const imageSpawnBoxRight = imageSpawnBoxSizeFactorX * (bgRectangleRight - bgRectangleLeft) + imageSpawnBoxLeft; const imageSpawnBoxTop = bgRectangleTop + plannerBgParams.bgHeight * imageSpawnBoxOffsetFactorY; const imageSpawnBoxBottom = imageSpawnBoxSizeFactorY * (bgRectangleBottom - bgRectangleTop) + imageSpawnBoxTop; handleAddImage({ src, size: [imageSizePx, imageSizePx], isLocked: false, point: [ randomInt(imageSpawnBoxLeft, imageSpawnBoxRight), randomInt(imageSpawnBoxTop, imageSpawnBoxBottom), ], cb: () => app?.selectTool("select"), }); }, [ app, handleAddImage, plannerBgParams.bgHeight, plannerBgParams.bgWidth, plannerBgParams.pointOffsetX, plannerBgParams.pointOffsetY, ] ); const handleAddBackgroundImage = React.useCallback( (urlArgs: { stageId: StageId; mode: ModeShort; style: StageBackgroundStyle; }) => { if (!app) return; app.resetDocument(); handleAddImage({ src: stageMinimapImageUrlWithEnding(urlArgs), size: [plannerBgParams.bgWidth, plannerBgParams.bgHeight], isLocked: true, point: [plannerBgParams.pointOffsetX, plannerBgParams.pointOffsetY], }); }, [ app, handleAddImage, plannerBgParams.bgHeight, plannerBgParams.bgWidth, plannerBgParams.pointOffsetX, plannerBgParams.pointOffsetY, ] ); return ( <>
{t("common:plans.poweredBy", { name: "tldraw" })}
); } function WeaponImageSelector({ handleAddWeapon, }: { handleAddWeapon: (src: string) => void; }) { const { t } = useTranslation(["weapons", "common"]); return (
{weaponCategories.map((category) => { return (
{t(`common:weapon.category.${category.name}`)} {t(`common:weapon.category.${category.name}`)}
{category.weaponIds.map((weaponId) => { return ( ); })}
); })}
{t("common:weapon.category.subs")}
{subWeaponIds.map((subWeaponId) => { return ( ); })}
{t("common:weapon.category.specials")}
{specialWeaponIds.map((specialWeaponId) => { return ( ); })}
); } function StageBackgroundSelector({ onAddBackground, }: { onAddBackground: (args: { stageId: StageId; mode: ModeShort; style: StageBackgroundStyle; }) => void; }) { const { t } = useTranslation(["game-misc", "common"]); const [stageId, setStageId] = React.useState(stageIds[0]); const [mode, setMode] = React.useState("SZ"); const [backgroundStyle, setBackgroundStyle] = React.useState("ITEMS"); return (
); } // when adding new language check from Tldraw codebase what is the matching // language in TRANSLATIONS constant, or default to english if none found const ourLanguageToTldrawLanguageMap: Record = { "es-US": "es", "es-ES": "es", ko: "ko-kr", nl: "en", zh: "zh-ch", // map to itself da: "da", de: "de", en: "en", fr: "fr", it: "it", ja: "ja", ru: "ru", pl: "pl", }; function ourLanguageToTldrawLanguage(ourLanguageUserSelected: string) { for (const [ourLanguage, tldrawLanguage] of Object.entries( ourLanguageToTldrawLanguageMap )) { if (ourLanguage === ourLanguageUserSelected) { return tldrawLanguage; } } console.error(`No tldraw language found for: ${ourLanguageUserSelected}`); return "en"; }