save progress
77
package-lock.json
generated
@@ -8,9 +8,8 @@
|
|||||||
"name": "svelte-app",
|
"name": "svelte-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p5": "^1.6.0",
|
"p5": "^1.11.4",
|
||||||
"p5-svelte": "^3.1.2",
|
"p5play": "^3.22.0",
|
||||||
"p5play": "^3.8.14",
|
|
||||||
"sirv-cli": "^2.0.0",
|
"sirv-cli": "^2.0.0",
|
||||||
"svelte-spa-router": "^3.3.0"
|
"svelte-spa-router": "^3.3.0"
|
||||||
},
|
},
|
||||||
@@ -194,12 +193,6 @@
|
|||||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/p5": {
|
|
||||||
"version": "1.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.6.2.tgz",
|
|
||||||
"integrity": "sha512-iy1B7DBMc9e5j0DrWnIjSJh+Ro9UkKxPHspbx/GvIeGFTOIJF0H2lICnIN4go1OZEAQSHBinLd7t+oe8lrrLvA==",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -641,26 +634,30 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/p5": {
|
"node_modules/p5": {
|
||||||
"version": "1.6.0",
|
"version": "1.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/p5/-/p5-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/p5/-/p5-1.11.4.tgz",
|
||||||
"integrity": "sha512-RowF+RxfVUhJm/YKXL5TCFzTqnwAIwK6W1VGs9LAqSf3PCmLz9Igbxzlf0Ry5IMV71L42wipCdH/bDiNsqAstA=="
|
"integrity": "sha512-N7tM2XYSmuNX8S295RvgHoJS7kpYLYxLjVFeySkwkbxwVrGnrwY8yAwciTxlonBjP422W7WW9pihpUVP8bAVgg==",
|
||||||
},
|
"license": "LGPL-2.1"
|
||||||
"node_modules/p5-svelte": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/p5-svelte/-/p5-svelte-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-lcfWh+cJ1/wRdIXHnjpYmDgj2h3TCy1QJVQnf/cBcFWS8CSkvyAN5F8u8H2U8qBUtZ4XaD3nd+1NoYUMHaMExQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"p5": "^1.4.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/p5": "^1.4.2",
|
|
||||||
"p5": "^1.4.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/p5play": {
|
"node_modules/p5play": {
|
||||||
"version": "3.8.14",
|
"version": "3.35.4",
|
||||||
"resolved": "https://registry.npmjs.org/p5play/-/p5play-3.8.14.tgz",
|
"resolved": "https://registry.npmjs.org/p5play/-/p5play-3.35.4.tgz",
|
||||||
"integrity": "sha512-z2TjIIJ4td9KGIubsyftaJJsh9FXxI+Wl/JDXWfshwJyErYl9wjQpMHdeWxlC+zrg40PHXeYzxG6Gq939bVe4A=="
|
"integrity": "sha512-5C0QobV0a36JhFacV0rrMvgeJNFWYtIpS1EcvHYptmzGXFRt6x9/mvEegiWPqNq5LqRf30XeD3g1JJLfoHYzwQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/q5play"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ko-fi",
|
||||||
|
"url": "https://ko-fi.com/q5play"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/quinton-ashley"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "p5play Personal License"
|
||||||
},
|
},
|
||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
@@ -1142,12 +1139,6 @@
|
|||||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/p5": {
|
|
||||||
"version": "1.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.6.2.tgz",
|
|
||||||
"integrity": "sha512-iy1B7DBMc9e5j0DrWnIjSJh+Ro9UkKxPHspbx/GvIeGFTOIJF0H2lICnIN4go1OZEAQSHBinLd7t+oe8lrrLvA==",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"@types/resolve": {
|
"@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -1478,22 +1469,14 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"p5": {
|
"p5": {
|
||||||
"version": "1.6.0",
|
"version": "1.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/p5/-/p5-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/p5/-/p5-1.11.4.tgz",
|
||||||
"integrity": "sha512-RowF+RxfVUhJm/YKXL5TCFzTqnwAIwK6W1VGs9LAqSf3PCmLz9Igbxzlf0Ry5IMV71L42wipCdH/bDiNsqAstA=="
|
"integrity": "sha512-N7tM2XYSmuNX8S295RvgHoJS7kpYLYxLjVFeySkwkbxwVrGnrwY8yAwciTxlonBjP422W7WW9pihpUVP8bAVgg=="
|
||||||
},
|
|
||||||
"p5-svelte": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/p5-svelte/-/p5-svelte-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-lcfWh+cJ1/wRdIXHnjpYmDgj2h3TCy1QJVQnf/cBcFWS8CSkvyAN5F8u8H2U8qBUtZ4XaD3nd+1NoYUMHaMExQ==",
|
|
||||||
"requires": {
|
|
||||||
"p5": "^1.4.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"p5play": {
|
"p5play": {
|
||||||
"version": "3.8.14",
|
"version": "3.35.4",
|
||||||
"resolved": "https://registry.npmjs.org/p5play/-/p5play-3.8.14.tgz",
|
"resolved": "https://registry.npmjs.org/p5play/-/p5play-3.35.4.tgz",
|
||||||
"integrity": "sha512-z2TjIIJ4td9KGIubsyftaJJsh9FXxI+Wl/JDXWfshwJyErYl9wjQpMHdeWxlC+zrg40PHXeYzxG6Gq939bVe4A=="
|
"integrity": "sha512-5C0QobV0a36JhFacV0rrMvgeJNFWYtIpS1EcvHYptmzGXFRt6x9/mvEegiWPqNq5LqRf30XeD3g1JJLfoHYzwQ=="
|
||||||
},
|
},
|
||||||
"path-parse": {
|
"path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
"svelte": "^3.55.0"
|
"svelte": "^3.55.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p5": "^1.6.0",
|
"p5": "^1.11.4",
|
||||||
"p5-svelte": "^3.1.2",
|
|
||||||
"p5play": "^3.8.14",
|
"p5play": "^3.22.0",
|
||||||
"sirv-cli": "^2.0.0",
|
"sirv-cli": "^2.0.0",
|
||||||
"svelte-spa-router": "^3.3.0"
|
"svelte-spa-router": "^3.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/assets/Fragment.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/enemy.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
public/assets/heart_empty.png
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
public/assets/heart_full.png
Normal file
|
After Width: | Height: | Size: 739 B |
|
Before Width: | Height: | Size: 32 KiB |
BIN
public/assets/player.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/assets/player_jump.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/assets/splat.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/assets/tar.png
Normal file
|
After Width: | Height: | Size: 477 B |
BIN
public/assets/tile.png
Normal file
|
After Width: | Height: | Size: 687 B |
BIN
public/backgrounds/temp-background.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
@@ -2,24 +2,22 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<meta charset='utf-8'>
|
||||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
|
|
||||||
<title>Svelte app</title>
|
<title>ColorQuest</title>
|
||||||
|
|
||||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
<link rel='stylesheet' href='/global.css'>
|
<link rel='stylesheet' href='/global.css'>
|
||||||
<link rel='stylesheet' href='/build/bundle.css'>
|
<link rel='stylesheet' href='/build/bundle.css'>
|
||||||
|
|
||||||
<!-- p5 -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/p5.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/addons/p5.sound.min.js"></script>
|
|
||||||
<!-- p5play -->
|
|
||||||
<script src="https://p5play.org/v3/planck.min.js"></script>
|
|
||||||
<script src="https://p5play.org/v3/p5play.js"></script>
|
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.4/lib/p5.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.4/lib/addons/p5.sound.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/planck@1.0.0/dist/planck.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/p5play@3.22.0/p5play.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<script defer src='/build/bundle.js'></script>
|
<script src='/build/bundle.js'></script>
|
||||||
</body </html>
|
</body>
|
||||||
|
</html>
|
||||||
@@ -7,6 +7,8 @@ import LevelSelect from './routes/LevelSelect.svelte';
|
|||||||
import Game from './routes/Game.svelte';
|
import Game from './routes/Game.svelte';
|
||||||
import GameOver from './routes/GameOver.svelte';
|
import GameOver from './routes/GameOver.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// this is the application of the svelte-spa-router
|
// this is the application of the svelte-spa-router
|
||||||
// e.g. /#/game will show the Game component
|
// e.g. /#/game will show the Game component
|
||||||
// this is kind of like switching between HTML pages but we switch components instead
|
// this is kind of like switching between HTML pages but we switch components instead
|
||||||
@@ -18,7 +20,6 @@ const routes = {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- the router swaps components based on URL -->
|
|
||||||
<Router {routes} />
|
<Router {routes} />
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<script>
|
||||||
|
import {onMount, onDestroy} from 'svelte';
|
||||||
|
import {Player} from '../game/Player.js';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import {lives, colorOpacity, levelColor, onFragmentCollection, resetLevel, completeLevel} from '../stores/colorStore.js';
|
||||||
|
import {push} from 'svelte-spa-router';
|
||||||
|
import {Enemy} from '../game/Enemy.js';
|
||||||
|
import {TarPuddle} from '../game/TarPuddle.js';
|
||||||
|
import {getLevel} from '../game/levelData.js';
|
||||||
|
import {Fragment} from '../game/Fragment.js';
|
||||||
|
|
||||||
|
export let levelNumber = 1;
|
||||||
|
|
||||||
|
let canvasContainer;
|
||||||
|
let p5Instance;
|
||||||
|
|
||||||
|
const keysDown = {left: false, right: false, jump: false};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// IMPORTANT: parameter is named "p" not "p5"
|
||||||
|
// "p5" is the global constructor from the CDN script
|
||||||
|
// if you name the parameter "p5" it shadows the global and breaks new p5(sketch)
|
||||||
|
const sketch = (p) => {
|
||||||
|
let player;
|
||||||
|
let platforms;
|
||||||
|
let fragments;
|
||||||
|
let bgImg;
|
||||||
|
let splatImg;
|
||||||
|
let enemies = [];
|
||||||
|
let tarPuddles = [];
|
||||||
|
let gameState = 'playing';
|
||||||
|
let splats = [];
|
||||||
|
let levelData; // declared once here at sketch level
|
||||||
|
|
||||||
|
// onFragmentCollected lives here so both setup and draw can see it
|
||||||
|
function onFragmentCollected(hexColor, x, y) {
|
||||||
|
player.collectFragment(hexColor);
|
||||||
|
onFragmentCollection(fragments.length, hexColor);
|
||||||
|
splats.push({x, y, alpha: 200, size: 30, color: hexColor});
|
||||||
|
if (fragments.every(f => f.collected)) {
|
||||||
|
gameState = 'levelcomplete';
|
||||||
|
completeLevel(levelData.color);
|
||||||
|
setTimeout(() => push('/levelselect'), 2500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.preload = () => {
|
||||||
|
bgImg = p.loadImage('/backgrounds/temp-background.png');
|
||||||
|
splatImg = p.loadImage('/assets/splat.png');
|
||||||
|
};
|
||||||
|
|
||||||
|
p.setup = () => {
|
||||||
|
const canvas = p.createCanvas(800, 450);
|
||||||
|
canvas.parent(canvasContainer);
|
||||||
|
|
||||||
|
// assign to the outer levelData, no "const" here
|
||||||
|
levelData = getLevel(levelNumber);
|
||||||
|
|
||||||
|
resetLevel(levelNumber, levelData.color);
|
||||||
|
|
||||||
|
// gravity must be set before creating sprites
|
||||||
|
p.world.gravity.y = 10;
|
||||||
|
|
||||||
|
player = new Player(p, levelData.spawnX, levelData.spawnY);
|
||||||
|
|
||||||
|
platforms = new p.Group();
|
||||||
|
for (const platData of levelData.platforms) {
|
||||||
|
const plat = new p.Sprite(platData.x, platData.y, platData.w, platData.h);
|
||||||
|
plat.collider = 'static';
|
||||||
|
plat.image = '/assets/tile.png';
|
||||||
|
platforms.add(plat);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragments = [];
|
||||||
|
for (const fragData of levelData.fragments) {
|
||||||
|
fragments.push(new Fragment(p, fragData.x, fragData.y, fragData.color));
|
||||||
|
}
|
||||||
|
|
||||||
|
enemies = [];
|
||||||
|
for (const enemyData of levelData.enemies) {
|
||||||
|
const e = new Enemy(p, enemyData.x, enemyData.y);
|
||||||
|
e.patrolDistance = enemyData.patrol;
|
||||||
|
enemies.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
tarPuddles = [];
|
||||||
|
for (const tarData of levelData.tar) {
|
||||||
|
tarPuddles.push(new TarPuddle(p, tarData.x, tarData.y));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
p.draw = () => {
|
||||||
|
p.background(30);
|
||||||
|
|
||||||
|
// draw background image if loaded
|
||||||
|
if (bgImg) {
|
||||||
|
p.image(bgImg, 0, 0, p.width, p.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// color tint overlay based on fragments collected
|
||||||
|
const reveal = get(colorOpacity);
|
||||||
|
const hex = get(levelColor);
|
||||||
|
|
||||||
|
if (reveal > 0 && hex) {
|
||||||
|
const col = p.color(hex);
|
||||||
|
const r = p.lerp(128, p.red(col), reveal);
|
||||||
|
const g = p.lerp(128, p.green(col), reveal);
|
||||||
|
const b = p.lerp(128, p.blue(col), reveal);
|
||||||
|
p.noStroke();
|
||||||
|
p.fill(r, g, b, reveal * 120);
|
||||||
|
p.rect(0, 0, p.width, p.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fragments
|
||||||
|
for (const frag of fragments) {
|
||||||
|
frag.drawGlow();
|
||||||
|
frag.update(player.sprite, onFragmentCollected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// player glow then update
|
||||||
|
player.drawGlow();
|
||||||
|
player.update(keysDown);
|
||||||
|
player.sprite.visible = player.isVisible();
|
||||||
|
|
||||||
|
// enemies
|
||||||
|
for (const enemy of enemies) {
|
||||||
|
enemy.update();
|
||||||
|
if (enemy.overlapsPlayer(player.sprite)) {
|
||||||
|
const dead = player.loseLife();
|
||||||
|
lives.set(player.lives);
|
||||||
|
if (dead) {
|
||||||
|
gameState = 'gameover';
|
||||||
|
setTimeout(() => push('/gameover'), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tar puddles
|
||||||
|
for (const tar of tarPuddles) {
|
||||||
|
if (tar.overlapsPlayer(player.sprite)) {
|
||||||
|
const dead = player.loseLife();
|
||||||
|
lives.set(player.lives);
|
||||||
|
if (dead) {
|
||||||
|
gameState = 'gameover';
|
||||||
|
setTimeout(() => push('/gameover'), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splat effects
|
||||||
|
splats = splats.filter(s => s.alpha > 0);
|
||||||
|
for (const s of splats) {
|
||||||
|
if (!splatImg) continue;
|
||||||
|
p.push();
|
||||||
|
p.tint(255, s.alpha);
|
||||||
|
p.image(splatImg, s.x - s.size, s.y - s.size, s.size * 2, s.size * 2);
|
||||||
|
p.pop();
|
||||||
|
s.alpha -= 4;
|
||||||
|
s.size += 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fell off screen
|
||||||
|
if (player.sprite.y > p.height + 100) {
|
||||||
|
player.loseLife();
|
||||||
|
lives.set(player.lives);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.allSprites.update();
|
||||||
|
p.allSprites.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
p.keyPressed = () => {
|
||||||
|
if (p.keyCode === p.LEFT_ARROW || p.key === 'a') keysDown.left = true;
|
||||||
|
if (p.keyCode === p.RIGHT_ARROW || p.key === 'd') keysDown.right = true;
|
||||||
|
if (p.keyCode === p.UP_ARROW || p.key === 'w' || p.key === ' ') keysDown.jump = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
p.keyReleased = () => {
|
||||||
|
if (p.keyCode === p.LEFT_ARROW || p.key === 'a') keysDown.left = false;
|
||||||
|
if (p.keyCode === p.RIGHT_ARROW || p.key === 'd') keysDown.right = false;
|
||||||
|
if (p.keyCode === p.UP_ARROW || p.key === 'w' || p.key === ' ') keysDown.jump = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// p5 here refers to the global from the CDN script, not the sketch parameter
|
||||||
|
p5Instance = new p5(sketch);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (p5Instance) p5Instance.remove();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={canvasContainer} class="canvas-wrap"></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.canvas-wrap {
|
||||||
|
width: 800px;
|
||||||
|
height: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
import {lives, fragmentsCollected, levelColor} from '../stores/colorStore.js'
|
||||||
|
import { LEVELS } from '../game/levelData.js'
|
||||||
|
|
||||||
|
export let levelNumber = 1;
|
||||||
|
|
||||||
|
// get total fragments for the level
|
||||||
|
$: totalFragments = LEVELS.find(l => l.id === levelNumber)?.fragments.length ?? 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="hud">
|
||||||
|
<!-- lives counter displayed on the left-->
|
||||||
|
<div class = "lives">
|
||||||
|
{#each {length: 3} as _, i}
|
||||||
|
<img
|
||||||
|
src={i < $lives ? '/assets/heart_full.png' : '/assets/heart_empty.png'}
|
||||||
|
alt="life"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- fragment collections shown on the right-->
|
||||||
|
<div class="frags" style="color: {$levelColor}">
|
||||||
|
{$fragmentsCollected} / {totalFragments}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hud {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
pointer-events: none; /* this allows the clicks to pass through to the game */
|
||||||
|
z-index: 10; /* shows on top */
|
||||||
|
}
|
||||||
|
|
||||||
|
.lives {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frags{
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
text-shadow: 0 1px 4px rgba(0,0,0,0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export class Enemy{
|
||||||
|
constructor(p5, x, y) {
|
||||||
|
this.sprite = new p5.Sprite(x,y,36,36);
|
||||||
|
this.sprite.image = '/assets/enemy.png';
|
||||||
|
this.sprite.collider = 'dynamic';
|
||||||
|
this.sprite.rotationLock = true;
|
||||||
|
this.sprite.bounciness = 0;
|
||||||
|
|
||||||
|
this.speed = 1.5;
|
||||||
|
this.direction = 1 // 1 is right, -1 is left
|
||||||
|
this.startX = x;
|
||||||
|
this.patrolDisctance = 100; // how far it can walk
|
||||||
|
}
|
||||||
|
|
||||||
|
update(){
|
||||||
|
// move in current direction
|
||||||
|
this.sprite.vel.x = this.speed * this.direction;
|
||||||
|
|
||||||
|
// flip direction once it reaches patrol distance
|
||||||
|
if (this.sprite.x > this.startX + this.patrolDistance){
|
||||||
|
this.direction = -1;
|
||||||
|
this.sprite.mirror.x = -1; // we flip the image
|
||||||
|
}
|
||||||
|
if (this.sprite.x < this.startX - this.patrolDistance){
|
||||||
|
this.direction = 1;
|
||||||
|
this.sprite.mirror.x = 1; // flip image back
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overlapsPlayer(playerSprite){
|
||||||
|
return playerSprite.overlaps(this.sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export class Fragment {
|
||||||
|
constructor(p5, x, y, hexColor){
|
||||||
|
this.p5 = p5;
|
||||||
|
this.hexColor = hexColor;
|
||||||
|
this.collected = false;
|
||||||
|
this.baseY = y // this Y will be used for bobbing math
|
||||||
|
this.bobAngle = p5.random(0, 360); // so they are not all in sync
|
||||||
|
|
||||||
|
// not a solid body so player can pass through
|
||||||
|
this.sprite = new p5.Sprite(x,y,20,20);
|
||||||
|
this.sprite.image = '/assets/Fragment.png';
|
||||||
|
this.sprite.collider = 'none';
|
||||||
|
this.sprite.rotationLock = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// call it every frame
|
||||||
|
// callback function when picked up
|
||||||
|
update(playerSprite, onCollect){
|
||||||
|
if (this.collected) return;
|
||||||
|
|
||||||
|
// bob up and down based on sine wave
|
||||||
|
this.bobAngle += 2;
|
||||||
|
this.sprite.y = this.baseY + Math.sin(this.bobAngle * 0.04) * 7;
|
||||||
|
|
||||||
|
// slow spin for funsies
|
||||||
|
this.sprite.rotation += 1;
|
||||||
|
|
||||||
|
// check if player overlap with fragment
|
||||||
|
if (playerSprite.overlaps(this.sprite)){
|
||||||
|
this.collect(onCollect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect(onCollect) {
|
||||||
|
this.collected = true;
|
||||||
|
// save position BEFORE removing the sprite
|
||||||
|
const x = this.sprite.x;
|
||||||
|
const y = this.sprite.y;
|
||||||
|
this.sprite.remove();
|
||||||
|
onCollect(this.hexColor, x, y);
|
||||||
|
}
|
||||||
|
// draw color glow around fragment
|
||||||
|
// before drawing the sprite
|
||||||
|
drawGlow(){
|
||||||
|
if (this.collected) return;
|
||||||
|
|
||||||
|
this.p5.push();
|
||||||
|
this.p5.noStroke();
|
||||||
|
|
||||||
|
// glow pulsates based on sine wave
|
||||||
|
const pulse = Math.sin(this.bobAngle * 0.08) * 8;
|
||||||
|
const glowSize = 45 + pulse;
|
||||||
|
|
||||||
|
const col = this.p5.color(this.hexColor);
|
||||||
|
col.setAlpha(60);
|
||||||
|
this.p5.fill(col);
|
||||||
|
this.p5.circle(this.sprite.x, this.sprite.y, glowSize);
|
||||||
|
|
||||||
|
this.p5.pop();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
export class Player {
|
||||||
|
// p5 = the instance, x and y are start position
|
||||||
|
constructor(p5, x, y){
|
||||||
|
this.p5 = p5;
|
||||||
|
this.spawnX = x;
|
||||||
|
this.spawnY = y;
|
||||||
|
|
||||||
|
// p5play sprite - this is the body with physics
|
||||||
|
this.sprite = new p5.Sprite(x,y,28,28);
|
||||||
|
|
||||||
|
// get the character image
|
||||||
|
this.sprite.image = '/assets/player.png';
|
||||||
|
|
||||||
|
// we dont want player to tip over
|
||||||
|
// lock rotation
|
||||||
|
this.sprite.rotationLock = true;
|
||||||
|
|
||||||
|
// how slidey the movement is
|
||||||
|
// 0 is not friction 1 is very high friction
|
||||||
|
this.sprite.friction = 0; // we change later based on vibes
|
||||||
|
|
||||||
|
// we dont want the player to be bouncy... for now at least
|
||||||
|
this.sprite.bounciness = 0;
|
||||||
|
|
||||||
|
// game state
|
||||||
|
this.lives = 3;
|
||||||
|
this.collectedColors = []; // hex color strings
|
||||||
|
this.isInvincible = false;
|
||||||
|
this.invincibleTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will be called every frame from the game loop
|
||||||
|
// KeysDwon is an object we can use it is {left, right, jump}
|
||||||
|
update(keysDown){
|
||||||
|
const SPEED = 4; // we can change this later
|
||||||
|
|
||||||
|
if (keysDown.left) this.sprite.vel.x = -SPEED;
|
||||||
|
if (keysDown.right) this.sprite.vel.x = SPEED;
|
||||||
|
|
||||||
|
// If no key is pressed, slow down
|
||||||
|
if (!keysDown.left && !keysDown.right){
|
||||||
|
this.sprite.vel.x *= 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// jump but only if the character is on the ground
|
||||||
|
// p5play built in = sprite.touching.bottom
|
||||||
|
if (keysDown.jump && this.sprite.touching && this.sprite.touching.bottom){
|
||||||
|
this.sprite.vel.y = -11; // negative is up in p5
|
||||||
|
keysDown.jump = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for when the player is invincible
|
||||||
|
if (this.isInvincible){
|
||||||
|
this.invincibleTimer--;
|
||||||
|
if (this.invincibleTimer <= 0){
|
||||||
|
this.isInvincible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if player hits tar or enemym they lose a life
|
||||||
|
/// if no lives left, game over
|
||||||
|
loseLife(){
|
||||||
|
if (this.isInvincible) return false;
|
||||||
|
|
||||||
|
this.lives--;
|
||||||
|
this.isInvincible = true; // wont gain damage a little bit after losign a life
|
||||||
|
this.invincibleTimer = 90; // 1.5 seconds if we are at 60fps
|
||||||
|
this.respawn();
|
||||||
|
|
||||||
|
return this.lives <=0; // if true = game over
|
||||||
|
}
|
||||||
|
|
||||||
|
// respawn at level start spawn point and be static
|
||||||
|
respawn(){
|
||||||
|
this.sprite.x = this.spawnX;
|
||||||
|
this.sprite.y = this.spawnY;
|
||||||
|
this.sprite.vel.x = 0;
|
||||||
|
this.sprite.vel.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// player walks into a frament
|
||||||
|
collectFragment(hexColor){
|
||||||
|
this.collectedColors.push(hexColor);
|
||||||
|
// we start getting color!
|
||||||
|
}
|
||||||
|
|
||||||
|
//glow effect around the player
|
||||||
|
// this should be called before drawing character so its under it
|
||||||
|
drawGlow(){
|
||||||
|
if (this.collectedColors.length === 0) return;
|
||||||
|
|
||||||
|
// we want the last collected color to be the glow
|
||||||
|
const c = this.collectedColors[this.collectedColors.length -1];
|
||||||
|
|
||||||
|
this.p5.push();
|
||||||
|
this.p5.noStroke();
|
||||||
|
|
||||||
|
// draw a gradiet with circles that lessen in opacity
|
||||||
|
// fake flow effect lol
|
||||||
|
// measurements based on 64 by 64 character
|
||||||
|
for (let radius = 60; radius > 0; radius -= 8){
|
||||||
|
const alpha = (radius / 60) * 40; // max is 40
|
||||||
|
const col = this.p5.color(c);
|
||||||
|
col.setAlpha(alpha);
|
||||||
|
this.p5.fill(col);
|
||||||
|
this.p5.circle(this.sprite.x, this.sprite.y, radius*2);
|
||||||
|
}
|
||||||
|
this.p5.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// flickery effect for invincibility
|
||||||
|
isVisible(){
|
||||||
|
if (!this.isInvincible) return true;
|
||||||
|
// show/hide every 6 frames so it blinks
|
||||||
|
return Math.floor(this.invincibleTimer / 6) % 2 === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// the tar puddle is like an enemy
|
||||||
|
|
||||||
|
export class TarPuddle {
|
||||||
|
constructor(p5, x, y) {
|
||||||
|
this.sprite = new p5.Sprite(x, y, 64, 16);
|
||||||
|
this.sprite.image = '/assets/tar.png';
|
||||||
|
this.sprite.collider = 'none'; // no physics needed just check for overlap
|
||||||
|
this.sprite.rotationLock = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlapsPlayer(playerSprite){
|
||||||
|
return playerSprite.overlaps(this.sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// level config will be in one array
|
||||||
|
// index 0 = level 1, etc
|
||||||
|
// edit level change calculations here
|
||||||
|
// x,y = center position. w, h = width and height of the platform
|
||||||
|
|
||||||
|
export const LEVELS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'The Gray Beginning',
|
||||||
|
color: '#FF4136',
|
||||||
|
//bgFar: '/backgrounds/level1_far/png',
|
||||||
|
//bgMid: '/backgrounds/level1_mid.png',
|
||||||
|
spawnX: 80,
|
||||||
|
spawnY: 380,
|
||||||
|
|
||||||
|
// each platform: {x,y,w,h}
|
||||||
|
platforms: [
|
||||||
|
{x: 400, y: 440, w: 800, h: 20}, // ground
|
||||||
|
{x: 220, y: 360, w: 160, h: 16},
|
||||||
|
{x: 430, y: 300, w: 140, h: 16 },
|
||||||
|
{x: 640, y: 240, w: 160, h: 16 },
|
||||||
|
{x: 320, y: 210, w: 120, h: 16 },
|
||||||
|
],
|
||||||
|
|
||||||
|
// each fragment: {x,y,color}
|
||||||
|
fragments: [
|
||||||
|
{x: 220, y: 330, color: '#FF4136'},
|
||||||
|
{x: 430, y: 270, color: '#FF4136'},
|
||||||
|
{x: 640, y: 210, color: '#FF4136'},
|
||||||
|
],
|
||||||
|
|
||||||
|
// each enemy: {x,y, patrol}
|
||||||
|
enemies: [
|
||||||
|
{x: 430, y: 280, patrol: 50},
|
||||||
|
],
|
||||||
|
|
||||||
|
// each puddle: {x, y}
|
||||||
|
tar: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Warmer skies',
|
||||||
|
color: '#FF851B',
|
||||||
|
bgFar: '/backgrounds/level2_far.png',
|
||||||
|
bgMid: '/backgrounds/level2_mid.png',
|
||||||
|
spawnX: 80,
|
||||||
|
spawnY: 380,
|
||||||
|
|
||||||
|
platforms: [
|
||||||
|
{ x: 400, y: 440, w: 800, h: 20 },
|
||||||
|
{ x: 180, y: 370, w: 140, h: 16 },
|
||||||
|
{ x: 380, y: 310, w: 120, h: 16 },
|
||||||
|
{ x: 560, y: 250, w: 120, h: 16 },
|
||||||
|
{ x: 700, y: 330, w: 100, h: 16 },
|
||||||
|
{ x: 300, y: 200, w: 100, h: 16 },
|
||||||
|
],
|
||||||
|
|
||||||
|
fragments: [
|
||||||
|
{ x: 180, y: 340, color: '#FF851B' },
|
||||||
|
{ x: 380, y: 280, color: '#FF851B' },
|
||||||
|
{ x: 300, y: 170, color: '#FF851B' },
|
||||||
|
{ x: 700, y: 300, color: '#FF851B' },
|
||||||
|
],
|
||||||
|
|
||||||
|
enemies: [
|
||||||
|
{ x: 380, y: 290, patrol: 45 },
|
||||||
|
{ x: 560, y: 230, patrol: 40 },
|
||||||
|
],
|
||||||
|
|
||||||
|
tar: [
|
||||||
|
{ x: 150, y: 432 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Levels 3–5 will follow the same pattern
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// get level by ID (indexed)
|
||||||
|
export function getLevel(id){
|
||||||
|
return LEVELS.find(level => level.id === id)
|
||||||
|
}
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
<h1 style="color:black; padding:20px">
|
<script>
|
||||||
ColorQuest - Game (this is a placeholder)
|
import { querystring } from 'svelte-spa-router';
|
||||||
</h1>
|
import GameCanvas from '../components/GameCanvas.svelte';
|
||||||
|
import HUD from '../components/HUD.svelte';
|
||||||
|
|
||||||
<a href='#/gameover'>
|
// querystring is the part after ? in the URL e.g. "level=2"
|
||||||
<button>Game over</button>
|
// we parse it safely — if missing, default to level 1
|
||||||
</a>
|
$: levelNum = $querystring
|
||||||
|
? parseInt(new URLSearchParams($querystring).get('level')) || 1
|
||||||
|
: 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="game-wrapper">
|
||||||
|
<GameCanvas levelNumber={levelNum}/>
|
||||||
|
<HUD levelNumber={levelNum}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.game-wrapper{
|
||||||
|
position: relative;
|
||||||
|
width: 800px;
|
||||||
|
height: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,67 @@
|
|||||||
<h1 style="color:black; padding:20px">
|
<script>
|
||||||
ColorQuest - Game Over (this is a placeholder)
|
import {push} from 'svelte-spa-router';
|
||||||
</h1>
|
import {querystring} from 'svelte-spa-router';
|
||||||
|
import {resetLevel} from '../stores/colorStore.js';
|
||||||
|
import {getLevel} from '../game/levelData.js';
|
||||||
|
|
||||||
<a href='#/levelselect'>
|
$: levelNum = parseInt(new URLSearchParams($querystring).get('level')) || 1;
|
||||||
<button>Play Again</button>
|
|
||||||
</a>
|
function retry(){
|
||||||
|
const level = getLevel(levelNum);
|
||||||
|
resetLevel(levelNum, level.color);
|
||||||
|
push(`/game/${levelNum}`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="screen">
|
||||||
|
<h1>the light faded...</h1>
|
||||||
|
<p>the world stays gray a little longer</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<button on:click={retry}>try again</button>
|
||||||
|
<button on:click={() => push('/levelselect')}>level select</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.screen{
|
||||||
|
width: 800px;
|
||||||
|
height: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
h1{
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
.buttons{
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
padding: 10px 32px;
|
||||||
|
border-radius: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
button:hover{
|
||||||
|
background: rgba(255,255,255,0.18);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,72 @@
|
|||||||
<h1 style="color:black; padding:20px">
|
<script>
|
||||||
ColorQuest - Home (this is a placeholder)
|
import {push} from 'svelte-spa-router';
|
||||||
</h1>
|
import {unlockedColors} from '../stores/colorStore.js';
|
||||||
|
|
||||||
<a href='#/game'>
|
function startGame() {
|
||||||
<button>Start Game</button>
|
push('/levelselect');
|
||||||
</a>
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="title-screen">
|
||||||
|
<!--<img src="/backgrounds/title_bg.png" class="bg" alt="title background"/>-->
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h1>ColorQuest</h1>
|
||||||
|
<p>Bring color back to your world</p>
|
||||||
|
<button on:click={startGame}>begin</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title-screen{
|
||||||
|
width: 800px;
|
||||||
|
height: 450;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #111;
|
||||||
|
height: 450px;
|
||||||
|
}
|
||||||
|
.bg{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
h1{
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 64px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-shadow: 0 2px 12px rgba(0,0,0,0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 22px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px 40px;
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 22px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255,255,255,0.4);
|
||||||
|
border-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
button:hover{background: rgba(255,255,255,0.3);}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,100 @@
|
|||||||
<h1 style="color:black; padding:20px">
|
<script>
|
||||||
ColorQuest - Level Select (this is a placeholder)
|
import {push} from 'svelte-spa-router';
|
||||||
</h1>
|
import {unlockedColors} from '../stores/colorStore.js';
|
||||||
|
import {LEVELS} from '../game/levelData.js';
|
||||||
|
|
||||||
<a href='#/game'>
|
function playLevel(id) {
|
||||||
<button>Start Game</button>
|
push(`/game?level=${id}`);
|
||||||
</a>
|
}
|
||||||
|
|
||||||
|
// level 1 is always unlocked, other based on prev level completion
|
||||||
|
function isUnlocked(level){
|
||||||
|
if (level.id === 1) return true;
|
||||||
|
const prevLevel = LEVELS.find(l => l.id === level.id -1);
|
||||||
|
return $unlockedColors.includes(prevLevel.color);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="screen">
|
||||||
|
<h1>Choose a level</h1>
|
||||||
|
|
||||||
|
<div class="level-grid">
|
||||||
|
{#each LEVELS as level}
|
||||||
|
<button
|
||||||
|
class="level-card"
|
||||||
|
class:locked={!isUnlocked(level)}
|
||||||
|
style="--c: {level.color}"
|
||||||
|
on:click={() => isUnlocked(level) && playLevel(level.id)}
|
||||||
|
>
|
||||||
|
<div class="swatch"></div>
|
||||||
|
<span class="name">{isUnlocked(level) ? level.name: '?'}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="back" on:click={() => push('/')}>back</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.screen{
|
||||||
|
width: 800px;
|
||||||
|
height: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #111;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
h1{
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.level-grid{
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.level-card{
|
||||||
|
width: 120px;
|
||||||
|
height: 140px;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: white;
|
||||||
|
transition: transform 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.level-card:hover:not(.locked){
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: var(--c);
|
||||||
|
}
|
||||||
|
.level-card.locked{
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.level-card.locked .swatch{
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
.name{
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.back{
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #555;
|
||||||
|
color: #aaa;
|
||||||
|
padding: 8px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
// so I can place my color states here so they are updated everywhere
|
// so I can place my color states here so they are updated everywhere
|
||||||
|
|
||||||
|
|
||||||
// a writebale is a box that holds a value
|
// a writable is a box that holds a value
|
||||||
// any svelte comonent can read from within it
|
// any svelte component can read from within it
|
||||||
//autupdated when value changes
|
// autoupdated when value changes
|
||||||
// its like the dollar sign notation
|
// its like the dollar sign notation
|
||||||
|
|
||||||
// -- so these are the global variables ----
|
// -- so these are the global variables ----
|
||||||
|
|
||||||
import {writeable} from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
// world starts gray so unlockedColors start as an empty array (there is none)
|
// world starts gray so unlockedColors start as an empty array (there is none)
|
||||||
export const unlockedColors = writable({});
|
export const unlockedColors = writable([]);
|
||||||
|
|
||||||
// the current level number
|
// the current level number
|
||||||
export const currentLevel = writable(1);
|
export const currentLevel = writable(1);
|
||||||
|
|||||||