Compare commits

..

10 Commits

Author SHA1 Message Date
Marius Unsel
b2e9e784e5 design tweaks 2026-02-26 01:59:35 +01:00
Marius Unsel
d2c71a22d1 fix UI bugs 2026-02-24 16:31:16 +01:00
Marius Unsel
64c777a38e make startbutton prettier 2026-02-24 15:13:38 +01:00
Marius Unsel
4095189e8f add levels button to editor 2026-02-24 03:55:15 +01:00
Marius Unsel
841d94df94 make buttons beautiful 2026-02-24 03:37:36 +01:00
Marius Unsel
bbbd5869e6 make scene more beautiful 2026-02-24 03:19:45 +01:00
Marius Unsel
85f0054fd9 remove flickering 2026-02-24 01:37:52 +01:00
Marius Unsel
7b0dbddc45 make player model changable 2026-02-24 01:26:17 +01:00
Marius Unsel
efd4369f41 fixed voxel reset 2026-02-23 03:23:36 +01:00
Marius Unsel
c76df16a5c fix winning modal 2026-02-23 03:07:59 +01:00
13 changed files with 364 additions and 119 deletions

10
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@types/lodash": "^4.17.23", "@types/lodash": "^4.17.23",
"@types/three": "^0.182.0", "@types/three": "^0.182.0",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"lucide-react": "^0.575.0",
"popmotion": "^11.0.5", "popmotion": "^11.0.5",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
@ -2787,6 +2788,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.575.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz",
"integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/meshoptimizer": { "node_modules/meshoptimizer": {
"version": "0.22.0", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",

View File

@ -14,6 +14,7 @@
"@types/lodash": "^4.17.23", "@types/lodash": "^4.17.23",
"@types/three": "^0.182.0", "@types/three": "^0.182.0",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"lucide-react": "^0.575.0",
"popmotion": "^11.0.5", "popmotion": "^11.0.5",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -1,10 +1,12 @@
import * as React from "react"; import * as React from "react";
import { Play, Bug, StepForward, RotateCcw, Square, ChevronUp, ChevronDown, LayoutGrid } from "lucide-react";
import { CodeBlockContainer, INIT_CODEBLOCKS, CodeBlock, INSERT_CODEBLOCK, REMOVE_CODEBLOCK, SET_DROPZONE_Z_INDEX } from "../store/codeBlocks/types"; import { CodeBlockContainer, INIT_CODEBLOCKS, CodeBlock, INSERT_CODEBLOCK, REMOVE_CODEBLOCK, SET_DROPZONE_Z_INDEX } from "../store/codeBlocks/types";
import { LevelInitializer, IPlayerData } from "../game/levels"; import { LevelInitializer, IPlayerData } from "../game/levels";
import { initCodeBlocks, insertCodeBlock, removeCodeBlock } from "../store/codeBlocks/actions"; import { initCodeBlocks, insertCodeBlock, removeCodeBlock } from "../store/codeBlocks/actions";
import { AppState } from "../store"; import { AppState } from "../store";
import game from "../game/game" import game from "../game/game"
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { START_EXECUTING, STEP_EXECUTION, TOGGLE_AUTOSTEP, TOGGLE_SPEED } from "../store/executionState/types"; import { START_EXECUTING, STEP_EXECUTION, TOGGLE_AUTOSTEP, TOGGLE_SPEED } from "../store/executionState/types";
@ -100,7 +102,7 @@ const FnContainers = ({ code, dropZoneZIdx, setDropZoneZIdx, remove, insert }) =
code.map((cv, idx) => { code.map((cv, idx) => {
return ( return (
<div className="fncontainer" key={idx} > <div className="fncontainer" key={idx} >
<h3>{cv.name + "(){"}</h3> <h2>{cv.name + "(){"}</h2>
<div style={{ height: Math.floor(cv.nMaxBlocks / 4 + 1) * 4 + 'rem' }}> <div style={{ height: Math.floor(cv.nMaxBlocks / 4 + 1) * 4 + 'rem' }}>
<OpIndicators n={cv.nMaxBlocks} /> <OpIndicators n={cv.nMaxBlocks} />
<div className="ops" > <div className="ops" >
@ -118,7 +120,7 @@ const FnContainers = ({ code, dropZoneZIdx, setDropZoneZIdx, remove, insert }) =
insert={insert} insert={insert}
style={{ zIndex: dropZoneZIdx }} /> style={{ zIndex: dropZoneZIdx }} />
</div> </div>
<h3>{"}"}</h3> <h2>{"}"}</h2>
</div>) </div>)
}) })
} }
@ -143,48 +145,60 @@ const OpsPalette = ({ ops, setDropZoneZIdx }) => {
const RuntimeControls = ({ start, fast, step, running, level, code, autostep, toggleAutoStep, toggleSpeed, finished }) => { const RuntimeControls = ({ start, fast, step, running, level, code, autostep, toggleAutoStep, toggleSpeed, finished }) => {
let DebugButton = () => ( let DebugButton = () => (
<div className="controlbutton" <div className="controlbutton debug"
onClick={() => { onClick={() => {
start(level, code, level.playerPos) start(level, code, level.playerPos)
}}> }}>
debug <Bug size={16} />
<span>debug</span>
</div> </div>
) )
let StepButton = () => ( let StepButton = () => (
<div className="controlbutton" <div className="controlbutton step"
onClick={step}> onClick={step}>
step <StepForward size={16} />
<span>step</span>
</div> </div>
) )
let StartButton = () => ( let StartButton = () => (
<div className="controlbutton" <div className="controlbutton run"
onClick={() => { onClick={() => {
start(level, code, level.playerPos) start(level, code, level.playerPos)
toggleAutoStep() toggleAutoStep()
step() step()
}}> }}>
run <Play size={16} />
<span>run</span>
</div> </div>
) )
let LevelsButton = () => (
<Link to="/levels/" className="controlbutton levels">
<LayoutGrid size={16} />
<span>levels</span>
</Link>
)
let ResetButton = () => ( let ResetButton = () => (
<div className="controlbutton" <div className="controlbutton reset"
onClick={() => game.reset()}> onClick={() => game.reset()}>
reset <RotateCcw size={16} />
<span>reset</span>
</div> </div>
) )
let ToggleSpeedButton = () => ( let ToggleSpeedButton = () => (
<div className="controlbutton" <div className="controlbutton speed"
onClick={toggleSpeed}> onClick={toggleSpeed}>
{fast ? "slower" : "faster"} {fast ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
<span>{fast ? "slower" : "faster"}</span>
</div> </div>
) )
let AbortButton = () => ( let AbortButton = () => (
<div className="controlbutton" <div className="controlbutton abort"
onClick={() => console.log("TODO")}> onClick={() => console.log("TODO")}>
abort <Square size={16} />
<span>abort</span>
</div> </div>
) )
@ -210,6 +224,7 @@ const RuntimeControls = ({ start, fast, step, running, level, code, autostep, to
</div>) </div>)
} }
return (<div className="controlbuttons"> return (<div className="controlbuttons">
<LevelsButton />
<StartButton /> <StartButton />
<DebugButton /> <DebugButton />
</div>) </div>)

View File

@ -15,6 +15,9 @@ import { openModal, closeModal } from "../store/gameScreenState/slice";
import game from "../game/game" import game from "../game/game"
import { setLevel, nextLevel } from "../store/level/slice"; import { setLevel, nextLevel } from "../store/level/slice";
import { RESET_EXECUTION } from "../store/executionState/types"; import { RESET_EXECUTION } from "../store/executionState/types";
import { INIT_CODEBLOCKS } from "../store/codeBlocks/types";
import store from "../store/store";
import { LayoutGrid, ArrowRight } from "lucide-react";
const MODAL_STYLES = { const MODAL_STYLES = {
'overlay': { background: '#11111166', zIndex: 99999 }, 'overlay': { background: '#11111166', zIndex: 99999 },
@ -80,8 +83,16 @@ const WonMessage = ({ nextRoute }) => {
<SpeechBubble message={"Good Job!"} /> <SpeechBubble message={"Good Job!"} />
</div> </div>
</div> </div>
<Link to={'/levels/'}>to levels</Link> <div style={{ display: "flex", justifyContent: "center", gap: "1rem", marginTop: "1rem" }}>
<Link to={nextRoute}> next</Link> <Link to={'/levels/'} className="modal-button levels">
<LayoutGrid size={18} />
<span>levels</span>
</Link>
<Link to={nextRoute} className="modal-button next">
<span>next</span>
<ArrowRight size={18} />
</Link>
</div>
</div> </div>
) )
} }
@ -100,24 +111,28 @@ const GameScreen = ({ nextLevel, won, open, openModal, closeModal, setLevel, res
let nextRoute = levelIdx === levels.length - 1 ? '/won' : '/level/' + levels[levelIdx + 1].name; let nextRoute = levelIdx === levels.length - 1 ? '/won' : '/level/' + levels[levelIdx + 1].name;
let [idxMsgs, setMsgIdx] = useState(level.introMessages && level.introMessages.length > 0 ? 0 : null); let [idxMsgs, setMsgIdx] = useState(level.introMessages && level.introMessages.length > 0 ? 0 : null);
console.log("GameScreen():won=", won)
// Auto-open modal for intro messages when level starts or when won // Auto-open modal for intro messages when level starts or when won
useEffect(() => { useEffect(() => {
// Auto-open modal for intro messages when level starts // Auto-open modal for intro messages when level starts
if (level.introMessages && level.introMessages.length > 0 && idxMsgs !== null && !open && !won) { if (level.introMessages && level.introMessages.length > 0 && idxMsgs !== null && !open && !won) {
openModal(); openModal();
console.log("GameScreen():autoopen")
} }
// Auto-open modal when won // Auto-open modal when won
if (won && !open) { if (won && !open) {
openModal(); openModal();
console.log("GameScreen():autoopen-won")
} }
}, [idxMsgs, won, level.introMessages]); // Proper dependencies }, [idxMsgs, won, level.introMessages]); // Proper dependencies
useEffect(() => game.init(level), []); // only called at creation useEffect(() => {
resetState();
game.init(level);
store.dispatch({ type: INIT_CODEBLOCKS, level: level });
if (!level.introMessages || level.introMessages.length === 0) {
closeModal();
}
}, [level]);
return ( return (
<div className="game" ref={modalContainerRef}> <div className="game" ref={modalContainerRef}>

View File

@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { levels } from "../game/levels"; import { levels } from "../game/levels";
import { Route, Link } from "react-router-dom"; import { Route, Link } from "react-router-dom";
import LevelPreview from "./LevelPreview" import LevelPreview from "./LevelPreview"
import { Play } from "lucide-react";
const LevelItem = ({ level, idx, cb, active, unlocked }) => { const LevelItem = ({ level, idx, cb, active, unlocked }) => {
@ -44,7 +45,11 @@ const LevelSelectScreen = ({saveState}) =>{
unlocked={unlockedState[idx]} unlocked={unlockedState[idx]}
/> />
})} })}
<Link to={'/level/' + levels[activeIdx].name}>start</Link> <Link to={'/level/' + levels[activeIdx].name} className="start-button">
<span> start </span>
<Play size={20} />
</Link>
</div> </div>
</div>) </div>)

View File

@ -1,14 +1,16 @@
import * as React from "react"; import * as React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom"; import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { LayoutGrid, Settings } from "lucide-react";
const StartScreen = () => ( const StartScreen = () => (
<div className="startContainer"> <div className="startContainer start-screen">
<img className="gameName" src="/assets/textures/gamename.png" /> <Link to="/levels/" className="controlbutton levels">
<Link to="/levels"> <LayoutGrid size={18} />
<div className="startButton">Levels</div> <span>Levels</span>
</Link> </Link>
<Link to="/settings"> <Link to="/settings" className="controlbutton settings">
<div className="startButton">Settings</div> <Settings size={18} />
<span>Settings</span>
</Link> </Link>
</div> </div>
); );

View File

@ -4,6 +4,7 @@ import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { AppState } from '../store'; import { AppState } from '../store';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { PLAYER_MODEL_FILENAME } from '../game/player';
interface TalkyFaceProps { interface TalkyFaceProps {
@ -45,20 +46,20 @@ class TalkyFace extends Component<TalkyFaceProps> {
this.scene.background = new THREE.Color(0x555555); this.scene.background = new THREE.Color(0x555555);
// LIGHTS // LIGHTS
this.scene.add(new THREE.HemisphereLight(0x443333, 0x111122,30)); this.scene.add(new THREE.HemisphereLight(0x443333, 0x111122,3));
let pl = new THREE.PointLight(0xFFFFFFF, 1, 1000); let pl = new THREE.PointLight(0xFFFFFFF, 10, 0,0.3);
pl.position.set(50, -50, 10); pl.position.set(50, -50, 10);
this.scene.add(pl); this.scene.add(pl);
let pl1 = new THREE.PointLight(0xFFFFFFF, 1, 1000); let pl1 = new THREE.PointLight(0xFFFFFFF, 10,0,0.3);
pl1.position.set(-50, 50, 50); pl1.position.set(-50, 50, 50);
this.scene.add(pl1); this.scene.add(pl1);
// ROBOT MODEL // ROBOT MODEL
this.clock = new THREE.Clock() this.clock = new THREE.Clock()
let loader = new GLTFLoader(); let loader = new GLTFLoader();
loader.load('assets/models/RobotExpressive.glb', (gltf) => { loader.load(PLAYER_MODEL_FILENAME, (gltf) => {
this.model = gltf.scene; this.model = gltf.scene;
this.animations = gltf.animations; this.animations = gltf.animations;
this.mixer = new THREE.AnimationMixer(this.model) this.mixer = new THREE.AnimationMixer(this.model)

View File

@ -84,7 +84,25 @@ class Game implements ThreeScene {
init = (lev: LevelInitializer) => { init = (lev: LevelInitializer) => {
this.scene = new THREE.Scene(); this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0a0a12); // Dark blue-gray instead of pure black
// Gradient background: orange → pink → purple → dark
const canvas = document.createElement('canvas');
canvas.width = 2;
canvas.height = 512;
const ctx = canvas.getContext('2d')!;
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#0a0515'); // dark at top
gradient.addColorStop(0.4, '#2a1040'); // purple
gradient.addColorStop(0.7, '#ff4080'); // pink
gradient.addColorStop(1, '#ff6b35'); // orange at bottom
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 2, 512);
const bgTexture = new THREE.CanvasTexture(canvas);
bgTexture.magFilter = THREE.LinearFilter;
this.scene.background = bgTexture;
// Fog to blend distant terrain into sky
this.scene.fog = new THREE.FogExp2(0x2a1040, 0.008);
// Groundplane // Groundplane
initGround(this.scene); initGround(this.scene);
@ -97,21 +115,27 @@ class Game implements ThreeScene {
this.player = new Player(this.scene, { ...level, voxels: mutableVoxels }) this.player = new Player(this.scene, { ...level, voxels: mutableVoxels })
initBoxes(this.scene, mutableVoxels, this.player); initBoxes(this.scene, mutableVoxels, this.player);
// Lights // Lights - Synthwave sunset style
// Subtle ambient light for base visibility // Subtle ambient light for base visibility
this.scene.add(new THREE.AmbientLight(0x2a2a3a, 2.4)); this.scene.add(new THREE.AmbientLight(0x3a1a4a, 2.4));
// Improved hemisphere light for mood // Hemisphere light: orange sky, purple ground
this.scene.add(new THREE.HemisphereLight(0x4a4a5a, 0x1a1a2a, 10)); this.scene.add(new THREE.HemisphereLight(0xff6b35, 0x2a1040, 2));
// Dramatic point lights with better positioning // Dramatic point lights with warm pink tones
let pl = new THREE.PointLight(0x8090ff, 10, 0,0.3); let pl = new THREE.PointLight(0xffffff, 10, 0,0.3);
pl.position.set(40, -40, 25); pl.position.set(40, -40, 25);
pl.castShadow = true; pl.castShadow = true;
pl.shadow.mapSize.width = 2048;
pl.shadow.mapSize.height = 2048;
pl.shadow.bias = -0.001;
this.scene.add(pl); this.scene.add(pl);
let pl1 = new THREE.PointLight(0xff9080, 10, 0,0.3); let pl1 = new THREE.PointLight(0xffffff, 10, 0,0.3);
pl1.position.set(-40, 40, 25); pl1.position.set(-40, 40, 25);
pl1.castShadow = true; pl1.castShadow = true;
pl1.shadow.mapSize.width = 2048;
pl1.shadow.mapSize.height = 2048;
pl1.shadow.bias = -0.001;
this.scene.add(pl1); this.scene.add(pl1);
} }
@ -208,6 +232,10 @@ function initBoxes(scene: THREE.Scene, voxels: Array<Voxel>, player: Player) {
mesh.castShadow = true; mesh.castShadow = true;
mesh.receiveShadow = true; mesh.receiveShadow = true;
mesh.position.set(x, y, z); mesh.position.set(x, y, z);
const edges = new THREE.EdgesGeometry(geometry);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 }));
mesh.add(line);
console.log(player.voxelStandingOn) console.log(player.voxelStandingOn)
if (idx != player.level.playerPos.voxelIdx) { if (idx != player.level.playerPos.voxelIdx) {
mesh.position.setZ(10) mesh.position.setZ(10)
@ -243,69 +271,84 @@ function initGround(scene: THREE.Scene) {
var data = generateHeight(128, 128); var data = generateHeight(128, 128);
let vertices = planeGeometry.attributes.position.array; let vertices = planeGeometry.attributes.position.array;
console.log(vertices.length)
for (var i = 0, j = 0, l = vertices.length; i < l; i++ , j += 3) { 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]; (vertices[j + 1] as number) = data[i];
} }
planeGeometry.computeVertexNormals();
var planeWire = new THREE.Mesh(planeGeometry, var planeWire = new THREE.Mesh(planeGeometry,
new THREE.MeshPhongMaterial({ new THREE.MeshPhongMaterial({
color: 0x320032, color: 0xff00ff,
specular: 0x0, specular: 0x0,
// shininess: 0xffffff, wireframeLinewidth: 2,
wireframeLinewidth: 3,
wireframe: true wireframe: true
})
);
var planeSolid = new THREE.Mesh(planeGeometry,
new THREE.MeshPhongMaterial({
color: 0x030211,
specular: 0x0,
shininess: 0x0,
}) })
); );
planeWire.position.y = 0; planeWire.position.y = 0;
planeWire.rotateX(Math.PI * 0.5) planeWire.rotateX(Math.PI * 0.5)
var planeSolid = new THREE.Mesh(planeGeometry,
new THREE.MeshPhongMaterial({
color: 0x0a0510,
specular: 0x0,
shininess: 0x0,
})
);
planeSolid.position.y = 0; planeSolid.position.y = 0;
planeSolid.rotateX(Math.PI * 0.5) planeSolid.rotateX(Math.PI * 0.5)
planeSolid.receiveShadow = true; planeSolid.receiveShadow = true;
var grid = new THREE.GridHelper(200, 20, 0xff00ff, 0xff00ff);
var grid = new THREE.GridHelper(400, 30, 0x99bbff, 0x99bbff);
grid.rotateX(Math.PI * 0.5); grid.rotateX(Math.PI * 0.5);
// grid.position.setZ(0.5); grid.position.z = 0.05;
// console.log(grid.material)
(grid.material as THREE.LineBasicMaterial).opacity = 0.2; (grid.material as THREE.LineBasicMaterial).opacity = 0.2;
(grid.material as THREE.LineBasicMaterial).transparent = true; (grid.material as THREE.LineBasicMaterial).transparent = true;
// scene.add(planeWire); var gridFar = new THREE.GridHelper(200, 40, 0xff00ff, 0xff00ff);
// scene.add(planeSolid); gridFar.rotateX(Math.PI * 0.5);
scene.add(grid); gridFar.position.z = 0.02;
(gridFar.material as THREE.LineBasicMaterial).opacity = 0.1;
(gridFar.material as THREE.LineBasicMaterial).transparent = true;
scene.add(planeWire);
scene.add(planeSolid);
//scene.add(gridFar);
//scene.add(grid);
} }
// used to generate terrain
function generateHeight(width, height) { function generateHeight(width, height) {
var size = width * height, data = new Uint8Array(size), var size = width * height, data = new Uint8Array(size),
perlin = new ImprovedNoise(), quality = 1, z = Math.random() * 200; perlin = new ImprovedNoise(), quality = 1, z = Math.random() * 20;
for (var j = 0; j < 4; j++) { for (var j = 0; j < 4; j++) {
for (var i = 0; i < size; i++) { for (var i = 0; i < size; i++) {
var x = i % width, y = ~ ~(i / width); var ix = i % width, iy = ~ ~(i / width);
data[i] += Math.abs(perlin.noise(x / quality, y / quality, z) * quality * 1.75); data[i] += Math.abs(perlin.noise(ix / quality, iy / quality, z) * quality * 1.75);
} }
quality *= 5; quality *= 5;
} }
for (var j = 0; j < size; j++) {
var x = ((j % width) - 64) for (var i = 0; i < size; i++) {
var y = ((~ ~(j / width)) - 64) var x = ((i % width) - 64);
data[j] *= Math.sqrt(x * x + y * y) * 0.005 var y = ((~ ~(i / width)) - 64);
var distFromCenter = Math.sqrt(x * x + y * y);
var flatRadius = 4;
var mountainStart = 50;
if (distFromCenter < flatRadius) {
data[i] = 0;
} else if (distFromCenter < mountainStart) {
var t = (distFromCenter - flatRadius) / (mountainStart - flatRadius);
var rise = Math.pow(t, 1.5);
data[i] = data[i] * rise * 1.5;
} else {
var valleyWidth = 25;
var valleyDepth = Math.max(0, 1 - Math.abs(x) / valleyWidth);
valleyDepth = Math.pow(valleyDepth, 2);
data[i] = data[i] * (2.5 - valleyDepth * 1.2);
}
} }
return data; return data;
} }

View File

@ -45,7 +45,7 @@ export const levels: LevelInitializer[] = [
{ name: "action", containerIndex: 0 }] { name: "action", containerIndex: 0 }]
}], }],
activeCodeview: "main", activeCodeview: "main",
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"], allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action"],
introMessages: [{ message: "Hit run and watch.", image: null }, introMessages: [{ message: "Hit run and watch.", image: null },
{ message: "second page of intro message", image: null }] { message: "second page of intro message", image: null }]
}, },
@ -59,6 +59,25 @@ export const levels: LevelInitializer[] = [
{ x: 3, y: 2, z: 0 }, { x: 3, y: 2, z: 0 },
{ x: 3, y: 3, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }], { x: 3, y: 3, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }],
playerPos: { voxelIdx: 0, direction: "east" }, playerPos: { voxelIdx: 0, direction: "east" },
codeviews: [{
name: "main",
nMaxBlocks: 9,
blocks: [{ name: "forward" }, { name: "forward" }]
}],
activeCodeview: "main",
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action"],
introMessages: [{ message: "light up all dark cubes!", image: null }]
},
{
name: "hello fn",
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: [{ codeviews: [{
name: "main", name: "main",
nMaxBlocks: 3, nMaxBlocks: 3,
@ -72,16 +91,17 @@ export const levels: LevelInitializer[] = [
}], }],
activeCodeview: "main", activeCodeview: "main",
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"], allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
introMessages: [{ message: "light up all dark cubes!", image: null }] introMessages: [{ message: "Use a function for repeating patterns!", image: null }]
}, },
{ {
name: "jump n run", name: "jump n run",
voxels: [{ x: 0, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }, voxels: [{ x: 0, y: 0, z: 0},
{ x: 0, y: 1, z: 1 }, { x: 0, y: 1, z: 1 },
{ x: 0, y: 2, z: 1 }, { x: 0, y: 2, z: 1 , actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
{ x: 0, y: 3, z: 0 }, { x: 0, y: 3, z: 2 },
{ x: 0, y: 4, z: 0 }, { x: 0, y: 4, z: 2 , actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
{ x: 0, y: 5, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }], { x: 0, y: 5, z: 3},
{ x: 0, y: 6, z: 3, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }],
playerPos: { voxelIdx: 0, direction: "north" }, playerPos: { voxelIdx: 0, direction: "north" },
codeviews: [{ name: "main", nMaxBlocks: 4, blocks: [] }, codeviews: [{ name: "main", nMaxBlocks: 4, blocks: [] },
{ name: "f1", nMaxBlocks: 3, blocks: [] }], { name: "f1", nMaxBlocks: 3, blocks: [] }],
@ -91,15 +111,16 @@ export const levels: LevelInitializer[] = [
}, },
{ {
name: "jump n run 2", name: "jump n run 2",
voxels: [{ x: 0, y: 0, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }, voxels: [{ x: 0, y: 0, z: 0},
{ x: 0, y: 1, z: 1 }, { x: 0, y: 1, z: 1 },
{ x: 0, y: 2, z: 1 }, { x: 0, y: 2, z: 1 , actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
{ x: 1, y: 2, z: 0 }, { x: 1, y: 2, z: 2 },
{ x: 2, y: 2, z: 0 }, { x: 2, y: 2, z: 2 , actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } },
{ x: 3, y: 2, z: 0, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed } }], { x: 2, y: 1, z: 3},
{ x: 2, y: 0, z: 3, actionable: { type: BUTTON_ACTIONABLE.type, pushed: BUTTON_ACTIONABLE.pushed }}],
playerPos: { voxelIdx: 0, direction: "north" }, playerPos: { voxelIdx: 0, direction: "north" },
codeviews: [{ name: "main", nMaxBlocks: 4, blocks: [] }, codeviews: [{ name: "main", nMaxBlocks: 1, blocks: [] },
{ name: "f1", nMaxBlocks: 3, blocks: [] }], { name: "f1", nMaxBlocks: 5, blocks: [] }],
activeCodeview: "main", activeCodeview: "main",
allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"], allowedCodeBlocks: ["forward", "turn_right", "turn_left", "jump", "action", "f1"],
introMessages: null introMessages: null

View File

@ -13,7 +13,8 @@ import { wonLevel } from '../store/executionState/actions';
// model control from https://github.com/mrdoob/three.js/blob/master/examples/webgl_animation_skinning_morph.html // 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 states = ['Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing'];
// var emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp']; // var emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp'];
export const PLAYER_MODEL_FILENAME = '/assets/models/RobotExpressive.glb'
//export const PLAYER_MODEL_FILENAME = '/assets/models/robot.glb'
export class Player { export class Player {
scene: THREE.Scene; scene: THREE.Scene;
model: THREE.Object3D; model: THREE.Object3D;
@ -32,7 +33,7 @@ export class Player {
this.level = level; this.level = level;
this.clock = new THREE.Clock() this.clock = new THREE.Clock()
let loader = new GLTFLoader(); let loader = new GLTFLoader();
loader.load('assets/models/RobotExpressive.glb', (gltf) => { loader.load(PLAYER_MODEL_FILENAME, (gltf) => {
this.model = gltf.scene; this.model = gltf.scene;
this.animations = gltf.animations; this.animations = gltf.animations;
this.mixer = new THREE.AnimationMixer(this.model) this.mixer = new THREE.AnimationMixer(this.model)
@ -69,11 +70,11 @@ export class Player {
} }
reset() { reset() {
this.voxels = resetVoxels(this.voxels)
this.voxelStandingOn = this.voxels[this.level.playerPos.voxelIdx] this.voxelStandingOn = this.voxels[this.level.playerPos.voxelIdx]
this.setToVoxelPosition(this.voxelStandingOn) this.setToVoxelPosition(this.voxelStandingOn)
this.direction = this.level.playerPos.direction this.direction = this.level.playerPos.direction
this.initRotation(this.direction) this.initRotation(this.direction)
resetVoxels(this.voxels)
} }
doneAnimating() { doneAnimating() {

View File

@ -1,5 +1,4 @@
body { body {
background:#223;
} }
* { * {
@ -93,11 +92,12 @@ h1 {
width:16rem; width:16rem;
margin-bottom:0.8rem; margin-bottom:0.8rem;
height:12rem; height:12rem;
background:#2D2A2E;
} }
.fncontainer h3 { .fncontainer h2 {
text-indent:-2rem; text-indent:-2rem;
color:#9bf; color:#AB9DF2;
display:block; display:block;
margin-bottom:0.5rem; margin-bottom:0.5rem;
margin-top:0.4rem; margin-top:0.4rem;
@ -116,8 +116,8 @@ h1 {
.op { .op {
list-style-type:none; list-style-type:none;
background:#444; background:#3E3D32;
border: 0.1rem solid black; border: 0.1rem solid #49483E;
border-radius: 0.2rem; border-radius: 0.2rem;
width:3.8rem; width:3.8rem;
height:3.8rem; height:3.8rem;
@ -131,7 +131,7 @@ h1 {
} }
.opspalette { .opspalette {
background:#122; background:#1E1F1C;
display:flex; display:flex;
flex-flow: row; flex-flow: row;
padding-top: 1.6rem; padding-top: 1.6rem;
@ -144,14 +144,15 @@ h1 {
display:flex; display:flex;
flex-flow: row wrap; flex-flow: row wrap;
position:absolute; position:absolute;
z-index:-2; z-index:0;
width:16rem; width:16rem;
} }
.opindicator { .opindicator {
width:4rem; width:4rem;
height:4rem; height:4rem;
background:#555; background:#444640;
} }
.opdropzones { .opdropzones {
@ -164,8 +165,6 @@ h1 {
.opdropzone { .opdropzone {
width:4rem; width:4rem;
height:4rem; height:4rem;
background:#1119;
} }
.frontdropper { .frontdropper {
@ -177,12 +176,12 @@ h1 {
margin-top:1rem; margin-top:1rem;
margin-bottom:0.4rem; margin-bottom:0.4rem;
margin-left:2rem; margin-left:2rem;
color:#666; color:#FCFCFA;
user-select:none; user-select:none;
} }
.titlebar{ .titlebar{
background:#122; background:#1E1F1C;
width:100%; width:100%;
display:flex; display:flex;
flex-direction:row; flex-direction:row;
@ -204,8 +203,8 @@ h1 {
width:100%; width:100%;
height:100vh; height:100vh;
font-family:iosevka; font-family:iosevka;
color:#fff; color:#FCFCFA;
border-left: solid #112 0.4rem; background:#2D2A2E;
} }
.controlbuttons { .controlbuttons {
@ -214,20 +213,118 @@ h1 {
margin-right:2rem; margin-right:2rem;
margin-top:0.4rem; margin-top:0.4rem;
height:2.4rem; height:2.4rem;
gap: 0.5rem;
} }
.controlbutton { .controlbutton {
padding:0.5rem; padding: 0.7rem 1.4rem;
display:block; display: flex;
border: solid #112 0.05rem; align-items: center;
border-radius:0.3rem; gap: 0.5rem;
background:#221; border: none;
border-radius: 0.5rem;
cursor:pointer; cursor:pointer;
user-select:none; user-select:none;
font-size: 1rem;
font-weight: 500;
transition: all 0.15s ease;
color: #fff;
} }
.controlbutton:hover { .controlbutton span {
background:#332; line-height: 1;
}
.controlbutton.run {
background: #2a7;
}
.controlbutton.run:hover {
background: #3b8;
}
.controlbutton.levels {
background: #48b;
}
.controlbutton.levels:hover {
background: #59c;
}
.controlbutton.settings {
background: #636;
}
.controlbutton.settings:hover {
background: #747;
}
.controlbutton.debug {
background: #eA5;
color: #222;
}
.controlbutton.debug:hover {
background: #fb6;
}
.controlbutton.step {
background: #48b;
}
.controlbutton.step:hover {
background: #59c;
}
.controlbutton.reset {
background: #4af;
}
.controlbutton.reset:hover {
background: #5bf;
}
.controlbutton.speed {
background: #636;
}
.controlbutton.speed:hover {
background: #747;
}
.controlbutton.abort {
background: #e55;
}
.controlbutton.abort:hover {
background: #f66;
}
.modal-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
border: none;
border-radius: 0.4rem;
font-size: 1rem;
font-weight: 500;
color: #fff;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.modal-button:hover {
filter: brightness(1.1);
}
.modal-button.levels {
background: #48b;
}
.modal-button.next {
background: #2a7;
} }
.levelSelectCaption { .levelSelectCaption {
@ -284,19 +381,53 @@ h1 {
margin:auto; margin:auto;
} }
.start-screen {
background: url('/assets/textures/startscreen.png') no-repeat center center fixed;
background-size: cover;
}
.startContainer { .startContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 850px; align-items: flex-end;
height:600px; justify-content: flex-end;
margin:auto; height: 80vh;
justify-content: flex-start; padding-right: 30%;
padding-bottom: 20%;
gap: 3rem;
} }
.startContainer a { .startContainer a {
margin-top:2rem; margin-top: 0;
} }
.gameName { .gameName {
margin-top: 4rem; margin-top: 4rem;
} }
.start-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin: 1.5rem auto 0;
padding: 0.7rem 2rem;
background: #2a7;
border: none;
border-radius: 0.4rem;
font-size: 1.1rem;
font-weight: 600;
color: #fff;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
width:auto;
}
.start-button:hover {
background: #3b8;
}
.start-button span {
line-height: 1;
}

View File

@ -5,7 +5,7 @@ import { cloneDeep, pullAt } from "lodash"
export function codeBlocksReducer(state: Array<CodeBlockContainer> = [], action: CodeBlocksActionTypes): Array<CodeBlockContainer> { export function codeBlocksReducer(state: Array<CodeBlockContainer> = [], action: CodeBlocksActionTypes): Array<CodeBlockContainer> {
switch (action.type) { switch (action.type) {
case INIT_CODEBLOCKS: case INIT_CODEBLOCKS:
return Object.assign([], state, action.level.codeviews) return cloneDeep(action.level.codeviews)
case REMOVE_CODEBLOCK: case REMOVE_CODEBLOCK:
return reduceRemoveBlock(state, action) return reduceRemoveBlock(state, action)
case INSERT_CODEBLOCK: case INSERT_CODEBLOCK: