Spaces:
Runtime error
Runtime error
Commit
·
ab75c71
1
Parent(s):
78a9e11
changing the URL again as the public server is being abused by some users
Browse files- .env +1 -1
- src/app/main.tsx +134 -86
- src/app/queries/getActionnables.ts +3 -3
- src/app/queries/getBackground.ts +5 -5
- src/app/queries/getBase.ts +3 -3
- src/app/queries/getDialogue.ts +5 -5
- src/app/queries/getSound.ts +77 -0
- src/app/queries/getSoundTrack.ts +0 -22
- src/app/store.ts +10 -0
- src/app/types.ts +21 -5
- src/components/inventory/draggable-item.tsx +31 -11
- src/components/inventory/index.tsx +12 -17
- src/components/renderer/cartesian-image.tsx +45 -3
- src/components/renderer/cartesian-video.tsx +45 -3
- src/components/renderer/index.tsx +23 -6
- src/components/renderer/spherical-image.tsx +42 -2
- src/components/renderer/types.ts +2 -2
- src/lib/defaultActionnables.ts +13 -0
- src/lib/formatActionnableName.ts +4 -0
- src/lib/normalizeActionnables.ts +2 -10
.env
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-fishtank.hf.space
|
2 |
# NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
|
3 |
-
RENDERING_ENGINE_API=https://jbilcke-hf-videochain
|
|
|
1 |
NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-fishtank.hf.space
|
2 |
# NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
|
3 |
+
RENDERING_ENGINE_API=https://jbilcke-hf-videochain.hf.space
|
src/app/main.tsx
CHANGED
@@ -2,7 +2,8 @@
|
|
2 |
|
3 |
import { ReactNode, useEffect, useRef, useState, useTransition } from "react"
|
4 |
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
5 |
-
|
|
|
6 |
|
7 |
import { SceneRenderer } from "@/components/renderer"
|
8 |
|
@@ -17,7 +18,7 @@ import { Switch } from "@/components/ui/switch"
|
|
17 |
import { Label } from "@/components/ui/label"
|
18 |
|
19 |
import { newRender, getRender } from "./render"
|
20 |
-
import { InventoryEvent, InventoryItem, RenderedScene, SceneEvent } from "./types"
|
21 |
import { Game, GameType } from "./games/types"
|
22 |
import { defaultGame, games, getGame } from "./games"
|
23 |
import { getBackground } from "@/app/queries/getBackground"
|
@@ -26,6 +27,8 @@ import { getActionnables } from "@/app/queries/getActionnables"
|
|
26 |
import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
|
27 |
import { normalizeActionnables } from "@/lib/normalizeActionnables"
|
28 |
import { Inventory } from "@/components/inventory"
|
|
|
|
|
29 |
|
30 |
const getInitialRenderedScene = (): RenderedScene => ({
|
31 |
renderId: "",
|
@@ -57,6 +60,12 @@ export default function Main() {
|
|
57 |
|
58 |
const [dialogue, setDialogue] = useState("")
|
59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
const [isBusy, setBusy] = useState<boolean>(true)
|
61 |
const busyRef = useRef(true)
|
62 |
|
@@ -88,6 +97,8 @@ export default function Main() {
|
|
88 |
console.log("got the first version of our scene!", newRendered)
|
89 |
|
90 |
// detect if type game type changed while we were busy
|
|
|
|
|
91 |
if (game?.type !== gameRef?.current) {
|
92 |
console.log("game type changed! aborting..")
|
93 |
return
|
@@ -160,35 +171,54 @@ export default function Main() {
|
|
160 |
checkRenderedLoop()
|
161 |
}, [])
|
162 |
|
163 |
-
const handleClickOnActionnable = async (actionnable: string = "", userAction: string = "") => {
|
164 |
|
165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
|
167 |
-
|
168 |
-
// we need a frame and some actionnables,
|
169 |
-
// perhaps even some music or sound effects
|
170 |
|
171 |
await startTransition(async () => {
|
172 |
|
173 |
-
|
|
|
|
|
|
|
|
|
174 |
|
175 |
-
let newDialogue = ""
|
176 |
try {
|
177 |
-
|
178 |
-
|
179 |
-
setDialogue(newDialogue)
|
180 |
} catch (err) {
|
181 |
-
console.log(`failed to generate
|
182 |
-
setDialogue("")
|
183 |
}
|
184 |
|
185 |
try {
|
186 |
-
const newActionnables = await getActionnables({ game, situation, userAction })
|
187 |
-
console.log(`newActionnables:`, newActionnables)
|
188 |
|
189 |
// todo rename this background/situation mess
|
190 |
// it should be only one word
|
191 |
-
const newBackground = await getBackground({ game, situation,
|
192 |
console.log(`newBackground:`, newBackground)
|
193 |
setSituation(newBackground)
|
194 |
|
@@ -197,11 +227,12 @@ export default function Main() {
|
|
197 |
|
198 |
// todo we could also use useEffect
|
199 |
} catch (err) {
|
200 |
-
console.error(`failed to get
|
201 |
}
|
202 |
})
|
203 |
}
|
204 |
|
|
|
205 |
const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
|
206 |
|
207 |
// console.log("segments:", rendered.segments)
|
@@ -273,15 +304,28 @@ export default function Main() {
|
|
273 |
}
|
274 |
|
275 |
const handleSceneEvent = (event: SceneEvent, actionnable?: string) => {
|
|
|
|
|
|
|
276 |
const actionnableName = actionnable || "nothing"
|
277 |
let newEvent = null
|
278 |
let newEventString = ""
|
279 |
if (event === "HoveringNothing") {
|
280 |
-
|
281 |
-
|
|
|
|
|
|
|
|
|
|
|
282 |
} else if (event === "HoveringActionnable") {
|
283 |
-
|
284 |
-
|
|
|
|
|
|
|
|
|
|
|
285 |
} else if (event === "ClickOnNothing") {
|
286 |
newEvent = <>🔎 There is nothing here.</>
|
287 |
newEventString = `User clicked on nothing.`
|
@@ -297,43 +341,39 @@ export default function Main() {
|
|
297 |
}
|
298 |
|
299 |
if (event === "ClickOnActionnable" || event === "ClickOnNothing") {
|
300 |
-
|
|
|
301 |
}
|
302 |
}
|
303 |
|
304 |
-
const
|
305 |
-
await startTransition(async () => {
|
306 |
-
const game = getGame(gameRef.current)
|
307 |
-
let newDialogue = ""
|
308 |
-
try {
|
309 |
-
newDialogue = await getDialogue({ game, situation, userAction })
|
310 |
-
} catch (err) {
|
311 |
-
console.log(`failed to generate dialoguee, let's try again maybe`)
|
312 |
-
try {
|
313 |
-
newDialogue = await getDialogue({ game, situation, userAction: `${userAction}.` })
|
314 |
-
} catch (err) {
|
315 |
-
console.log(`failed to generate dialogue.. again (but it's only a nice to have, so..)`)
|
316 |
-
setDialogue("")
|
317 |
-
return
|
318 |
-
}
|
319 |
-
}
|
320 |
-
|
321 |
-
// try to remove whatever hallucination might come up next
|
322 |
-
newDialogue = newDialogue.split("As the player")[0]
|
323 |
-
newDialogue = newDialogue.split("As they use")[0]
|
324 |
-
setDialogue(newDialogue)
|
325 |
-
})
|
326 |
-
}
|
327 |
-
|
328 |
-
const handleInventoryEvent = (event: InventoryEvent, item: InventoryItem, target?: InventoryItem) => {
|
329 |
let newEvent = null
|
330 |
let newEventString = ""
|
331 |
if (newEvent === "Grabbing") {
|
332 |
newEventString = `Player just grabbed "${item.name}".`
|
333 |
-
newEvent = <>You just grabbed <span className="font-bold"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
} else if (event === "DroppedOnAnotherItem") {
|
335 |
-
newEventString =
|
336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
337 |
} else if (event === "ClickOnItem") {
|
338 |
newEventString = `Player is inspecting "${item.name}" from their inventory, which has the following description: "${item.description}". Can you invent a funny back story?`
|
339 |
newEvent = <>🔎 You are inspecting <span className="font-bold">"{item.name}".</span> {item.description}</>
|
@@ -345,11 +385,16 @@ export default function Main() {
|
|
345 |
setLastEvent(newEvent)
|
346 |
}
|
347 |
|
348 |
-
if (event === "DroppedOnAnotherItem" || event === "ClickOnItem") {
|
349 |
-
|
|
|
|
|
|
|
|
|
350 |
}
|
351 |
}
|
352 |
|
|
|
353 |
// determine when to show the spinner
|
354 |
const isLoading = isBusy || rendered.status === "pending"
|
355 |
|
@@ -401,41 +446,44 @@ export default function Main() {
|
|
401 |
</div>
|
402 |
</div>
|
403 |
|
404 |
-
<
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
<
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
418 |
</div>
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
{
|
423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
424 |
</div>
|
425 |
-
|
426 |
-
rendered={rendered}
|
427 |
-
onEvent={handleSceneEvent}
|
428 |
-
isLoading={isLoading}
|
429 |
-
game={game}
|
430 |
-
engine={engine}
|
431 |
-
debug={debug}
|
432 |
-
/>
|
433 |
-
<div
|
434 |
-
className="text-xl rounded-xl backdrop-blur-sm bg-white/10 p-4"
|
435 |
-
style={{
|
436 |
-
textShadow: "1px 0px 2px #000000ab"
|
437 |
-
}}>{dialogue}</div>
|
438 |
-
</div>
|
439 |
</div>
|
440 |
)
|
441 |
}
|
|
|
2 |
|
3 |
import { ReactNode, useEffect, useRef, useState, useTransition } from "react"
|
4 |
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
5 |
+
import { DndProvider } from "react-dnd"
|
6 |
+
import { HTML5Backend } from "react-dnd-html5-backend"
|
7 |
|
8 |
import { SceneRenderer } from "@/components/renderer"
|
9 |
|
|
|
18 |
import { Label } from "@/components/ui/label"
|
19 |
|
20 |
import { newRender, getRender } from "./render"
|
21 |
+
import { InventoryEvent, InventoryItem, OnInventoryEvent, RenderedScene, SceneEvent } from "./types"
|
22 |
import { Game, GameType } from "./games/types"
|
23 |
import { defaultGame, games, getGame } from "./games"
|
24 |
import { getBackground } from "@/app/queries/getBackground"
|
|
|
27 |
import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
|
28 |
import { normalizeActionnables } from "@/lib/normalizeActionnables"
|
29 |
import { Inventory } from "@/components/inventory"
|
30 |
+
import { store } from "./store"
|
31 |
+
import { defaultActionnables } from "@/lib/defaultActionnables"
|
32 |
|
33 |
const getInitialRenderedScene = (): RenderedScene => ({
|
34 |
renderId: "",
|
|
|
60 |
|
61 |
const [dialogue, setDialogue] = useState("")
|
62 |
|
63 |
+
/*
|
64 |
+
const [grabbedItem, setGrabbedItem] = useState<InventoryItem>()
|
65 |
+
const grabbedItemRef = useRef<InventoryItem | undefined>()
|
66 |
+
grabbedItemRef.current = grabbedItem
|
67 |
+
*/
|
68 |
+
|
69 |
const [isBusy, setBusy] = useState<boolean>(true)
|
70 |
const busyRef = useRef(true)
|
71 |
|
|
|
97 |
console.log("got the first version of our scene!", newRendered)
|
98 |
|
99 |
// detect if type game type changed while we were busy
|
100 |
+
// note that currently we reload the whol page when tha happens,
|
101 |
+
// so this code isn't that useful
|
102 |
if (game?.type !== gameRef?.current) {
|
103 |
console.log("game type changed! aborting..")
|
104 |
return
|
|
|
171 |
checkRenderedLoop()
|
172 |
}, [])
|
173 |
|
|
|
174 |
|
175 |
+
const askGameMasterForEpicDialogue = async (lastEvent: string) => {
|
176 |
+
|
177 |
+
await startTransition(async () => {
|
178 |
+
// const game = getGame(gameRef.current)
|
179 |
+
let newDialogue = ""
|
180 |
+
try {
|
181 |
+
newDialogue = await getDialogue({ game, situation, lastEvent })
|
182 |
+
} catch (err) {
|
183 |
+
console.log(`failed to generate dialogue, let's try again maybe`)
|
184 |
+
try {
|
185 |
+
newDialogue = await getDialogue({ game, situation, lastEvent: `${lastEvent}.` })
|
186 |
+
} catch (err) {
|
187 |
+
console.log(`failed to generate dialogue.. again (but it's only a nice to have, so..)`)
|
188 |
+
setDialogue("")
|
189 |
+
return
|
190 |
+
}
|
191 |
+
}
|
192 |
+
|
193 |
+
// try to remove whatever hallucination might come up next
|
194 |
+
newDialogue = newDialogue.split("As the player")[0]
|
195 |
+
newDialogue = newDialogue.split("As they use")[0]
|
196 |
+
setDialogue(newDialogue)
|
197 |
+
})
|
198 |
+
}
|
199 |
|
200 |
+
const askGameMasterForEpicBackground = async (lastEvent: string = "") => {
|
|
|
|
|
201 |
|
202 |
await startTransition(async () => {
|
203 |
|
204 |
+
setBusy(busyRef.current = true) // this will be set to false once the scene finish loading
|
205 |
+
|
206 |
+
// const game = getGame(gameRef.current)
|
207 |
+
|
208 |
+
let newActionnables = [...defaultActionnables]
|
209 |
|
|
|
210 |
try {
|
211 |
+
newActionnables = await getActionnables({ game, situation, lastEvent })
|
212 |
+
console.log(`newActionnables:`, newActionnables)
|
|
|
213 |
} catch (err) {
|
214 |
+
console.log(`failed to generate actionnables, using default value`)
|
|
|
215 |
}
|
216 |
|
217 |
try {
|
|
|
|
|
218 |
|
219 |
// todo rename this background/situation mess
|
220 |
// it should be only one word
|
221 |
+
const newBackground = await getBackground({ game, situation, lastEvent, newActionnables })
|
222 |
console.log(`newBackground:`, newBackground)
|
223 |
setSituation(newBackground)
|
224 |
|
|
|
227 |
|
228 |
// todo we could also use useEffect
|
229 |
} catch (err) {
|
230 |
+
console.error(`failed to get the scene: ${err}`)
|
231 |
}
|
232 |
})
|
233 |
}
|
234 |
|
235 |
+
|
236 |
const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
|
237 |
|
238 |
// console.log("segments:", rendered.segments)
|
|
|
304 |
}
|
305 |
|
306 |
const handleSceneEvent = (event: SceneEvent, actionnable?: string) => {
|
307 |
+
// TODO use Zustand
|
308 |
+
const item = store.currentlyDraggedItem
|
309 |
+
|
310 |
const actionnableName = actionnable || "nothing"
|
311 |
let newEvent = null
|
312 |
let newEventString = ""
|
313 |
if (event === "HoveringNothing") {
|
314 |
+
if (item) {
|
315 |
+
newEvent = <>🔎 You are holding <span className="font-bold">"{item.name}"</span> and looking around, wondering how to use it.</>
|
316 |
+
newEventString = `User is holding "${item.name}" from their inventory and wonder how they can use it.`
|
317 |
+
} else {
|
318 |
+
newEvent = <>🔎 You are looking at the scene, looking for clues.</>
|
319 |
+
newEventString = `User is looking at the scene, looking for clues.`
|
320 |
+
}
|
321 |
} else if (event === "HoveringActionnable") {
|
322 |
+
if (item) {
|
323 |
+
newEvent = <>🔎 You are holding <span className="font-bold">"{item.name}"</span>, waving it over <span className="font-bold">"{actionnableName}"</span></>
|
324 |
+
newEventString = `User is holding "${item.name}" from their inventory and wonder if they can use it on "${actionnableName}"`
|
325 |
+
} else {
|
326 |
+
newEvent = <>🔎 You are looking at <span className="font-bold">"{actionnableName}"</span></>
|
327 |
+
newEventString = `User is looking at "${actionnableName}"`
|
328 |
+
}
|
329 |
} else if (event === "ClickOnNothing") {
|
330 |
newEvent = <>🔎 There is nothing here.</>
|
331 |
newEventString = `User clicked on nothing.`
|
|
|
341 |
}
|
342 |
|
343 |
if (event === "ClickOnActionnable" || event === "ClickOnNothing") {
|
344 |
+
askGameMasterForEpicDialogue(newEventString)
|
345 |
+
askGameMasterForEpicBackground(newEventString)
|
346 |
}
|
347 |
}
|
348 |
|
349 |
+
const handleInventoryEvent: OnInventoryEvent = async (event, item, target) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
350 |
let newEvent = null
|
351 |
let newEventString = ""
|
352 |
if (newEvent === "Grabbing") {
|
353 |
newEventString = `Player just grabbed "${item.name}".`
|
354 |
+
newEvent = <>You just grabbed <span className="font-bold">"{item.name}"</span></>
|
355 |
+
} else if (event === "DroppedOnActionnable") {
|
356 |
+
newEventString = [
|
357 |
+
`Player is trying to use`,
|
358 |
+
`"${item.name}"`,
|
359 |
+
item.description ? `(described as: "${item.description}")` : "",
|
360 |
+
`from their inventory on "${target?.title}"`,
|
361 |
+
target?.description ? `(described as: "${target?.description}")` : "",
|
362 |
+
`within the scene.`,
|
363 |
+
`What do you think should be the outcome?`
|
364 |
+
].filter(i => i).join(" ")
|
365 |
+
newEvent = <>You try to use <span className="font-bold">"{item.name}"</span> on <span className="font-bold">"{target?.title}"</span></>
|
366 |
} else if (event === "DroppedOnAnotherItem") {
|
367 |
+
newEventString = [
|
368 |
+
`Player is trying to use`,
|
369 |
+
`"${item.name}"`,
|
370 |
+
item.description ? `(described as: "${item.description}")` : "",
|
371 |
+
`on "${target?.name}"`,
|
372 |
+
target?.description ? `(described as: "${target?.description}")` : "",
|
373 |
+
`What do you think could the use or combination of ${item.name} and ${target?.name} lead to?`,
|
374 |
+
`Invent a funny outcome!`
|
375 |
+
].filter(i => i).join(" ")
|
376 |
+
newEvent = <>You try to combine <span className="font-bold">"{item.name}"</span> with <span className="font-bold">"{target?.title}"</span></>
|
377 |
} else if (event === "ClickOnItem") {
|
378 |
newEventString = `Player is inspecting "${item.name}" from their inventory, which has the following description: "${item.description}". Can you invent a funny back story?`
|
379 |
newEvent = <>🔎 You are inspecting <span className="font-bold">"{item.name}".</span> {item.description}</>
|
|
|
385 |
setLastEvent(newEvent)
|
386 |
}
|
387 |
|
388 |
+
if (event === "DroppedOnAnotherItem" || event === "ClickOnItem" || event === "DroppedOnActionnable") {
|
389 |
+
askGameMasterForEpicDialogue(newEventString)
|
390 |
+
}
|
391 |
+
|
392 |
+
if (event === "DroppedOnActionnable") {
|
393 |
+
askGameMasterForEpicBackground(newEventString)
|
394 |
}
|
395 |
}
|
396 |
|
397 |
+
|
398 |
// determine when to show the spinner
|
399 |
const isLoading = isBusy || rendered.status === "pending"
|
400 |
|
|
|
446 |
</div>
|
447 |
</div>
|
448 |
|
449 |
+
<DndProvider backend={HTML5Backend}>
|
450 |
+
<div className={[
|
451 |
+
"flex flex-col w-full pt-4 space-y-3 text-gray-50 dark:text-gray-50",
|
452 |
+
getGame(gameRef.current).className // apply the game theme
|
453 |
+
].join(" ")}
|
454 |
+
>
|
455 |
+
<div className="flex flex-row">
|
456 |
+
<div className="text-xl px-2">{lastEvent}</div>
|
457 |
+
</div>
|
458 |
+
<Inventory game={game} onEvent={handleInventoryEvent} />
|
459 |
+
<div className="flex flex-row">
|
460 |
+
<div className="text-xl mr-2">
|
461 |
+
{rendered.segments.length
|
462 |
+
? <span>💡 Try to click on:</span>
|
463 |
+
: <span>⌛ Generating areas for clicks and drag & drop, please wait..</span>
|
464 |
+
}
|
465 |
+
</div>
|
466 |
+
{clickables.map((clickable, i) =>
|
467 |
+
<div key={i} className="flex flex-row text-xl mr-2">
|
468 |
+
<div className="">{clickable}</div>
|
469 |
+
{i < (clickables.length - 1) ? <div>,</div> : null}
|
470 |
+
</div>)}
|
471 |
</div>
|
472 |
+
<SceneRenderer
|
473 |
+
rendered={rendered}
|
474 |
+
onEvent={handleSceneEvent}
|
475 |
+
isLoading={isLoading}
|
476 |
+
game={game}
|
477 |
+
engine={engine}
|
478 |
+
debug={debug}
|
479 |
+
/>
|
480 |
+
<div
|
481 |
+
className="text-xl rounded-xl backdrop-blur-sm bg-white/10 p-4"
|
482 |
+
style={{
|
483 |
+
textShadow: "1px 0px 2px #000000ab"
|
484 |
+
}}>{dialogue}</div>
|
485 |
</div>
|
486 |
+
</DndProvider>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
487 |
</div>
|
488 |
)
|
489 |
}
|
src/app/queries/getActionnables.ts
CHANGED
@@ -9,14 +9,14 @@ import { normalizeActionnables } from "@/lib/normalizeActionnables"
|
|
9 |
export const getActionnables = async ({
|
10 |
game,
|
11 |
situation = "",
|
12 |
-
|
13 |
}: {
|
14 |
game: Game;
|
15 |
situation: string;
|
16 |
-
|
17 |
}) => {
|
18 |
|
19 |
-
const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation,
|
20 |
|
21 |
const basePrompt = initialPrompt !== currentPrompt
|
22 |
? `Here is some context information about the initial scene: ${initialPrompt}`
|
|
|
9 |
export const getActionnables = async ({
|
10 |
game,
|
11 |
situation = "",
|
12 |
+
lastEvent = "",
|
13 |
}: {
|
14 |
game: Game;
|
15 |
situation: string;
|
16 |
+
lastEvent: string;
|
17 |
}) => {
|
18 |
|
19 |
+
const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, lastEvent })
|
20 |
|
21 |
const basePrompt = initialPrompt !== currentPrompt
|
22 |
? `Here is some context information about the initial scene: ${initialPrompt}`
|
src/app/queries/getBackground.ts
CHANGED
@@ -7,12 +7,12 @@ import { predict } from "./predict"
|
|
7 |
export const getBackground = async ({
|
8 |
game,
|
9 |
situation = "",
|
10 |
-
|
11 |
newActionnables = [],
|
12 |
}: {
|
13 |
game: Game;
|
14 |
situation: string;
|
15 |
-
|
16 |
newActionnables: string[],
|
17 |
}) => {
|
18 |
|
@@ -23,7 +23,7 @@ export const getBackground = async ({
|
|
23 |
} = getBase({
|
24 |
game,
|
25 |
situation,
|
26 |
-
|
27 |
})
|
28 |
|
29 |
const basePrompt = initialPrompt !== currentPrompt
|
@@ -38,9 +38,9 @@ Here is the original scene in which the user was located at first, which will in
|
|
38 |
`You are the AI game master of a role video game.`,
|
39 |
basePrompt,
|
40 |
`You are going to receive new information about the current whereabouts of the player.`,
|
41 |
-
`Please write a caption for the next plausible scene to display in intricate details: the environment, lights, era, characters, objects, textures, light etc.`,
|
42 |
`You MUST include the following important objects that the user can click on: ${newActionnables}.`,
|
43 |
-
`Be straight to the point, and do not say things like "As the player clicks on.." or "the scene shifts to" (the best is not not mention the player at all)`
|
44 |
].filter(item => item).join("\n")
|
45 |
},
|
46 |
{
|
|
|
7 |
export const getBackground = async ({
|
8 |
game,
|
9 |
situation = "",
|
10 |
+
lastEvent = "",
|
11 |
newActionnables = [],
|
12 |
}: {
|
13 |
game: Game;
|
14 |
situation: string;
|
15 |
+
lastEvent: string;
|
16 |
newActionnables: string[],
|
17 |
}) => {
|
18 |
|
|
|
23 |
} = getBase({
|
24 |
game,
|
25 |
situation,
|
26 |
+
lastEvent
|
27 |
})
|
28 |
|
29 |
const basePrompt = initialPrompt !== currentPrompt
|
|
|
38 |
`You are the AI game master of a role video game.`,
|
39 |
basePrompt,
|
40 |
`You are going to receive new information about the current whereabouts of the player.`,
|
41 |
+
`Please write a photo caption for the next plausible scene to display in intricate details: the environment, lights, era, characters, objects, textures, light etc.`,
|
42 |
`You MUST include the following important objects that the user can click on: ${newActionnables}.`,
|
43 |
+
`As this is a caption be synthetic: describe things, but don't comment on them. Be straight to the point, and do not say things like "As the player clicks on.." or "the scene shifts to" (the best is not not mention the player at all)`
|
44 |
].filter(item => item).join("\n")
|
45 |
},
|
46 |
{
|
src/app/queries/getBase.ts
CHANGED
@@ -3,11 +3,11 @@ import { Game } from "@/app/games/types"
|
|
3 |
export const getBase = ({
|
4 |
game,
|
5 |
situation = "",
|
6 |
-
|
7 |
}: {
|
8 |
game: Game;
|
9 |
situation: string;
|
10 |
-
|
11 |
}) => {
|
12 |
const initialPrompt = [...game.getScenePrompt()].join(", ")
|
13 |
|
@@ -17,7 +17,7 @@ export const getBase = ({
|
|
17 |
|
18 |
const userSituationPrompt = [
|
19 |
`Player is currently in "${currentPrompt}".`,
|
20 |
-
|
21 |
].join(" ")
|
22 |
|
23 |
return { initialPrompt, currentPrompt, userSituationPrompt }
|
|
|
3 |
export const getBase = ({
|
4 |
game,
|
5 |
situation = "",
|
6 |
+
lastEvent = "",
|
7 |
}: {
|
8 |
game: Game;
|
9 |
situation: string;
|
10 |
+
lastEvent: string;
|
11 |
}) => {
|
12 |
const initialPrompt = [...game.getScenePrompt()].join(", ")
|
13 |
|
|
|
17 |
|
18 |
const userSituationPrompt = [
|
19 |
`Player is currently in "${currentPrompt}".`,
|
20 |
+
lastEvent
|
21 |
].join(" ")
|
22 |
|
23 |
return { initialPrompt, currentPrompt, userSituationPrompt }
|
src/app/queries/getDialogue.ts
CHANGED
@@ -8,17 +8,17 @@ import { predict } from "./predict"
|
|
8 |
export const getDialogue = async ({
|
9 |
game,
|
10 |
situation = "",
|
11 |
-
|
12 |
}: {
|
13 |
game: Game;
|
14 |
situation: string;
|
15 |
-
|
16 |
}) => {
|
17 |
|
18 |
-
const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation,
|
19 |
|
20 |
console.log("DEBUG", {
|
21 |
-
game, situation,
|
22 |
currentPrompt,
|
23 |
initialPrompt,
|
24 |
userSituationPrompt,
|
@@ -31,7 +31,7 @@ export const getDialogue = async ({
|
|
31 |
*/
|
32 |
|
33 |
const basePrompt = initialPrompt !== currentPrompt
|
34 |
-
? `
|
35 |
Here is the original scene in which the user was located at first, which will inform you about the general settings to follow (you must respect this): "${initialPrompt}".`
|
36 |
: ""
|
37 |
|
|
|
8 |
export const getDialogue = async ({
|
9 |
game,
|
10 |
situation = "",
|
11 |
+
lastEvent = "",
|
12 |
}: {
|
13 |
game: Game;
|
14 |
situation: string;
|
15 |
+
lastEvent: string;
|
16 |
}) => {
|
17 |
|
18 |
+
const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, lastEvent })
|
19 |
|
20 |
console.log("DEBUG", {
|
21 |
+
game, situation, lastEvent,
|
22 |
currentPrompt,
|
23 |
initialPrompt,
|
24 |
userSituationPrompt,
|
|
|
31 |
*/
|
32 |
|
33 |
const basePrompt = initialPrompt !== currentPrompt
|
34 |
+
? `You must imagine the most plausible next dialogue line from the game master, based on where the player was located before and is now, and also what the player did before and are doing now.
|
35 |
Here is the original scene in which the user was located at first, which will inform you about the general settings to follow (you must respect this): "${initialPrompt}".`
|
36 |
: ""
|
37 |
|
src/app/queries/getSound.ts
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Game } from "@/app/games/types"
|
2 |
+
import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
|
3 |
+
|
4 |
+
import { getBase } from "./getBase"
|
5 |
+
import { predict } from "./predict"
|
6 |
+
|
7 |
+
|
8 |
+
export const getSound = async ({
|
9 |
+
game,
|
10 |
+
situation = "",
|
11 |
+
lastEvent = "",
|
12 |
+
}: {
|
13 |
+
game: Game;
|
14 |
+
situation: string;
|
15 |
+
lastEvent: string;
|
16 |
+
}) => {
|
17 |
+
|
18 |
+
const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, lastEvent })
|
19 |
+
|
20 |
+
console.log("DEBUG", {
|
21 |
+
game, situation, lastEvent,
|
22 |
+
currentPrompt,
|
23 |
+
initialPrompt,
|
24 |
+
userSituationPrompt,
|
25 |
+
|
26 |
+
})
|
27 |
+
/*
|
28 |
+
const basePrompt = initialPrompt !== currentPrompt
|
29 |
+
? `for your information, the initial game panel and scene was: ${initialPrompt}`
|
30 |
+
: ""
|
31 |
+
*/
|
32 |
+
|
33 |
+
const basePrompt = initialPrompt !== currentPrompt
|
34 |
+
? `Here is the original scene in which the user was located at first, which will inform you about the general settings to follow (you must respect this): "${initialPrompt}".`
|
35 |
+
: ""
|
36 |
+
|
37 |
+
const prompt = createLlamaPrompt([
|
38 |
+
{
|
39 |
+
role: "system",
|
40 |
+
content: [
|
41 |
+
`You are the AI game master of a role video game.`,
|
42 |
+
`You are going to receive new information about the current whereabouts and action of the player.`,
|
43 |
+
basePrompt,
|
44 |
+
`You must imagine a sound effect in reaction to the player action.`,
|
45 |
+
`Here are some examples, but don't copy them verbatim:\n`,
|
46 |
+
`- "An excited crowd cheering at a sports game"\n`,
|
47 |
+
`- "A cat is meowing for attention"\n`,
|
48 |
+
`- "Birds singing sweetly in a blooming garden"\n`,
|
49 |
+
`- "A modern synthesizer creating futuristic soundscapes"\n`,
|
50 |
+
`- "The vibrant beat of Brazilian samba drums"\n`,
|
51 |
+
`Here are some more instructions, to enhance the Qqality of your generated audio:`,
|
52 |
+
`1. Try to use more adjectives to describe your sound. For example: "A man is speaking clearly and slowly in a large room" is better than "A man is speaking".\n`,
|
53 |
+
`2. It's better to use general terms like 'man' or 'woman' instead of specific names for individuals or abstract objects that humans may not be familiar with, such as 'mummy'.\n`
|
54 |
+
].filter(item => item).join("\n")
|
55 |
+
},
|
56 |
+
{
|
57 |
+
role: "user",
|
58 |
+
content: userSituationPrompt
|
59 |
+
}
|
60 |
+
])
|
61 |
+
|
62 |
+
|
63 |
+
let result = ""
|
64 |
+
try {
|
65 |
+
result = await predict(prompt)
|
66 |
+
} catch (err) {
|
67 |
+
console.log(`prediction of the dialogue failed, trying again..`)
|
68 |
+
try {
|
69 |
+
result = await predict(prompt)
|
70 |
+
} catch (err) {
|
71 |
+
console.error(`prediction of the dialogue failed again!`)
|
72 |
+
throw new Error(`failed to generate the dialogue ${err}`)
|
73 |
+
}
|
74 |
+
}
|
75 |
+
|
76 |
+
return result
|
77 |
+
}
|
src/app/queries/getSoundTrack.ts
DELETED
@@ -1,22 +0,0 @@
|
|
1 |
-
import { Game } from "@/app/games/types"
|
2 |
-
import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
|
3 |
-
|
4 |
-
import { getBase } from "./getBase"
|
5 |
-
import { predict } from "./predict"
|
6 |
-
|
7 |
-
export const getSoundTrack = async ({
|
8 |
-
game,
|
9 |
-
situation = "",
|
10 |
-
actionnable = "",
|
11 |
-
newDialogue = "",
|
12 |
-
newActionnables = [],
|
13 |
-
}: {
|
14 |
-
game: Game;
|
15 |
-
situation: string;
|
16 |
-
actionnable: string;
|
17 |
-
newDialogue: string;
|
18 |
-
newActionnables: string[];
|
19 |
-
}) => {
|
20 |
-
|
21 |
-
return ""
|
22 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/store.ts
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { InventoryItem } from "./types"
|
4 |
+
|
5 |
+
// could also be Zustand or something
|
6 |
+
export const store: {
|
7 |
+
currentlyDraggedItem?: InventoryItem
|
8 |
+
} = {
|
9 |
+
currentlyDraggedItem: undefined
|
10 |
+
}
|
src/app/types.ts
CHANGED
@@ -46,9 +46,11 @@ export type RenderedSceneStatus =
|
|
46 |
export type SceneEvent =
|
47 |
| "HoveringNothing"
|
48 |
| "HoveringActionnable"
|
|
|
49 |
| "ClickOnNothing"
|
50 |
| "ClickOnActionnable"
|
51 |
|
|
|
52 |
export interface RenderedScene {
|
53 |
renderId: string
|
54 |
status: RenderedSceneStatus
|
@@ -69,9 +71,23 @@ export type InventoryEvent =
|
|
69 |
| "DroppedOnAnotherItem" // the item has been dropped on another inventory item
|
70 |
| "DroppedBackToInventory" // the drag & drop is cancelled, the item is back in the inventory
|
71 |
|
72 |
-
export interface InventoryItem {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
name: string
|
74 |
-
title
|
75 |
-
|
76 |
-
|
77 |
-
}
|
|
|
46 |
export type SceneEvent =
|
47 |
| "HoveringNothing"
|
48 |
| "HoveringActionnable"
|
49 |
+
// | "ItemIsOverActionnable"
|
50 |
| "ClickOnNothing"
|
51 |
| "ClickOnActionnable"
|
52 |
|
53 |
+
|
54 |
export interface RenderedScene {
|
55 |
renderId: string
|
56 |
status: RenderedSceneStatus
|
|
|
71 |
| "DroppedOnAnotherItem" // the item has been dropped on another inventory item
|
72 |
| "DroppedBackToInventory" // the drag & drop is cancelled, the item is back in the inventory
|
73 |
|
74 |
+
export interface InventoryItem {
|
75 |
+
name: string
|
76 |
+
title: string
|
77 |
+
caption: string
|
78 |
+
description: string
|
79 |
+
}
|
80 |
+
|
81 |
+
export interface DropZoneTarget {
|
82 |
+
type: "InventoryItem" | "Actionnable"
|
83 |
+
name: string
|
84 |
+
title?: string
|
85 |
+
caption?: string
|
86 |
+
description?: string
|
87 |
+
}
|
88 |
+
|
89 |
+
export type OnInventoryEvent = (event: InventoryEvent, item: InventoryItem, target?: {
|
90 |
name: string
|
91 |
+
title?: string
|
92 |
+
description?: string
|
93 |
+
}) => void
|
|
src/components/inventory/draggable-item.tsx
CHANGED
@@ -1,26 +1,39 @@
|
|
1 |
-
|
2 |
import Image from "next/image"
|
3 |
import { useDrag, useDrop } from "react-dnd"
|
4 |
|
5 |
import { Game } from "@/app/games/types"
|
6 |
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
|
7 |
-
import { InventoryEvent, InventoryItem } from "@/app/types"
|
8 |
-
import { useEffect } from "react"
|
9 |
|
10 |
-
|
11 |
|
12 |
export function DraggableItem({ game, item, onEvent }: {
|
13 |
-
game: Game
|
14 |
-
item: InventoryItem
|
15 |
-
onEvent:
|
16 |
}) {
|
17 |
const [{ isDragging }, drag] = useDrag(() => ({
|
18 |
type: "item",
|
19 |
item,
|
20 |
end: (item, monitor) => {
|
21 |
-
const
|
22 |
-
if (item &&
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
}
|
25 |
},
|
26 |
collect: (monitor) => ({
|
@@ -31,7 +44,12 @@ export function DraggableItem({ game, item, onEvent }: {
|
|
31 |
|
32 |
const [{ isOver, canDrop }, drop] = useDrop({
|
33 |
accept: "item",
|
34 |
-
drop: () => (
|
|
|
|
|
|
|
|
|
|
|
35 |
collect: (monitor) => ({
|
36 |
isOver: monitor.isOver(),
|
37 |
canDrop: monitor.canDrop(),
|
@@ -40,8 +58,10 @@ export function DraggableItem({ game, item, onEvent }: {
|
|
40 |
|
41 |
useEffect(() => {
|
42 |
if (isDragging) {
|
|
|
43 |
onEvent("Grabbing", item)
|
44 |
} else {
|
|
|
45 |
onEvent("DroppedBackToInventory", item)
|
46 |
}
|
47 |
}, [isDragging])
|
|
|
1 |
+
import { useEffect, useRef } from "react"
|
2 |
import Image from "next/image"
|
3 |
import { useDrag, useDrop } from "react-dnd"
|
4 |
|
5 |
import { Game } from "@/app/games/types"
|
6 |
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
|
7 |
+
import { DropZoneTarget, InventoryEvent, InventoryItem, OnInventoryEvent } from "@/app/types"
|
|
|
8 |
|
9 |
+
import { store } from "@/app/store"
|
10 |
|
11 |
export function DraggableItem({ game, item, onEvent }: {
|
12 |
+
game: Game
|
13 |
+
item: InventoryItem
|
14 |
+
onEvent: OnInventoryEvent
|
15 |
}) {
|
16 |
const [{ isDragging }, drag] = useDrag(() => ({
|
17 |
type: "item",
|
18 |
item,
|
19 |
end: (item, monitor) => {
|
20 |
+
const target = monitor.getDropResult<DropZoneTarget>()
|
21 |
+
if (item && target) {
|
22 |
+
if ( target.type === "InventoryItem" && item.name === target.name) {
|
23 |
+
// user is trying to drop the item on itself.. that's not possible
|
24 |
+
return
|
25 |
+
}
|
26 |
+
onEvent(
|
27 |
+
target.type === "Actionnable"
|
28 |
+
? "DroppedOnActionnable"
|
29 |
+
: "DroppedOnAnotherItem",
|
30 |
+
item,
|
31 |
+
{
|
32 |
+
name: target.name,
|
33 |
+
title: target.title,
|
34 |
+
description: target.description
|
35 |
+
}
|
36 |
+
)
|
37 |
}
|
38 |
},
|
39 |
collect: (monitor) => ({
|
|
|
44 |
|
45 |
const [{ isOver, canDrop }, drop] = useDrop({
|
46 |
accept: "item",
|
47 |
+
drop: (): DropZoneTarget => ({
|
48 |
+
type: "InventoryItem",
|
49 |
+
name: item.name,
|
50 |
+
title: item.title,
|
51 |
+
description: item.description
|
52 |
+
}),
|
53 |
collect: (monitor) => ({
|
54 |
isOver: monitor.isOver(),
|
55 |
canDrop: monitor.canDrop(),
|
|
|
58 |
|
59 |
useEffect(() => {
|
60 |
if (isDragging) {
|
61 |
+
store.currentlyDraggedItem = item
|
62 |
onEvent("Grabbing", item)
|
63 |
} else {
|
64 |
+
store.currentlyDraggedItem = undefined
|
65 |
onEvent("DroppedBackToInventory", item)
|
66 |
}
|
67 |
}, [isDragging])
|
src/components/inventory/index.tsx
CHANGED
@@ -1,29 +1,24 @@
|
|
1 |
-
import { DndProvider } from "react-dnd"
|
2 |
-
import { HTML5Backend } from "react-dnd-html5-backend"
|
3 |
-
|
4 |
import { Game } from "@/app/games/types"
|
5 |
import { DraggableItem } from "./draggable-item"
|
6 |
-
import {
|
7 |
|
8 |
export function Inventory({
|
9 |
game,
|
10 |
onEvent
|
11 |
}: {
|
12 |
game: Game;
|
13 |
-
onEvent:
|
14 |
}) {
|
15 |
return (
|
16 |
-
<
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
</div>
|
27 |
-
</DndProvider>
|
28 |
)
|
29 |
}
|
|
|
|
|
|
|
|
|
1 |
import { Game } from "@/app/games/types"
|
2 |
import { DraggableItem } from "./draggable-item"
|
3 |
+
import { OnInventoryEvent } from "@/app/types"
|
4 |
|
5 |
export function Inventory({
|
6 |
game,
|
7 |
onEvent
|
8 |
}: {
|
9 |
game: Game;
|
10 |
+
onEvent: OnInventoryEvent;
|
11 |
}) {
|
12 |
return (
|
13 |
+
<div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12 gap-4 w-full bg-stone-500 p-4 rounded-xl">
|
14 |
+
{game.inventory.map(item => (
|
15 |
+
<DraggableItem
|
16 |
+
key={item.name}
|
17 |
+
game={game}
|
18 |
+
item={item}
|
19 |
+
onEvent={onEvent}
|
20 |
+
/>
|
21 |
+
))}
|
22 |
+
</div>
|
|
|
|
|
23 |
)
|
24 |
}
|
src/components/renderer/cartesian-image.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
-
import { useRef } from "react"
|
2 |
-
import {
|
3 |
import { RenderedScene } from "@/app/types"
|
4 |
|
5 |
export function CartesianImage({
|
@@ -9,12 +9,53 @@ export function CartesianImage({
|
|
9 |
debug
|
10 |
}: {
|
11 |
rendered: RenderedScene
|
12 |
-
onEvent:
|
13 |
className?: string
|
14 |
debug?: boolean
|
15 |
}) {
|
16 |
|
17 |
const ref = useRef<HTMLImageElement>(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
const handleEvent = async (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
|
19 |
|
20 |
if (!ref.current) {
|
@@ -35,6 +76,7 @@ export function CartesianImage({
|
|
35 |
const relativeY = containerY / boundingRect.height
|
36 |
|
37 |
const eventType = isClick ? "click" : "hover"
|
|
|
38 |
onEvent(eventType, relativeX, relativeY)
|
39 |
}
|
40 |
|
|
|
1 |
+
import { useEffect, useRef } from "react"
|
2 |
+
import { MouseEventHandler } from "./types"
|
3 |
import { RenderedScene } from "@/app/types"
|
4 |
|
5 |
export function CartesianImage({
|
|
|
9 |
debug
|
10 |
}: {
|
11 |
rendered: RenderedScene
|
12 |
+
onEvent: MouseEventHandler
|
13 |
className?: string
|
14 |
debug?: boolean
|
15 |
}) {
|
16 |
|
17 |
const ref = useRef<HTMLImageElement>(null)
|
18 |
+
|
19 |
+
const cacheRef = useRef("")
|
20 |
+
useEffect(() => {
|
21 |
+
const listener = (e: DragEvent) => {
|
22 |
+
if (!ref.current) { return }
|
23 |
+
|
24 |
+
// TODO: check if we are currently dragging an object
|
25 |
+
// if yes, then we should check if clientX and clientY are matching the
|
26 |
+
const boundingRect = ref.current.getBoundingClientRect()
|
27 |
+
|
28 |
+
// abort if we are not currently dragging over our display area
|
29 |
+
if (e.clientX < boundingRect.left) { return }
|
30 |
+
if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
|
31 |
+
if (e.clientY < boundingRect.top) { return }
|
32 |
+
if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
|
33 |
+
|
34 |
+
const containerX = e.clientX - boundingRect.left
|
35 |
+
const containerY = e.clientY - boundingRect.top
|
36 |
+
|
37 |
+
const relativeX = containerX / boundingRect.width
|
38 |
+
const relativeY = containerY / boundingRect.height
|
39 |
+
|
40 |
+
const key = `${relativeX},${relativeY}`
|
41 |
+
|
42 |
+
// to avoid use
|
43 |
+
if (cacheRef.current === key) {
|
44 |
+
return
|
45 |
+
}
|
46 |
+
// console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
|
47 |
+
|
48 |
+
cacheRef.current = key
|
49 |
+
onEvent("hover", relativeX, relativeY)
|
50 |
+
}
|
51 |
+
|
52 |
+
document.addEventListener('drag', listener)
|
53 |
+
|
54 |
+
return () => {
|
55 |
+
document.removeEventListener('drag', listener)
|
56 |
+
}
|
57 |
+
}, [onEvent])
|
58 |
+
|
59 |
const handleEvent = async (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
|
60 |
|
61 |
if (!ref.current) {
|
|
|
76 |
const relativeY = containerY / boundingRect.height
|
77 |
|
78 |
const eventType = isClick ? "click" : "hover"
|
79 |
+
|
80 |
onEvent(eventType, relativeX, relativeY)
|
81 |
}
|
82 |
|
src/components/renderer/cartesian-video.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
-
import { useRef } from "react"
|
2 |
-
import {
|
3 |
import { RenderedScene } from "@/app/types"
|
4 |
|
5 |
export function CartesianVideo({
|
@@ -9,11 +9,53 @@ export function CartesianVideo({
|
|
9 |
debug,
|
10 |
}: {
|
11 |
rendered: RenderedScene
|
12 |
-
onEvent:
|
13 |
className?: string
|
14 |
debug?: boolean
|
15 |
}) {
|
16 |
const ref = useRef<HTMLVideoElement>(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
|
18 |
|
19 |
if (!ref.current) {
|
|
|
1 |
+
import { useEffect, useRef } from "react"
|
2 |
+
import { MouseEventHandler } from "./types"
|
3 |
import { RenderedScene } from "@/app/types"
|
4 |
|
5 |
export function CartesianVideo({
|
|
|
9 |
debug,
|
10 |
}: {
|
11 |
rendered: RenderedScene
|
12 |
+
onEvent: MouseEventHandler
|
13 |
className?: string
|
14 |
debug?: boolean
|
15 |
}) {
|
16 |
const ref = useRef<HTMLVideoElement>(null)
|
17 |
+
|
18 |
+
|
19 |
+
const cacheRef = useRef("")
|
20 |
+
useEffect(() => {
|
21 |
+
const listener = (e: DragEvent) => {
|
22 |
+
if (!ref.current) { return }
|
23 |
+
|
24 |
+
// TODO: check if we are currently dragging an object
|
25 |
+
// if yes, then we should check if clientX and clientY are matching the
|
26 |
+
const boundingRect = ref.current.getBoundingClientRect()
|
27 |
+
|
28 |
+
// abort if we are not currently dragging over our display area
|
29 |
+
if (e.clientX < boundingRect.left) { return }
|
30 |
+
if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
|
31 |
+
if (e.clientY < boundingRect.top) { return }
|
32 |
+
if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
|
33 |
+
|
34 |
+
const containerX = e.clientX - boundingRect.left
|
35 |
+
const containerY = e.clientY - boundingRect.top
|
36 |
+
|
37 |
+
const relativeX = containerX / boundingRect.width
|
38 |
+
const relativeY = containerY / boundingRect.height
|
39 |
+
|
40 |
+
const key = `${relativeX},${relativeY}`
|
41 |
+
|
42 |
+
// to avoid use
|
43 |
+
if (cacheRef.current === key) {
|
44 |
+
return
|
45 |
+
}
|
46 |
+
// console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
|
47 |
+
|
48 |
+
cacheRef.current = key
|
49 |
+
onEvent("hover", relativeX, relativeY)
|
50 |
+
}
|
51 |
+
|
52 |
+
document.addEventListener('drag', listener)
|
53 |
+
|
54 |
+
return () => {
|
55 |
+
document.removeEventListener('drag', listener)
|
56 |
+
}
|
57 |
+
}, [onEvent])
|
58 |
+
|
59 |
const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
|
60 |
|
61 |
if (!ref.current) {
|
src/components/renderer/index.tsx
CHANGED
@@ -1,14 +1,16 @@
|
|
1 |
import { useEffect, useRef, useState } from "react"
|
2 |
|
3 |
-
import { ImageSegment, RenderedScene, SceneEvent } from "@/app/types"
|
4 |
import { ProgressBar } from "../misc/progress"
|
5 |
import { Game } from "@/app/games/types"
|
6 |
import { Engine } from "@/app/engines"
|
7 |
import { CartesianImage } from "./cartesian-image"
|
8 |
-
import {
|
9 |
import { CartesianVideo } from "./cartesian-video"
|
10 |
import { SphericalImage } from "./spherical-image"
|
11 |
import { useImageDimension } from "@/lib/useImageDimension"
|
|
|
|
|
12 |
|
13 |
export const SceneRenderer = ({
|
14 |
rendered,
|
@@ -16,7 +18,7 @@ export const SceneRenderer = ({
|
|
16 |
isLoading,
|
17 |
game,
|
18 |
engine,
|
19 |
-
debug
|
20 |
}: {
|
21 |
rendered: RenderedScene
|
22 |
onEvent: (event: SceneEvent, actionnable?: string) => void
|
@@ -29,11 +31,26 @@ export const SceneRenderer = ({
|
|
29 |
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
30 |
const contextRef = useRef<CanvasRenderingContext2D | null>(null)
|
31 |
const [actionnable, setActionnable] = useState<string>("")
|
|
|
32 |
const [progressPercent, setProcessPercent] = useState(0)
|
33 |
const progressRef = useRef(0)
|
34 |
const isLoadingRef = useRef(isLoading)
|
35 |
const maskDimension = useImageDimension(rendered.maskUrl)
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
useEffect(() => {
|
38 |
if (!rendered.maskUrl) {
|
39 |
return
|
@@ -104,7 +121,7 @@ export const SceneRenderer = ({
|
|
104 |
}
|
105 |
|
106 |
// note: coordinates must be between 0 and 1
|
107 |
-
const handleMouseEvent:
|
108 |
if (!contextRef.current) return; // Return early if mask image has not been loaded yet
|
109 |
if (!rendered.maskUrl) return;
|
110 |
|
@@ -134,7 +151,7 @@ export const SceneRenderer = ({
|
|
134 |
}
|
135 |
|
136 |
// update the actionnable immediately, so we can show the hand / finger cursor pointer
|
137 |
-
setActionnable(newSegment.label)
|
138 |
}
|
139 |
|
140 |
if (type === "click") {
|
@@ -181,7 +198,7 @@ export const SceneRenderer = ({
|
|
181 |
}, [isLoading, rendered.assetUrl, engine?.type])
|
182 |
|
183 |
return (
|
184 |
-
<div className="w-full pt-2">
|
185 |
<div
|
186 |
className={[
|
187 |
"relative border-2 border-gray-50 rounded-xl overflow-hidden min-h-[512px]",
|
|
|
1 |
import { useEffect, useRef, useState } from "react"
|
2 |
|
3 |
+
import { DropZoneTarget, ImageSegment, RenderedScene, SceneEvent } from "@/app/types"
|
4 |
import { ProgressBar } from "../misc/progress"
|
5 |
import { Game } from "@/app/games/types"
|
6 |
import { Engine } from "@/app/engines"
|
7 |
import { CartesianImage } from "./cartesian-image"
|
8 |
+
import { MouseEventHandler, MouseEventType } from "./types"
|
9 |
import { CartesianVideo } from "./cartesian-video"
|
10 |
import { SphericalImage } from "./spherical-image"
|
11 |
import { useImageDimension } from "@/lib/useImageDimension"
|
12 |
+
import { useDrop } from "react-dnd"
|
13 |
+
import { formatActionnableName } from "@/lib/formatActionnableName"
|
14 |
|
15 |
export const SceneRenderer = ({
|
16 |
rendered,
|
|
|
18 |
isLoading,
|
19 |
game,
|
20 |
engine,
|
21 |
+
debug,
|
22 |
}: {
|
23 |
rendered: RenderedScene
|
24 |
onEvent: (event: SceneEvent, actionnable?: string) => void
|
|
|
31 |
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
32 |
const contextRef = useRef<CanvasRenderingContext2D | null>(null)
|
33 |
const [actionnable, setActionnable] = useState<string>("")
|
34 |
+
const actionnableRef = useRef<string>("")
|
35 |
const [progressPercent, setProcessPercent] = useState(0)
|
36 |
const progressRef = useRef(0)
|
37 |
const isLoadingRef = useRef(isLoading)
|
38 |
const maskDimension = useImageDimension(rendered.maskUrl)
|
39 |
|
40 |
+
const [{ isOver, canDrop }, drop] = useDrop({
|
41 |
+
accept: "item",
|
42 |
+
drop: (): DropZoneTarget => ({
|
43 |
+
type: "Actionnable",
|
44 |
+
name: actionnable,
|
45 |
+
title: formatActionnableName(actionnable),
|
46 |
+
description: ""
|
47 |
+
}),
|
48 |
+
collect: (monitor) => ({
|
49 |
+
isOver: monitor.isOver(),
|
50 |
+
canDrop: monitor.canDrop(),
|
51 |
+
}),
|
52 |
+
})
|
53 |
+
|
54 |
useEffect(() => {
|
55 |
if (!rendered.maskUrl) {
|
56 |
return
|
|
|
121 |
}
|
122 |
|
123 |
// note: coordinates must be between 0 and 1
|
124 |
+
const handleMouseEvent: MouseEventHandler = async (type: MouseEventType, relativeX: number, relativeY: number) => {
|
125 |
if (!contextRef.current) return; // Return early if mask image has not been loaded yet
|
126 |
if (!rendered.maskUrl) return;
|
127 |
|
|
|
151 |
}
|
152 |
|
153 |
// update the actionnable immediately, so we can show the hand / finger cursor pointer
|
154 |
+
setActionnable(actionnableRef.current = newSegment.label)
|
155 |
}
|
156 |
|
157 |
if (type === "click") {
|
|
|
198 |
}, [isLoading, rendered.assetUrl, engine?.type])
|
199 |
|
200 |
return (
|
201 |
+
<div className="w-full pt-2" ref={drop}>
|
202 |
<div
|
203 |
className={[
|
204 |
"relative border-2 border-gray-50 rounded-xl overflow-hidden min-h-[512px]",
|
src/components/renderer/spherical-image.tsx
CHANGED
@@ -4,7 +4,7 @@ import { LensflarePlugin, ReactPhotoSphereViewer } from "react-photo-sphere-view
|
|
4 |
|
5 |
import { RenderedScene } from "@/app/types"
|
6 |
|
7 |
-
import {
|
8 |
import { useImageDimension } from "@/lib/useImageDimension"
|
9 |
import { lightSourceNames } from "@/lib/lightSourceNames"
|
10 |
|
@@ -17,7 +17,7 @@ export function SphericalImage({
|
|
17 |
debug,
|
18 |
}: {
|
19 |
rendered: RenderedScene
|
20 |
-
onEvent:
|
21 |
className?: string
|
22 |
debug?: boolean
|
23 |
}) {
|
@@ -56,6 +56,46 @@ export function SphericalImage({
|
|
56 |
}
|
57 |
|
58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
useEffect(() => {
|
60 |
const task = async () => {
|
61 |
// console.log("SphericalImage: useEffect")
|
|
|
4 |
|
5 |
import { RenderedScene } from "@/app/types"
|
6 |
|
7 |
+
import { MouseEventHandler } from "./types"
|
8 |
import { useImageDimension } from "@/lib/useImageDimension"
|
9 |
import { lightSourceNames } from "@/lib/lightSourceNames"
|
10 |
|
|
|
17 |
debug,
|
18 |
}: {
|
19 |
rendered: RenderedScene
|
20 |
+
onEvent: MouseEventHandler
|
21 |
className?: string
|
22 |
debug?: boolean
|
23 |
}) {
|
|
|
56 |
}
|
57 |
|
58 |
|
59 |
+
const cacheRef = useRef("")
|
60 |
+
useEffect(() => {
|
61 |
+
const listener = (e: DragEvent) => {
|
62 |
+
if (!rootContainerRef.current) { return }
|
63 |
+
|
64 |
+
// TODO: check if we are currently dragging an object
|
65 |
+
// if yes, then we should check if clientX and clientY are matching the
|
66 |
+
const boundingRect = rootContainerRef.current.getBoundingClientRect()
|
67 |
+
|
68 |
+
// abort if we are not currently dragging over our display area
|
69 |
+
if (e.clientX < boundingRect.left) { return }
|
70 |
+
if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
|
71 |
+
if (e.clientY < boundingRect.top) { return }
|
72 |
+
if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
|
73 |
+
|
74 |
+
const containerX = e.clientX - boundingRect.left
|
75 |
+
const containerY = e.clientY - boundingRect.top
|
76 |
+
|
77 |
+
const relativeX = containerX / boundingRect.width
|
78 |
+
const relativeY = containerY / boundingRect.height
|
79 |
+
|
80 |
+
const key = `${relativeX},${relativeY}`
|
81 |
+
|
82 |
+
// to avoid use
|
83 |
+
if (cacheRef.current === key) {
|
84 |
+
return
|
85 |
+
}
|
86 |
+
// console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
|
87 |
+
|
88 |
+
cacheRef.current = key
|
89 |
+
onEvent("hover", relativeX, relativeY)
|
90 |
+
}
|
91 |
+
|
92 |
+
document.addEventListener('drag', listener)
|
93 |
+
|
94 |
+
return () => {
|
95 |
+
document.removeEventListener('drag', listener)
|
96 |
+
}
|
97 |
+
}, [onEvent])
|
98 |
+
|
99 |
useEffect(() => {
|
100 |
const task = async () => {
|
101 |
// console.log("SphericalImage: useEffect")
|
src/components/renderer/types.ts
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
-
export type
|
2 |
|
3 |
-
export type
|
|
|
1 |
+
export type MouseEventType = "hover" | "click"
|
2 |
|
3 |
+
export type MouseEventHandler = (type: MouseEventType, x: number, y: number) => Promise<void>
|
src/lib/defaultActionnables.ts
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const defaultActionnables = [
|
2 |
+
"door",
|
3 |
+
"rock",
|
4 |
+
"window",
|
5 |
+
"table",
|
6 |
+
"ground",
|
7 |
+
"sky",
|
8 |
+
"object",
|
9 |
+
"tree",
|
10 |
+
"wall",
|
11 |
+
"floor"
|
12 |
+
// but we still only want 10 here
|
13 |
+
]
|
src/lib/formatActionnableName.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const formatActionnableName = (input: string) => {
|
2 |
+
input = input.replaceAll("-", " ")
|
3 |
+
return input.charAt(0).toUpperCase() + input.slice(1)
|
4 |
+
}
|
src/lib/normalizeActionnables.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
import { lightSourceNames } from "./lightSourceNames"
|
2 |
|
3 |
export function normalizeActionnables(rawActionnables: string[]) {
|
@@ -10,16 +11,7 @@ export function normalizeActionnables(rawActionnables: string[]) {
|
|
10 |
const deduplicated = new Set<string>([
|
11 |
...tmp,
|
12 |
// in case result is too small, we add a reserve of useful words here
|
13 |
-
|
14 |
-
"rock",
|
15 |
-
"window",
|
16 |
-
"table",
|
17 |
-
"ground",
|
18 |
-
"sky",
|
19 |
-
"object",
|
20 |
-
"tree",
|
21 |
-
"wall",
|
22 |
-
"floor"
|
23 |
// but we still only want 10 here
|
24 |
].slice(0, 10)
|
25 |
)
|
|
|
1 |
+
import { defaultActionnables } from "./defaultActionnables"
|
2 |
import { lightSourceNames } from "./lightSourceNames"
|
3 |
|
4 |
export function normalizeActionnables(rawActionnables: string[]) {
|
|
|
11 |
const deduplicated = new Set<string>([
|
12 |
...tmp,
|
13 |
// in case result is too small, we add a reserve of useful words here
|
14 |
+
...defaultActionnables,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
// but we still only want 10 here
|
16 |
].slice(0, 10)
|
17 |
)
|