mayor refactor to use vite instead of weboack and version upgrade
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
73
README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
23
eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Floppy Venture</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3614
package-lock.json
generated
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "floppy-venture",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
|
"@types/lodash": "^4.17.23",
|
||||||
|
"@types/three": "^0.182.0",
|
||||||
|
"lodash": "^4.17.23",
|
||||||
|
"popmotion": "^11.0.5",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"react-responsive-modal": "^7.1.0",
|
||||||
|
"react-router-dom": "^7.13.0",
|
||||||
|
"three": "^0.182.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.13",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.48.0",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
164
public/assets/icons/action.svg
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="action.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="32.816993"
|
||||||
|
inkscape:cx="17.094802"
|
||||||
|
inkscape:cy="17.094802"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
style="fill:#ebebeb;fill-opacity:1"
|
||||||
|
id="g5624-3-9"
|
||||||
|
transform="matrix(0.01349241,0,0,0.01200303,22.186671,311.35051)">
|
||||||
|
<g
|
||||||
|
style="fill:#ebebeb;fill-opacity:1"
|
||||||
|
transform="matrix(0.41416684,0.60970794,-0.63915791,0.41296461,-761.04902,-1586.167)"
|
||||||
|
id="g5180-6-9-4">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m -365.29901,319.17882 c -64.51383,0.87628 -118.06335,50.09804 -124.36035,114.30982 h 43.3679 a 97.895835,98.273803 0 0 1 80.99245,-74.50708 z"
|
||||||
|
id="path5162-6-1-9"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:2.51792049;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path5173-49-0-0"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="-360.84305"
|
||||||
|
sodipodi:cy="342.57666"
|
||||||
|
sodipodi:r1="65.992462"
|
||||||
|
sodipodi:r2="32.996231"
|
||||||
|
sodipodi:arg1="0"
|
||||||
|
sodipodi:arg2="1.0471976"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m -294.85059,342.57666 -98.98869,57.15115 v -114.3023 z"
|
||||||
|
inkscape:transform-center-x="-16.498123"
|
||||||
|
inkscape:transform-center-y="-1.4044677e-05" />
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
inkscape:transform-center-y="-179.39898"
|
||||||
|
inkscape:transform-center-x="-20.84786"
|
||||||
|
transform="matrix(-0.07280782,-0.99734599,0.99581965,-0.09134122,0,0)"
|
||||||
|
y="-1213.037"
|
||||||
|
x="1530.4231"
|
||||||
|
height="40.792873"
|
||||||
|
width="267.9798"
|
||||||
|
id="rect5545-4-9"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4.96418476;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<ellipse
|
||||||
|
inkscape:transform-center-y="-267.69154"
|
||||||
|
inkscape:transform-center-x="-13.307385"
|
||||||
|
transform="matrix(-0.07280782,-0.99734599,0.99581965,-0.09134122,0,0)"
|
||||||
|
ry="43.453278"
|
||||||
|
rx="49.625893"
|
||||||
|
cy="-1194.4142"
|
||||||
|
cx="1808.3281"
|
||||||
|
id="path5549-1-1"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4.77678871;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<rect
|
||||||
|
y="-1452.454"
|
||||||
|
x="-1462.3748"
|
||||||
|
height="17.87746"
|
||||||
|
width="352.64636"
|
||||||
|
id="rect5551-1-7"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4.96375561;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<rect
|
||||||
|
y="-1418.5864"
|
||||||
|
x="-1462.3748"
|
||||||
|
height="17.87746"
|
||||||
|
width="352.64636"
|
||||||
|
id="rect5551-5-8-7"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4.96375561;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<rect
|
||||||
|
y="-1452.454"
|
||||||
|
x="-1480.0957"
|
||||||
|
height="51.745029"
|
||||||
|
width="17.720922"
|
||||||
|
id="rect5574-6-1"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4.78777838;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<rect
|
||||||
|
y="-1452.454"
|
||||||
|
x="-1127.4493"
|
||||||
|
height="51.745033"
|
||||||
|
width="17.720922"
|
||||||
|
id="rect5574-0-4-1"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4.78777838;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<g
|
||||||
|
style="fill:#ebebeb;fill-opacity:1"
|
||||||
|
transform="matrix(-0.02612219,0.7657773,0.73477013,-0.07405195,-1736.1954,-1284.7167)"
|
||||||
|
id="g5180-6-4-3-5">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m -365.29901,319.17882 c -64.51383,0.87628 -118.06335,50.09804 -124.36035,114.30982 h 43.3679 a 97.895835,98.273803 0 0 1 80.99245,-74.50708 z"
|
||||||
|
id="path5162-6-8-4-9"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:2.51792049;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path5173-49-7-7-7"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="-360.84305"
|
||||||
|
sodipodi:cy="342.57666"
|
||||||
|
sodipodi:r1="65.992462"
|
||||||
|
sodipodi:r2="32.996231"
|
||||||
|
sodipodi:arg1="0"
|
||||||
|
sodipodi:arg2="1.0471976"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m -294.85059,342.57666 -98.98869,57.15115 v -114.3023 z"
|
||||||
|
inkscape:transform-center-x="-16.498123"
|
||||||
|
inkscape:transform-center-y="-1.4044677e-05" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.5 KiB |
78
public/assets/icons/f1.svg
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="f1.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="8.2042482"
|
||||||
|
inkscape:cx="12.477605"
|
||||||
|
inkscape:cy="1.0794568"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
aria-label="f1() "
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.5181675px;line-height:1.25;font-family:Iosevka;-inkscape-font-specification:Iosevka;letter-spacing:0px;word-spacing:0px;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:0.02353211"
|
||||||
|
id="text1735-3-3-0-7">
|
||||||
|
<path
|
||||||
|
d="M 1.1929156,293.89529 V 292.2322 H 0.75382637 v -0.27978 H 1.1929156 v -0.36914 q 0,-0.12046 0.031086,-0.23704 0.031086,-0.11657 0.1088009,-0.20983 0.077715,-0.0971 0.1904015,-0.14377 0.1126867,-0.0466 0.2331448,-0.0466 0.1360011,0 0.2603449,0.0583 0.1243439,0.0583 0.1981731,0.17486 0.073829,0.11657 0.093258,0.24869 l -0.2992025,0.0738 q -0.00777,-0.0661 -0.038857,-0.12822 -0.0272,-0.0661 -0.085486,-0.10492 -0.058286,-0.0427 -0.1282296,-0.0427 -0.081601,0 -0.1476584,0.0583 -0.062172,0.0583 -0.085486,0.13989 -0.019429,0.0777 -0.019429,0.15932 v 0.36914 h 0.6528053 v 0.27978 H 1.5037753 v 1.66309 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3362" />
|
||||||
|
<path
|
||||||
|
d="m 3.3961335,293.89529 v -2.5063 l -0.4973755,0.33417 -0.1709728,-0.23314 0.6683483,-0.45075 h 0.3108596 v 2.85602 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3364" />
|
||||||
|
<path
|
||||||
|
d="m 5.9684972,294.62581 q -0.3574886,-0.2176 -0.6022906,-0.56731 -0.2409163,-0.34584 -0.3380599,-0.75384 -0.093258,-0.41189 -0.093258,-0.82766 0,-0.41578 0.093258,-0.82378 0.097144,-0.41189 0.3380599,-0.75772 0.244802,-0.34972 0.6022906,-0.56732 l 0.1593156,0.24092 q -0.8820643,0.55954 -0.8820643,1.9079 0,1.34835 0.8820643,1.9079 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3366" />
|
||||||
|
<path
|
||||||
|
d="M 6.7301032,294.62581 6.5707877,294.3849 q 0.8820642,-0.55955 0.8820642,-1.9079 0,-1.34836 -0.8820642,-1.9079 l 0.1593155,-0.24092 q 0.3574886,0.2176 0.5984049,0.56732 0.244802,0.34583 0.3380599,0.75772 0.097144,0.408 0.097144,0.82378 0,0.41577 -0.097144,0.82766 -0.093258,0.408 -0.3380599,0.75384 -0.2409163,0.34971 -0.5984049,0.56731 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3368" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
78
public/assets/icons/f2.svg
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="f2.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="8.2042482"
|
||||||
|
inkscape:cx="12.477605"
|
||||||
|
inkscape:cy="1.0794568"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
aria-label="f2() "
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.5181675px;line-height:1.25;font-family:Iosevka;-inkscape-font-specification:Iosevka;letter-spacing:0px;word-spacing:0px;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:0.02353211"
|
||||||
|
id="text1735-3-3-0-7">
|
||||||
|
<path
|
||||||
|
d="M 1.1929156,293.89529 V 292.2322 H 0.75382637 v -0.27978 H 1.1929156 v -0.36914 q 0,-0.12046 0.031086,-0.23704 0.031086,-0.11657 0.1088009,-0.20983 0.077715,-0.0971 0.1904015,-0.14377 0.1126867,-0.0466 0.2331448,-0.0466 0.1360011,0 0.2603449,0.0583 0.1243439,0.0583 0.1981731,0.17486 0.073829,0.11657 0.093258,0.24869 l -0.2992025,0.0738 q -0.00777,-0.0661 -0.038857,-0.12822 -0.0272,-0.0661 -0.085486,-0.10492 -0.058286,-0.0427 -0.1282296,-0.0427 -0.081601,0 -0.1476584,0.0583 -0.062172,0.0583 -0.085486,0.13989 -0.019429,0.0777 -0.019429,0.15932 v 0.36914 h 0.6528053 v 0.27978 H 1.5037753 v 1.66309 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3327" />
|
||||||
|
<path
|
||||||
|
d="m 2.6966992,293.89529 v -0.004 q 0,-0.21372 0.0272,-0.42355 0.031086,-0.20983 0.1398868,-0.39634 0.1088009,-0.18652 0.2642307,-0.33029 0.1593156,-0.14766 0.3264027,-0.27589 0.167087,-0.13212 0.2953166,-0.31086 0.1282297,-0.17874 0.1282297,-0.39246 0,-0.12434 -0.0544,-0.23703 -0.050515,-0.11657 -0.1632013,-0.17486 -0.1088009,-0.0622 -0.2331447,-0.0622 -0.1088009,0 -0.2137161,0.0505 -0.1010294,0.0505 -0.1593155,0.15155 -0.058286,0.0971 -0.073829,0.20594 l -0.3030882,-0.0544 q 0.015543,-0.136 0.077715,-0.25646 0.066058,-0.12434 0.1709728,-0.20983 0.1049152,-0.0894 0.2331448,-0.12823 0.1321153,-0.0388 0.2681165,-0.0388 0.151544,0 0.2953166,0.0505 0.1476584,0.0466 0.2525735,0.15543 0.1088009,0.1088 0.1593156,0.25257 0.0544,0.14378 0.0544,0.29532 0,0.17874 -0.077715,0.34972 -0.073829,0.17097 -0.198173,0.30697 -0.1204581,0.13212 -0.260345,0.24869 -0.1398868,0.11269 -0.2720022,0.24091 -0.1321154,0.12435 -0.2253733,0.28366 -0.093258,0.15543 -0.1204581,0.33806 -0.00777,0.0428 -0.011657,0.0855 h 1.1501807 v 0.27977 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3329" />
|
||||||
|
<path
|
||||||
|
d="m 5.9684972,294.62581 q -0.3574886,-0.2176 -0.6022906,-0.56731 -0.2409163,-0.34584 -0.3380599,-0.75384 -0.093258,-0.41189 -0.093258,-0.82766 0,-0.41578 0.093258,-0.82378 0.097144,-0.41189 0.3380599,-0.75772 0.244802,-0.34972 0.6022906,-0.56732 l 0.1593156,0.24092 q -0.8820643,0.55954 -0.8820643,1.9079 0,1.34835 0.8820643,1.9079 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3331" />
|
||||||
|
<path
|
||||||
|
d="M 6.7301032,294.62581 6.5707877,294.3849 q 0.8820642,-0.55955 0.8820642,-1.9079 0,-1.34836 -0.8820642,-1.9079 l 0.1593155,-0.24092 q 0.3574886,0.2176 0.5984049,0.56732 0.244802,0.34583 0.3380599,0.75772 0.097144,0.408 0.097144,0.82378 0,0.41577 -0.097144,0.82766 -0.093258,0.408 -0.3380599,0.75384 -0.2409163,0.34971 -0.5984049,0.56731 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;font-family:Iosevka;-inkscape-font-specification:Iosevka;fill:#ebebeb;fill-opacity:1;stroke-width:0.02353211"
|
||||||
|
id="path3333" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
77
public/assets/icons/f3.svg
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="f3.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="8.2042482"
|
||||||
|
inkscape:cx="12.477605"
|
||||||
|
inkscape:cy="1.0794568"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path3295"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;line-height:1.25;font-family:Iosevka;-inkscape-font-specification:Iosevka;letter-spacing:0px;word-spacing:0px;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:0.02353211"
|
||||||
|
d="M 1.1929156,293.89529 V 292.2322 H 0.75382637 v -0.27978 H 1.1929156 v -0.36914 q 0,-0.12046 0.031086,-0.23704 0.031086,-0.11657 0.1088009,-0.20983 0.077715,-0.0971 0.1904015,-0.14377 0.1126867,-0.0466 0.2331448,-0.0466 0.1360011,0 0.2603449,0.0583 0.1243439,0.0583 0.1981731,0.17486 0.073829,0.11657 0.093258,0.24869 l -0.2992025,0.0738 q -0.00777,-0.0661 -0.038857,-0.12822 -0.0272,-0.0661 -0.085486,-0.10492 -0.058286,-0.0427 -0.1282296,-0.0427 -0.081601,0 -0.1476584,0.0583 -0.062172,0.0583 -0.085486,0.13989 -0.019429,0.0777 -0.019429,0.15932 v 0.36914 h 0.6528053 v 0.27978 H 1.5037753 v 1.66309 z" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path3297"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;line-height:1.25;font-family:Iosevka;-inkscape-font-specification:Iosevka;letter-spacing:0px;word-spacing:0px;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:0.02353211"
|
||||||
|
d="m 3.4349909,293.92638 q -0.1865158,0 -0.3613743,-0.0699 -0.1748586,-0.07 -0.2875452,-0.22538 -0.1088009,-0.15543 -0.1398868,-0.33806 l 0.3030881,-0.0661 q 0.015543,0.11657 0.081601,0.2176 0.066058,0.10103 0.1748585,0.15154 0.1088009,0.0505 0.229259,0.0505 0.1321154,0 0.2525735,-0.0661 0.1204581,-0.0661 0.1787443,-0.1904 0.058286,-0.12435 0.058286,-0.26035 0,-0.136 -0.062172,-0.26034 -0.062172,-0.12823 -0.1787443,-0.20983 -0.1126867,-0.0816 -0.2486878,-0.10103 -0.1360011,-0.0233 -0.2758879,-0.0233 v -0.27977 q 0.1243439,0 0.2486877,-0.0155 0.1243439,-0.0194 0.229259,-0.0894 0.1088009,-0.0699 0.1670871,-0.18263 0.058286,-0.11657 0.058286,-0.24091 0,-0.11269 -0.050515,-0.2176 -0.050515,-0.1088 -0.1554298,-0.16321 -0.1049152,-0.0583 -0.2176018,-0.0583 -0.1088009,0 -0.2098303,0.0505 -0.097144,0.0466 -0.1515441,0.14378 -0.0544,0.0971 -0.062172,0.20205 l -0.3069739,-0.0427 q 0.011657,-0.13211 0.069943,-0.25646 0.062172,-0.12434 0.1632013,-0.20983 0.1010294,-0.0894 0.229259,-0.12823 0.1321154,-0.0388 0.2681165,-0.0388 0.1437726,0 0.2836594,0.0466 0.1398869,0.0428 0.2409163,0.14766 0.1049151,0.10103 0.1554298,0.24092 0.0544,0.136 0.0544,0.27977 0,0.17875 -0.085486,0.34195 -0.085486,0.1632 -0.2409162,0.25646 -0.069943,0.0427 -0.1398869,0.0738 0.097144,0.035 0.1865158,0.0933 0.1632014,0.10492 0.2525735,0.28366 0.089372,0.17486 0.089372,0.36526 0,0.15932 -0.058286,0.31086 -0.0544,0.14766 -0.1670871,0.26423 -0.1126866,0.11269 -0.2681164,0.16321 -0.1515441,0.0505 -0.306974,0.0505 z" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path3299"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;line-height:1.25;font-family:Iosevka;-inkscape-font-specification:Iosevka;letter-spacing:0px;word-spacing:0px;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:0.02353211"
|
||||||
|
d="m 5.9684972,294.62581 q -0.3574886,-0.2176 -0.6022906,-0.56731 -0.2409163,-0.34584 -0.3380599,-0.75384 -0.093258,-0.41189 -0.093258,-0.82766 0,-0.41578 0.093258,-0.82378 0.097144,-0.41189 0.3380599,-0.75772 0.244802,-0.34972 0.6022906,-0.56732 l 0.1593156,0.24092 q -0.8820643,0.55954 -0.8820643,1.9079 0,1.34835 0.8820643,1.9079 z" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path3301"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88574576px;line-height:1.25;font-family:Iosevka;-inkscape-font-specification:Iosevka;letter-spacing:0px;word-spacing:0px;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:0.02353211"
|
||||||
|
d="M 6.7301032,294.62581 6.5707877,294.3849 q 0.8820642,-0.55955 0.8820642,-1.9079 0,-1.34836 -0.8820642,-1.9079 l 0.1593155,-0.24092 q 0.3574886,0.2176 0.5984049,0.56732 0.244802,0.34583 0.3380599,0.75772 0.097144,0.408 0.097144,0.82378 0,0.41577 -0.097144,0.82766 -0.093258,0.408 -0.3380599,0.75384 -0.2409163,0.34971 -0.5984049,0.56731 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.8 KiB |
91
public/assets/icons/forward.svg
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="forward.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="32.816993"
|
||||||
|
inkscape:cx="17.094802"
|
||||||
|
inkscape:cy="17.094802"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
style="fill:#ebebeb;fill-opacity:1"
|
||||||
|
transform="matrix(0.03667475,0,0,0.03667475,60.535177,309.32139)"
|
||||||
|
id="g5820-9-1">
|
||||||
|
<g
|
||||||
|
id="g5180-5-5-4"
|
||||||
|
transform="matrix(0.00834057,-0.61577557,0.61577557,0.00834057,-1734.933,-701.6135)"
|
||||||
|
style="fill:#ebebeb;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
inkscape:transform-center-y="-1.4044677e-05"
|
||||||
|
inkscape:transform-center-x="-16.498123"
|
||||||
|
d="m -294.85059,342.57666 -98.98869,57.15115 v -114.3023 z"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:arg2="1.0471976"
|
||||||
|
sodipodi:arg1="0"
|
||||||
|
sodipodi:r2="32.996231"
|
||||||
|
sodipodi:r1="65.992462"
|
||||||
|
sodipodi:cy="342.57666"
|
||||||
|
sodipodi:cx="-360.84305"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
id="path5173-3-2-9"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:2.51792049;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
sodipodi:type="star" />
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect5330-0-2"
|
||||||
|
width="27.261492"
|
||||||
|
height="67.886459"
|
||||||
|
x="-1540.8772"
|
||||||
|
y="-469.27454" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
107
public/assets/icons/jump.svg
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="jump.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="32.816993"
|
||||||
|
inkscape:cx="17.094802"
|
||||||
|
inkscape:cy="17.094802"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
id="g1545"
|
||||||
|
transform="matrix(0.98625975,0,0,1.3477349,-260.80995,-24.975358)">
|
||||||
|
<g
|
||||||
|
id="g1558"
|
||||||
|
transform="translate(23.617043,0.31233851)">
|
||||||
|
<path
|
||||||
|
transform="matrix(-1.6095136e-4,-0.01669562,0.02233565,-1.2030915e-4,237.73638,224.99466)"
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:2.51792049;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path5173-4-7-7-5"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="-584.64044"
|
||||||
|
sodipodi:cy="339.21793"
|
||||||
|
sodipodi:r1="65.992462"
|
||||||
|
sodipodi:r2="32.996231"
|
||||||
|
sodipodi:arg1="0"
|
||||||
|
sodipodi:arg2="1.0471976"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m -518.64798,339.21793 -98.98869,57.15114 V 282.06678 Z"
|
||||||
|
inkscape:transform-center-x="-0.13717797"
|
||||||
|
inkscape:transform-center-y="-22.29288" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ebebeb;stroke-width:0.21684135;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 245.3594,234.77668 c 0,0 -1.68348,0.13427 -0.0237,0.33001 1.77015,0.20876 -0.0357,0.32945 -0.0357,0.32945"
|
||||||
|
id="path1499"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="csc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ebebeb;stroke-width:0.21684135;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 245.35941,235.43073 c 0,0 -1.68348,0.13427 -0.0237,0.33001 1.77015,0.20876 -0.0357,0.32945 -0.0357,0.32945"
|
||||||
|
id="path1499-6"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="csc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ebebeb;stroke-width:0.21684135;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 245.35941,236.0848 c 0,0 -1.68348,0.13427 -0.0237,0.33001 1.77015,0.20876 -0.0357,0.32945 -0.0357,0.32945"
|
||||||
|
id="path1499-7"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="csc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ebebeb;stroke-width:0.21684135;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 245.30577,236.40791 c 0,0 -1.75025,0.24422 0.0147,0.33635"
|
||||||
|
id="path1522"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
84
public/assets/icons/turn_left.svg
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="turn_left.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="32.816993"
|
||||||
|
inkscape:cx="17.094802"
|
||||||
|
inkscape:cy="17.094802"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
style="fill:#ebebeb;fill-opacity:1"
|
||||||
|
transform="matrix(-0.02376905,0,0,0.02376905,-4.8005283,283.93305)"
|
||||||
|
id="g5180-70-6-2">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m -365.29901,319.17882 c -64.51383,0.87628 -118.06335,50.09804 -124.36035,114.30982 h 43.3679 a 97.895835,98.273803 0 0 1 80.99245,-74.50708 z"
|
||||||
|
id="path5162-74-1-9"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:2.51792049;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path5173-5-5-3"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="-360.84305"
|
||||||
|
sodipodi:cy="342.57666"
|
||||||
|
sodipodi:r1="65.992462"
|
||||||
|
sodipodi:r2="32.996231"
|
||||||
|
sodipodi:arg1="0"
|
||||||
|
sodipodi:arg2="1.0471976"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m -294.85059,342.57666 -98.98869,57.15115 v -114.3023 z"
|
||||||
|
inkscape:transform-center-x="-16.498123"
|
||||||
|
inkscape:transform-center-y="-1.4044677e-05" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
84
public/assets/icons/turn_right.svg
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="9.0459995mm"
|
||||||
|
height="9.0459995mm"
|
||||||
|
viewBox="0 0 9.0459995 9.0459995"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2422"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
|
sodipodi:docname="turn_right.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2416" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="32.816993"
|
||||||
|
inkscape:cx="17.094802"
|
||||||
|
inkscape:cy="17.094802"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="64"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2419">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-287.954)">
|
||||||
|
<g
|
||||||
|
style="fill:#ebebeb;fill-opacity:1"
|
||||||
|
transform="matrix(0.02376905,0,0,0.02376905,13.846528,283.93305)"
|
||||||
|
id="g5180-70-6-2">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m -365.29901,319.17882 c -64.51383,0.87628 -118.06335,50.09804 -124.36035,114.30982 h 43.3679 a 97.895835,98.273803 0 0 1 80.99245,-74.50708 z"
|
||||||
|
id="path5162-74-1-9"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="opacity:1;fill:#ebebeb;fill-opacity:1;stroke:none;stroke-width:2.51792049;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path5173-5-5-3"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="-360.84305"
|
||||||
|
sodipodi:cy="342.57666"
|
||||||
|
sodipodi:r1="65.992462"
|
||||||
|
sodipodi:r2="32.996231"
|
||||||
|
sodipodi:arg1="0"
|
||||||
|
sodipodi:arg2="1.0471976"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m -294.85059,342.57666 -98.98869,57.15115 v -114.3023 z"
|
||||||
|
inkscape:transform-center-x="-16.498123"
|
||||||
|
inkscape:transform-center-y="-1.4044677e-05" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/assets/iosevka-regular.ttf
Normal file
BIN
public/assets/models/RobotExpressive.glb
Normal file
BIN
public/assets/textures/crate.gif
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
public/assets/textures/gamename.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/textures/name.gif
Normal file
|
After Width: | Height: | Size: 21 KiB |
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
324
src/components/Editor.tsx
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { CodeBlockContainer, INIT_CODEBLOCKS, CodeBlock, INSERT_CODEBLOCK, REMOVE_CODEBLOCK, SET_DROPZONE_Z_INDEX } from "../store/codeBlocks/types";
|
||||||
|
import { LevelInitializer, IPlayerData } from "../game/levels";
|
||||||
|
import { initCodeBlocks, insertCodeBlock, removeCodeBlock } from "../store/codeBlocks/actions";
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import game from "../game/game"
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { START_EXECUTING, STEP_EXECUTION, TOGGLE_AUTOSTEP, TOGGLE_SPEED } from "../store/executionState/types";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const CodeBlockComponent = ({ type, blockIdx, remove, container, setDropZoneZIdx }) => {
|
||||||
|
return (<div className="op"
|
||||||
|
draggable={true}
|
||||||
|
onDrag={(e) =>
|
||||||
|
e.preventDefault()}
|
||||||
|
onDragStart={(e) => {
|
||||||
|
// e.preventDefault()
|
||||||
|
for (let e of document.getElementsByClassName("opdropzone")) {
|
||||||
|
(e as HTMLElement).style.background = ""
|
||||||
|
}
|
||||||
|
// console.log("BOOM START DRAGGOING");
|
||||||
|
(e.target as HTMLElement).style.zIndex = "3"
|
||||||
|
|
||||||
|
setDropZoneZIdx(2)
|
||||||
|
|
||||||
|
dragged = type
|
||||||
|
}}
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
for (let e of document.getElementsByClassName("opdropzone")) {
|
||||||
|
(e as HTMLElement).style.background = ""
|
||||||
|
}
|
||||||
|
(e.target as HTMLElement).style.zIndex = ""
|
||||||
|
setDropZoneZIdx(-1)
|
||||||
|
remove(container, blockIdx)
|
||||||
|
// console.log("LE END OF DRAG")
|
||||||
|
}}
|
||||||
|
> <img src={"assets/icons/" + type + ".svg"}></img>
|
||||||
|
</div >)
|
||||||
|
}
|
||||||
|
|
||||||
|
// show how many operations are possible
|
||||||
|
const OpIndicators = ({ n }) => {
|
||||||
|
let ops = []
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
ops.push(<div className="opindicator"
|
||||||
|
key={i}
|
||||||
|
></div>)
|
||||||
|
}
|
||||||
|
return (<div className="opindicators">{ops}</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropzones which determine how to insert
|
||||||
|
const OpDropZones = ({ n, cidx, insert, style }) => {
|
||||||
|
let dropZones = []
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
dropZones.push(
|
||||||
|
<div className="opdropzone"
|
||||||
|
key={i}
|
||||||
|
onDragEnter={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
(e.target as HTMLElement).style.background = "purple"
|
||||||
|
}}
|
||||||
|
onDragLeave={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
(e.target as HTMLElement).style.background = ""
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => { e.preventDefault() }}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
let dragOver = ((e.target as HTMLElement).className);
|
||||||
|
// console.log(dragOver)
|
||||||
|
// console.log("DRAGGED", dragged)
|
||||||
|
insert({ name: dragged }, cidx, i, true)
|
||||||
|
}}>
|
||||||
|
<div className="frontdropper"
|
||||||
|
onDragEnter={e => {
|
||||||
|
(e.target as HTMLElement).style.background = "purple"
|
||||||
|
}}
|
||||||
|
onDragOver={e => { e.preventDefault() }}
|
||||||
|
onDragLeave={e => {
|
||||||
|
(e.target as HTMLElement).style.background = ""
|
||||||
|
}}
|
||||||
|
onDrop={e => {
|
||||||
|
(e.target as HTMLElement).style.background = ""
|
||||||
|
insert({ name: dragged }, cidx, i, false)
|
||||||
|
}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (<div className="opdropzones" style={style}>{dropZones}</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FnContainers = ({ code, dropZoneZIdx, setDropZoneZIdx, remove, insert }) => {
|
||||||
|
return (
|
||||||
|
<div className="fncontainers">
|
||||||
|
{
|
||||||
|
code.map((cv, idx) => {
|
||||||
|
return (
|
||||||
|
<div className="fncontainer" key={idx} >
|
||||||
|
<h3>{cv.name + "(){"}</h3>
|
||||||
|
<div style={{ height: Math.floor(cv.nMaxBlocks / 4 + 1) * 4 + 'rem' }}>
|
||||||
|
<OpIndicators n={cv.nMaxBlocks} />
|
||||||
|
<div className="ops" >
|
||||||
|
{cv.blocks.map((b, i) => {
|
||||||
|
return (<CodeBlockComponent type={b.name}
|
||||||
|
key={i}
|
||||||
|
blockIdx={i}
|
||||||
|
container={idx}
|
||||||
|
remove={remove}
|
||||||
|
setDropZoneZIdx={setDropZoneZIdx} />)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<OpDropZones n={cv.nMaxBlocks}
|
||||||
|
cidx={idx}
|
||||||
|
insert={insert}
|
||||||
|
style={{ zIndex: dropZoneZIdx }} />
|
||||||
|
</div>
|
||||||
|
<h3>{"}"}</h3>
|
||||||
|
</div>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var dragged: string;
|
||||||
|
|
||||||
|
const OpsPalette = ({ ops, setDropZoneZIdx }) => {
|
||||||
|
let allowedOps = ops.map((e, i) => {
|
||||||
|
return (
|
||||||
|
<CodeBlockComponent type={e}
|
||||||
|
key={i}
|
||||||
|
blockIdx={0}
|
||||||
|
container={0}
|
||||||
|
remove={() => undefined}
|
||||||
|
setDropZoneZIdx={setDropZoneZIdx} />)
|
||||||
|
})
|
||||||
|
return (<div className="opspalette">{allowedOps}</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuntimeControls = ({ start, fast, step, running, level, code, autostep, toggleAutoStep, toggleSpeed, finished }) => {
|
||||||
|
let DebugButton = () => (
|
||||||
|
<div className="controlbutton"
|
||||||
|
onClick={() => {
|
||||||
|
start(level, code, level.playerPos)
|
||||||
|
}}>
|
||||||
|
debug
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
let StepButton = () => (
|
||||||
|
<div className="controlbutton"
|
||||||
|
onClick={step}>
|
||||||
|
step
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
let StartButton = () => (
|
||||||
|
<div className="controlbutton"
|
||||||
|
onClick={() => {
|
||||||
|
start(level, code, level.playerPos)
|
||||||
|
toggleAutoStep()
|
||||||
|
step()
|
||||||
|
}}>
|
||||||
|
run
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
let ResetButton = () => (
|
||||||
|
<div className="controlbutton"
|
||||||
|
onClick={() => game.reset()}>
|
||||||
|
reset
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
let ToggleSpeedButton = () => (
|
||||||
|
<div className="controlbutton"
|
||||||
|
onClick={toggleSpeed}>
|
||||||
|
{fast ? "slower" : "faster"}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
let AbortButton = () => (
|
||||||
|
<div className="controlbutton"
|
||||||
|
onClick={() => console.log("TODO")}>
|
||||||
|
abort
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (running) {
|
||||||
|
if (autostep) {
|
||||||
|
return (
|
||||||
|
<div className="controlbuttons">
|
||||||
|
<ToggleSpeedButton />
|
||||||
|
<AbortButton />
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="controlbuttons">
|
||||||
|
<StepButton />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finished) {
|
||||||
|
return (<div className="controlbuttons">
|
||||||
|
<ResetButton />
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
return (<div className="controlbuttons">
|
||||||
|
<StartButton />
|
||||||
|
<DebugButton />
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface EditorProps {
|
||||||
|
level: LevelInitializer,
|
||||||
|
code?: Array<CodeBlockContainer>,
|
||||||
|
init: typeof initCodeBlocks,
|
||||||
|
insert: typeof insertCodeBlock,
|
||||||
|
remove: typeof removeCodeBlock,
|
||||||
|
setDropZoneZIdx: (n: number) => void
|
||||||
|
dropZoneZIdx: number,
|
||||||
|
start: (level: LevelInitializer, code: Array<Array<string>>, playerPos: IPlayerData) => void,
|
||||||
|
running: boolean,
|
||||||
|
step: () => void,
|
||||||
|
toggleSpeed: () => void,
|
||||||
|
autostep: boolean,
|
||||||
|
fast: boolean,
|
||||||
|
toggleAutoStep: () => void,
|
||||||
|
finished: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class Editor extends React.Component<EditorProps> {
|
||||||
|
|
||||||
|
constructor(props: EditorProps) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
let { init, level } = props
|
||||||
|
// console.log(props)
|
||||||
|
init(level)
|
||||||
|
// initCodeBlocks(props.level)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// console.log("YES IT MOUNTS")
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// console.log("ME NOW UNMOUNT!")
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="editor">
|
||||||
|
<div className="titlebar">
|
||||||
|
<h2 className="levelname">{this.props.level.name}</h2>
|
||||||
|
|
||||||
|
<RuntimeControls
|
||||||
|
start={this.props.start}
|
||||||
|
fast={this.props.fast}
|
||||||
|
running={this.props.running}
|
||||||
|
level={this.props.level}
|
||||||
|
code={this.props.code}
|
||||||
|
step={this.props.step}
|
||||||
|
autostep={this.props.autostep}
|
||||||
|
toggleAutoStep={this.props.toggleAutoStep}
|
||||||
|
toggleSpeed={this.props.toggleSpeed}
|
||||||
|
finished={this.props.finished} />
|
||||||
|
</div>
|
||||||
|
<FnContainers code={this.props.code}
|
||||||
|
dropZoneZIdx={this.props.dropZoneZIdx}
|
||||||
|
setDropZoneZIdx={this.props.setDropZoneZIdx}
|
||||||
|
remove={this.props.remove}
|
||||||
|
insert={this.props.insert} />
|
||||||
|
<OpsPalette ops={this.props.level.allowedCodeBlocks}
|
||||||
|
setDropZoneZIdx={this.props.setDropZoneZIdx} />
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState, ownProps?: { level: LevelInitializer }) => {
|
||||||
|
// console.log(ownProps)
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
code: state.codeBlocks,
|
||||||
|
dropZoneZIdx: state.opDropZoneZIdx,
|
||||||
|
running: state.executionState.running,
|
||||||
|
autostep: state.executionState.autostep,
|
||||||
|
finished: state.executionState.finished,
|
||||||
|
fast: state.executionState.fast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
init: (level: LevelInitializer) => dispatch({ type: INIT_CODEBLOCKS, level: level }),
|
||||||
|
insert: (cb: CodeBlock, cidx: number, bidx: number, ov: boolean) =>
|
||||||
|
dispatch({ type: INSERT_CODEBLOCK, block: cb, containerIdx: cidx, blocksIdx: bidx, overwrite: ov }),
|
||||||
|
remove: (cidx: number, bidx: number) => {
|
||||||
|
dispatch({
|
||||||
|
type: REMOVE_CODEBLOCK,
|
||||||
|
containerIdx: cidx, blocksIdx: bidx
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setDropZoneZIdx: (idx: number) => {
|
||||||
|
dispatch({ type: SET_DROPZONE_Z_INDEX, zIdx: idx })
|
||||||
|
},
|
||||||
|
start: (level: LevelInitializer, code: Array<Array<string>>, playerPos: IPlayerData) => {
|
||||||
|
dispatch({ type: START_EXECUTING, level: level, code: code, playerPos: playerPos })
|
||||||
|
},
|
||||||
|
step: () => dispatch({ type: STEP_EXECUTION }),
|
||||||
|
toggleAutoStep: () => dispatch({ type: TOGGLE_AUTOSTEP }),
|
||||||
|
toggleSpeed: () => dispatch({ type: TOGGLE_SPEED })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Editor);
|
||||||
|
|
||||||
|
|
||||||
159
src/components/GameScreen.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import Modal from 'react-responsive-modal';
|
||||||
|
import LevelScene from "./LevelScene";
|
||||||
|
import Editor from "./Editor";
|
||||||
|
import { levels, LevelInitializer } from "../game/levels";
|
||||||
|
// import { Redirect } from "react-router-dom"
|
||||||
|
import { Route, Link, useParams } from "react-router-dom";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import { bindActionCreators } from "redux";
|
||||||
|
import TalkyFace from './talkyFace'
|
||||||
|
import { openModal, closeModal } from "../store/gameScreenState/slice";
|
||||||
|
import game from "../game/game"
|
||||||
|
import { setLevel, nextLevel } from "../store/level/slice";
|
||||||
|
import { RESET_EXECUTION } from "../store/executionState/types";
|
||||||
|
|
||||||
|
const MODAL_STYLES = {
|
||||||
|
'overlay': { background: '#11111166' },
|
||||||
|
'modal': {
|
||||||
|
background: '#555',
|
||||||
|
width: '40rem',
|
||||||
|
height: '25rem',
|
||||||
|
borderRadius: '1rem'
|
||||||
|
},
|
||||||
|
'closeButton': {},
|
||||||
|
'closeIcon': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpeechBubble = ({ message }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||||
|
<svg height="180px" width="50">
|
||||||
|
<polygon points="0,100 50,100 50,150"
|
||||||
|
style={{ fill: "#ffe" }} />
|
||||||
|
</svg>
|
||||||
|
<div style={{
|
||||||
|
height: "110px",
|
||||||
|
minWidth: "200px",
|
||||||
|
maxWidth: "300px",
|
||||||
|
padding: "1.5rem",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-around",
|
||||||
|
background: "#ffe",
|
||||||
|
// textAlign: "center",
|
||||||
|
fontSize: "2em",
|
||||||
|
borderRadius: "0.3rem",
|
||||||
|
fontFamily: 'iosevka'
|
||||||
|
}}>
|
||||||
|
<p>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntroMessage = ({ name, message }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ height: "22rem", display: "flex", flexDirection: "column", justifyContent: "space-around" }}>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "row", justifyContent: "center" }}>
|
||||||
|
<TalkyFace />
|
||||||
|
<SpeechBubble message={message.message} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const WonMessage = ({ nextRoute }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ height: "22rem", display: "flex", flexDirection: "column", justifyContent: "space-around" }}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "row", justifyContent: "center" }}>
|
||||||
|
<TalkyFace />
|
||||||
|
<SpeechBubble message={"Good Job!"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link to={'/levels/'}>to levels</Link>
|
||||||
|
<Link to={nextRoute}> next</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameScreen = ({ nextLevel, won, open, openModal, closeModal, setLevel, resetState }) => {
|
||||||
|
// first set level to levelname in url params
|
||||||
|
const { name } = useParams();
|
||||||
|
let levelIdx = levels.findIndex((l) => name == l.name)
|
||||||
|
levelIdx = levelIdx === -1 ? 0 : levelIdx; // first level when no name matches
|
||||||
|
let level = levels[levelIdx]
|
||||||
|
setLevel(level, levelIdx)
|
||||||
|
// resetState() // introduces bug when called after won
|
||||||
|
|
||||||
|
// when there is no next level route to WonScreen
|
||||||
|
let nextRoute = levelIdx === levels.length - 1 ? '/won' : '/level/' + levels[levelIdx + 1].name;
|
||||||
|
|
||||||
|
let [idxMsgs, setMsgIdx] = useState(level.introMessages && level.introMessages.length > 0 ? 0 : null);
|
||||||
|
console.log("WON:", won)
|
||||||
|
|
||||||
|
// Auto-open modal for intro messages when level starts or when won
|
||||||
|
useEffect(() => {
|
||||||
|
// Auto-open modal for intro messages when level starts
|
||||||
|
if (level.introMessages && level.introMessages.length > 0 && idxMsgs !== null && !open && !won) {
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-open modal when won
|
||||||
|
if (won && !open) {
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
}, [idxMsgs, won, open, level.introMessages]); // Proper dependencies
|
||||||
|
|
||||||
|
useEffect(() => game.init(level), []); // only called at creation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game">
|
||||||
|
<LevelScene level={level}></LevelScene>
|
||||||
|
<Editor level={level}></Editor>
|
||||||
|
{createPortal(
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => closeModal()} // Only close, no resetState
|
||||||
|
styles={MODAL_STYLES}
|
||||||
|
center>
|
||||||
|
{won ? <WonMessage nextRoute={nextRoute} />
|
||||||
|
: (level.introMessages !== null) ? <IntroMessage name={level.name} message={level.introMessages[idxMsgs]} /> : ""
|
||||||
|
}
|
||||||
|
</Modal>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState, ownprops?) => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownprops,
|
||||||
|
won: state.executionState.won,
|
||||||
|
open: state.gameScreenState.modalOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
openModal: () => dispatch(openModal()),
|
||||||
|
closeModal: () => dispatch(closeModal()),
|
||||||
|
// nextLevel: game.nextLevel,
|
||||||
|
setLevel: (level, idx) => dispatch(setLevel({ level, idx })),
|
||||||
|
resetState: () => dispatch({ type: RESET_EXECUTION })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(GameScreen);
|
||||||
201
src/components/LevelPreview.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import { AppState } from '../store';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Voxel, DEFAULT_MATERIAL, SWITCHED_ON_MATERIAL, SWITCHED_OFF_MATERIAL } from '../game/voxel';
|
||||||
|
import { LevelInitializer } from '../game/levels';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface LevelPreviewProps {
|
||||||
|
level: LevelInitializer;
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class LevelPreview extends Component<LevelPreviewProps> {
|
||||||
|
camera: THREE.OrthographicCamera;
|
||||||
|
controls: OrbitControls;
|
||||||
|
renderer: THREE.WebGLRenderer;
|
||||||
|
scene: THREE.Scene;
|
||||||
|
mount: HTMLElement;
|
||||||
|
frameId: number;
|
||||||
|
clock: THREE.Clock;
|
||||||
|
|
||||||
|
constructor(props: LevelPreviewProps) {
|
||||||
|
super(props);
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init = () => {
|
||||||
|
let width = 6;
|
||||||
|
let height = 6;
|
||||||
|
let w = 96;
|
||||||
|
let h = 96;
|
||||||
|
this.camera = new THREE.OrthographicCamera(width / - 2, width / 2, height / 2, height / - 2, 1, 30);
|
||||||
|
this.camera.position.set(0, -1, 2);
|
||||||
|
// this.camera.lookAt(1, 1, 0)
|
||||||
|
this.camera.up.set(0, 0, 1);
|
||||||
|
|
||||||
|
// RENDERER
|
||||||
|
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
this.renderer.setSize(w, h);
|
||||||
|
|
||||||
|
|
||||||
|
this.renderer.shadowMap.enabled = true;
|
||||||
|
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0x555555);
|
||||||
|
|
||||||
|
// LIGHTS
|
||||||
|
this.scene.add(new THREE.HemisphereLight(0x443333, 0x111122,30));
|
||||||
|
|
||||||
|
let pl = new THREE.PointLight(0xFFFFFFF, 100, 0,0.3);
|
||||||
|
pl.position.set(50, -50, 10);
|
||||||
|
this.scene.add(pl);
|
||||||
|
|
||||||
|
let pl1 = new THREE.PointLight(0xFFFFFFF, 10, 0,0.3);
|
||||||
|
pl1.position.set(-50, 50, 50);
|
||||||
|
this.scene.add(pl1);
|
||||||
|
// camera controller
|
||||||
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
this.controls.enabled = false;
|
||||||
|
this.controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
||||||
|
this.controls.autoRotateSpeed = 5;
|
||||||
|
this.controls.autoRotate = true;
|
||||||
|
this.controls.dampingFactor = 0.25;
|
||||||
|
this.controls.screenSpacePanning = false;
|
||||||
|
this.controls.minDistance = 8;
|
||||||
|
this.controls.maxDistance = 13;
|
||||||
|
// this.controls.minPolarAngle = -Math.PI;
|
||||||
|
this.controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
||||||
|
|
||||||
|
|
||||||
|
let middle = getMeanVoxelPos(this.props.level.voxels)
|
||||||
|
|
||||||
|
this.controls.target = new THREE.Vector3(middle.x, middle.y, 0)
|
||||||
|
// const axesHelper = new THREE.AxesHelper(5);
|
||||||
|
// axesHelper.position.set(middle.x, middle.y, 0)
|
||||||
|
// this.scene.add(axesHelper);
|
||||||
|
|
||||||
|
initScene(this.scene, this.props.level.voxels);
|
||||||
|
|
||||||
|
this.controls.update()
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
|
||||||
|
if (this.props.active) {
|
||||||
|
this.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
animate = () => {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate);
|
||||||
|
this.controls.update()
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimating = () => {
|
||||||
|
if (!this.frameId) {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAnimating = () => {
|
||||||
|
cancelAnimationFrame(this.frameId);
|
||||||
|
this.frameId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.mount.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
compoentWillUnmount() {
|
||||||
|
this.stopAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: LevelPreviewProps) {
|
||||||
|
if (prevProps.active && !this.props.active) this.stopAnimating()
|
||||||
|
if (!prevProps.active && this.props.active) this.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (<div
|
||||||
|
className="LevelPreview"
|
||||||
|
ref={(mount: HTMLElement) => { this.mount = mount }}
|
||||||
|
|
||||||
|
// style={{
|
||||||
|
// width: "200px", height: "200px", display: "block"
|
||||||
|
// }}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMeanVoxelPos(voxels: Array<Voxel>) {
|
||||||
|
let vsum = voxels.reduce((p, c, i, a) => { return { x: p.x + c.x, y: p.y + c.y, z: 0 } });
|
||||||
|
return { x: vsum.x / voxels.length, y: vsum.y / voxels.length, z: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initScene(scene: THREE.Scene, voxels: Array<Voxel>) {
|
||||||
|
const h = 1;
|
||||||
|
voxels.map((v, idx) => {
|
||||||
|
let { x, y, z } = v
|
||||||
|
var geometry = new THREE.BoxGeometry(h, h, h);
|
||||||
|
// geometry.translate(-meanX, -meanY, (0.5) * h);
|
||||||
|
geometry.translate(0, 0, (0.5) * h);
|
||||||
|
|
||||||
|
let material = DEFAULT_MATERIAL();
|
||||||
|
if (v.actionable) {
|
||||||
|
switch (v.actionable.type) {
|
||||||
|
case "BUTTON":
|
||||||
|
material = v.actionable.pushed ? SWITCHED_ON_MATERIAL()
|
||||||
|
: SWITCHED_OFF_MATERIAL();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
let mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.castShadow = true;
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
mesh.position.set(x, y, z);
|
||||||
|
scene.add(mesh);
|
||||||
|
})
|
||||||
|
initGround(scene)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function initGround(scene: THREE.Scene) {
|
||||||
|
const geometry = new THREE.PlaneGeometry(40, 40);
|
||||||
|
|
||||||
|
// geometry.rotateX(Math.PI * -0.5)
|
||||||
|
const material = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x030211,
|
||||||
|
specular: 0x0,
|
||||||
|
shininess: 0x0
|
||||||
|
});
|
||||||
|
const plane = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(plane)
|
||||||
|
var grid = new THREE.GridHelper(40, 20, 0x99bbff, 0x99bbff);
|
||||||
|
grid.rotateX(Math.PI * 0.5);
|
||||||
|
grid.position.setX(0.5);
|
||||||
|
grid.position.setY(0.5);
|
||||||
|
// console.log(grid.material)
|
||||||
|
(grid.material as THREE.LineBasicMaterial).opacity = 0.2;
|
||||||
|
(grid.material as THREE.LineBasicMaterial).transparent = true;
|
||||||
|
scene.add(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
modalOpen: state.gameScreenState.modalOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(LevelPreview);
|
||||||
82
src/components/LevelScene.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import game from "../game/game"
|
||||||
|
import { AppState } from '../store';
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { STEP_EXECUTION, ExecutionState } from '../store/executionState/types';
|
||||||
|
import { LevelInitializer } from '../game/levels';
|
||||||
|
|
||||||
|
|
||||||
|
interface LevelSceneProps {
|
||||||
|
execState: ExecutionState,
|
||||||
|
fast: boolean,
|
||||||
|
step: () => void
|
||||||
|
level: LevelInitializer
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LevelScene extends Component<LevelSceneProps> {
|
||||||
|
mount: HTMLElement;
|
||||||
|
retryTimer: number | null = null;
|
||||||
|
retryCount: number = 0;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('resize', game.onWindowResize, false);
|
||||||
|
this.mount.addEventListener('resize', game.onWindowResize, false);
|
||||||
|
|
||||||
|
// Simplified initialization without visual corruption
|
||||||
|
const initializeGame = () => {
|
||||||
|
if (game.canStart()) {
|
||||||
|
console.log('Player ready, starting game');
|
||||||
|
game.start();
|
||||||
|
game.onWindowResize();
|
||||||
|
this.mount.appendChild(game.renderer.domElement);
|
||||||
|
} else {
|
||||||
|
// Single retry attempt after delay
|
||||||
|
setTimeout(initializeGame, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
game.stop()
|
||||||
|
if (this.retryTimer) {
|
||||||
|
clearInterval(this.retryTimer);
|
||||||
|
this.retryTimer = null;
|
||||||
|
}
|
||||||
|
if (this.mount.contains(game.renderer.domElement)) {
|
||||||
|
this.mount.removeChild(game.renderer.domElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevprops: LevelSceneProps) {
|
||||||
|
if (prevprops.execState.instructionPtr !== this.props.execState.instructionPtr
|
||||||
|
&& this.props.execState.running) {
|
||||||
|
let ins = this.props.execState.instruction
|
||||||
|
if (ins) {
|
||||||
|
game.player[ins]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let { running } = this.props.execState
|
||||||
|
return (
|
||||||
|
<div className="levelScene" ref={(mount: HTMLElement) => { this.mount = mount }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState, ownprops?) => {
|
||||||
|
|
||||||
|
return { ...ownprops, execState: state.executionState }
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
step: () => { dispatch({ type: STEP_EXECUTION }) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(LevelScene)
|
||||||
|
|
||||||
69
src/components/LevelSelectScreen.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { levels } from "../game/levels";
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
import LevelPreview from "./LevelPreview"
|
||||||
|
|
||||||
|
|
||||||
|
const LevelItem = ({ level, idx, cb, active, unlocked }) => {
|
||||||
|
return (
|
||||||
|
<div style={{display:'flex', flexDirection:'row'}}>
|
||||||
|
<div className="levelItem"
|
||||||
|
onClick={unlocked ? cb : ()=>{} }
|
||||||
|
style={{background: active ? '#9bf': '#79d',
|
||||||
|
filter: !unlocked ? 'grayscale(70%)': 'none'}}>
|
||||||
|
<LevelPreview level={level} active={active} />
|
||||||
|
<p style={{fontSize:'1.5em'}}>{idx + 1+": "+level.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
const LevelSelectScreen = ({saveState}) =>{
|
||||||
|
const [activeIdx, setActiveIdx] = useState(0);
|
||||||
|
const unlockedState: Array<boolean> = [true, true];
|
||||||
|
// const levelSaveState = store.get
|
||||||
|
for(let i = 2; i < saveState.levels.length; ++i){
|
||||||
|
let {done} = saveState.levels[i];
|
||||||
|
unlockedState.push((!done && saveState.levels[i-1].done) || done)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{padding:'1rem'}}>
|
||||||
|
<div className="levelItems">
|
||||||
|
<h1 className="levelSelectCaption">select level:</h1>
|
||||||
|
{levels.map((level, idx) => {
|
||||||
|
let active = (idx === activeIdx);
|
||||||
|
// let locked =
|
||||||
|
return <LevelItem level={level}
|
||||||
|
idx={idx}
|
||||||
|
cb={()=>setActiveIdx(idx)}
|
||||||
|
active={active}
|
||||||
|
key={idx}
|
||||||
|
unlocked={unlockedState[idx]}
|
||||||
|
/>
|
||||||
|
})}
|
||||||
|
<Link to={'/level/' + levels[activeIdx].name}>start</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState, ownprops?) => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownprops,
|
||||||
|
saveState: state.saveState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
// openModal: () => dispatch({ type: OPEN_MODAL })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(LevelSelectScreen);
|
||||||
|
|
||||||
44
src/components/Root.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import StartScreen from "./StartScreen"
|
||||||
|
import GameScreen from "./GameScreen"
|
||||||
|
import LevelSelectScreen from "./LevelSelectScreen"
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
// import { BrowserRouter as Router, Route } from 'react-router-dom'
|
||||||
|
import { HashRouter as Router, Routes, Route } from 'react-router-dom'
|
||||||
|
|
||||||
|
const Root = ({ store }) => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<StartScreen />} />
|
||||||
|
<Route path="/levels" element={<LevelSelectScreen />} />
|
||||||
|
<Route path="/level/:name" element={<GameScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// const Root = ({ screen }) => {
|
||||||
|
// switch (screen) {
|
||||||
|
// case SET_STARTSCREEN:
|
||||||
|
// return <StartScreen></StartScreen>;
|
||||||
|
// case SET_LEVELSELECTSCREEN:
|
||||||
|
// return <LevelSelectScreen></LevelSelectScreen>;
|
||||||
|
// default:
|
||||||
|
// return <StartScreen></StartScreen>;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const mapStateToProps = (state: AppState) => {
|
||||||
|
// return {
|
||||||
|
// screen: state.screen
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const mapDispatchToProps = dispatch => {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default Root
|
||||||
13
src/components/SettingsScreen.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const SettingsScreen = () => (
|
||||||
|
<div className="start_container">
|
||||||
|
<img src="/assets/screenelements/gamename.png" />
|
||||||
|
<Link to="/levels">
|
||||||
|
<img src="/assets/screenelements/startlevels.png" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SettingsScreen;
|
||||||
16
src/components/StartScreen.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const StartScreen = () => (
|
||||||
|
<div className="startContainer">
|
||||||
|
<img className="gameName" src="/assets/textures/gamename.png" />
|
||||||
|
<Link to="/levels">
|
||||||
|
<div className="startButton">Levels</div>
|
||||||
|
</Link>
|
||||||
|
<Link to="/settings">
|
||||||
|
<div className="startButton">Settings</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default StartScreen;
|
||||||
42
src/components/WonScreen.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
|
const WonScreen = ({ saveState }) => {
|
||||||
|
const [activeIdx, setActiveIdx] = useState(0);
|
||||||
|
let actuallyWon = false;
|
||||||
|
// const levelSaveState = store.get
|
||||||
|
for (let i = 2; i < saveState.levels.length; ++i) {
|
||||||
|
let { done } = saveState.levels[i];
|
||||||
|
actuallyWon = actuallyWon && done;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '1rem' }}>
|
||||||
|
<div className="levelItems">
|
||||||
|
<h1 className="levelSelectCaption">select level:</h1>
|
||||||
|
<Link to={'/levels'}>back</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState, ownprops?) => {
|
||||||
|
return {
|
||||||
|
...ownprops,
|
||||||
|
saveState: state.saveState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
// openModal: () => dispatch({ type: OPEN_MODAL })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(WonScreen);
|
||||||
|
|
||||||
135
src/components/talkyFace.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
import { AppState } from '../store';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
|
interface TalkyFaceProps {
|
||||||
|
modalOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class TalkyFace extends Component<TalkyFaceProps> {
|
||||||
|
camera: THREE.PerspectiveCamera;
|
||||||
|
renderer: THREE.WebGLRenderer;
|
||||||
|
scene: THREE.Scene;
|
||||||
|
mount: HTMLElement;
|
||||||
|
animations: THREE.AnimationClip[];
|
||||||
|
mixer: THREE.AnimationMixer;
|
||||||
|
model: THREE.Group;
|
||||||
|
actions: {};
|
||||||
|
frameId: number;
|
||||||
|
clock: THREE.Clock;
|
||||||
|
|
||||||
|
constructor(props: TalkyFaceProps) {
|
||||||
|
super(props);
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init = () => {
|
||||||
|
this.camera = new THREE.PerspectiveCamera(55, 1, 1, 3000);
|
||||||
|
this.camera.position.set(0, 2, -1);
|
||||||
|
this.camera.lookAt(0, 0, -1)
|
||||||
|
// this.camera.up.set(0, 0, 1);
|
||||||
|
|
||||||
|
// RENDERER
|
||||||
|
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
this.renderer.setSize(200, 200);
|
||||||
|
|
||||||
|
|
||||||
|
this.renderer.shadowMap.enabled = true;
|
||||||
|
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0x555555);
|
||||||
|
|
||||||
|
// LIGHTS
|
||||||
|
this.scene.add(new THREE.HemisphereLight(0x443333, 0x111122,30));
|
||||||
|
|
||||||
|
let pl = new THREE.PointLight(0xFFFFFFF, 1, 1000);
|
||||||
|
pl.position.set(50, -50, 10);
|
||||||
|
this.scene.add(pl);
|
||||||
|
|
||||||
|
let pl1 = new THREE.PointLight(0xFFFFFFF, 1, 1000);
|
||||||
|
pl1.position.set(-50, 50, 50);
|
||||||
|
this.scene.add(pl1);
|
||||||
|
|
||||||
|
// ROBOT MODEL
|
||||||
|
this.clock = new THREE.Clock()
|
||||||
|
let loader = new GLTFLoader();
|
||||||
|
loader.load('assets/models/RobotExpressive.glb', (gltf) => {
|
||||||
|
this.model = gltf.scene;
|
||||||
|
this.animations = gltf.animations;
|
||||||
|
this.mixer = new THREE.AnimationMixer(this.model)
|
||||||
|
this.actions = {};
|
||||||
|
for (var i = 0; i < this.animations.length; i++) {
|
||||||
|
var clip = this.animations[i];
|
||||||
|
var action = this.mixer.clipAction(clip);
|
||||||
|
this.actions[clip.name] = action;
|
||||||
|
if (clip.name != "Idle") action.repetitions = 1
|
||||||
|
}
|
||||||
|
// this.actions["Idle"].play()
|
||||||
|
this.actions["ThumbsUp"].timeScale = 0.2
|
||||||
|
console.log(this.actions["ThumbsUp"])
|
||||||
|
this.actions["ThumbsUp"].play();
|
||||||
|
this.model.children[0].rotation.x = Math.PI * 0.5;
|
||||||
|
this.model.children[0].rotation.y = Math.PI - 0.3;
|
||||||
|
this.model.children[0].rotation.z = Math.PI;
|
||||||
|
this.model.position.setZ(-0.3)
|
||||||
|
this.model.scale.set(0.3, 0.3, 0.3)
|
||||||
|
this.scene.add(this.model);
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
animate = () => {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate);
|
||||||
|
var dt = this.clock.getDelta();
|
||||||
|
if (this.mixer) this.mixer.update(dt);
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimating = () => {
|
||||||
|
if (!this.frameId) {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopAnimating = () => {
|
||||||
|
cancelAnimationFrame(this.frameId);
|
||||||
|
this.frameId = 0;
|
||||||
|
console.log("now talkyface should have stopped rendering")
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.mount.appendChild(this.renderer.domElement);
|
||||||
|
this.startAnimating();
|
||||||
|
}
|
||||||
|
|
||||||
|
compoentWillUnmount() {
|
||||||
|
this.stopAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: TalkyFaceProps) {
|
||||||
|
if (prevProps.modalOpen && !this.props.modalOpen) this.stopAnimating()
|
||||||
|
if (!prevProps.modalOpen && this.props.modalOpen) this.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (<div
|
||||||
|
className="TalkyFace"
|
||||||
|
ref={(mount: HTMLElement) => { this.mount = mount }}
|
||||||
|
style={{
|
||||||
|
width: "200px", height: "200px", display: "block"
|
||||||
|
}} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
modalOpen: state.gameScreenState.modalOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(TalkyFace);
|
||||||
331
src/game/game.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import store from "../store/store"
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import Stats from 'three/examples/jsm/libs/stats.module.js';
|
||||||
|
// import { keyframes } from "popmotion";
|
||||||
|
import { Voxel, DEFAULT_MATERIAL, SWITCHED_ON_MATERIAL, SWITCHED_OFF_MATERIAL } from '../game/voxel';
|
||||||
|
import { Player } from '../game/player';
|
||||||
|
import { LevelInitializer, levels } from "./levels";
|
||||||
|
import { STEP_EXECUTION, RESET_EXECUTION } from "../store/executionState/types";
|
||||||
|
import { ImprovedNoise } from 'three/examples/jsm/math/ImprovedNoise.js';
|
||||||
|
import { nextLevel } from "../store/level/slice";
|
||||||
|
import { INIT_CODEBLOCKS } from "../store/codeBlocks/types";
|
||||||
|
import { openModal, closeModal } from "../store/gameScreenState/slice";
|
||||||
|
import { SAVESTATE, SaveState, SET_LEVEL_DONE } from "../store/saveState/types";
|
||||||
|
// import { LevelInitializer } from '../game/levels';
|
||||||
|
// import { meanBy } from "lodash"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface ThreeScene {
|
||||||
|
camera: THREE.PerspectiveCamera,
|
||||||
|
scene: THREE.Scene,
|
||||||
|
renderer: THREE.WebGLRenderer,
|
||||||
|
controls: OrbitControls,
|
||||||
|
frameId: number,
|
||||||
|
stats: Stats
|
||||||
|
}
|
||||||
|
|
||||||
|
class Game implements ThreeScene {
|
||||||
|
camera: THREE.PerspectiveCamera;
|
||||||
|
scene: THREE.Scene;
|
||||||
|
renderer: THREE.WebGLRenderer;
|
||||||
|
controls: OrbitControls;
|
||||||
|
frameId: number;
|
||||||
|
stats: Stats;
|
||||||
|
player: Player;
|
||||||
|
levelIdx: number;
|
||||||
|
unsibscribe: () => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 3000);
|
||||||
|
this.camera.position.set(-40, 40, 30);
|
||||||
|
this.camera.up.set(0, 0, 1);
|
||||||
|
|
||||||
|
// renderer
|
||||||
|
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
// Enhanced rendering settings for atmospheric lighting
|
||||||
|
this.renderer.shadowMap.enabled = true;
|
||||||
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
this.renderer.toneMappingExposure = 1.2;
|
||||||
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
|
// camera controller
|
||||||
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
this.controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
||||||
|
// this.controls.autoRotateSpeed = 0.1;
|
||||||
|
// this.controls.autoRotate = true;
|
||||||
|
this.controls.dampingFactor = 0.25;
|
||||||
|
this.controls.screenSpacePanning = false;
|
||||||
|
this.controls.minDistance = 8;
|
||||||
|
this.controls.maxDistance = 13;
|
||||||
|
// this.controls.minPolarAngle = -Math.PI;
|
||||||
|
this.controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
||||||
|
// stats
|
||||||
|
this.stats = new Stats();
|
||||||
|
this.levelIdx = store.getState().level.idx;
|
||||||
|
this.unsibscribe = store.subscribe(this.handleLevelChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLevelChange = () => {
|
||||||
|
let state = store.getState();
|
||||||
|
let levelIdx = state.level.idx;
|
||||||
|
if (levelIdx != this.levelIdx) {
|
||||||
|
this.levelIdx = levelIdx;
|
||||||
|
console.log("Game initializing new level now", levelIdx, this.levelIdx);
|
||||||
|
this.init(state.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init = (lev: LevelInitializer) => {
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0x0a0a12); // Dark blue-gray instead of pure black
|
||||||
|
|
||||||
|
// Groundplane
|
||||||
|
initGround(this.scene);
|
||||||
|
|
||||||
|
let level = lev
|
||||||
|
|
||||||
|
// Create mutable copy of voxels for both Player and initBoxes
|
||||||
|
const mutableVoxels = level.voxels.map(v => ({ ...v }));
|
||||||
|
|
||||||
|
this.player = new Player(this.scene, { ...level, voxels: mutableVoxels })
|
||||||
|
initBoxes(this.scene, mutableVoxels, this.player);
|
||||||
|
|
||||||
|
// Lights
|
||||||
|
// Subtle ambient light for base visibility
|
||||||
|
this.scene.add(new THREE.AmbientLight(0x2a2a3a, 2.4));
|
||||||
|
|
||||||
|
// Improved hemisphere light for mood
|
||||||
|
this.scene.add(new THREE.HemisphereLight(0x4a4a5a, 0x1a1a2a, 10));
|
||||||
|
|
||||||
|
// Dramatic point lights with better positioning
|
||||||
|
let pl = new THREE.PointLight(0x8090ff, 10, 0,0.3);
|
||||||
|
pl.position.set(40, -40, 25);
|
||||||
|
pl.castShadow = true;
|
||||||
|
this.scene.add(pl);
|
||||||
|
let pl1 = new THREE.PointLight(0xff9080, 10, 0,0.3);
|
||||||
|
pl1.position.set(-40, 40, 25);
|
||||||
|
pl1.castShadow = true;
|
||||||
|
this.scene.add(pl1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
step = () => {
|
||||||
|
let state = store.getState()
|
||||||
|
if (state.executionState.autostep && state.executionState.running) {
|
||||||
|
store.dispatch({ "type": STEP_EXECUTION })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
animate = () => {
|
||||||
|
requestAnimationFrame(this.animate);
|
||||||
|
this.controls.update();
|
||||||
|
if (this.player?.ready) {
|
||||||
|
this.player.update()
|
||||||
|
}
|
||||||
|
this.stats.update();
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
start = () => {
|
||||||
|
if (!this.frameId && this.player?.ready) {
|
||||||
|
this.frameId = requestAnimationFrame(this.animate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canStart = () => {
|
||||||
|
return this.player?.ready === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop = () => {
|
||||||
|
cancelAnimationFrame(this.frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset = () => {
|
||||||
|
this.player.reset()
|
||||||
|
store.dispatch({ type: RESET_EXECUTION })
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowResize = () => {
|
||||||
|
// conso e.log(this);
|
||||||
|
let w = Math.max(window.innerWidth * 0.6, 300);
|
||||||
|
if (this.camera) {
|
||||||
|
this.camera.aspect = w / window.innerHeight;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
this.renderer.setSize(w, window.innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLevel = () => {
|
||||||
|
store.dispatch(nextLevel());
|
||||||
|
let state = store.getState();
|
||||||
|
store.dispatch({ type: INIT_CODEBLOCKS, level: state.level });
|
||||||
|
store.dispatch({ type: RESET_EXECUTION });
|
||||||
|
if (!(state.saveState.showTips && state.level.introMessages)) store.dispatch(closeModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = new Game()
|
||||||
|
export default game
|
||||||
|
|
||||||
|
|
||||||
|
function initBoxes(scene: THREE.Scene, voxels: Array<Voxel>, player: Player) {
|
||||||
|
const h = 1;
|
||||||
|
let ctx = 0;
|
||||||
|
// const off = 2
|
||||||
|
|
||||||
|
// var texture = new THREE.TextureLoader().load('assets/textures/crate.gif');
|
||||||
|
// var material = new THREE.MeshBasicMaterial({ map: texture });
|
||||||
|
// let meanX = meanBy(voxels, (v) => v.x)
|
||||||
|
// let meanY = meanBy(voxels, (v) => v.y)
|
||||||
|
|
||||||
|
voxels.forEach((v, idx) => {
|
||||||
|
let { x, y, z } = v
|
||||||
|
var geometry = new THREE.BoxGeometry(h, h, h);
|
||||||
|
// geometry.translate(-meanX, -meanY, (0.5) * h);
|
||||||
|
geometry.translate(0, 0, (0.5) * h);
|
||||||
|
|
||||||
|
let material = DEFAULT_MATERIAL();
|
||||||
|
if (v.actionable) {
|
||||||
|
switch (v.actionable.type) {
|
||||||
|
case "BUTTON":
|
||||||
|
material = v.actionable.pushed ? SWITCHED_ON_MATERIAL()
|
||||||
|
: SWITCHED_OFF_MATERIAL();
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
let mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.castShadow = true;
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
mesh.position.set(x, y, z);
|
||||||
|
console.log(player.voxelStandingOn)
|
||||||
|
if (idx != player.level.playerPos.voxelIdx) {
|
||||||
|
mesh.position.setZ(10)
|
||||||
|
setTimeout(() => {
|
||||||
|
// Simplified animation using setTimeout
|
||||||
|
const startZ = mesh.position.z;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const duration = 1500;
|
||||||
|
const animateStep = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
mesh.position.setZ(startZ + (z - startZ) * progress);
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateStep);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animateStep();
|
||||||
|
}, ctx++ * 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the voxel and add the model property
|
||||||
|
const extendedVoxel = { ...v, model: mesh };
|
||||||
|
voxels[idx] = extendedVoxel;
|
||||||
|
scene.add(mesh);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function initGround(scene: THREE.Scene) {
|
||||||
|
var planeGeometry = new THREE.PlaneGeometry(400, 400, 127, 127)
|
||||||
|
planeGeometry.rotateX(Math.PI / -2);
|
||||||
|
var data = generateHeight(128, 128);
|
||||||
|
let vertices = planeGeometry.attributes.position.array;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(vertices.length)
|
||||||
|
for (var i = 0, j = 0, l = vertices.length; i < l; i++ , j += 3) {
|
||||||
|
// console.log(vertices[j + 1])
|
||||||
|
(vertices[j + 1] as number) = data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var planeWire = new THREE.Mesh(planeGeometry,
|
||||||
|
|
||||||
|
new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x320032,
|
||||||
|
specular: 0x0,
|
||||||
|
// shininess: 0xffffff,
|
||||||
|
wireframeLinewidth: 3,
|
||||||
|
wireframe: true
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
var planeSolid = new THREE.Mesh(planeGeometry,
|
||||||
|
|
||||||
|
new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x030211,
|
||||||
|
specular: 0x0,
|
||||||
|
shininess: 0x0,
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
planeWire.position.y = 0;
|
||||||
|
planeWire.rotateX(Math.PI * 0.5)
|
||||||
|
planeSolid.position.y = 0;
|
||||||
|
planeSolid.rotateX(Math.PI * 0.5)
|
||||||
|
planeSolid.receiveShadow = true;
|
||||||
|
|
||||||
|
|
||||||
|
var grid = new THREE.GridHelper(400, 30, 0x99bbff, 0x99bbff);
|
||||||
|
grid.rotateX(Math.PI * 0.5);
|
||||||
|
// grid.position.setZ(0.5);
|
||||||
|
// console.log(grid.material)
|
||||||
|
(grid.material as THREE.LineBasicMaterial).opacity = 0.2;
|
||||||
|
(grid.material as THREE.LineBasicMaterial).transparent = true;
|
||||||
|
|
||||||
|
// scene.add(planeWire);
|
||||||
|
// scene.add(planeSolid);
|
||||||
|
scene.add(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// used to generate terrain
|
||||||
|
function generateHeight(width, height) {
|
||||||
|
var size = width * height, data = new Uint8Array(size),
|
||||||
|
perlin = new ImprovedNoise(), quality = 1, z = Math.random() * 200;
|
||||||
|
for (var j = 0; j < 4; j++) {
|
||||||
|
for (var i = 0; i < size; i++) {
|
||||||
|
var x = i % width, y = ~ ~(i / width);
|
||||||
|
data[i] += Math.abs(perlin.noise(x / quality, y / quality, z) * quality * 1.75);
|
||||||
|
|
||||||
|
}
|
||||||
|
quality *= 5;
|
||||||
|
}
|
||||||
|
for (var j = 0; j < size; j++) {
|
||||||
|
var x = ((j % width) - 64)
|
||||||
|
var y = ((~ ~(j / width)) - 64)
|
||||||
|
data[j] *= Math.sqrt(x * x + y * y) * 0.005
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// check if all button-voxels are currently pressed
|
||||||
|
export const hasWonGame = (player: Player) => {
|
||||||
|
for (let v of player.voxels) {
|
||||||
|
if (v.actionable && v.actionable.type == "BUTTON") {
|
||||||
|
if (!v.actionable.pushed) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let idx = store.getState().level.idx
|
||||||
|
store.dispatch({ type: SET_LEVEL_DONE, levelIdx: idx });
|
||||||
|
saveState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function saveState() {
|
||||||
|
let state = store.getState().saveState
|
||||||
|
localStorage.setItem(SAVESTATE, btoa(JSON.stringify(state)))
|
||||||
|
}
|
||||||
122
src/game/levels.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { CodeBlockContainer } from "../store/codeBlocks/types";
|
||||||
|
import { Voxel, BUTTON_ACTIONABLE } from "./voxel";
|
||||||
|
|
||||||
|
export interface LevelInitializer {
|
||||||
|
name: string;
|
||||||
|
voxels: Array<Voxel>;
|
||||||
|
playerPos: IPlayerData;
|
||||||
|
codeviews: Array<CodeBlockContainer>;
|
||||||
|
allowedCodeBlocks: Array<string>;
|
||||||
|
activeCodeview: string;
|
||||||
|
introMessages: Array<IIntroMessage> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOperationsDict {
|
||||||
|
[key: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICodeview {
|
||||||
|
name: string;
|
||||||
|
nLines: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPlayerData {
|
||||||
|
voxelIdx: number;
|
||||||
|
direction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIntroMessage {
|
||||||
|
message: string;
|
||||||
|
image: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const levels: LevelInitializer[] = [
|
||||||
|
{
|
||||||
|
name: "tutorial",
|
||||||
|
voxels: [{ x: 0, y: 0, z: 0 },
|
||||||
|
{ x: 1, y: 0, z: 0 },
|
||||||
|
{ x: 2, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
|
||||||
|
],
|
||||||
|
playerPos: { voxelIdx: 0, direction: "east" },
|
||||||
|
codeviews: [{
|
||||||
|
name: "main", nMaxBlocks: 3, blocks: [
|
||||||
|
{ name: "forward", containerIndex: 0 },
|
||||||
|
{ name: "forward", containerIndex: 0 },
|
||||||
|
{ name: "action", containerIndex: 0 }]
|
||||||
|
}],
|
||||||
|
activeCodeview: "main",
|
||||||
|
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
|
||||||
|
introMessages: [{ message: "Hit run and watch.", image: null },
|
||||||
|
{ message: "second page of intro message", image: null }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hello world",
|
||||||
|
voxels: [{ x: 0, y: 0, z: 0 },
|
||||||
|
{ x: 1, y: 0, z: 0 },
|
||||||
|
{ x: 2, y: 0, z: 0 },
|
||||||
|
{ x: 3, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
|
||||||
|
{ x: 3, y: 1, z: 0 },
|
||||||
|
{ x: 3, y: 2, z: 0 },
|
||||||
|
{ x: 3, y: 3, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }],
|
||||||
|
playerPos: { voxelIdx: 0, direction: "east" },
|
||||||
|
codeviews: [{
|
||||||
|
name: "main",
|
||||||
|
nMaxBlocks: 3,
|
||||||
|
blocks: [{ name: "f1" }, { name: "turn_left" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "f1", nMaxBlocks: 4, blocks: [{ name: "forward" },
|
||||||
|
{ name: "forward" },
|
||||||
|
{ name: "forward" },
|
||||||
|
{ name: "action" }]
|
||||||
|
}],
|
||||||
|
activeCodeview: "main",
|
||||||
|
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
|
||||||
|
introMessages: [{ message: "light up all dark cubes!", image: null }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jump n run",
|
||||||
|
voxels: [{ x: 0, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
|
||||||
|
{ x: 0, y: 1, z: 1 },
|
||||||
|
{ x: 0, y: 2, z: 1 },
|
||||||
|
{ x: 0, y: 3, z: 0 },
|
||||||
|
{ x: 0, y: 4, z: 0 },
|
||||||
|
{ x: 0, y: 5, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }],
|
||||||
|
playerPos: { voxelIdx: 0, direction: "north" },
|
||||||
|
codeviews: [{ name: "main", nMaxBlocks: 4, blocks: [] },
|
||||||
|
{ name: "f1", nMaxBlocks: 3, blocks: [] }],
|
||||||
|
activeCodeview: "main",
|
||||||
|
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
|
||||||
|
introMessages: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jump n run 2",
|
||||||
|
voxels: [{ x: 0, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
|
||||||
|
{ x: 0, y: 1, z: 1 },
|
||||||
|
{ x: 0, y: 2, z: 1 },
|
||||||
|
{ x: 1, y: 2, z: 0 },
|
||||||
|
{ x: 2, y: 2, z: 0 },
|
||||||
|
{ x: 3, y: 2, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }],
|
||||||
|
playerPos: { voxelIdx: 0, direction: "north" },
|
||||||
|
codeviews: [{ name: "main", nMaxBlocks: 4, blocks: [] },
|
||||||
|
{ name: "f1", nMaxBlocks: 3, blocks: [] }],
|
||||||
|
activeCodeview: "main",
|
||||||
|
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
|
||||||
|
introMessages: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jump n run 3",
|
||||||
|
voxels: [{ x: 0, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
|
||||||
|
{ x: 0, y: 1, z: 1 },
|
||||||
|
{ x: 0, y: 2, z: 1 },
|
||||||
|
{ x: 0, y: 3, z: 0 },
|
||||||
|
{ x: 0, y: 4, z: 0 },
|
||||||
|
{ x: 0, y: 5, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }],
|
||||||
|
playerPos: { voxelIdx: 0, direction: "north" },
|
||||||
|
codeviews: [{ name: "main", nMaxBlocks: 4, blocks: [] },
|
||||||
|
{ name: "f1", nMaxBlocks: 3, blocks: [] }],
|
||||||
|
activeCodeview: "main",
|
||||||
|
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
|
||||||
|
introMessages: null
|
||||||
|
}
|
||||||
|
];
|
||||||
248
src/game/player.ts
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { LevelInitializer } from './levels';
|
||||||
|
import { Voxel, SingleSideNeighbors, buildNeighborhoodMap, buttonVoxelToggle, resetVoxels } from './voxel';
|
||||||
|
// import { keyframes } from "popmotion";
|
||||||
|
import { ExecutionState } from '../store/executionState/types'
|
||||||
|
import game, { hasWonGame } from "./game"
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
import store from "../store/store"
|
||||||
|
import { levelDone } from '../store/saveState/actions';
|
||||||
|
import { wonLevel } from '../store/executionState/actions';
|
||||||
|
|
||||||
|
// interface Player
|
||||||
|
// model control from https://github.com/mrdoob/three.js/blob/master/examples/webgl_animation_skinning_morph.html
|
||||||
|
// var states = ['Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing'];
|
||||||
|
// var emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp'];
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
scene: THREE.Scene;
|
||||||
|
model: THREE.Object3D;
|
||||||
|
mixer: THREE.AnimationMixer;
|
||||||
|
clock: THREE.Clock;
|
||||||
|
direction: string;
|
||||||
|
level: LevelInitializer;
|
||||||
|
voxels: Array<Voxel>;
|
||||||
|
voxelStandingOn: Voxel;
|
||||||
|
ready: boolean = false;
|
||||||
|
actions: {}
|
||||||
|
animations: THREE.AnimationClip[]
|
||||||
|
|
||||||
|
constructor(scene: THREE.Scene, level: LevelInitializer) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.level = level;
|
||||||
|
this.clock = new THREE.Clock()
|
||||||
|
let loader = new GLTFLoader();
|
||||||
|
loader.load('assets/models/RobotExpressive.glb', (gltf) => {
|
||||||
|
this.model = gltf.scene;
|
||||||
|
this.animations = gltf.animations;
|
||||||
|
this.mixer = new THREE.AnimationMixer(this.model)
|
||||||
|
this.actions = {};
|
||||||
|
for (var i = 0; i < this.animations.length; i++) {
|
||||||
|
var clip = this.animations[i];
|
||||||
|
var action = this.mixer.clipAction(clip);
|
||||||
|
this.actions[clip.name] = action;
|
||||||
|
if (clip.name != "Idle") action.repetitions = 1
|
||||||
|
}
|
||||||
|
this.actions["Idle"].play()
|
||||||
|
this.model.children[0].rotation.x = Math.PI * 0.5;
|
||||||
|
this.model.children[0].rotation.y = Math.PI;
|
||||||
|
this.model.scale.set(0.3, 0.3, 0.3)
|
||||||
|
this.scene.add(this.model);
|
||||||
|
this.voxels = buildNeighborhoodMap(this.level.voxels)
|
||||||
|
this.reset()
|
||||||
|
this.ready = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.ready) return; // Early return if not ready
|
||||||
|
var dt = this.clock.getDelta();
|
||||||
|
if (this.mixer) this.mixer.update(dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
setToVoxelPosition(v: Voxel) {
|
||||||
|
this.model.position.set(v.x, v.y, v.z + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.voxelStandingOn = this.voxels[this.level.playerPos.voxelIdx]
|
||||||
|
this.setToVoxelPosition(this.voxelStandingOn)
|
||||||
|
this.direction = this.level.playerPos.direction
|
||||||
|
this.initRotation(this.direction)
|
||||||
|
resetVoxels(this.voxels)
|
||||||
|
}
|
||||||
|
|
||||||
|
doneAnimating() {
|
||||||
|
// if (this.mixer) this.mixer.stopAllAction()
|
||||||
|
// this.actions["waling"]
|
||||||
|
game.step()
|
||||||
|
}
|
||||||
|
|
||||||
|
initRotation(dir: string) {
|
||||||
|
let getAngle = dir => {
|
||||||
|
switch (dir) {
|
||||||
|
case "east":
|
||||||
|
return 1.5 * Math.PI
|
||||||
|
case "west":
|
||||||
|
return 0.5 * Math.PI
|
||||||
|
case "south":
|
||||||
|
return Math.PI
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.model.rotation.z = getAngle(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRotation(l: number) {
|
||||||
|
let z0 = this.model.rotation.z
|
||||||
|
let rz = z0 + l * 0.5 * Math.PI
|
||||||
|
// Simplified rotation animation
|
||||||
|
const startRotation = this.model.rotation.z;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const duration = 1000;
|
||||||
|
const animateStep = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
this.model.rotation.z = startRotation + (rz - startRotation) * progress;
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateStep);
|
||||||
|
} else {
|
||||||
|
this.doneAnimating();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animateStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
jump() {
|
||||||
|
let x0 = this.voxelStandingOn
|
||||||
|
let nextVoxel = this.voxelStandingOn.neighs[this.direction]
|
||||||
|
if (nextVoxel["up"]) {
|
||||||
|
this.voxelStandingOn = nextVoxel["up"]
|
||||||
|
}
|
||||||
|
if (nextVoxel["down"]) {
|
||||||
|
this.voxelStandingOn = nextVoxel["down"]
|
||||||
|
}
|
||||||
|
this.actions['Jump'].reset()
|
||||||
|
this.actions['Jump'].play()
|
||||||
|
if (nextVoxel["down"] || nextVoxel["up"]) {
|
||||||
|
|
||||||
|
let x1 = this.voxelStandingOn
|
||||||
|
let v0x = x1.x - x0.x
|
||||||
|
let v0y = x1.y - x0.y
|
||||||
|
let v0z = x1.z - x0.z + 0.5 * 9.81
|
||||||
|
|
||||||
|
// Simplified jump animation
|
||||||
|
const startTime = Date.now();
|
||||||
|
const duration = 1000;
|
||||||
|
const animateStep = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const t = progress;
|
||||||
|
this.model.position.set(x0.x + v0x * t,
|
||||||
|
x0.y + v0y * t,
|
||||||
|
x0.z + v0z * t - 0.5 * 9.81 * t * t + 1)
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateStep);
|
||||||
|
} else {
|
||||||
|
this.doneAnimating();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animateStep();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.doneAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forward() {
|
||||||
|
let nextVoxel = this.voxelStandingOn.neighs[this.direction]
|
||||||
|
if (nextVoxel["forward"]) {
|
||||||
|
this.voxelStandingOn = nextVoxel["forward"]
|
||||||
|
let x = this.model.position.x
|
||||||
|
let y = this.model.position.y
|
||||||
|
this.actions['Walking'].reset()
|
||||||
|
this.actions['Walking'].play()
|
||||||
|
// Simplified walk animation
|
||||||
|
const startTime = Date.now();
|
||||||
|
const startX = x;
|
||||||
|
const startY = y;
|
||||||
|
const duration = 800;
|
||||||
|
const animateStep = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const t = progress;
|
||||||
|
this.model.position.set(startX * (1 - t) + this.voxelStandingOn.x * t,
|
||||||
|
startY * (1 - t) + this.voxelStandingOn.y * t,
|
||||||
|
this.model.position.z);
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateStep);
|
||||||
|
} else {
|
||||||
|
this.doneAnimating();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animateStep();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.actions["No"].reset()
|
||||||
|
this.actions["No"].play()
|
||||||
|
setTimeout(this.doneAnimating, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
action() {
|
||||||
|
this.actions['Jump'].reset()
|
||||||
|
this.actions['Jump'].play()
|
||||||
|
if (this.voxelStandingOn.actionable) {
|
||||||
|
switch (this.voxelStandingOn.actionable.type) {
|
||||||
|
case "BUTTON":
|
||||||
|
buttonVoxelToggle(this.voxelStandingOn)
|
||||||
|
// console.log("PUSH DE BOTON")
|
||||||
|
if (hasWonGame(this)) {
|
||||||
|
store.dispatch(wonLevel())
|
||||||
|
// console.log("WON THE GAME")
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(this.doneAnimating, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
turn_left() {
|
||||||
|
switch (this.direction) {
|
||||||
|
case "east":
|
||||||
|
this.direction = "north"
|
||||||
|
break
|
||||||
|
case "north":
|
||||||
|
this.direction = "west"
|
||||||
|
break
|
||||||
|
case "west":
|
||||||
|
this.direction = "south"
|
||||||
|
break
|
||||||
|
case "south":
|
||||||
|
this.direction = "east"
|
||||||
|
}
|
||||||
|
this.setRotation(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
turn_right() {
|
||||||
|
switch (this.direction) {
|
||||||
|
case "east":
|
||||||
|
this.direction = "south"
|
||||||
|
break
|
||||||
|
case "north":
|
||||||
|
this.direction = "east"
|
||||||
|
break
|
||||||
|
case "west":
|
||||||
|
this.direction = "north"
|
||||||
|
break
|
||||||
|
case "south":
|
||||||
|
this.direction = "west"
|
||||||
|
}
|
||||||
|
this.setRotation(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
123
src/game/voxel.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
export const DEFAULT_MATERIAL = () => new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x3335cc,
|
||||||
|
specular: 0x666688, // Brighter specular for atmospheric highlights
|
||||||
|
shininess: 25, // Allow light reflection
|
||||||
|
emissive: 0x0a0a15, // Subtle glow for base visibility
|
||||||
|
emissiveIntensity: 0.15 // Maintain darkness but ensure visibility
|
||||||
|
});
|
||||||
|
export const SWITCHED_ON_MATERIAL = () => new THREE.MeshPhongMaterial({
|
||||||
|
color: 0xfefffe,
|
||||||
|
specular: 0xcccccc,
|
||||||
|
shininess: 30,
|
||||||
|
emissive: 0x404050,
|
||||||
|
emissiveIntensity: 0.3
|
||||||
|
});
|
||||||
|
export const SWITCHED_OFF_MATERIAL = () => new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x222488,
|
||||||
|
specular: 0x444466,
|
||||||
|
shininess: 20,
|
||||||
|
emissive: 0x0a0a15,
|
||||||
|
emissiveIntensity: 0.12
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Actionable {
|
||||||
|
type: string,
|
||||||
|
pushed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUTTON_ACTIONABLE: Actionable = {
|
||||||
|
type: "BUTTON",
|
||||||
|
pushed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleSideNeighbors {
|
||||||
|
up?: Voxel,
|
||||||
|
down?: Voxel,
|
||||||
|
forward?: Voxel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Neighbors {
|
||||||
|
west: SingleSideNeighbors,
|
||||||
|
east: SingleSideNeighbors,
|
||||||
|
north: SingleSideNeighbors,
|
||||||
|
south: SingleSideNeighbors
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Voxel {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
neighs?: Neighbors,
|
||||||
|
actionable?: Actionable,
|
||||||
|
model?: THREE.Mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchForNeighbors = (
|
||||||
|
v: Voxel,
|
||||||
|
vxs: Array<Voxel>,
|
||||||
|
incx: number,
|
||||||
|
incy: number): SingleSideNeighbors => {
|
||||||
|
let up = vxs.filter(vt => (vt.x == v.x + incx)
|
||||||
|
&& (vt.y == v.y + incy)
|
||||||
|
&& (vt.z == v.z + 1))[0]
|
||||||
|
let down = vxs.filter(vt => (vt.x == v.x + incx)
|
||||||
|
&& (vt.y == v.y + incy)
|
||||||
|
&& (vt.z == v.z - 1))[0]
|
||||||
|
let fwd = vxs.filter(vt => (vt.x == v.x + incx)
|
||||||
|
&& (vt.y == v.y + incy)
|
||||||
|
&& (vt.z == v.z))[0]
|
||||||
|
return { up: up, down: down, forward: fwd }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildNeighborhoodMap = (voxels: Array<Voxel>) => {
|
||||||
|
return voxels.map((v) => {
|
||||||
|
// Ensure voxel is extensible before adding neighs
|
||||||
|
const extensibleVoxel = { ...v }; // Create fresh copy
|
||||||
|
let neighs = {
|
||||||
|
west: searchForNeighbors(extensibleVoxel, voxels, -1, 0),
|
||||||
|
east: searchForNeighbors(extensibleVoxel, voxels, 1, 0),
|
||||||
|
north: searchForNeighbors(extensibleVoxel, voxels, 0, 1),
|
||||||
|
south: searchForNeighbors(extensibleVoxel, voxels, 0, -1)
|
||||||
|
}
|
||||||
|
extensibleVoxel.neighs = neighs
|
||||||
|
return extensibleVoxel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buttonVoxelToggle = (voxel: Voxel) => {
|
||||||
|
// This function modifies the actual voxel in place, but we need to handle read-only issues
|
||||||
|
// Create extensible copies of the actionable object
|
||||||
|
if (voxel.actionable) {
|
||||||
|
const newPushedState = !voxel.actionable.pushed;
|
||||||
|
const extensibleActionable = { ...voxel.actionable, pushed: newPushedState };
|
||||||
|
|
||||||
|
if (voxel.model) {
|
||||||
|
voxel.model.material = newPushedState ? SWITCHED_ON_MATERIAL() : SWITCHED_OFF_MATERIAL();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the actionable object (this should work if the main voxel reference is extensible)
|
||||||
|
voxel.actionable = extensibleActionable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetVoxels = (voxels: Voxel[]) => {
|
||||||
|
return voxels.map((v) => {
|
||||||
|
// Create extensible copies to avoid read-only errors
|
||||||
|
const extensibleVoxel = { ...v };
|
||||||
|
if (extensibleVoxel.actionable) {
|
||||||
|
const extensibleActionable = { ...extensibleVoxel.actionable };
|
||||||
|
switch (extensibleActionable.type) {
|
||||||
|
case ("BUTTON"):
|
||||||
|
if (extensibleVoxel.model) {
|
||||||
|
extensibleVoxel.model.material = SWITCHED_OFF_MATERIAL()
|
||||||
|
}
|
||||||
|
extensibleActionable.pushed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
extensibleVoxel.actionable = extensibleActionable;
|
||||||
|
}
|
||||||
|
return extensibleVoxel;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
302
src/index.css
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
body {
|
||||||
|
background:#223;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top:5rem;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "iosevka";
|
||||||
|
src: url('/assets/iosevka-regular.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: fixed;
|
||||||
|
top:0;
|
||||||
|
left: 90px;
|
||||||
|
z-index: 90000;
|
||||||
|
display:flex;
|
||||||
|
color:#ffeeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer {
|
||||||
|
background: #000;
|
||||||
|
opacity: .2;
|
||||||
|
z-index: 1;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-moz-background-clip: padding;
|
||||||
|
-webkit-background-clip: padding;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer:hover {
|
||||||
|
-webkit-transition: all 2s ease;
|
||||||
|
transition: all 2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer.horizontal {
|
||||||
|
height: 11px;
|
||||||
|
margin: -5px 0;
|
||||||
|
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||||
|
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||||
|
cursor: row-resize;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer.horizontal:hover {
|
||||||
|
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||||
|
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer.vertical {
|
||||||
|
width: 11px;
|
||||||
|
margin: 0 -5px;
|
||||||
|
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||||
|
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer.vertical:hover {
|
||||||
|
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||||
|
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Resizer.disabled:hover {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fncontainers {
|
||||||
|
display:flex;
|
||||||
|
height:fit-content;
|
||||||
|
width:8rem;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fncontainer {
|
||||||
|
width:16rem;
|
||||||
|
margin-bottom:0.8rem;
|
||||||
|
height:12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fncontainer h3 {
|
||||||
|
text-indent:-2rem;
|
||||||
|
color:#9bf;
|
||||||
|
display:block;
|
||||||
|
margin-bottom:0.5rem;
|
||||||
|
margin-top:0.4rem;
|
||||||
|
pointer-events:none;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops {
|
||||||
|
list-style-type:none;
|
||||||
|
display:flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
min-height:8rem;
|
||||||
|
position:absolute;
|
||||||
|
width:16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op {
|
||||||
|
list-style-type:none;
|
||||||
|
background:#444;
|
||||||
|
border: 0.1rem solid black;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
width:3.8rem;
|
||||||
|
height:3.8rem;
|
||||||
|
user-select:none;
|
||||||
|
cursor:grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op img {
|
||||||
|
width:100%;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opspalette {
|
||||||
|
background:#122;
|
||||||
|
display:flex;
|
||||||
|
flex-flow: row;
|
||||||
|
padding-top: 1.6rem;
|
||||||
|
padding-bottom:1rem;
|
||||||
|
width:100%;
|
||||||
|
justify-content:space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opindicators {
|
||||||
|
display:flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
position:absolute;
|
||||||
|
z-index:-2;
|
||||||
|
width:16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opindicator {
|
||||||
|
width:4rem;
|
||||||
|
height:4rem;
|
||||||
|
background:#555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opdropzones {
|
||||||
|
position:absolute;
|
||||||
|
display:flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
width:16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opdropzone {
|
||||||
|
width:4rem;
|
||||||
|
height:4rem;
|
||||||
|
|
||||||
|
background:#1119;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frontdropper {
|
||||||
|
width:1rem;
|
||||||
|
height:4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelname {
|
||||||
|
margin-top:1rem;
|
||||||
|
margin-bottom:0.4rem;
|
||||||
|
margin-left:2rem;
|
||||||
|
color:#666;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar{
|
||||||
|
background:#122;
|
||||||
|
width:100%;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:row;
|
||||||
|
justify-content:space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game {
|
||||||
|
/* width:100vw; */
|
||||||
|
/* height:100vh; */
|
||||||
|
display:flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
align-items: center;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
justify-content:space-between;
|
||||||
|
width:100%;
|
||||||
|
height:100vh;
|
||||||
|
font-family:iosevka;
|
||||||
|
color:#fff;
|
||||||
|
border-left: solid #112 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlbuttons {
|
||||||
|
display:flex;
|
||||||
|
flex-direction:row;
|
||||||
|
margin-right:2rem;
|
||||||
|
margin-top:0.4rem;
|
||||||
|
height:2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlbutton {
|
||||||
|
padding:0.5rem;
|
||||||
|
display:block;
|
||||||
|
border: solid #112 0.05rem;
|
||||||
|
border-radius:0.3rem;
|
||||||
|
background:#221;
|
||||||
|
cursor:pointer;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlbutton:hover {
|
||||||
|
background:#332;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelSelectCaption {
|
||||||
|
font-family:iosevka;
|
||||||
|
color:#9bf;
|
||||||
|
text-align:left;
|
||||||
|
margin-bottom:1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItems {
|
||||||
|
max-width: 25rem;
|
||||||
|
width:100%;
|
||||||
|
margin:auto;
|
||||||
|
text-align: center;
|
||||||
|
/* padding: 1rem; */
|
||||||
|
/* display: flex; */
|
||||||
|
/* flex-direction: col; */
|
||||||
|
/* justify-content:space-between */
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItem {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 25rem;
|
||||||
|
height: 96px;
|
||||||
|
user-select:none;
|
||||||
|
/* margin:1rem; */
|
||||||
|
/* background:#343; */
|
||||||
|
border: 0.1rem solid #112;
|
||||||
|
display:flex;
|
||||||
|
justify-content:center;
|
||||||
|
text-align:center;
|
||||||
|
font-family:iosevka;
|
||||||
|
/* box-shadow: 0.2rem 0.5rem 1rem 0px rgba(24, 14, 28, 0.5); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItems a {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: block;
|
||||||
|
background: #555;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color:#3da;
|
||||||
|
font-family:iosevka;
|
||||||
|
font-size: 2em;
|
||||||
|
text-decoration:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItems a:hover div{
|
||||||
|
color: #3c0;
|
||||||
|
background:#363;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItem p {
|
||||||
|
margin:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startContainer {
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 850px;
|
||||||
|
height:600px;
|
||||||
|
margin:auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startContainer a {
|
||||||
|
margin-top:2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameName {
|
||||||
|
margin-top:4rem;
|
||||||
|
}
|
||||||
9
src/index.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import Root from "./components/Root";
|
||||||
|
import store from "./store/store";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
const container = document.getElementById("root");
|
||||||
|
const root = createRoot(container!);
|
||||||
|
root.render(<Root store={store} />);
|
||||||
218
src/index_old.css
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
body {
|
||||||
|
background: #223;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "iosevka";
|
||||||
|
src: url('/assets/iosevka-regular.ttf') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GameScreen Styles */
|
||||||
|
.game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelname {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
color: #666;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar {
|
||||||
|
background: #122;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: iosevka;
|
||||||
|
color: #fff;
|
||||||
|
border-left: solid #112 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlbuttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-right: 2rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlbutton {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: block;
|
||||||
|
border: solid #112 0.05rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
background: #221;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlbutton:hover {
|
||||||
|
background: #332;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Operation Blocks (Drag & Drop) */
|
||||||
|
.op {
|
||||||
|
list-style-type: none;
|
||||||
|
background: #444;
|
||||||
|
border: 0.1rem solid black;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
width: 3.8rem;
|
||||||
|
height: 3.8rem;
|
||||||
|
user-select: none;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op img {
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opspalette {
|
||||||
|
background: #122;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
padding-top: 1.6rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opindicators {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -2;
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opindicator {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opdropzones {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opdropzone {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
background: #1119;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frontdropper {
|
||||||
|
width: 1rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelSelectCaption {
|
||||||
|
font-family: iosevka;
|
||||||
|
color: #9bf;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItems {
|
||||||
|
max-width: 25rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItem {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 25rem;
|
||||||
|
height: 96px;
|
||||||
|
user-select: none;
|
||||||
|
border: 0.1rem solid #112;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
font-family: iosevka;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItems a {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: block;
|
||||||
|
background: #555;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #3da;
|
||||||
|
font-family: iosevka;
|
||||||
|
font-size: 2em;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItems a:hover {
|
||||||
|
color: #3c0;
|
||||||
|
background: #363;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelItem p {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 850px;
|
||||||
|
height: 600px;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startContainer a {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameName {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startButton {
|
||||||
|
display: block;
|
||||||
|
background: #555;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #3da;
|
||||||
|
font-family: iosevka;
|
||||||
|
font-size: 2em;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startButton:hover {
|
||||||
|
color: #3c0;
|
||||||
|
background: #363;
|
||||||
|
}
|
||||||
25
src/store/codeBlocks/actions.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { CodeBlocksActionTypes, INIT_CODEBLOCKS, CodeBlock, REMOVE_CODEBLOCK, INSERT_CODEBLOCK } from "./types";
|
||||||
|
import { LevelInitializer } from "../../game/levels";
|
||||||
|
|
||||||
|
export function initCodeBlocks(level: LevelInitializer): CodeBlocksActionTypes {
|
||||||
|
return { type: INIT_CODEBLOCKS, level: level }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCodeBlock(idxCodeblocks: number, idxBlocks: number): CodeBlocksActionTypes {
|
||||||
|
return {
|
||||||
|
type: REMOVE_CODEBLOCK,
|
||||||
|
containerIdx: idxCodeblocks,
|
||||||
|
blocksIdx: idxBlocks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertCodeBlock(block: CodeBlock, containerIdx: number, blockIdx: number, overwrite: boolean): CodeBlocksActionTypes {
|
||||||
|
return {
|
||||||
|
type: INSERT_CODEBLOCK,
|
||||||
|
containerIdx: containerIdx,
|
||||||
|
blocksIdx: blockIdx,
|
||||||
|
block: block,
|
||||||
|
overwrite: overwrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
49
src/store/codeBlocks/reducers.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { CodeBlockContainer, CodeBlocksActionTypes, INIT_CODEBLOCKS, REMOVE_CODEBLOCK, INSERT_CODEBLOCK, InsertCodeBlockAction, RemoveCodeBlockAction, SetDropZoneZIdxAction, SET_DROPZONE_Z_INDEX } from "./types";
|
||||||
|
import { cloneDeep, pullAt } from "lodash"
|
||||||
|
|
||||||
|
|
||||||
|
export function codeBlocksReducer(state: Array<CodeBlockContainer> = [], action: CodeBlocksActionTypes): Array<CodeBlockContainer> {
|
||||||
|
switch (action.type) {
|
||||||
|
case INIT_CODEBLOCKS:
|
||||||
|
return Object.assign([], state, action.level.codeviews)
|
||||||
|
case REMOVE_CODEBLOCK:
|
||||||
|
return reduceRemoveBlock(state, action)
|
||||||
|
case INSERT_CODEBLOCK:
|
||||||
|
return reduceInsertBlock(state, action)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceRemoveBlock = (state: Array<CodeBlockContainer>, action: RemoveCodeBlockAction) => {
|
||||||
|
let newState = cloneDeep(state)
|
||||||
|
pullAt(newState[action.containerIdx].blocks, action.blocksIdx)
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceInsertBlock = (state: Array<CodeBlockContainer>, action: InsertCodeBlockAction) => {
|
||||||
|
if (state[action.containerIdx].blocks.length > state[action.containerIdx].nMaxBlocks)
|
||||||
|
return state
|
||||||
|
if (state[action.containerIdx].blocks.length == state[action.containerIdx].nMaxBlocks
|
||||||
|
&& !action.overwrite)
|
||||||
|
return state
|
||||||
|
let newState = cloneDeep(state)
|
||||||
|
if (action.overwrite) {
|
||||||
|
let bIdx = action.blocksIdx >= state[action.containerIdx].blocks.length ? state[action.containerIdx].blocks.length : action.blocksIdx;
|
||||||
|
newState[action.containerIdx].blocks[bIdx] = action.block;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newState[action.containerIdx].blocks.splice(action.blocksIdx, 0, action.block)
|
||||||
|
}
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
export const opDropZoneZIdxReducer = (state: number = -1, action: SetDropZoneZIdxAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_DROPZONE_Z_INDEX:
|
||||||
|
return action.zIdx
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/store/codeBlocks/types.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { LevelInitializer } from "../../game/levels";
|
||||||
|
|
||||||
|
export const INIT_CODEBLOCKS = "INIT_CODEBLOCKS"
|
||||||
|
export const REMOVE_CODEBLOCK = "REMOVE_CODEBLOCK"
|
||||||
|
export const INSERT_CODEBLOCK = "INSERT_CODEBLOCK"
|
||||||
|
export const SET_DROPZONE_Z_INDEX = "SET_DROPZONE_Z_INDEX"
|
||||||
|
|
||||||
|
export interface CodeBlock {
|
||||||
|
name: string,
|
||||||
|
containerIndex?: number,
|
||||||
|
// parent: CodeBlockContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeBlockContainer {
|
||||||
|
name: string,
|
||||||
|
nMaxBlocks: number,
|
||||||
|
blocks: Array<CodeBlock>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitCodeBlocksAction {
|
||||||
|
type: typeof INIT_CODEBLOCKS,
|
||||||
|
level: LevelInitializer
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveCodeBlockAction {
|
||||||
|
type: typeof REMOVE_CODEBLOCK,
|
||||||
|
containerIdx: number,
|
||||||
|
blocksIdx: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InsertCodeBlockAction {
|
||||||
|
type: typeof INSERT_CODEBLOCK,
|
||||||
|
containerIdx: number,
|
||||||
|
blocksIdx: number
|
||||||
|
block: CodeBlock,
|
||||||
|
overwrite: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetDropZoneZIdxAction {
|
||||||
|
type: typeof SET_DROPZONE_Z_INDEX,
|
||||||
|
zIdx: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CodeBlocksActionTypes =
|
||||||
|
InitCodeBlocksAction
|
||||||
|
| RemoveCodeBlockAction
|
||||||
|
| InsertCodeBlockAction
|
||||||
|
| SetDropZoneZIdxAction
|
||||||
5
src/store/executionState/actions.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { WON_LEVEL, wonLevelAction } from "./types";
|
||||||
|
|
||||||
|
export function wonLevel(): wonLevelAction {
|
||||||
|
return { type: WON_LEVEL }
|
||||||
|
}
|
||||||
119
src/store/executionState/reducers.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { ExecutionState, ExecutionStateActionTypes, START_EXECUTING, PAUSE_EXECUTION, STEP_EXECUTION, startExecutionAction, TOGGLE_SPEED, TOGGLE_AUTOSTEP, RESET_EXECUTION, resetExecutionAction, WON_LEVEL } from "./types";
|
||||||
|
import { cloneDeep, pullAt } from "lodash"
|
||||||
|
|
||||||
|
const INIT_STATE: ExecutionState = {
|
||||||
|
running: false,
|
||||||
|
autostep: false,
|
||||||
|
won: false,
|
||||||
|
instructionPtr: { fn: "main", fnIdx: 0 },
|
||||||
|
callStack: [],
|
||||||
|
fast: false,
|
||||||
|
finished: false,
|
||||||
|
playerPos: { voxelIdx: 0, direction: "west" },
|
||||||
|
code: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executionStateReducer(state: ExecutionState = INIT_STATE, action: ExecutionStateActionTypes) {
|
||||||
|
switch (action.type) {
|
||||||
|
case START_EXECUTING:
|
||||||
|
return reduceStart(state, action)
|
||||||
|
case PAUSE_EXECUTION:
|
||||||
|
return reducePause(state)
|
||||||
|
case STEP_EXECUTION:
|
||||||
|
return reduceStep(state)
|
||||||
|
case TOGGLE_SPEED:
|
||||||
|
return reduceToggleSpeed(state)
|
||||||
|
case TOGGLE_AUTOSTEP:
|
||||||
|
return reduceToggleAutoStep(state)
|
||||||
|
case RESET_EXECUTION:
|
||||||
|
return reduceReset(state)
|
||||||
|
case WON_LEVEL:
|
||||||
|
return reduceWonLevel(state)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceReset = (state: ExecutionState) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
won: false,
|
||||||
|
running: false,
|
||||||
|
finished: false,
|
||||||
|
instructionPtr: { fn: "main", fnIdx: 0 },
|
||||||
|
callStack: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceStart = (state: ExecutionState, action: startExecutionAction) => {
|
||||||
|
let code = {}
|
||||||
|
for (let i = 0; i < action.level.codeviews.length; i++) {
|
||||||
|
code[action.level.codeviews[i].name] = action.code[i]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...INIT_STATE,
|
||||||
|
won: false,
|
||||||
|
running: true,
|
||||||
|
playerPos: action.playerPos,
|
||||||
|
code: code,
|
||||||
|
// level: cloneDeep(action.level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducePause = (state: ExecutionState) => {
|
||||||
|
return { ...state, running: !state.running }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceToggleAutoStep = (state: ExecutionState) => {
|
||||||
|
return { ...state, autostep: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceStep = (state: ExecutionState) => {
|
||||||
|
// here the machine makes one single instructoin.
|
||||||
|
// it reads the instruction from the code variable at the
|
||||||
|
// instruction pointer indices, executes the instruction
|
||||||
|
// and changes the instructoin pointers.
|
||||||
|
let newState = cloneDeep(state)
|
||||||
|
while (newState.instructionPtr.fnIdx >= newState.code[newState.instructionPtr.fn]['blocks'].length) {
|
||||||
|
let newPtr = newState.callStack.pop()
|
||||||
|
if (!newPtr) {
|
||||||
|
newState.running = false
|
||||||
|
newState.finished = true
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
newState.instructionPtr = newPtr
|
||||||
|
}
|
||||||
|
|
||||||
|
let instruction = newState.code[newState.instructionPtr.fn]['blocks'][newState.instructionPtr.fnIdx].name
|
||||||
|
|
||||||
|
while (instruction in newState.code) { // set instruction pointer to fn and put returnaddr on stack
|
||||||
|
let returnAddr = { fn: newState.instructionPtr.fn, fnIdx: newState.instructionPtr.fnIdx + 1 }
|
||||||
|
newState.instructionPtr.fn = instruction
|
||||||
|
newState.instructionPtr.fnIdx = 0
|
||||||
|
newState.callStack.push(returnAddr)
|
||||||
|
instruction = newState.code[newState.instructionPtr.fn]['blocks'][newState.instructionPtr.fnIdx].name
|
||||||
|
}
|
||||||
|
|
||||||
|
// now check if the istruction is a valid command
|
||||||
|
newState.instruction = undefined
|
||||||
|
if (instruction in instructions)
|
||||||
|
newState.instruction = instruction
|
||||||
|
newState.instructionPtr.fnIdx++
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceToggleSpeed = (state: ExecutionState) => {
|
||||||
|
return { ...state, fast: !state.fast }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceWonLevel = (state: ExecutionState) => {
|
||||||
|
return { ...state, won: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructions = {
|
||||||
|
"forward": (state: ExecutionState) => { },
|
||||||
|
"turn_right": (state: ExecutionState) => { },
|
||||||
|
"turn_left": (state: ExecutionState) => { },
|
||||||
|
"action": (state: ExecutionState) => { },
|
||||||
|
"jump": (state: ExecutionState) => { }
|
||||||
|
}
|
||||||
67
src/store/executionState/types.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { IPlayerData, LevelInitializer } from "../../game/levels";
|
||||||
|
|
||||||
|
export const START_EXECUTING = "START_EXECUTING"
|
||||||
|
export const PAUSE_EXECUTION = "PAUSE_EXECUTION"
|
||||||
|
export const STEP_EXECUTION = "STEP_EXECUTION"
|
||||||
|
export const TOGGLE_SPEED = "TOGGLE_SPEED"
|
||||||
|
export const TOGGLE_AUTOSTEP = "TOGGLE_AUTOSTEP"
|
||||||
|
export const RESET_EXECUTION = "RESET_EXECUTION"
|
||||||
|
export const WON_LEVEL = "WON_LEVEL"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ip { fn: string, fnIdx: number }
|
||||||
|
|
||||||
|
export interface ExecutionState {
|
||||||
|
running: boolean,
|
||||||
|
autostep: boolean,
|
||||||
|
instructionPtr: ip,
|
||||||
|
callStack: Array<ip>,
|
||||||
|
instruction?: string,
|
||||||
|
playerPos: IPlayerData,
|
||||||
|
fast: boolean,
|
||||||
|
finished: boolean,
|
||||||
|
won: boolean,
|
||||||
|
code: { [key: string]: Array<string> },
|
||||||
|
level?: LevelInitializer
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface startExecutionAction {
|
||||||
|
type: typeof START_EXECUTING,
|
||||||
|
playerPos: IPlayerData,
|
||||||
|
code: Array<Array<string>>,
|
||||||
|
level: LevelInitializer
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface pauseExecutionAction {
|
||||||
|
type: typeof PAUSE_EXECUTION
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface stepExecutionAction {
|
||||||
|
type: typeof STEP_EXECUTION
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface toggleSpeedAction {
|
||||||
|
type: typeof TOGGLE_SPEED
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface toggleAutoStepAction {
|
||||||
|
type: typeof TOGGLE_AUTOSTEP
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface resetExecutionAction {
|
||||||
|
type: typeof RESET_EXECUTION
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface wonLevelAction {
|
||||||
|
type: typeof WON_LEVEL
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExecutionStateActionTypes =
|
||||||
|
startExecutionAction
|
||||||
|
| pauseExecutionAction
|
||||||
|
| stepExecutionAction
|
||||||
|
| toggleSpeedAction
|
||||||
|
| toggleAutoStepAction
|
||||||
|
| resetExecutionAction
|
||||||
|
| wonLevelAction
|
||||||
0
src/store/gameScreenState/actions.ts
Normal file
25
src/store/gameScreenState/slice.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
interface GameScreenState {
|
||||||
|
modalOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: GameScreenState = {
|
||||||
|
modalOpen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gameScreenSlice = createSlice({
|
||||||
|
name: 'gameScreen',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
openModal: (state) => {
|
||||||
|
state.modalOpen = true;
|
||||||
|
},
|
||||||
|
closeModal: (state) => {
|
||||||
|
state.modalOpen = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { openModal, closeModal } = gameScreenSlice.actions;
|
||||||
|
export const GameScreenReducer = gameScreenSlice.reducer;
|
||||||
19
src/store/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { codeBlocksReducer, opDropZoneZIdxReducer } from './codeBlocks/reducers';
|
||||||
|
import { executionStateReducer } from './executionState/reducers';
|
||||||
|
import levelReducer from './level/slice';
|
||||||
|
import { saveStateReducer } from './saveState/reducers';
|
||||||
|
import { GameScreenReducer } from './gameScreenState/slice';
|
||||||
|
|
||||||
|
|
||||||
|
export const rootReducer = combineReducers({
|
||||||
|
codeBlocks: codeBlocksReducer,
|
||||||
|
opDropZoneZIdx: opDropZoneZIdxReducer,
|
||||||
|
executionState: executionStateReducer,
|
||||||
|
level: levelReducer,
|
||||||
|
saveState: saveStateReducer,
|
||||||
|
gameScreenState: GameScreenReducer
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>
|
||||||
|
export type AppState = ReturnType<typeof rootReducer>
|
||||||
37
src/store/level/slice.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { LevelInitializer, levels } from "../../game/levels";
|
||||||
|
|
||||||
|
interface LevelState {
|
||||||
|
idx: number;
|
||||||
|
name: string;
|
||||||
|
voxels: Array<any>;
|
||||||
|
playerPos: any;
|
||||||
|
codeviews: Array<any>;
|
||||||
|
allowedCodeBlocks: Array<string>;
|
||||||
|
activeCodeview: string;
|
||||||
|
introMessages: Array<any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: LevelState = {
|
||||||
|
...levels[0],
|
||||||
|
idx: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelSlice = createSlice({
|
||||||
|
name: 'level',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLevel: (state, action: PayloadAction<{ level: LevelInitializer; idx: number }>) => {
|
||||||
|
const { level, idx } = action.payload;
|
||||||
|
return { ...level, idx };
|
||||||
|
},
|
||||||
|
nextLevel: (state) => {
|
||||||
|
if (levels.length <= state.idx + 1) return state;
|
||||||
|
const nextLevel = levels[state.idx + 1];
|
||||||
|
return { ...nextLevel, idx: state.idx + 1 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setLevel, nextLevel } = levelSlice.actions;
|
||||||
|
export default levelSlice.reducer;
|
||||||
21
src/store/saveState/actions.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { setLevelDoneAction, SET_LEVEL_DONE, TOGGLE_SHOW_TIPS, toggleShowTipsAction, toggleSoundsAction, TOGGLE_SOUNDS, TOGGLE_MUSIC, toggleMusicAction, muteAllAction, MUTE_ALL } from "./types";
|
||||||
|
|
||||||
|
export function toggleSounds(): toggleMusicAction {
|
||||||
|
return { type: TOGGLE_MUSIC }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleMusic(): toggleSoundsAction {
|
||||||
|
return { type: TOGGLE_SOUNDS }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleShowTips(): toggleShowTipsAction {
|
||||||
|
return { type: TOGGLE_SHOW_TIPS }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function levelDone(idx: number, stars: number): setLevelDoneAction {
|
||||||
|
return { type: SET_LEVEL_DONE, levelIdx: idx, stars: stars }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function muteAll(): muteAllAction {
|
||||||
|
return { type: MUTE_ALL }
|
||||||
|
}
|
||||||
60
src/store/saveState/reducers.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { SaveState, SaveStateActions, LevelSaveState, MUTE_ALL, TOGGLE_MUSIC, TOGGLE_SOUNDS, SET_LEVEL_DONE, TOGGLE_SHOW_TIPS, setLevelDoneAction, SAVESTATE } from "./types";
|
||||||
|
import { levels } from "../../game/levels";
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
|
||||||
|
|
||||||
|
const getInitState = () => {
|
||||||
|
let storedState = localStorage.getItem[SAVESTATE]
|
||||||
|
if (storedState) {
|
||||||
|
return JSON.parse(atob(storedState))
|
||||||
|
}
|
||||||
|
let levelStates = new Array<LevelSaveState>()
|
||||||
|
for (let i = 0; i < levels.length; i++) {
|
||||||
|
levelStates.push({ stars: 0, done: false })
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
levels: levelStates,
|
||||||
|
playSounds: false,
|
||||||
|
playMusic: false,
|
||||||
|
showTips: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveStateReducer = (state: SaveState = getInitState(), action: SaveStateActions) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case MUTE_ALL:
|
||||||
|
return reduceMuteAll(state);
|
||||||
|
case TOGGLE_MUSIC:
|
||||||
|
return reduceToggleMusic(state);
|
||||||
|
case TOGGLE_SOUNDS:
|
||||||
|
return reduceToggleSounds(state);
|
||||||
|
case SET_LEVEL_DONE:
|
||||||
|
return reduceSetLevelDone(state, action);
|
||||||
|
case TOGGLE_SHOW_TIPS:
|
||||||
|
return reduceToggleShowTips(state);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceToggleMusic = (state: SaveState) => {
|
||||||
|
return { ...state, playMusic: !state.playMusic }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceToggleSounds = (state: SaveState) => {
|
||||||
|
return { ...state, playSounds: !state.playSounds }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceToggleShowTips = (state: SaveState) => {
|
||||||
|
return { ...state, showTips: !state.showTips }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceMuteAll = (state: SaveState) => {
|
||||||
|
return { ...state, playSounds: false, playMusic: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceSetLevelDone = (state: SaveState, action: setLevelDoneAction) => {
|
||||||
|
let newLevelState = cloneDeep(state.levels)
|
||||||
|
newLevelState[action.levelIdx] = { stars: action.stars, done: true }
|
||||||
|
return { ...state, levels: newLevelState }
|
||||||
|
}
|
||||||
50
src/store/saveState/types.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// this savestate holds everything, a user can set and expects to get stored
|
||||||
|
// it will be saved in local storage and when present, loaded
|
||||||
|
|
||||||
|
export const MUTE_ALL = "MUTE_ALL";
|
||||||
|
export const TOGGLE_MUSIC = "TOGGLE_MUSIC";
|
||||||
|
export const TOGGLE_SOUNDS = "TOGGLE_SOUNDS";
|
||||||
|
export const TOGGLE_SHOW_TIPS = "TOGGLE_SHOW_TIPS";
|
||||||
|
export const SET_LEVEL_DONE = "SET_LEVEL_DONE";
|
||||||
|
export const SAVESTATE = "SaveState";
|
||||||
|
|
||||||
|
export interface SaveState {
|
||||||
|
levels: Array<LevelSaveState>,
|
||||||
|
playSounds: boolean,
|
||||||
|
playMusic: boolean,
|
||||||
|
showTips: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LevelSaveState {
|
||||||
|
stars: number,
|
||||||
|
done: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface muteAllAction {
|
||||||
|
type: typeof MUTE_ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface toggleSoundsAction {
|
||||||
|
type: typeof TOGGLE_SOUNDS
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface toggleMusicAction {
|
||||||
|
type: typeof TOGGLE_MUSIC
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface toggleShowTipsAction {
|
||||||
|
type: typeof TOGGLE_SHOW_TIPS
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface setLevelDoneAction {
|
||||||
|
type: typeof SET_LEVEL_DONE,
|
||||||
|
levelIdx: number,
|
||||||
|
stars: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveStateActions =
|
||||||
|
muteAllAction
|
||||||
|
| toggleMusicAction
|
||||||
|
| toggleSoundsAction
|
||||||
|
| setLevelDoneAction
|
||||||
|
| toggleShowTipsAction
|
||||||
19
src/store/store.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { rootReducer } from "."
|
||||||
|
|
||||||
|
// create store using Redux Toolkit
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: rootReducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: ['persist/PERSIST'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
// export store singleton instance
|
||||||
|
export default store;
|
||||||
28
tsconfig.app.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": false,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
21
vite.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
assetsInclude: ['**/*.glb', '**/*.gltf', '**/*.fbx', '**/*.png', '**/*.jpg', '**/*.jpeg'],
|
||||||
|
})
|
||||||