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