Spaces:
Runtime error
Runtime error
File size: 18,748 Bytes
4f64da5 b1ecc22 637dd5c ab75c71 4f64da5 fd2aa6b 637dd5c fd2aa6b f0dc1c3 fd2aa6b 1e2c870 fd2aa6b ab75c71 fd2aa6b 4f64da5 40fde09 793c1b3 40fde09 819e443 40fde09 4f64da5 40fde09 1be0bd5 637dd5c 008456e 637dd5c bf8d4d8 e66b0b0 bf8d4d8 0be4978 819e443 e0c4dc2 6896326 1be0bd5 6896326 fd2aa6b 6896326 ab75c71 9349de1 40fde09 9802882 b1ecc22 40fde09 9802882 637dd5c 9802882 1be0bd5 a438bb5 f1f03f6 6896326 a438bb5 6896326 1e2c870 6896326 e0c4dc2 9802882 e0c4dc2 40fde09 637dd5c ab75c71 bf8d4d8 637dd5c 9802882 e0c4dc2 1e61892 e0c4dc2 40fde09 9802882 1be0bd5 e0c4dc2 9802882 40fde09 1be0bd5 40fde09 e0c4dc2 40fde09 1be0bd5 40fde09 1be0bd5 40fde09 1be0bd5 40fde09 1be0bd5 9349de1 40fde09 9802882 40fde09 4f64da5 b1ecc22 ab75c71 fd2aa6b ab75c71 fd2aa6b ab75c71 fd2aa6b ab75c71 8fb2ec4 ab75c71 9802882 6896326 ab75c71 6896326 ab75c71 f0dc1c3 ab75c71 f0dc1c3 6896326 f0dc1c3 6896326 b1ecc22 ab75c71 f0dc1c3 6896326 f0dc1c3 6896326 f0dc1c3 6896326 ab75c71 6896326 9802882 ab75c71 a3e95be 1e2c870 0be4978 e0c4dc2 637dd5c 40fde09 637dd5c 40fde09 637dd5c 819e443 637dd5c 40fde09 637dd5c a438bb5 637dd5c bf8d4d8 1e61892 bf8d4d8 1e61892 bf8d4d8 1e61892 a438bb5 1e61892 bf8d4d8 b1ecc22 ab75c71 b1ecc22 ab75c71 e62f50c ab75c71 b1ecc22 ab75c71 b1ecc22 65b89b5 fd2aa6b ab75c71 b1ecc22 ab75c71 b1ecc22 ab75c71 b1ecc22 ab75c71 b1ecc22 673e438 b1ecc22 ab75c71 65b89b5 ab75c71 b1ecc22 fd2aa6b b1ecc22 fd2aa6b 4f64da5 637dd5c a438bb5 637dd5c fd2aa6b e0c4dc2 fd2aa6b e0c4dc2 fd2aa6b a438bb5 ab75c71 fd2aa6b ab75c71 fd2aa6b ab75c71 fd2aa6b ab75c71 fd2aa6b f479593 ab75c71 4f64da5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
"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<RenderedScene>(getInitialRenderedScene())
const historyRef = useRef<RenderedScene[]>([])
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const requestedGame = (searchParams.get('game') as GameType) || defaultGame
const gameRef = useRef<GameType>(requestedGame)
const [game, setGame] = useState<Game>(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<Engine>(getEngine(usableEngine))
const requestedDebug = (searchParams.get('debug') === "true")
const [debug, setDebug] = useState<boolean>(requestedDebug)
const requestedClearCache = (searchParams.get('clearCache') ? (searchParams.get('clearCache') === "true") : false)
const [clearCache, setClearCache] = useState<boolean>(requestedClearCache)
const [situation, setSituation] = useState("")
const [dialogue, setDialogue] = useState("")
const [isLoadingDialogue, setLoadingDialogue] = useState(false)
/*
const [grabbedItem, setGrabbedItem] = useState<InventoryItem>()
const grabbedItemRef = useRef<InventoryItem | undefined>()
grabbedItemRef.current = grabbedItem
*/
const [isBusy, setBusy] = useState<boolean>(true)
const busyRef = useRef(true)
const loopRef = useRef<any>(null)
const [lastEventString, setLastEventString] = useState<string>("")
const [lastEvent, setLastEvent] = useState<ReactNode>(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 <span className="font-bold">"{item.name}"</span> 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 <span className="font-bold">"{item.name}"</span>, waving it over <span className="font-bold">"{actionnableName}"</span></>
newEventString = `User is holding "${item.name}" from their inventory and wonder if they can use it on "${actionnableName}"`
} else {
newEvent = <>π You are looking at <span className="font-bold">"{actionnableName}"</span></>
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 <span className="font-bold">{actionnableName}</span></>
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 <span className="font-bold">"{item.name}"</span></>
} 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 <span className="font-bold">"{item.name}"</span> on <span className="font-bold">"{target?.title}"</span></>
} 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 <span className="font-bold">"{item.name}"</span> with <span className="font-bold">"{target?.title}"</span></>
} 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 <span className="font-bold">"{item.name}".</span> {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 (
<div
className="flex flex-col w-full max-w-5xl"
>
<TopMenu
engine={engine}
game={game}
defaultGame={gameRef.current}
debug={debug}
clearCache={clearCache}
onToggleDebug={handleToggleDebug}
onChangeGame={handleSelectGame}
onChangeEngine={handleSelectEngine}
onToggleClearCache={handleToggleClearCache}
/>
<DndProvider backend={HTML5Backend}>
<div className={cn(
"flex flex-col w-full pt-4 space-y-3 text-gray-50 dark:text-gray-50",
getGame(gameRef.current).className // apply the game theme
)}
>
<SceneRenderer
rendered={rendered}
onEvent={handleSceneEvent}
isLoading={isLoadingScene}
game={game}
engine={engine}
debug={debug}
/>
<LastEvent>{lastEvent}</LastEvent>
<Inventory game={game} isLoading={!rendered.segments.length} onEvent={handleInventoryEvent} />
{/*
<Help
clickables={clickables}
isLoading={!rendered.segments.length}
/>
*/}
<Dialogue
className={
addBottomMarginForPhotoSphereMenu ? `bottom-16`: `bottom-6`
}
isLoading={isLoadingDialogue}>{
dialogue
? dialogue
: <Help clickables={clickables} isLoading={isLoadingSegments} />
}</Dialogue>
<Progress
isLoading={isLoading}
resetKey={[
rendered?.segments?.length,
rendered?.assetUrl,
].join("&&")}
/>
</div>
</DndProvider>
</div>
)
} |