init
This commit is contained in:
commit
4ea26b08a3
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.unstable": true
|
||||||
|
}
|
155
app.js
Normal file
155
app.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
//@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<StateGoal>}} State
|
||||||
|
*/
|
||||||
|
/** @typedef {(inState:State, inAction:Action)=>void} Reducer */
|
||||||
|
/** @typedef {{Initial:State, Reducer:React.Reducer<State, Action>, Consume:()=>GameBinding}} Game */
|
||||||
|
/** @typedef {[State:State, Dispatcher:React.Dispatch<Action>]} GameBinding */
|
||||||
|
|
||||||
|
/** @type Game */
|
||||||
|
const Game =
|
||||||
|
{
|
||||||
|
Initial:
|
||||||
|
{
|
||||||
|
Round:0,
|
||||||
|
Goals:[{Done:false, Pos:[0, 0, 0]}, {Done:true, Pos:[2, 0, 0]}],
|
||||||
|
Done:false
|
||||||
|
},
|
||||||
|
Reducer(inState, inAction)
|
||||||
|
{
|
||||||
|
/**@type {(inGoals:Array<StateGoal>)=>boolean} */
|
||||||
|
const allDone =(inGoals)=>
|
||||||
|
{
|
||||||
|
for(let i=0; i<inGoals.length; i++)
|
||||||
|
{
|
||||||
|
if(!clone.Goals[i].Done)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/** @type State */ const clone = {...inState};
|
||||||
|
switch(inAction.Type)
|
||||||
|
{
|
||||||
|
case "click":
|
||||||
|
{
|
||||||
|
/**@type StateGoal|undefined*/
|
||||||
|
const goal = clone.Goals[inAction.Payload];
|
||||||
|
if(goal)
|
||||||
|
{
|
||||||
|
goal.Done = !goal.Done;
|
||||||
|
}
|
||||||
|
clone.Done = allDone(clone.Goals);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "round":
|
||||||
|
{
|
||||||
|
clone.Round++;
|
||||||
|
clone.Goals = [];
|
||||||
|
clone.Done = false;
|
||||||
|
for(let i=0; i<1+Math.random()*10; i++)
|
||||||
|
{
|
||||||
|
/**@type StateGoal*/ const goal = {Done:false, Pos:[
|
||||||
|
(Math.random()-0.5)*5,
|
||||||
|
(Math.random()-0.5)*5,
|
||||||
|
(Math.random()-0.5)*5
|
||||||
|
]};
|
||||||
|
clone.Goals.push(goal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
},
|
||||||
|
Consume()
|
||||||
|
{
|
||||||
|
return React.useReducer(Game.Reducer, Game.Initial);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {(props:{goal:StateGoal, handler:()=>void})=>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`
|
||||||
|
<group position=${props.goal.Pos} dispose=${null}>
|
||||||
|
<mesh
|
||||||
|
ref=${Anim.ref}
|
||||||
|
name="Cube"
|
||||||
|
castShadow
|
||||||
|
receiveShadow
|
||||||
|
geometry=${nodes.Cube.geometry}
|
||||||
|
material=${nodes.Cube.material}
|
||||||
|
onClick=${e=>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
props.handler();
|
||||||
|
}
|
||||||
|
} />
|
||||||
|
</group>`
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload("/bounce.gltf");
|
||||||
|
|
||||||
|
function App()
|
||||||
|
{
|
||||||
|
const [State, Dispatch] = Game.Consume();
|
||||||
|
/** @type Array<JSX.Element> */
|
||||||
|
const children = State.Goals.map((g, i)=>
|
||||||
|
{
|
||||||
|
return React.createElement(Model,
|
||||||
|
{
|
||||||
|
goal:g,
|
||||||
|
handler:()=>Dispatch({Type:"click", Payload:i}),
|
||||||
|
key:i
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Round</dt>
|
||||||
|
<dd>${State.Round}</dd>
|
||||||
|
<dt>Done</dt>
|
||||||
|
<dd>${ State.Done ? "Done!" : "Not Done :("}</dd>
|
||||||
|
<dd><button disabled=${!State.Done} onClick=${()=>Dispatch({Type:"round"})}>Next</button></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<${RTF.Canvas}>
|
||||||
|
<pointLight position=${[10, 10, 10]} />
|
||||||
|
<${PerspectiveCamera} makeDefault fov=${100} position=${[0, 0, 10]} />
|
||||||
|
<${React.Suspense}>
|
||||||
|
${children}
|
||||||
|
<//>
|
||||||
|
<//>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
React.createElement(App),
|
||||||
|
document.getElementById('root')
|
||||||
|
)
|
149
bounce.gltf
Normal file
149
bounce.gltf
Normal file
File diff suppressed because one or more lines are too long
7
index.html
Normal file
7
index.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="app.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user