"use client" import { ReactNode, useEffect, useRef, useState, useTransition } from "react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { DndProvider } from "react-dnd" import { HTML5Backend } from "react-dnd-html5-backend" import { SceneRenderer } from "@/app/interface/renderer" import { newRender, getRender } from "@/app/engine/render" import { Engine, EngineType, defaultEngine, getEngine } from "@/app/engine/engines" import { OnInventoryEvent, RenderedScene, SceneEvent } from "@/types" import { Game, GameType } from "./games/types" import { defaultGame, getGame } from "./games" import { getBackground } from "@/app/queries/getBackground" import { getDialogue } from "@/app/queries/getDialogue" import { getActionnables } from "@/app/queries/getActionnables" import { normalizeActionnables } from "@/lib/normalizeActionnables" import { Inventory } from "@/app/interface/inventory" import { store } from "./store" import { defaultActionnables } from "@/lib/defaultActionnables" import { TopMenu } from "./interface/top-menu" import { LastEvent } from "./interface/last-event" import { Help } from "./interface/help" import { Dialogue } from "./interface/dialogue" import { Progress } from "./interface/progress" import { cn } from "@/lib/utils" const getInitialRenderedScene = (): RenderedScene => ({ renderId: "", status: "pending", assetUrl: "", alt: "", error: "", maskUrl: "", segments: [] }) export default function Main() { const [isPending, startTransition] = useTransition() const [rendered, setRendered] = useState(getInitialRenderedScene()) const historyRef = useRef([]) const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const requestedGame = (searchParams.get('game') as GameType) || defaultGame const gameRef = useRef(requestedGame) const [game, setGame] = useState(getGame(gameRef.current)) const requestedEngine = (searchParams.get('engine') as EngineType) || defaultEngine const usableEngine = game.engines.includes(requestedEngine) ? requestedEngine : game.engines[0] const [engine, setEngine] = useState(getEngine(usableEngine)) const requestedDebug = (searchParams.get('debug') === "true") const [debug, setDebug] = useState(requestedDebug) const requestedClearCache = (searchParams.get('clearCache') ? (searchParams.get('clearCache') === "true") : false) const [clearCache, setClearCache] = useState(requestedClearCache) const [situation, setSituation] = useState("") const [dialogue, setDialogue] = useState("") const [isLoadingDialogue, setLoadingDialogue] = useState(false) /* const [grabbedItem, setGrabbedItem] = useState() const grabbedItemRef = useRef() grabbedItemRef.current = grabbedItem */ const [isBusy, setBusy] = useState(true) const busyRef = useRef(true) const loopRef = useRef(null) const [lastEventString, setLastEventString] = useState("") const [lastEvent, setLastEvent] = useState(null) const loadNextScene = async (nextSituation?: string, nextActionnables?: string[]) => { await startTransition(async () => { console.log("Rendering a scene for " + game.type) // console.log(`rendering scene..`) const newRendered = await newRender({ engine, // SCENE PROMPT prompt: game.getScenePrompt(nextSituation).join(", "), // ACTIONNABLES actionnables: normalizeActionnables( Array.isArray(nextActionnables) && nextActionnables.length ? nextActionnables : game.initialActionnables ), clearCache }) // detect if type game type changed while we were busy // note that currently we reload the whol page when tha happens, // so this code isn't that useful if (game?.type !== gameRef?.current) { console.log("game type changed! aborting..") return } console.log("got the first version of our scene!", newRendered) // in cache we didn't hit the cache and the request is still pending // we cheat a bit by displaying the previous image as a placeholder if (!newRendered.assetUrl && rendered.assetUrl && rendered.maskUrl) { console.log("image is not in cache, using previous image as a placeholder..") // this is better than displaying a blank image, no? newRendered.assetUrl = rendered.assetUrl newRendered.maskUrl = rendered.maskUrl } historyRef.current.unshift(newRendered) setRendered(newRendered) if (newRendered.status === "completed") { setBusy(busyRef.current = false) } }) } const checkRenderedLoop = async () => { // console.log("checkRenderedLoop! rendered:", renderedRef.current) clearTimeout(loopRef.current) if (!historyRef.current[0]?.renderId || historyRef.current[0]?.status !== "pending") { // console.log("let's try again in a moments") loopRef.current = setTimeout(() => checkRenderedLoop(), 1000) return } // console.log("checking rendering..") await startTransition(async () => { // console.log(`getting latest updated scene..`) try { if (!historyRef.current[0]?.renderId) { throw new Error(`missing renderId`) } // console.log(`calling getRender(${renderedRef.current.renderId})`) const newRendered = await getRender(historyRef.current[0]?.renderId) // console.log(`got latest updated scene:`, renderedRef.current) // detect if type game type changed while we were busy if (game?.type !== gameRef?.current) { console.log("game type changed! aborting..") return } const before = JSON.stringify(historyRef.current[0]) const after = JSON.stringify(newRendered) if (after !== before) { console.log("updating scene..") historyRef.current[0] = newRendered setRendered(historyRef.current[0]) if (newRendered.status === "completed") { setBusy(busyRef.current = false) } } } catch (err) { console.error(err) } clearTimeout(loopRef.current) loopRef.current = setTimeout(() => checkRenderedLoop(), 1000) }) } useEffect(() => { loadNextScene() checkRenderedLoop() }, []) const askGameMasterForEpicDialogue = async (lastEvent: string) => { await startTransition(async () => { setLoadingDialogue(true) // const game = getGame(gameRef.current) let newDialogue = "" try { newDialogue = await getDialogue({ game, situation, lastEvent }) } catch (err) { console.log(`failed to generate dialogue, let's try again maybe`) try { newDialogue = await getDialogue({ game, situation, lastEvent: `${lastEvent}.` }) } catch (err) { console.log(`failed to generate dialogue.. again (but it's only a nice to have, so..)`) setDialogue("") setLoadingDialogue(false) return } } // try to remove whatever hallucination might come up next newDialogue = newDialogue.split("As the player")[0] newDialogue = newDialogue.split("As they use")[0] setDialogue(newDialogue) setLoadingDialogue(false) }) } const askGameMasterForEpicBackground = async (lastEvent: string = "") => { await startTransition(async () => { setBusy(busyRef.current = true) // this will be set to false once the scene finish loading // const game = getGame(gameRef.current) let newActionnables = [...defaultActionnables] try { newActionnables = await getActionnables({ game, situation, lastEvent }) console.log(`newActionnables:`, newActionnables) } catch (err) { console.log(`failed to generate actionnables, using default value`) } try { // todo rename this background/situation mess // it should be only one word const newBackground = await getBackground({ game, situation, lastEvent, newActionnables }) console.log(`newBackground:`, newBackground) setSituation(newBackground) console.log("loading next scene..") await loadNextScene(newBackground, newActionnables) // todo we could also use useEffect } catch (err) { console.error(`failed to get the scene: ${err}`) } }) } const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values()) // console.log("segments:", rendered.segments) const handleToggleDebug = (isToggledOn: boolean) => { const current = new URLSearchParams(Array.from(searchParams.entries())) current.set("debug", `${isToggledOn}`) const search = current.toString() const query = search ? `?${search}` : "" // for some reason, this doesn't work?! router.replace(`${pathname}${query}`, { }) // workaround.. but it is strange that router.replace doesn't work.. let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() window.history.pushState({path: newurl}, '', newurl) setDebug(isToggledOn) } const handleToggleClearCache = (shouldClearCache: boolean) => { const current = new URLSearchParams(Array.from(searchParams.entries())) current.set("clearCache", `${shouldClearCache}`) const search = current.toString() const query = search ? `?${search}` : "" // for some reason, this doesn't work?! router.replace(`${pathname}${query}`, { }) // workaround.. but it is strange that router.replace doesn't work.. let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() window.history.pushState({path: newurl}, '', newurl) setClearCache(shouldClearCache) } const handleSelectGame = (newGameType: GameType) => { gameRef.current = newGameType setGame(getGame(newGameType)) /* setRendered({ renderId: "", status: "pending", assetUrl: "", error: "", maskUrl: "", segments:[] }) */ const current = new URLSearchParams(Array.from(searchParams.entries())) current.set("game", newGameType) const search = current.toString() const query = search ? `?${search}` : "" // for some reason, this doesn't work?! router.replace(`${pathname}${query}`, { }) // workaround.. but it is strange that router.replace doesn't work.. //let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() //window.history.pushState({path: newurl}, '', newurl) // actually we don't handle partial reload very well, so let's reload the whole page window.location = `${window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString()}` as any } const handleSelectEngine = (newEngine: EngineType) => { setEngine(getEngine(newEngine)) const current = new URLSearchParams(Array.from(searchParams.entries())) current.set("engine", newEngine) const search = current.toString() //const query = search ? `?${search}` : "" // for some reason, this doesn't work?! //router.replace(`${pathname}${query}`, { }) // workaround.. but it is strange that router.replace doesn't work.. //let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() //window.history.pushState({path: newurl}, '', newurl) // actually we don't handle partial reload very well, so let's reload the whole page window.location = `${window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString()}` as any } const handleSceneEvent = (event: SceneEvent, actionnable?: string) => { // TODO use Zustand const item = store.currentlyDraggedItem const actionnableName = actionnable || "nothing" let newEvent = null let newEventString = "" if (event === "HoveringNothing") { if (item) { newEvent = <>🔎 You are holding "{item.name}" and looking around, wondering how to use it. newEventString = `User is holding "${item.name}" from their inventory and wonder how they can use it.` } else { newEvent = <>🔎 You are looking at the scene, searching for clues. newEventString = `User is looking at the scene, searching for clues.` } } else if (event === "HoveringActionnable") { if (item) { newEvent = <>🔎 You are holding "{item.name}", waving it over "{actionnableName}" newEventString = `User is holding "${item.name}" from their inventory and wonder if they can use it on "${actionnableName}"` } else { newEvent = <>🔎 You are looking at "{actionnableName}" newEventString = `User is looking at "${actionnableName}"` } } else if (event === "ClickOnNothing") { newEvent = <>🔎 There is nothing here. newEventString = `User clicked on nothing.` } else if (event === "ClickOnActionnable") { newEvent = <>🔎 You have clicked on {actionnableName} newEventString = `User has clicked on "${actionnableName}"` } if (newEventString && newEventString !== lastEventString) { console.log(newEventString) setLastEventString(newEventString) setLastEvent(newEvent) } if (event === "ClickOnActionnable" || event === "ClickOnNothing") { setBusy(busyRef.current = true) // this will be set to false once the scene finish loading const dialogue = askGameMasterForEpicDialogue(newEventString) askGameMasterForEpicBackground(newEventString) } } const handleInventoryEvent: OnInventoryEvent = async (event, item, target) => { let newEvent = null let newEventString = "" if (newEvent === "Grabbing") { newEventString = `Player just grabbed "${item.name}".` newEvent = <>You just grabbed "{item.name}" } else if (event === "DroppedOnActionnable") { newEventString = [ `Player is trying to use`, `"${item.name}"`, item.description ? `(described as: "${item.description}")` : "", `from their inventory on "${target?.title}"`, target?.description ? `(described as: "${target?.description}")` : "", `within the scene.`, `What do you think should be the outcome?` ].filter(i => i).join(" ") newEvent = <>You try to use "{item.name}" on "{target?.title}" } else if (event === "DroppedOnAnotherItem") { newEventString = [ `Player is trying to use`, `"${item.name}"`, item.description ? `(described as: "${item.description}")` : "", `on "${target?.name}"`, target?.description ? `(described as: "${target?.description}")` : "", `What do you think could the use or combination of ${item.name} and ${target?.name} lead to?`, `Invent a funny outcome!` ].filter(i => i).join(" ") newEvent = <>You try to combine "{item.name}" with "{target?.title}" } else if (event === "ClickOnItem") { newEventString = `Player is inspecting "${item.name}" from their inventory, which has the following description: "${item.description}". Can you invent a funny back story?` newEvent = <>🔎 You are inspecting "{item.name}". {item.description} } if (newEventString && newEventString !== lastEventString) { console.log(newEventString) setLastEventString(newEventString) setLastEvent(newEvent) } if (event === "DroppedOnAnotherItem" || event === "ClickOnItem" || event === "DroppedOnActionnable") { askGameMasterForEpicDialogue(newEventString) } if (event === "DroppedOnActionnable") { setBusy(busyRef.current = true) // this will be set to false once the scene finish loading askGameMasterForEpicBackground(newEventString) } } const isLoadingScene = isBusy || rendered.status === "pending" const isLoadingSegments = !rendered.segments.length const isLoading = isLoadingScene || isLoadingSegments || isLoadingDialogue const addBottomMarginForPhotoSphereMenu = engine.type.startsWith("spherical") return (
{lastEvent} {/* */} { dialogue ? dialogue : }
) }