//@ts-check import ReactDOM from 'https://esm.sh/react-dom' import React from 'https://esm.sh/react' import * as RTF from 'https://esm.sh/@react-three/fiber' import { useGLTF, useAnimations, PerspectiveCamera } from 'https://esm.sh/@react-three/drei' import {html} from 'https://esm.sh/htm/react' /** @typedef {[x:number, y:number, z:number]} V3 @typedef {{Type:"click", Payload:number}} ActionClick @typedef {{Type:"round"}} ActionRound @typedef {ActionClick | ActionRound} Action @typedef {{Done:boolean, Pos:V3}} StateGoal @typedef {{Round:number, Done:boolean, Goals:Array}} State @typedef {(inState:State, inAction:Action)=>void} Reducer @typedef {{Initial:State, Reducer:React.Reducer, Consume:()=>GameBinding}} Game @typedef {[State:State, Dispatcher:React.Dispatch]} GameBinding */ /** @type Game */ const Game = { Initial: { Round:0, Goals:[], Done:true }, Reducer(inState, inAction) { /**@type {(inGoals:Array)=>boolean} */ const allDone =(inGoals)=> { for(let i=0; ivoid})=>JSX.Element} */ function Model(props) { const { nodes, animations } = useGLTF("/bounce.gltf"); const Anim = useAnimations(animations); React.useEffect(()=> { const action = Anim.actions.CubeAction; props.goal.Done ? action.play() : action.stop(); }, [props.goal.Done]); return html` { e.stopPropagation(); props.handler(); } }> ` } useGLTF.preload("/bounce.gltf"); function App() { const [State, Dispatch] = Game.Consume(); /** @type Array */ const children = State.Goals.map((g, i)=> { return React.createElement(Model, { goal:g, handler:()=>Dispatch({Type:"click", Payload:i}), key:i }); }); return html`
Round
${State.Round}
Done
${ State.Done ? "Done!" : "Not Done :("}
<${RTF.Canvas}> <${PerspectiveCamera} makeDefault fov=${100} position=${[0, 0, 10]} /> <${React.Suspense}> ${children}
`; } ReactDOM.render( React.createElement(App), document.getElementById('root') )