mayor refactor to use vite instead of weboack and version upgrade

This commit is contained in:
Marius Unsel 2026-02-23 00:22:10 +01:00
commit 795c4ffa63
55 changed files with 7614 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View 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"
}
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

1
public/vite.svg Normal file
View 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
View 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);

View 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);

View 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);

View 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)

View 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
View 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

View 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;

View 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;

View 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);

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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
}
}

View 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
}
}

View 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

View File

@ -0,0 +1,5 @@
import { WON_LEVEL, wonLevelAction } from "./types";
export function wonLevel(): wonLevelAction {
return { type: WON_LEVEL }
}

View 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) => { }
}

View 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

View File

View 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
View 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
View 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;

View 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 }
}

View 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 }
}

View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View 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
View 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'],
})