diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/AdvisorEmpty.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/AdvisorEmpty.tsx new file mode 100644 index 00000000000..353e86f6c2f --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/AdvisorEmpty.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react' +import { Card, CardContent } from 'ui' +import { AdvisorGame } from './components/AdvisorGame' +import { ResourceCounter } from './components/ResourceCounter' +import { useAdvisorGameStore } from './hooks/useAdvisorGameStore' + +interface AdvisorEmptyProps { + availableResources?: number +} + +export const AdvisorEmpty = ({ availableResources = 20 }: AdvisorEmptyProps) => { + const [isGameActive, setIsGameActive] = useState(false) + const setResources = useAdvisorGameStore((state) => state.setResources) + + useEffect(() => { + setResources(availableResources) + }, [availableResources, setResources]) + + const handleEnterGame = () => { + setIsGameActive(true) + } + + return ( + + + {!isGameActive && } + {isGameActive ? ( + + ) : ( + + You're all caught up + + Advisor found no pending security or performance issues + + + )} + + + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/AdvisorSection.tsx similarity index 94% rename from apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx rename to apps/studio/components/interfaces/HomeNew/Advisor/AdvisorSection.tsx index 8d2e466e4b0..13c1edeb57d 100644 --- a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/Advisor/AdvisorSection.tsx @@ -29,6 +29,7 @@ import { } from 'ui' import { Row } from 'ui-patterns' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { AdvisorEmpty } from './AdvisorEmpty' export const AdvisorSection = () => { const { ref: projectRef } = useParams() @@ -199,14 +200,7 @@ export const AdvisorSection = () => { > ) : ( - - - - - No security or performance errors found - - - + )} ) diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/components/AdvisorGame.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/components/AdvisorGame.tsx new file mode 100644 index 00000000000..5a958b0e4b4 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/components/AdvisorGame.tsx @@ -0,0 +1,531 @@ +// Reference: advisor-game-example/src/App.js +import { useEffect, useMemo, useRef } from 'react' +import { Physics } from '@react-three/cannon' +import { Sky } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' +import { nanoid } from 'nanoid' +import { createClient, type RealtimeChannel, type SupabaseClient } from '@supabase/supabase-js' +import { Ground } from './Ground' +import { Cubes } from './Cubes' +import { Camera } from './Camera' +import { ResourceCounter } from './ResourceCounter' +import { useAdvisorGameStore } from '../hooks/useAdvisorGameStore' +import { useParams } from 'common' + +interface AdvisorGameProps { + availableResources: number +} + +export const AdvisorGame = ({ availableResources }: AdvisorGameProps) => { + const setResources = useAdvisorGameStore((state) => state.setResources) + const setCubes = useAdvisorGameStore((state) => state.setCubes) + const setAllCubes = useAdvisorGameStore((state) => state.setAllCubes) + const getCubes = useAdvisorGameStore((state) => state.getCubes) + const setCurrentUserId = useAdvisorGameStore((state) => state.setCurrentUserId) + const setCurrentProjectRef = useAdvisorGameStore((state) => state.setCurrentProjectRef) + const setActionHandlers = useAdvisorGameStore((state) => state.setActionHandlers) + const { ref: projectRef } = useParams() + const supabaseRef = useRef(null) + const channelRef = useRef(null) + const gameInitialisedRef = useRef(false) + const playerInitialisedRef = useRef(false) + + const supabaseConfig = useMemo(() => { + const url = process.env.NEXT_PUBLIC_ADVISOR_GAME_URL + const key = process.env.NEXT_PUBLIC_ADVISOR_GAME_KEY + if (!url || !key) { + console.warn( + 'Supabase environment variables are not set. Advisor game multiplayer features are disabled.' + ) + return null + } + return { url, key } + }, []) + + useEffect(() => { + setResources(availableResources) + }, [availableResources, setResources]) + + useEffect(() => { + if (!projectRef || !supabaseConfig) { + return + } + + const supabase = createClient(supabaseConfig.url, supabaseConfig.key, { + auth: { + persistSession: true, + autoRefreshToken: true, + detectSessionInUrl: false, + }, + }) + supabaseRef.current = supabase + let isMounted = true + + type AdvisorBlockPayload = { + cube_key?: string + id?: string + position?: [number, number, number] + pos_x?: number + pos_y?: number + pos_z?: number + texture?: string | null + created_by?: string | null + } + + const mapBlocksToStore = (blocks: AdvisorBlockPayload[], projectRef?: string) => + blocks.map((block) => { + const cubeKey = + block.cube_key ?? + block.id ?? + (Array.isArray(block.position) ? block.position.join('-') : nanoid()) + const positionArray = Array.isArray(block.position) + ? block.position + : [block.pos_x, block.pos_y, block.pos_z] + const [x, y, z] = positionArray ?? [0, 0, 0] + return { + cubeKey, + pos: [Number(x), Number(y), Number(z)] as [number, number, number], + texture: block.texture ?? 'wood', + createdBy: block.created_by ?? null, + projectRef: projectRef, + } + }) + + const ensureGameInitialized = async () => { + if (gameInitialisedRef.current) { + return true + } + + const { data: existingGame, error: fetchError } = await supabase + .from('advisor_games') + .select('resources') + .eq('project_ref', projectRef) + .maybeSingle() + + if (fetchError) { + console.error('Failed to fetch advisor game', fetchError) + return false + } + + if (existingGame) { + if (isMounted && existingGame.resources !== undefined && existingGame.resources !== null) { + setResources(existingGame.resources) + } + gameInitialisedRef.current = true + return true + } + + const defaultResources = + typeof availableResources === 'number' && Number.isFinite(availableResources) + ? availableResources + : 10 + + const { error: insertError } = await supabase + .from('advisor_games') + .insert({ project_ref: projectRef, resources: defaultResources }) + + if (insertError) { + if (insertError.code === '23505') { + const { data: gameAfterConflict, error: refetchError } = await supabase + .from('advisor_games') + .select('resources') + .eq('project_ref', projectRef) + .maybeSingle() + + if (refetchError) { + console.error('Failed to fetch advisor game after conflict', refetchError) + return false + } + + if ( + isMounted && + gameAfterConflict?.resources !== undefined && + gameAfterConflict.resources !== null + ) { + setResources(gameAfterConflict.resources) + } + gameInitialisedRef.current = true + return true + } + + console.error('Failed to create advisor game', insertError) + return false + } + + if (isMounted) { + setResources(defaultResources) + } + gameInitialisedRef.current = true + return true + } + + const ensurePlayerInitialized = async (userId: string | null) => { + if (!userId) { + playerInitialisedRef.current = false + return true + } + + if (playerInitialisedRef.current) { + return true + } + + const { error: upsertError } = await supabase + .from('advisor_players') + .upsert( + { project_ref: projectRef, user_id: userId }, + { onConflict: 'project_ref,user_id', ignoreDuplicates: false } + ) + + if (upsertError) { + console.error('Failed to upsert advisor player', upsertError) + return false + } + + playerInitialisedRef.current = true + return true + } + + const setupActionHandlers = (userId: string | null) => { + const adjustResourcesOnServer = async (delta: number) => { + if (delta === 0) return + const adjustLocal = useAdvisorGameStore.getState().adjustResources + adjustLocal(delta) + + const desiredResources = useAdvisorGameStore.getState().getRemainingResources() + const { data: updatedGame, error: resourcesError } = await supabase + .from('advisor_games') + .update({ resources: desiredResources }) + .eq('project_ref', projectRef) + .select('resources') + .maybeSingle() + + if (resourcesError) { + console.error('Failed to update advisor resources', resourcesError) + adjustLocal(-delta) + return + } + + if (!updatedGame || updatedGame.resources === undefined || updatedGame.resources === null) { + console.warn('Advisor game resources update returned no data') + adjustLocal(-delta) + return + } + + setResources(updatedGame.resources) + } + + setActionHandlers({ + placeCube: async (position) => { + const [x, y, z] = position + const state = useAdvisorGameStore.getState() + + if (state.getRemainingResources() <= 0) { + return + } + + const existingCubes = state.getCubes(projectRef) + const alreadyExists = existingCubes.some( + (cube) => cube.pos[0] === x && cube.pos[1] === y && cube.pos[2] === z + ) + if (alreadyExists) { + return + } + + const supa = supabaseRef.current + if (!supa) return + + const gameReady = await ensureGameInitialized() + if (!gameReady) { + return + } + + const playerReady = await ensurePlayerInitialized(userId ?? null) + if (userId && !playerReady) { + return + } + + const payload: { + project_ref: string + cube_key: string + pos_x: number + pos_y: number + pos_z: number + texture: string + created_by?: string + } = { + project_ref: projectRef, + cube_key: nanoid(), + pos_x: x, + pos_y: y, + pos_z: z, + texture: 'wood', + } + + if (userId) { + payload.created_by = userId + } + + const { error } = await supa.from('advisor_blocks').insert(payload) + + if (error) { + console.error('Failed to place cube', error) + return + } + + const latestState = useAdvisorGameStore.getState() + const newCube = { + cubeKey: payload.cube_key, + pos: [payload.pos_x, payload.pos_y, payload.pos_z] as [number, number, number], + texture: payload.texture, + createdBy: payload.created_by ?? null, + projectRef: projectRef, + } + const latestCubes = latestState.getCubes(projectRef) + const updatedCubes = [...latestCubes, newCube] + setCubes(updatedCubes, projectRef) + + // Update all cubes to include the new cube + const allCubes = useAdvisorGameStore.getState().allCubes + const updatedAllCubes = [...allCubes, newCube] + setAllCubes(updatedAllCubes) + + await adjustResourcesOnServer(-1) + }, + removeCube: async (position) => { + const [x, y, z] = position + const state = useAdvisorGameStore.getState() + + const targetCubes = state.getCubes(projectRef) + const target = targetCubes.find( + (cube) => cube.pos[0] === x && cube.pos[1] === y && cube.pos[2] === z + ) + + if (!target) { + return + } + + if (target.createdBy && userId && target.createdBy !== userId) { + return + } + + const supa = supabaseRef.current + if (!supa) return + + const { error } = await supa + .from('advisor_blocks') + .delete() + .eq('project_ref', projectRef) + .eq('cube_key', target.cubeKey) + + if (error) { + console.error('Failed to remove cube', error) + return + } + + const latestState = useAdvisorGameStore.getState() + const remainingCubes = latestState.getCubes(projectRef) + const updatedCubes = remainingCubes.filter((cube) => cube.cubeKey !== target.cubeKey) + setCubes(updatedCubes, projectRef) + + // Update all cubes to remove the deleted cube + const allCubes = useAdvisorGameStore.getState().allCubes + const updatedAllCubes = allCubes.filter((cube) => cube.cubeKey !== target.cubeKey) + setAllCubes(updatedAllCubes) + + await adjustResourcesOnServer(1) + }, + }) + } + + const initialise = async () => { + const { + data: { session }, + error: sessionError, + } = await supabase.auth.getSession() + + if (sessionError) { + console.error('Failed to get Supabase session', sessionError) + } + + let userId = session?.user?.id ?? null + + if (!userId) { + const { data, error } = await supabase.auth.signInAnonymously() + if (error) { + console.error('Failed to sign in anonymously', error) + return + } + userId = data.user?.id ?? null + } + + if (!isMounted) return + setCurrentUserId(userId) + setCurrentProjectRef(projectRef) + playerInitialisedRef.current = false + + const gameReady = await ensureGameInitialized() + + if (!isMounted || !gameReady) return + + const playerReady = await ensurePlayerInitialized(userId) + + if (!isMounted) return + + if (userId && !playerReady) { + console.warn('Advisor player initialisation failed - multiplayer actions may be limited') + } + + setupActionHandlers(userId) + + const { data: blockRows, error: blocksError } = await supabase + .from('advisor_blocks') + .select('cube_key, pos_x, pos_y, pos_z, texture, created_by, project_ref') + + if (!isMounted) return + + if (blocksError) { + console.error('Failed to fetch advisor blocks', blocksError) + } else if (blockRows) { + // Load all blocks into allCubes + const allBlocks = blockRows.map((block) => { + const cubeKey = block.cube_key ?? nanoid() + const [x, y, z] = [block.pos_x ?? 0, block.pos_y ?? 0, block.pos_z ?? 0] + return { + cubeKey, + pos: [Number(x), Number(y), Number(z)] as [number, number, number], + texture: block.texture ?? 'wood', + createdBy: block.created_by ?? null, + projectRef: block.project_ref, + } + }) + setAllCubes(allBlocks) + + // Group blocks by project for project-specific operations + const blocksByProject = allBlocks.reduce( + (acc, block) => { + const projectRef = block.projectRef || 'unknown' + if (!acc[projectRef]) { + acc[projectRef] = [] + } + acc[projectRef].push(block) + return acc + }, + {} as Record + ) + + // Set cubes for each project + Object.entries(blocksByProject).forEach(([projectRef, blocks]) => { + setCubes(blocks, projectRef) + }) + } + + const channel = supabase + .channel(`advisor_game`) + .on('broadcast', { event: 'state_update' }, (event) => { + console.log('state_update', event.payload) + const payload = event.payload as { + project_ref?: string + resources?: number + remaining_resources?: number + blocks?: any[] + } + + // Only update resources for the current project + if (payload.project_ref === projectRef) { + const nextResources = payload.resources ?? payload.remaining_resources ?? null + + if (nextResources !== null && nextResources !== undefined) { + setResources(nextResources) + } + } + + // Always update blocks for all projects + if (Array.isArray(payload.blocks)) { + const updatedBlocks = payload.blocks.map((block) => { + const cubeKey = block.cube_key ?? block.id ?? nanoid() + const positionArray = Array.isArray(block.position) + ? block.position + : [block.pos_x, block.pos_y, block.pos_z] + const [x, y, z] = positionArray ?? [0, 0, 0] + return { + cubeKey, + pos: [Number(x), Number(y), Number(z)] as [number, number, number], + texture: block.texture ?? 'wood', + createdBy: block.created_by ?? null, + projectRef: payload.project_ref, + } + }) + + // Update only the blocks for the specific project that sent the update + const currentAllCubes = useAdvisorGameStore.getState().allCubes + const otherProjectBlocks = currentAllCubes.filter( + (cube) => cube.projectRef !== payload.project_ref + ) + const updatedAllCubes = [...otherProjectBlocks, ...updatedBlocks] + setAllCubes(updatedAllCubes) + + // Update project-specific cubes for the updated project + setCubes(updatedBlocks, payload.project_ref || 'unknown') + } + }) + .subscribe() + + channelRef.current = channel + } + + void initialise() + + return () => { + isMounted = false + setActionHandlers(null) + setCurrentUserId(null) + setCurrentProjectRef(null) + const channel = channelRef.current + if (channel && supabaseRef.current) { + supabaseRef.current.removeChannel(channel) + } + channelRef.current = null + supabaseRef.current = null + gameInitialisedRef.current = false + playerInitialisedRef.current = false + } + }, [ + availableResources, + projectRef, + setActionHandlers, + setCurrentUserId, + setCurrentProjectRef, + setCubes, + setAllCubes, + setResources, + supabaseConfig, + ]) + + return ( + + + + + + + + + + {projectRef && } + + + + + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/components/Camera.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/components/Camera.tsx new file mode 100644 index 00000000000..3253b593bcb --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/components/Camera.tsx @@ -0,0 +1,24 @@ +// Simplified camera with orbital controls instead of FPV +// Reference: advisor-game-example uses PointerLockControls, we use OrbitControls for easier interaction +import { OrbitControls } from '@react-three/drei' +import { useAdvisorGameStore } from '../hooks/useAdvisorGameStore' + +export const Camera = () => { + const markCameraInteraction = useAdvisorGameStore((state) => state.markCameraInteraction) + + return ( + + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/components/Cube.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/components/Cube.tsx new file mode 100644 index 00000000000..cf3ddc56333 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/components/Cube.tsx @@ -0,0 +1,128 @@ +// Reference: advisor-game-example/src/components/Cube.js +import { useBox } from '@react-three/cannon' +import { useState } from 'react' +import { useAdvisorGameStore } from '../hooks/useAdvisorGameStore' +import * as THREE from 'three' +import type { Mesh } from 'three' + +interface CubeProps { + cubeKey: string + position: [number, number, number] + texture: string + createdBy?: string | null + projectRef?: string + isCurrentProject?: boolean +} + +export const Cube = ({ position, createdBy, projectRef, isCurrentProject }: CubeProps) => { + const [hoveredFace, setHoveredFace] = useState(null) + const [ref] = useBox(() => ({ + type: 'Static', + position, + })) + const placeCube = useAdvisorGameStore((state) => state.placeCube) + const removeCube = useAdvisorGameStore((state) => state.removeCube) + const getRemainingResources = useAdvisorGameStore((state) => state.getRemainingResources) + const currentUserId = useAdvisorGameStore((state) => state.currentUserId) + const shouldBlockPlacement = useAdvisorGameStore((state) => state.shouldBlockPlacement) + + // Use gray color for blocks from other projects, green for current project + const baseColor = isCurrentProject ? 'rgb(62, 207, 142)' : 'rgb(128, 128, 128)' + const edgeColor = isCurrentProject ? 'rgb(62, 207, 142)' : 'rgb(128, 128, 128)' + + return ( + + { + e.stopPropagation() + const faceIndex = + e.faceIndex !== null && e.faceIndex !== undefined ? Math.floor(e.faceIndex / 2) : null + setHoveredFace(faceIndex) + }} + onPointerOut={(e) => { + e.stopPropagation() + setHoveredFace(null) + }} + onClick={(e) => { + const blockPlacement = shouldBlockPlacement() + if (blockPlacement && !e.altKey) { + return + } + e.stopPropagation() + const clickedFace = + e.faceIndex !== null && e.faceIndex !== undefined ? Math.floor(e.faceIndex / 2) : 0 + const { x, y, z } = (ref.current as Mesh).position + + // Alt+Click to remove + if (e.altKey) { + if (!removeCube) { + return + } + // Only allow deletion of cubes from the current project + if (!isCurrentProject) { + return + } + if (createdBy && currentUserId && createdBy !== currentUserId) { + return + } + void removeCube([x, y, z]) + return + } + + if (blockPlacement) { + return + } + + // Check if we have resources before adding + if (getRemainingResources() <= 0) { + return + } + + // Add cube adjacent to clicked face + if (!placeCube) { + return + } + if (clickedFace === 0) { + void placeCube([x + 1, y, z]) + } else if (clickedFace === 1) { + void placeCube([x - 1, y, z]) + } else if (clickedFace === 2) { + void placeCube([x, y + 1, z]) + } else if (clickedFace === 3) { + void placeCube([x, y - 1, z]) + } else if (clickedFace === 4) { + void placeCube([x, y, z + 1]) + } else if (clickedFace === 5) { + void placeCube([x, y, z - 1]) + } + }} + ref={ref as any} + castShadow + receiveShadow + > + + {/* Create 6 materials, one for each face */} + {[0, 1, 2, 3, 4, 5].map((faceIndex) => ( + + ))} + + {/* Tron-style edge glow */} + + + + + + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/components/Cubes.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/components/Cubes.tsx new file mode 100644 index 00000000000..d9b4668fff5 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/components/Cubes.tsx @@ -0,0 +1,31 @@ +// Reference: advisor-game-example/src/components/Cubes.js +import { useMemo } from 'react' +import { useAdvisorGameStore } from '../hooks/useAdvisorGameStore' +import { Cube } from './Cube' + +interface CubesProps { + projectRef: string +} + +export const Cubes = ({ projectRef }: CubesProps) => { + const allCubes = useAdvisorGameStore((state) => state.allCubes) + const currentProjectRef = useAdvisorGameStore((state) => state.currentProjectRef) + + const cubes = useMemo(() => allCubes, [allCubes]) + + return ( + <> + {cubes.map(({ cubeKey, pos, texture, createdBy, projectRef: cubeProjectRef }) => ( + + ))} + > + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/components/Ground.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/components/Ground.tsx new file mode 100644 index 00000000000..8fdaf80fab4 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/components/Ground.tsx @@ -0,0 +1,73 @@ +// Reference: advisor-game-example/src/components/Ground.js +import { usePlane } from '@react-three/cannon' +import { useAdvisorGameStore } from '../hooks/useAdvisorGameStore' +import { Grid } from '@react-three/drei' +import { useState } from 'react' + +export const Ground = () => { + const [ref] = usePlane(() => ({ + rotation: [-Math.PI / 2, 0, 0], + position: [0, -0.5, 0], + })) + const [hoverPos, setHoverPos] = useState<[number, number] | null>(null) + const placeCube = useAdvisorGameStore((state) => state.placeCube) + const getRemainingResources = useAdvisorGameStore((state) => state.getRemainingResources) + const shouldBlockPlacement = useAdvisorGameStore((state) => state.shouldBlockPlacement) + + return ( + <> + { + e.stopPropagation() + // Calculate grid cell position for hover preview + const [x, y, z] = Object.values(e.point).map((val) => Math.floor(val) + 0.5) + setHoverPos([x, z]) + }} + onPointerOut={(e) => { + e.stopPropagation() + setHoverPos(null) + }} + onClick={(e) => { + if (shouldBlockPlacement()) { + return + } + e.stopPropagation() + if (getRemainingResources() <= 0) { + return // No resources left + } + if (!placeCube) { + return + } + // Snap to grid cell centers (offset by 0.5 from grid lines) + const [x, y, z] = Object.values(e.point).map((val) => Math.floor(val) + 0.5) + void placeCube([x, 0, z]) + }} + ref={ref as any} + receiveShadow + > + + + + {/* Hover highlight for placement preview */} + {hoverPos && getRemainingResources() > 0 && ( + + + + + )} + + > + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/components/ResourceCounter.tsx b/apps/studio/components/interfaces/HomeNew/Advisor/components/ResourceCounter.tsx new file mode 100644 index 00000000000..fc6cb5ca748 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/components/ResourceCounter.tsx @@ -0,0 +1,25 @@ +import { KeyboardEvent } from 'react' +import { Cuboid } from 'lucide-react' +import { useAdvisorGameStore } from '../hooks/useAdvisorGameStore' +import { Button } from 'ui' + +interface ResourceCounterProps { + onClick?: () => void + className?: string +} + +export const ResourceCounter = ({ onClick, className }: ResourceCounterProps) => { + const remaining = useAdvisorGameStore((state) => state.getRemainingResources()) + + return ( + } + > + {!onClick && {remaining}} + + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/hooks/useAdvisorGameStore.ts b/apps/studio/components/interfaces/HomeNew/Advisor/hooks/useAdvisorGameStore.ts new file mode 100644 index 00000000000..7274b12ff7a --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/hooks/useAdvisorGameStore.ts @@ -0,0 +1,113 @@ +// Reference: advisor-game-example/src/hooks/useStore.js +import { create } from 'zustand' +interface Cube { + cubeKey: string + pos: [number, number, number] + texture: string + createdBy?: string | null + projectRef?: string +} + +interface ActionHandlers { + placeCube?: (position: [number, number, number]) => Promise + removeCube?: (position: [number, number, number]) => Promise +} + +interface GameStore { + resources: number + cubesByProject: Record + allCubes: Cube[] + setResources: (value: number) => void + adjustResources: (delta: number) => void + setCubes: (cubes: Cube[], projectRef: string) => void + setAllCubes: (cubes: Cube[]) => void + getCubes: (projectRef: string) => Cube[] + setCurrentUserId: (userId: string | null) => void + setActionHandlers: (handlers: ActionHandlers | null) => void + getRemainingResources: () => number + resetWorld: (projectRef?: string) => void + placeCube?: (position: [number, number, number]) => Promise + removeCube?: (position: [number, number, number]) => Promise + currentUserId?: string | null + currentProjectRef?: string | null + setCurrentProjectRef: (projectRef: string | null) => void + lastCameraInteractionTime: number | null + markCameraInteraction: () => void + shouldBlockPlacement: () => boolean +} + +export const useAdvisorGameStore = create((set, get) => ({ + resources: 10, + cubesByProject: {}, + allCubes: [], + setResources: (value) => { + set(() => ({ + resources: value, + })) + }, + adjustResources: (delta) => { + set((state) => { + const next = Math.max(state.resources + delta, 0) + return { resources: next } + }) + }, + setCubes: (cubes, projectRef) => { + set((state) => ({ + cubesByProject: { + ...state.cubesByProject, + [projectRef]: cubes, + }, + })) + }, + setAllCubes: (cubes) => { + set(() => ({ allCubes: cubes })) + }, + getCubes: (projectRef) => { + const state = get() + return state.cubesByProject[projectRef] || [] + }, + setCurrentUserId: (userId) => { + set(() => ({ currentUserId: userId })) + }, + setCurrentProjectRef: (projectRef) => { + set(() => ({ currentProjectRef: projectRef })) + }, + setActionHandlers: (handlers) => { + set(() => ({ + placeCube: handlers?.placeCube, + removeCube: handlers?.removeCube, + })) + }, + getRemainingResources: () => { + const state = get() + return state.resources + }, + resetWorld: (projectRef) => { + if (projectRef) { + set((state) => ({ + cubesByProject: { + ...state.cubesByProject, + [projectRef]: [], + }, + })) + } else { + set(() => ({ + cubesByProject: {}, + allCubes: [], + })) + } + }, + lastCameraInteractionTime: null, + markCameraInteraction: () => { + set(() => ({ + lastCameraInteractionTime: Date.now(), + })) + }, + shouldBlockPlacement: () => { + const { lastCameraInteractionTime } = get() + if (lastCameraInteractionTime === null) { + return false + } + return Date.now() - lastCameraInteractionTime < 200 + }, +})) diff --git a/apps/studio/components/interfaces/HomeNew/Advisor/textures.ts b/apps/studio/components/interfaces/HomeNew/Advisor/textures.ts new file mode 100644 index 00000000000..e8469a3e703 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/Advisor/textures.ts @@ -0,0 +1,53 @@ +// Reference: advisor-game-example/src/images/textures.js +import { NearestFilter, TextureLoader, RepeatWrapping, Texture } from 'three' + +import woodImg from './assets/wood.png' +import grassImg from './assets/grass.jpg' +import dirtImg from './assets/dirt.jpg' + +// Export image URLs for dynamic loading +export const textureUrls = { + wood: woodImg, + grass: grassImg, + dirt: dirtImg, +} + +// Lazy load textures to avoid SSR issues +let woodTexture: Texture | null = null +let grassTexture: Texture | null = null +let dirtTexture: Texture | null = null +let groundTexture: Texture | null = null + +export const getWoodTexture = () => { + if (!woodTexture) { + woodTexture = new TextureLoader().load(woodImg) + woodTexture.magFilter = NearestFilter + } + return woodTexture +} + +export const getGrassTexture = () => { + if (!grassTexture) { + grassTexture = new TextureLoader().load(grassImg) + grassTexture.magFilter = NearestFilter + } + return grassTexture +} + +export const getDirtTexture = () => { + if (!dirtTexture) { + dirtTexture = new TextureLoader().load(dirtImg) + dirtTexture.magFilter = NearestFilter + } + return dirtTexture +} + +export const getGroundTexture = () => { + if (!groundTexture) { + groundTexture = new TextureLoader().load(grassImg) + groundTexture.magFilter = NearestFilter + groundTexture.wrapS = RepeatWrapping + groundTexture.wrapT = RepeatWrapping + } + return groundTexture +} diff --git a/apps/studio/components/interfaces/HomeNew/Home.tsx b/apps/studio/components/interfaces/HomeNew/Home.tsx index 9e5632507ed..dc4e6e8de83 100644 --- a/apps/studio/components/interfaces/HomeNew/Home.tsx +++ b/apps/studio/components/interfaces/HomeNew/Home.tsx @@ -13,7 +13,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { cn } from 'ui' -import { AdvisorSection } from './AdvisorSection' +import { AdvisorSection } from './Advisor/AdvisorSection' import { CustomReportSection } from './CustomReportSection' import { type GettingStartedState } from './GettingStarted/GettingStarted.types' import { GettingStartedSection } from './GettingStarted/GettingStartedSection' diff --git a/apps/studio/package.json b/apps/studio/package.json index 5a0b527c809..7c7ccbc3abc 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -52,6 +52,9 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-visually-hidden": "^1.1.3", + "@react-three/cannon": "^6.6.0", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.18.0", "@sentry/nextjs": "^10.3.0", "@std/path": "npm:@jsr/std__path@^1.0.8", "@stripe/react-stripe-js": "^3.7.0", @@ -98,6 +101,7 @@ "memoize-one": "^5.0.1", "mime-db": "^1.53.0", "monaco-editor": "0.52.2", + "nanoid": "^5.1.6", "next": "catalog:", "next-themes": "^0.3.0", "nuqs": "2.7.1", @@ -136,6 +140,7 @@ "sql-formatter": "^15.0.0", "sqlstring": "^2.3.2", "streamdown": "^1.3.0", + "three": "^0.180.0", "tus-js-client": "^4.1.0", "ui": "workspace:*", "ui-patterns": "workspace:*", @@ -145,6 +150,7 @@ "valtio": "catalog:", "yup": "^1.4.0", "zod": "catalog:", + "zustand": "^5.0.8", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f8426a1e62..49d790e4516 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -385,7 +385,7 @@ importers: version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^10.3.0 - version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' version: 2.74.0 @@ -798,9 +798,18 @@ importers: '@radix-ui/react-visually-hidden': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-three/cannon': + specifier: ^6.6.0 + version: 6.6.0(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(react@18.3.1)(three@0.180.0)(typescript@5.9.2) + '@react-three/drei': + specifier: ^9.122.0 + version: 9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(@types/react@18.3.3)(@types/three@0.169.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0)(use-sync-external-store@1.5.0(react@18.3.1)) + '@react-three/fiber': + specifier: ^8.18.0 + version: 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) '@sentry/nextjs': specifier: ^10.3.0 - version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@std/path': specifier: npm:@jsr/std__path@^1.0.8 version: '@jsr/std__path@1.0.8' @@ -936,6 +945,9 @@ importers: monaco-editor: specifier: 0.52.2 version: 0.52.2 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 next: specifier: 'catalog:' version: 15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) @@ -1050,6 +1062,9 @@ importers: streamdown: specifier: ^1.3.0 version: 1.3.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) + three: + specifier: ^0.180.0 + version: 0.180.0 tus-js-client: specifier: ^4.1.0 version: 4.1.0 @@ -1077,6 +1092,9 @@ importers: zod: specifier: 'catalog:' version: 3.25.76 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) zxcvbn: specifier: ^4.4.2 version: 4.4.2 @@ -1215,7 +1233,7 @@ importers: version: 2.4.11(typescript@5.9.2) next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) node-mocks-http: specifier: ^1.17.2 version: 1.17.2(@types/node@22.13.14) @@ -1579,7 +1597,7 @@ importers: version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^10 - version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' version: 2.74.0 @@ -2550,7 +2568,7 @@ importers: version: link:../api-types next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) tsx: specifier: ^4.19.3 version: 4.19.3 @@ -4643,6 +4661,9 @@ packages: peerDependencies: react: '>=16' + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@mermaid-js/parser@0.6.2': resolution: {integrity: sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==} @@ -4688,6 +4709,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@monogrid/gainmap-js@3.1.0': + resolution: {integrity: sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==} + peerDependencies: + three: '>= 0.159.0' + '@motionone/animation@10.16.3': resolution: {integrity: sha512-QUGWpLbMFLhyqKlngjZhjtxM8IqiJQjLK0DF+XOF6od9nhSvlaeEpOY/UMCRVcZn/9Tr2rZO22EkuCIjYdI74g==} @@ -5888,6 +5914,11 @@ packages: engines: {node: '>=18'} hasBin: true + '@pmndrs/cannon-worker-api@2.4.0': + resolution: {integrity: sha512-oJA1Bboc+WObksRaGDKJG0Wna9Q75xi1MdXVAZ9qXzBOyPsadmAnrmiKOEF0R8v/4zsuJElvscNZmyo3msbZjA==} + peerDependencies: + three: '>=0.139' + '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} @@ -7785,6 +7816,34 @@ packages: typescript: optional: true + '@react-spring/animated@9.7.5': + resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.7.5': + resolution: {integrity: sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.7.5': + resolution: {integrity: sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==} + + '@react-spring/shared@9.7.5': + resolution: {integrity: sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/three@9.7.5': + resolution: {integrity: sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==} + peerDependencies: + '@react-three/fiber': '>=6.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + three: '>=0.126' + + '@react-spring/types@9.7.5': + resolution: {integrity: sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==} + '@react-stately/flags@3.1.1': resolution: {integrity: sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==} @@ -7793,6 +7852,49 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@react-three/cannon@6.6.0': + resolution: {integrity: sha512-lP9rJoVHQi0w+dYF8FJAm2xr5eLfNEckb04j72kjqndUkuOPr26N4rSBhQbHl5b5N3tEnhQaIMungAvHkcY8/A==} + peerDependencies: + '@react-three/fiber': '>=8' + react: '>=18' + three: '>=0.139' + + '@react-three/drei@9.122.0': + resolution: {integrity: sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==} + peerDependencies: + '@react-three/fiber': ^8 + react: ^18 + react-dom: ^18 + three: '>=0.137' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@8.18.0': + resolution: {integrity: sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=18 <19' + react-dom: '>=18 <19' + react-native: '>=0.64' + three: '>=0.133' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@react-types/shared@3.29.0': resolution: {integrity: sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==} peerDependencies: @@ -9396,6 +9498,9 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree-jsx@1.0.1': resolution: {integrity: sha512-sHyakZlAezNFxmYRo0fopDZW+XvK6ipeZkkp5EAOLjdPfZp8VjZBJ67vSRI99RSCAoqXVmXOHS4fnWoxpuGQtQ==} @@ -9512,6 +9617,9 @@ packages: '@types/node@22.13.14': resolution: {integrity: sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/papaparse@5.3.9': resolution: {integrity: sha512-sZcrKD63qA4/6GyBcVvX6AIp0AkpfyYk00CUQHMBvb4+OVXTZWyXUvidUZaai1wyKUVyJoxO7mgREam/pMRrDw==} @@ -9564,6 +9672,14 @@ packages: '@types/react-grid-layout@1.3.3': resolution: {integrity: sha512-VLgUoEN0PaAL3GeRsmYbHuoN0tpDNoRhlkdzHK4Ll8shHW6e16YpNCZWb7P9vrlZdzFtY75cM/hPr8p0ukZQCw==} + '@types/react-reconciler@0.26.7': + resolution: {integrity: sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==} + + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react-redux@7.1.27': resolution: {integrity: sha512-xj7d9z32p1K/eBmO+OEy+qfaWXtcPlN8f1Xk3Ne0p/ZRQ867RI5bQ/bpBtxbqU1AHNhKJSgGvld/P2myU2uYkg==} @@ -9762,6 +9878,14 @@ packages: peerDependencies: vue: '>=3.5.18' + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@usercentrics/cmp-browser-sdk@4.42.0': resolution: {integrity: sha512-/cik01TdeiYUV1+EasK83Ip05TDqmowM5tmWGfRDdaOrneSic5BedBdJHKya4pP2vwiYAtzDYVJe4BwPt2M16g==} @@ -10459,6 +10583,9 @@ packages: bezier-easing@2.1.0: resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -10616,12 +10743,30 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + camera-controls@2.10.1: + resolution: {integrity: sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==} + peerDependencies: + three: '>=0.126.1' + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} caniuse-lite@1.0.30001743: resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} + cannon-es-debugger@1.0.0: + resolution: {integrity: sha512-sE9lDOBAYFKlh+0w+cvWKwUhJef8HYnUSVPWPL0jD15MAuVRQKno4QYZSGxgOoJkMR3mQqxL4bxys2b3RSWH8g==} + peerDependencies: + cannon-es: 0.x + three: 0.x + typescript: '>=3.8' + peerDependenciesMeta: + typescript: + optional: true + + cannon-es@0.20.0: + resolution: {integrity: sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -11706,6 +11851,9 @@ packages: resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} engines: {node: '>=0.10.0'} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -11843,6 +11991,9 @@ packages: dotty@0.1.2: resolution: {integrity: sha512-V0EWmKeH3DEhMwAZ+8ZB2Ao4OK6p++Z0hsDtZq3N0+0ZMVqkzrcEGROvOnZpLnvBg5PTNG23JEDLAm64gPaotQ==} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + drange@1.1.1: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} @@ -12656,6 +12807,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + fflate@0.7.4: resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} @@ -13077,6 +13231,9 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -13367,6 +13524,9 @@ packages: highlightjs-curl@1.3.0: resolution: {integrity: sha512-50UEfZq1KR0Lfk2Tr6xb/MUIZH3h10oNC0OTy9g7WELcs5Fgy/mKN1vEhuKTkKbdo8vr5F9GXstu2eLhApfQ3A==} + hls.js@1.6.13: + resolution: {integrity: sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -13508,6 +13668,9 @@ packages: resolution: {integrity: sha512-lCYzQrWzdnA68K03oMj/BUlBJrVBnslzDOgGFymAp49NmdGEJxGeN7sHh5mCva0nQkq+kkKSuru2zLf1m04+3A==} engines: {node: '>=14.0.0'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} @@ -13839,6 +14002,9 @@ packages: resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} engines: {node: '>=0.10.0'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -14010,6 +14176,11 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + its-fine@1.2.5: + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} + peerDependencies: + react: '>=18.0' + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -14307,6 +14478,9 @@ packages: libpg-query@15.2.0: resolution: {integrity: sha512-fBi14tsU3/ahtfU0C+/tvmtlt0EJM6kXe0Dl4/cSbiNmJ3IwLIuk3lawwiK5ByXU4sXwyAzbkvSl26jVXzYuaA==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} @@ -14520,6 +14694,12 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + magic-regexp@0.10.0: resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} @@ -14804,6 +14984,11 @@ packages: '@types/node': optional: true + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + meshoptimizer@0.18.1: resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} @@ -15307,16 +15492,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@5.1.5: resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} engines: {node: ^18 || >=20} hasBin: true + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + nanotar@0.2.0: resolution: {integrity: sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==} @@ -16550,6 +16735,9 @@ packages: rrweb-snapshot: optional: true + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + preact@10.26.9: resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} @@ -16674,6 +16862,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} @@ -16814,6 +17005,11 @@ packages: peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental + react-composer@5.0.3: + resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-contexify@5.0.0: resolution: {integrity: sha512-2FIp7lxJ6dtfGr8EZ4uVV5p5TQjd0n2h/JU7PrejNIMiCeZWvSVPFh4lj1ZvjXosglBvP7q5JQQ8yUCdSaMSaw==} engines: {node: '>=10'} @@ -17002,6 +17198,12 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 + react-reconciler@0.27.0: + resolution: {integrity: sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.0.0 + react-redux@7.2.9: resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: @@ -17137,6 +17339,15 @@ packages: react: '*' tslib: '*' + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react-use@17.5.0: resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} peerDependencies: @@ -17633,6 +17844,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.21.0: + resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -18084,6 +18298,15 @@ packages: static-browser-server@1.0.3: resolution: {integrity: sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -18351,6 +18574,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + svgo@4.0.0: resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} engines: {node: '>=16'} @@ -18479,6 +18707,20 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + three-mesh-bvh@0.7.8: + resolution: {integrity: sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==} + deprecated: Deprecated due to three.js version incompatibility. Please use v0.8.0, instead. + peerDependencies: + three: '>= 0.151.0' + + three-stdlib@2.36.0: + resolution: {integrity: sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA==} + peerDependencies: + three: '>=0.128.0' + + three@0.180.0: + resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==} + throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} @@ -18642,6 +18884,19 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + trough@2.1.0: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} @@ -18748,6 +19003,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + turbo-darwin-64@2.3.3: resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==} cpu: [x64] @@ -19330,6 +19588,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -19701,6 +19963,12 @@ packages: web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -19980,6 +20248,15 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + zustand@4.4.7: resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} engines: {node: '>=12.7.0'} @@ -19995,6 +20272,24 @@ packages: react: optional: true + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -23629,6 +23924,8 @@ snapshots: '@types/react': 18.3.3 react: 18.3.1 + '@mediapipe/tasks-vision@0.10.17': {} + '@mermaid-js/parser@0.6.2': dependencies: langium: 3.3.1 @@ -23687,6 +23984,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@monogrid/gainmap-js@3.1.0(three@0.180.0)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.180.0 + '@motionone/animation@10.16.3': dependencies: '@motionone/easing': 10.16.3 @@ -25205,6 +25507,10 @@ snapshots: dependencies: playwright: 1.53.0 + '@pmndrs/cannon-worker-api@2.4.0(three@0.180.0)': + dependencies: + three: 0.180.0 + '@polka/url@1.0.0-next.25': {} '@popperjs/core@2.11.8': {} @@ -27227,6 +27533,39 @@ snapshots: optionalDependencies: typescript: 5.9.2 + '@react-spring/animated@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/core@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/rafz@9.7.5': {} + + '@react-spring/shared@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.7.5 + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/three@9.7.5(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(react@18.3.1)(three@0.180.0)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/core': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + '@react-three/fiber': 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) + react: 18.3.1 + three: 0.180.0 + + '@react-spring/types@9.7.5': {} + '@react-stately/flags@3.1.1': dependencies: '@swc/helpers': 0.5.15 @@ -27236,6 +27575,72 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-three/cannon@6.6.0(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(react@18.3.1)(three@0.180.0)(typescript@5.9.2)': + dependencies: + '@pmndrs/cannon-worker-api': 2.4.0(three@0.180.0) + '@react-three/fiber': 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) + cannon-es: 0.20.0 + cannon-es-debugger: 1.0.0(cannon-es@0.20.0)(three@0.180.0)(typescript@5.9.2) + react: 18.3.1 + three: 0.180.0 + transitivePeerDependencies: + - typescript + + '@react-three/drei@9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(@types/react@18.3.3)(@types/three@0.169.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0)(use-sync-external-store@1.5.0(react@18.3.1))': + dependencies: + '@babel/runtime': 7.26.10 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.1.0(three@0.180.0) + '@react-spring/three': 9.7.5(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(react@18.3.1)(three@0.180.0) + '@react-three/fiber': 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) + '@use-gesture/react': 10.3.1(react@18.3.1) + camera-controls: 2.10.1(three@0.180.0) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.13 + maath: 0.10.8(@types/three@0.169.0)(three@0.180.0) + meshline: 3.3.1(three@0.180.0) + react: 18.3.1 + react-composer: 5.0.3(react@18.3.1) + stats-gl: 2.4.2(@types/three@0.169.0)(three@0.180.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.180.0 + three-mesh-bvh: 0.7.8(three@0.180.0) + three-stdlib: 2.36.0(three@0.180.0) + troika-three-text: 0.52.4(three@0.180.0) + tunnel-rat: 0.1.2(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1) + utility-types: 3.11.0 + zustand: 5.0.8(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + - use-sync-external-store + + '@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0)': + dependencies: + '@babel/runtime': 7.26.10 + '@types/react-reconciler': 0.26.7 + '@types/webxr': 0.5.20 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 1.2.5(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-reconciler: 0.27.0(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + scheduler: 0.21.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.180.0 + zustand: 3.7.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + '@react-types/shared@3.29.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -27754,7 +28159,7 @@ snapshots: '@sentry/core@10.3.0': {} - '@sentry/nextjs@10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)': + '@sentry/nextjs@10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.36.0 @@ -29648,6 +30053,8 @@ snapshots: '@types/doctrine@0.0.9': {} + '@types/draco3d@1.4.10': {} + '@types/estree-jsx@1.0.1': dependencies: '@types/estree': 1.0.5 @@ -29764,6 +30171,8 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/offscreencanvas@2019.7.3': {} + '@types/papaparse@5.3.9': dependencies: '@types/node': 22.13.14 @@ -29834,6 +30243,14 @@ snapshots: dependencies: '@types/react': 18.3.3 + '@types/react-reconciler@0.26.7': + dependencies: + '@types/react': 18.3.3 + + '@types/react-reconciler@0.28.9(@types/react@18.3.3)': + dependencies: + '@types/react': 18.3.3 + '@types/react-redux@7.1.27': dependencies: '@types/hoist-non-react-statics': 3.3.2 @@ -30079,6 +30496,13 @@ snapshots: unhead: 2.0.14 vue: 3.5.21(typescript@5.9.2) + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + '@usercentrics/cmp-browser-sdk@4.42.0': dependencies: '@types/crypto-js': 4.2.2 @@ -31037,6 +31461,10 @@ snapshots: bezier-easing@2.1.0: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big-integer@1.6.51: {} big.js@5.2.2: {} @@ -31264,6 +31692,10 @@ snapshots: camelize@1.0.1: {} + camera-controls@2.10.1(three@0.180.0): + dependencies: + three: 0.180.0 + caniuse-api@3.0.0: dependencies: browserslist: 4.26.2 @@ -31273,6 +31705,15 @@ snapshots: caniuse-lite@1.0.30001743: {} + cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.180.0)(typescript@5.9.2): + dependencies: + cannon-es: 0.20.0 + three: 0.180.0 + optionalDependencies: + typescript: 5.9.2 + + cannon-es@0.20.0: {} + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -32360,6 +32801,10 @@ snapshots: detect-file@1.0.0: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-indent@6.1.0: {} detect-libc@1.0.3: {} @@ -32474,6 +32919,8 @@ snapshots: dotty@0.1.2: {} + draco3d@1.5.7: {} + drange@1.1.1: {} drizzle-kit@0.31.4(supports-color@8.1.1): @@ -33420,6 +33867,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.6.10: {} + fflate@0.7.4: {} fflate@0.8.2: {} @@ -33897,6 +34346,8 @@ snapshots: globrex@0.1.2: {} + glsl-noise@0.0.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -34360,6 +34811,8 @@ snapshots: highlightjs-curl@1.3.0: {} + hls.js@1.6.13: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -34505,6 +34958,8 @@ snapshots: imagescript@1.3.0: {} + immediate@3.0.6: {} + immer@10.1.1: optional: true @@ -34812,6 +35267,8 @@ snapshots: is-primitive@3.0.1: {} + is-promise@2.2.2: {} + is-promise@4.0.0: {} is-reference@1.2.1: @@ -34970,6 +35427,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + its-fine@1.2.5(@types/react@18.3.3)(react@18.3.1): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@18.3.3) + react: 18.3.1 + transitivePeerDependencies: + - '@types/react' + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -35283,6 +35747,10 @@ snapshots: - encoding - supports-color + lie@3.3.0: + dependencies: + immediate: 3.0.6 + light-my-request@5.14.0: dependencies: cookie: 0.7.2 @@ -35515,6 +35983,11 @@ snapshots: lz-string@1.5.0: {} + maath@0.10.8(@types/three@0.169.0)(three@0.180.0): + dependencies: + '@types/three': 0.169.0 + three: 0.180.0 + magic-regexp@0.10.0: dependencies: estree-walker: 3.0.3 @@ -36104,6 +36577,10 @@ snapshots: optionalDependencies: '@types/node': 22.13.14 + meshline@3.3.1(three@0.180.0): + dependencies: + three: 0.180.0 + meshoptimizer@0.18.1: {} methods@1.1.2: {} @@ -36921,10 +37398,10 @@ snapshots: nanoid@3.3.11: {} - nanoid@3.3.8: {} - nanoid@5.1.5: {} + nanoid@5.1.6: {} + nanotar@0.2.0: {} natural-compare@1.4.0: {} @@ -36971,7 +37448,7 @@ snapshots: dependencies: js-yaml-loader: 1.2.2 - next-router-mock@0.9.13(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1): + next-router-mock@0.9.13(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1): dependencies: next: 15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) react: 18.3.1 @@ -38578,7 +39055,7 @@ snapshots: postcss@8.5.3: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -38617,6 +39094,8 @@ snapshots: preact: 10.26.9 web-vitals: 4.2.4 + potpack@1.0.2: {} + preact@10.26.9: {} prelude-ls@1.2.1: {} @@ -38708,6 +39187,11 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + promise@7.3.1: dependencies: asap: 2.0.6 @@ -38863,6 +39347,11 @@ snapshots: dependencies: react: 18.3.1 + react-composer@5.0.3(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-contexify@5.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 @@ -39101,6 +39590,12 @@ snapshots: react-fast-compare: 3.2.2 warning: 4.0.3 + react-reconciler@0.27.0(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.21.0 + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.10 @@ -39257,6 +39752,12 @@ snapshots: react: 18.3.1 tslib: 2.6.2 + react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-use@17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@types/js-cookie': 2.2.7 @@ -40003,6 +40504,10 @@ snapshots: xmlchars: 2.2.0 optional: true + scheduler@0.21.0: + dependencies: + loose-envify: 1.4.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -40589,6 +41094,13 @@ snapshots: mime-db: 1.54.0 outvariant: 1.4.3 + stats-gl@2.4.2(@types/three@0.169.0)(three@0.180.0): + dependencies: + '@types/three': 0.169.0 + three: 0.180.0 + + stats.js@0.17.0: {} + statuses@2.0.1: {} std-env@3.8.1: {} @@ -40902,6 +41414,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + svgo@4.0.0: dependencies: commander: 11.1.0 @@ -41086,6 +41602,22 @@ snapshots: dependencies: real-require: 0.2.0 + three-mesh-bvh@0.7.8(three@0.180.0): + dependencies: + three: 0.180.0 + + three-stdlib@2.36.0(three@0.180.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.20 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.180.0 + + three@0.180.0: {} + throttle-debounce@3.0.1: {} throttleit@2.1.0: {} @@ -41221,6 +41753,20 @@ snapshots: trim-lines@3.0.1: {} + troika-three-text@0.52.4(three@0.180.0): + dependencies: + bidi-js: 1.0.3 + three: 0.180.0 + troika-three-utils: 0.52.4(three@0.180.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.180.0): + dependencies: + three: 0.180.0 + + troika-worker-utils@0.52.0: {} + trough@2.1.0: {} truncate-utf8-bytes@1.0.2: @@ -41324,6 +41870,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-rat@0.1.2(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1): + dependencies: + zustand: 4.4.7(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + turbo-darwin-64@2.3.3: optional: true @@ -41906,6 +42460,8 @@ snapshots: util-deprecate@1.0.2: {} + utility-types@3.11.0: {} + uuid@10.0.0: {} uuid@11.1.0: {} @@ -42582,6 +43138,10 @@ snapshots: web-vitals@4.2.4: {} + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: @@ -42928,6 +43488,10 @@ snapshots: zod@3.25.76: {} + zustand@3.7.2(react@18.3.1): + optionalDependencies: + react: 18.3.1 + zustand@4.4.7(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1): dependencies: use-sync-external-store: 1.2.0(react@18.3.1) @@ -42936,6 +43500,13 @@ snapshots: immer: 10.1.1 react: 18.3.1 + zustand@5.0.8(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.3 + immer: 10.1.1 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + zwitch@2.0.4: {} zxcvbn@4.4.2: {} diff --git a/supabase/migrations/20250917120000_update_advisor_game_resources.sql b/supabase/migrations/20250917120000_update_advisor_game_resources.sql new file mode 100644 index 00000000000..a3aab877d47 --- /dev/null +++ b/supabase/migrations/20250917120000_update_advisor_game_resources.sql @@ -0,0 +1,103 @@ +-- Rename max_resources column to resources so that it represents the mutable +-- count of resources available to an advisor game. +ALTER TABLE public.advisor_games +RENAME COLUMN max_resources TO resources; + +-- Recreate the non-negative constraint with an updated name. +ALTER TABLE public.advisor_games +DROP CONSTRAINT IF EXISTS advisor_games_max_resources_check; + +ALTER TABLE public.advisor_games +ADD CONSTRAINT advisor_games_resources_check CHECK (resources >= 0); + +-- Update broadcast function to emit the new resources column. +CREATE OR REPLACE FUNCTION public.broadcast_advisor_game_state() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path TO 'public', 'pg_temp' +AS $function$ +declare + v_project_ref text := coalesce(NEW.project_ref, OLD.project_ref); + v_resources integer; + v_used integer; + v_blocks jsonb; + v_players jsonb; +begin + if v_project_ref is null then + return null; + end if; + + select resources + into v_resources + from public.advisor_games + where project_ref = v_project_ref; + + if v_resources is null then + return null; + end if; + + select coalesce(count(*), 0) + into v_used + from public.advisor_blocks + where project_ref = v_project_ref; + + select coalesce( + jsonb_agg( + jsonb_build_object( + 'id', b.id, + 'cube_key', b.cube_key, + 'position', jsonb_build_array(b.pos_x, b.pos_y, b.pos_z), + 'texture', b.texture, + 'created_by', b.created_by, + 'created_at', b.created_at + ) + order by b.created_at + ), + '[]'::jsonb + ) + into v_blocks + from public.advisor_blocks b + where b.project_ref = v_project_ref; + + select coalesce( + jsonb_agg( + jsonb_build_object( + 'id', p.id, + 'user_id', p.user_id, + 'display_name', p.display_name, + 'presence', p.presence, + 'joined_at', p.joined_at, + 'last_seen_at', p.last_seen_at + ) + order by p.joined_at + ), + '[]'::jsonb + ) + into v_players + from public.advisor_players p + where p.project_ref = v_project_ref; + + perform realtime.send( + jsonb_build_object( + 'project_ref', v_project_ref, + 'resources', v_resources, + 'used_resources', v_used, + 'blocks', v_blocks, + 'players', v_players, + 'updated_at', now() + ), + 'state_update', + format('advisor_game:%s', v_project_ref), + false + ); + + return null; +exception + when undefined_function then + raise notice 'realtime.send function is not available; skipping advisor broadcast'; + when others then + raise warning 'advisor broadcast failed for %: %', v_project_ref, sqlerrm; + return null; +end; +$function$; diff --git a/supabase/migrations/20251005120000_update_advisor_multiplayer_access.sql b/supabase/migrations/20251005120000_update_advisor_multiplayer_access.sql new file mode 100644 index 00000000000..7d6b9297600 --- /dev/null +++ b/supabase/migrations/20251005120000_update_advisor_multiplayer_access.sql @@ -0,0 +1,25 @@ +DROP POLICY IF EXISTS "project members can select advisor_games" +ON public.advisor_games; + +CREATE POLICY "advisor games can be viewed by anyone" +ON public.advisor_games +FOR SELECT +TO public +USING (true); + +DROP POLICY IF EXISTS "project members can view advisor_blocks" +ON public.advisor_blocks; + +CREATE POLICY "advisor blocks can be viewed by anyone" +ON public.advisor_blocks +FOR SELECT +TO public +USING (true); + +-- Permit realtime broadcasts for advisor games to reach all authenticated clients. +ALTER POLICY "project members can receive advisor_game broadcasts" +ON realtime.messages +USING ( + (extension = 'broadcast'::text) + AND (split_part(realtime.topic(), ':'::text, 1) = 'advisor_game') +);
+ Advisor found no pending security or performance issues +
- No security or performance errors found -