working draft
@@ -20,7 +20,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"p5": "^1.11.4",
|
||||
|
||||
"p5play": "^3.22.0",
|
||||
"sirv-cli": "^2.0.0",
|
||||
"svelte-spa-router": "^3.3.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.0 KiB |
BIN
public/assets/player_level1.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/assets/player_level10.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/assets/player_level2.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/assets/player_level3.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/assets/player_level4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/assets/player_level5.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/player_level6.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/assets/player_level7.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/assets/player_level8.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/assets/player_level9.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/backgrounds/Level1.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
public/backgrounds/Level2.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/backgrounds/Level3.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/backgrounds/Level4.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
public/backgrounds/Level5.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
@@ -6,17 +6,14 @@ import Home from './routes/Home.svelte';
|
||||
import LevelSelect from './routes/LevelSelect.svelte';
|
||||
import Game from './routes/Game.svelte';
|
||||
import GameOver from './routes/GameOver.svelte';
|
||||
import Win from './routes/Win.svelte';
|
||||
|
||||
|
||||
|
||||
// this is the application of the svelte-spa-router
|
||||
// e.g. /#/game will show the Game component
|
||||
// this is kind of like switching between HTML pages but we switch components instead
|
||||
const routes = {
|
||||
'/': Home,
|
||||
'/': Home,
|
||||
'/levelselect': LevelSelect,
|
||||
'/game': Game,
|
||||
'/gameover': GameOver,
|
||||
'/win': Win,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,81 +1,127 @@
|
||||
<script>
|
||||
import {onMount, onDestroy} from 'svelte';
|
||||
import {Player} from '../game/Player.js';
|
||||
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';
|
||||
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';
|
||||
|
||||
// level to load - passed in from Game.svelte
|
||||
export let levelNumber = 1;
|
||||
|
||||
let canvasContainer;
|
||||
let canvasContainer; // the div p5 will draw into
|
||||
let p5Instance;
|
||||
|
||||
const keysDown = {left: false, right: false, jump: false};
|
||||
const keysDown = { left: false, right: false, jump: false };
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') { keysDown.left = true; e.preventDefault(); }
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') { keysDown.right = true; e.preventDefault(); }
|
||||
// e.repeat blocks the browser auto-repeat from re-queuing a jump while held
|
||||
if ((e.key === 'ArrowUp' || e.key === 'w' || e.key === ' ') && !e.repeat) {
|
||||
keysDown.jump = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyUp = (e) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') keysDown.left = false;
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') keysDown.right = false;
|
||||
// jump is not cleared here — player.update() consumes it so quick taps aren't lost
|
||||
};
|
||||
|
||||
// clear all keys when the window loses focus so nothing stays stuck
|
||||
const onBlur = () => {
|
||||
keysDown.left = false;
|
||||
keysDown.right = false;
|
||||
keysDown.jump = false;
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
window.addEventListener('blur', onBlur);
|
||||
|
||||
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)
|
||||
// naming it "p5" would shadow the global and break new p5(sketch)
|
||||
const sketch = (p) => {
|
||||
let player;
|
||||
let platforms;
|
||||
let fragments;
|
||||
let bgImg;
|
||||
let splatImg;
|
||||
let fragments = [];
|
||||
let enemies = [];
|
||||
let tarPuddles = [];
|
||||
let gameState = 'playing';
|
||||
let splats = [];
|
||||
let levelData; // declared once here at sketch level
|
||||
let splats = []; // paint splash effects on collection
|
||||
let levelData; // declared at sketch level so all functions can access it
|
||||
let gameState = 'playing'; // 'playing' | 'levelcomplete' | 'gameover'
|
||||
let bgImg;
|
||||
let playerImgLoaded;
|
||||
let splatImg;
|
||||
|
||||
// fragment collection callback
|
||||
// defined here at sketch level so both setup and draw can see it
|
||||
const LAST_LEVEL = 10;
|
||||
|
||||
// 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});
|
||||
onFragmentCollection(fragments.length, hexColor); // update the color store
|
||||
splats.push({ x, y, alpha: 200, size: 30 }); // add paint splash
|
||||
if (fragments.every(f => f.collected)) {
|
||||
gameState = 'levelcomplete';
|
||||
completeLevel(levelData.color);
|
||||
setTimeout(() => push('/levelselect'), 2500);
|
||||
const dest = levelNumber === LAST_LEVEL ? '/win' : '/levelselect';
|
||||
setTimeout(() => push(dest), 2500);
|
||||
}
|
||||
}
|
||||
|
||||
p.preload = () => {
|
||||
bgImg = p.loadImage('/backgrounds/temp-background.png');
|
||||
const data = getLevel(levelNumber);
|
||||
if (data?.bg) bgImg = p.loadImage(data.bg);
|
||||
if (data?.playerImg) playerImgLoaded = p.loadImage(data.playerImg);
|
||||
splatImg = p.loadImage('/assets/splat.png');
|
||||
};
|
||||
|
||||
p.setup = () => {
|
||||
// create canvas inside the container div
|
||||
const canvas = p.createCanvas(800, 450);
|
||||
canvas.parent(canvasContainer);
|
||||
|
||||
// assign to the outer levelData, no "const" here
|
||||
// gravity — makes sprites fall downward
|
||||
// must be set before creating sprites
|
||||
p.world.gravity.y = 30;
|
||||
|
||||
// load level data
|
||||
levelData = getLevel(levelNumber);
|
||||
|
||||
// reset color store for this level
|
||||
resetLevel(levelNumber, levelData.color);
|
||||
|
||||
// gravity must be set before creating sprites
|
||||
p.world.gravity.y = 10;
|
||||
|
||||
// create player at the level spawn point
|
||||
player = new Player(p, levelData.spawnX, levelData.spawnY);
|
||||
if (playerImgLoaded) player.sprite.img = playerImgLoaded;
|
||||
|
||||
platforms = new p.Group();
|
||||
// create platforms — rounded-corner graphic is drawn onto an offscreen canvas
|
||||
// and used as the sprite image so physics box stays rectangular
|
||||
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);
|
||||
const g = p.createGraphics(platData.w, platData.h);
|
||||
g.clear();
|
||||
g.noStroke();
|
||||
g.fill('#888888');
|
||||
g.rect(0, 0, platData.w, platData.h, 6);
|
||||
plat.img = g;
|
||||
}
|
||||
|
||||
// create fragments from level data
|
||||
fragments = [];
|
||||
for (const fragData of levelData.fragments) {
|
||||
fragments.push(new Fragment(p, fragData.x, fragData.y, fragData.color));
|
||||
}
|
||||
|
||||
// create enemies from level data
|
||||
enemies = [];
|
||||
for (const enemyData of levelData.enemies) {
|
||||
const e = new Enemy(p, enemyData.x, enemyData.y);
|
||||
@@ -83,6 +129,7 @@
|
||||
enemies.push(e);
|
||||
}
|
||||
|
||||
// create tar puddles from level data
|
||||
tarPuddles = [];
|
||||
for (const tarData of levelData.tar) {
|
||||
tarPuddles.push(new TarPuddle(p, tarData.x, tarData.y));
|
||||
@@ -90,107 +137,114 @@
|
||||
};
|
||||
|
||||
p.draw = () => {
|
||||
p.background(30);
|
||||
|
||||
// draw background image if loaded
|
||||
if (bgImg) {
|
||||
p.image(bgImg, 0, 0, p.width, p.height);
|
||||
} else {
|
||||
p.background(30);
|
||||
}
|
||||
|
||||
// color tint overlay based on fragments collected
|
||||
const reveal = get(colorOpacity);
|
||||
const hex = get(levelColor);
|
||||
// ── 1. PHYSICS FIRST ──────────────────────────────────────────
|
||||
// p5play needs this every frame to run gravity and collision
|
||||
// touching.bottom is only accurate AFTER this runs
|
||||
p.allSprites.update();
|
||||
|
||||
// ── 2. COLOR TINT OVERLAY ─────────────────────────────────────
|
||||
// as fragments are collected, the world gradually gains color
|
||||
// reveal goes from 0.0 (gray) to 1.0 (full color)
|
||||
const reveal = get(colorOpacity);
|
||||
const hex = get(levelColor);
|
||||
if (reveal > 0 && hex) {
|
||||
const col = p.color(hex);
|
||||
// lerp blends between gray (128) and the level color
|
||||
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.fill(r, g, b, reveal * 120); // max alpha 120 so its a subtle tint
|
||||
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();
|
||||
// ── 3. PLAYER INPUT ───────────────────────────────────────────
|
||||
// update runs AFTER allSprites.update() so touching.bottom is accurate
|
||||
player.update(keysDown);
|
||||
player.sprite.visible = player.isVisible();
|
||||
player.drawGlow();
|
||||
player.sprite.visible = player.isVisible(); // handles blink when invincible
|
||||
|
||||
// enemies
|
||||
// ── 4. FRAGMENTS ──────────────────────────────────────────────
|
||||
for (const frag of fragments) {
|
||||
frag.drawGlow(); // colored glow under the fragment
|
||||
frag.update(player.sprite, onFragmentCollected); // checks overlap with player
|
||||
}
|
||||
|
||||
// ── 5. ENEMIES ────────────────────────────────────────────────
|
||||
for (const enemy of enemies) {
|
||||
enemy.update();
|
||||
if (enemy.overlapsPlayer(player.sprite)) {
|
||||
const dead = player.loseLife();
|
||||
lives.set(player.lives);
|
||||
if (dead) {
|
||||
enemy.update(); // patrol movement
|
||||
// p5play overlap check — true when sprites are touching
|
||||
if (player.sprite.overlaps(enemy.sprite)) {
|
||||
const dead = player.loseLife(lives);
|
||||
if (dead && gameState === 'playing') {
|
||||
gameState = 'gameover';
|
||||
setTimeout(() => push('/gameover'), 1000);
|
||||
// short delay so player sees what happened
|
||||
setTimeout(() => push(`/gameover?level=${levelNumber}`), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tar puddles
|
||||
// ── 6. TAR PUDDLES ────────────────────────────────────────────
|
||||
for (const tar of tarPuddles) {
|
||||
if (tar.overlapsPlayer(player.sprite)) {
|
||||
const dead = player.loseLife();
|
||||
lives.set(player.lives);
|
||||
if (dead) {
|
||||
if (player.sprite.overlaps(tar.sprite)) {
|
||||
const dead = player.loseLife(lives);
|
||||
if (dead && gameState === 'playing') {
|
||||
gameState = 'gameover';
|
||||
setTimeout(() => push('/gameover'), 1000);
|
||||
setTimeout(() => push(`/gameover?level=${levelNumber}`), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splat effects
|
||||
splats = splats.filter(s => s.alpha > 0);
|
||||
// ── 7. SPLAT EFFECTS ──────────────────────────────────────────
|
||||
// paint splashes that appear at fragment collection sites
|
||||
// they fade out and grow over time
|
||||
splats = splats.filter(s => s.alpha > 0); // remove faded splats
|
||||
for (const s of splats) {
|
||||
if (!splatImg) continue;
|
||||
p.push();
|
||||
p.tint(255, s.alpha);
|
||||
p.tint(255, s.alpha); // fade out using tint 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;
|
||||
s.alpha -= 4; // fade speed
|
||||
s.size += 1.2; // grow speed
|
||||
}
|
||||
|
||||
// fell off screen
|
||||
if (player.sprite.y > p.height + 100) {
|
||||
player.loseLife();
|
||||
lives.set(player.lives);
|
||||
// ── 8. FELL OFF SCREEN ────────────────────────────────────────
|
||||
// if player falls below the canvas, lose a life and respawn
|
||||
if (player.sprite.y > p.height + 100 && gameState === 'playing') {
|
||||
const dead = player.loseLife(lives);
|
||||
if (dead) {
|
||||
gameState = 'gameover';
|
||||
setTimeout(() => push(`/gameover?level=${levelNumber}`), 500);
|
||||
}
|
||||
}
|
||||
|
||||
p.allSprites.update();
|
||||
p.allSprites.draw();
|
||||
// ── 9. DRAW SPRITES LAST ──────────────────────────────────────
|
||||
// p5play renders all sprites after everything else is drawn
|
||||
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
|
||||
// p5 here is the global constructor from the CDN script
|
||||
// NOT the sketch parameter named "p"
|
||||
p5Instance = new p5(sketch);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (p5Instance) p5Instance.remove();
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- the div that p5 draws the canvas into -->
|
||||
<div bind:this={canvasContainer} class="canvas-wrap"></div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -26,8 +26,4 @@ export class Enemy{
|
||||
this.sprite.mirror.x = 1; // flip image back
|
||||
}
|
||||
}
|
||||
|
||||
overlapsPlayer(playerSprite){
|
||||
return playerSprite.overlaps(this.sprite);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export class Fragment {
|
||||
|
||||
// check if player overlap with fragment
|
||||
if (playerSprite.overlaps(this.sprite)){
|
||||
//console.log('Player collected fragment');
|
||||
this.collect(onCollect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +1,119 @@
|
||||
export class Player {
|
||||
// p5 = the instance, x and y are start position
|
||||
constructor(p5, x, y){
|
||||
this.p5 = p5;
|
||||
// p = the p5 instance, x and y are start position
|
||||
constructor(p, x, y) {
|
||||
this.p = p;
|
||||
this.spawnX = x;
|
||||
this.spawnY = y;
|
||||
|
||||
// p5play sprite - this is the body with physics
|
||||
this.sprite = new p5.Sprite(x,y,28,28);
|
||||
// p5play sprite - this is the physics body
|
||||
this.sprite = new p.Sprite(x, y, 28, 28);
|
||||
|
||||
// get the character image
|
||||
this.sprite.image = '/assets/player.png';
|
||||
// p5play uses .img not .image for direct path strings
|
||||
this.sprite.img = '/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
|
||||
// we dont want the player to be bouncy
|
||||
this.sprite.bounciness = 0;
|
||||
|
||||
// game state
|
||||
this.lives = 3;
|
||||
this.collectedColors = []; // hex color strings
|
||||
this.isInvincible = false;
|
||||
this.invincibleTimer = 0;
|
||||
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
|
||||
// called every frame from the game loop
|
||||
// keysDown is an object: {left, right, jump}
|
||||
update(keysDown) {
|
||||
const SPEED = 5;
|
||||
|
||||
if (keysDown.left) this.sprite.vel.x = -SPEED;
|
||||
if (keysDown.right) this.sprite.vel.x = SPEED;
|
||||
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;
|
||||
// if no key is pressed, slow down (friction)
|
||||
if (!keysDown.left && !keysDown.right) {
|
||||
this.sprite.vel.x *= 0.78;
|
||||
}
|
||||
|
||||
// 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;
|
||||
const onGround = this.sprite.vel.y > -0.5 && this.sprite.vel.y < 1.5;
|
||||
|
||||
if (keysDown.jump && onGround) {
|
||||
this.sprite.vel.y = -11;
|
||||
keysDown.jump = false; // consume so it can't re-fire until the next keydown
|
||||
}
|
||||
|
||||
// for when the player is invincible
|
||||
if (this.isInvincible){
|
||||
// count down invincibility frames
|
||||
if (this.isInvincible) {
|
||||
this.invincibleTimer--;
|
||||
if (this.invincibleTimer <= 0){
|
||||
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;
|
||||
// call when player hits an enemy or tar puddle
|
||||
// returns true if no lives left (game over)
|
||||
loseLife(livesStore) {
|
||||
if (this.isInvincible) return false;
|
||||
|
||||
// update the store directly instead of a local variable
|
||||
livesStore.update(n => n - 1);
|
||||
|
||||
// grace period so player cant take damage immediately again
|
||||
this.isInvincible = true;
|
||||
this.invincibleTimer = 90; // 1.5 seconds at 60fps
|
||||
|
||||
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
|
||||
// read the current store value to check if game over
|
||||
let current;
|
||||
livesStore.subscribe(v => current = v)();
|
||||
return current <= 0; // true = game over
|
||||
}
|
||||
|
||||
// respawn at level start spawn point and be static
|
||||
respawn(){
|
||||
// send player back to level start
|
||||
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){
|
||||
// called when player walks into a fragment
|
||||
collectFragment(hexColor) {
|
||||
this.collectedColors.push(hexColor);
|
||||
// we start getting color!
|
||||
// color gets added to the glow effect
|
||||
}
|
||||
|
||||
//glow effect around the player
|
||||
// this should be called before drawing character so its under it
|
||||
drawGlow(){
|
||||
// draw the glow effect around the player
|
||||
// call this BEFORE drawing the sprite so glow appears underneath
|
||||
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];
|
||||
// use the most recently collected color as the glow color
|
||||
const c = this.collectedColors[this.collectedColors.length - 1];
|
||||
|
||||
this.p5.push();
|
||||
this.p5.noStroke();
|
||||
this.p.push();
|
||||
this.p.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);
|
||||
// draw concentric circles getting more transparent outward
|
||||
// this fakes a soft glow without any extra library
|
||||
for (let radius = 60; radius > 0; radius -= 8) {
|
||||
const alpha = (radius / 60) * 40; // max alpha is 40
|
||||
const col = this.p.color(c);
|
||||
col.setAlpha(alpha);
|
||||
this.p5.fill(col);
|
||||
this.p5.circle(this.sprite.x, this.sprite.y, radius*2);
|
||||
this.p.fill(col);
|
||||
this.p.circle(this.sprite.x, this.sprite.y, radius * 2);
|
||||
}
|
||||
this.p5.pop();
|
||||
this.p.pop();
|
||||
}
|
||||
|
||||
// flickery effect for invincibility
|
||||
isVisible(){
|
||||
// blink effect while invincible
|
||||
// returns false every 6 frames to hide the sprite
|
||||
isVisible() {
|
||||
if (!this.isInvincible) return true;
|
||||
// show/hide every 6 frames so it blinks
|
||||
// show/hide every 6 frames for a blink effect
|
||||
return Math.floor(this.invincibleTimer / 6) % 2 === 0;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,4 @@ export class TarPuddle {
|
||||
this.sprite.collider = 'none'; // no physics needed just check for overlap
|
||||
this.sprite.rotationLock = true;
|
||||
}
|
||||
|
||||
overlapsPlayer(playerSprite){
|
||||
return playerSprite.overlaps(this.sprite);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,441 @@
|
||||
// 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
|
||||
// level config — one entry per level
|
||||
// x,y = center w,h = dimensions
|
||||
// fragment colors match the level color (updated hex values)
|
||||
// bg → put your PNG in public/backgrounds/levelN.png
|
||||
// playerImg → put your PNG in public/assets/player_levelN.png
|
||||
|
||||
export const LEVELS = [
|
||||
|
||||
// ── LEVEL 1: CRIMSON ──────────────────────────────────────────────────────
|
||||
// platforms and enemies unchanged from original design
|
||||
{
|
||||
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}
|
||||
id: 1,
|
||||
name: 'Eruption',
|
||||
color: '#970505',
|
||||
bg: '/backgrounds/level1.png',
|
||||
playerImg: '/assets/player_level1.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
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 },
|
||||
{ x: 400, y: 440, w: 800, h: 12 }, // ground
|
||||
{ x: 170, y: 370, w: 160, h: 14 }, // left starter
|
||||
{ x: 390, y: 308, w: 150, h: 14 }, // middle step
|
||||
{ x: 610, y: 248, w: 150, h: 14 }, // upper right
|
||||
{ x: 395, y: 188, w: 140, h: 14 }, // top center
|
||||
],
|
||||
|
||||
// each fragment: {x,y,color}
|
||||
fragments: [
|
||||
{x: 220, y: 330, color: '#FF4136'},
|
||||
{x: 430, y: 270, color: '#FF4136'},
|
||||
{x: 640, y: 210, color: '#FF4136'},
|
||||
{ x: 170, y: 338, color: '#970505' },
|
||||
{ x: 390, y: 278, color: '#970505' },
|
||||
{ x: 610, y: 216, color: '#970505' },
|
||||
{ x: 395, y: 156, color: '#970505' },
|
||||
],
|
||||
|
||||
// each enemy: {x,y, patrol}
|
||||
enemies: [
|
||||
{x: 430, y: 280, patrol: 50},
|
||||
{ x: 390, y: 283, patrol: 50 },
|
||||
],
|
||||
|
||||
// each puddle: {x, y}
|
||||
tar: [],
|
||||
},
|
||||
|
||||
// ── LEVEL 2: AMBER ────────────────────────────────────────────────────────
|
||||
// platforms and enemies unchanged from original design
|
||||
{
|
||||
id: 2,
|
||||
name: 'Warmer skies',
|
||||
color: '#FF851B',
|
||||
bgFar: '/backgrounds/level2_far.png',
|
||||
bgMid: '/backgrounds/level2_mid.png',
|
||||
spawnX: 80,
|
||||
spawnY: 380,
|
||||
|
||||
name: 'Sunset',
|
||||
color: '#CF8917',
|
||||
bg: '/backgrounds/level2.png',
|
||||
playerImg: '/assets/player_level2.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
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 },
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 160, y: 375, w: 150, h: 14 }, // left low
|
||||
{ x: 360, y: 318, w: 140, h: 14 }, // center
|
||||
{ x: 560, y: 260, w: 130, h: 14 }, // right mid
|
||||
{ x: 710, y: 330, w: 110, h: 14 }, // right island
|
||||
{ x: 280, y: 238, w: 110, h: 14 }, // upper left — backtrack
|
||||
{ x: 478, y: 188, w: 110, h: 14 }, // top center
|
||||
],
|
||||
|
||||
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' },
|
||||
{ x: 160, y: 343, color: '#CF8917' },
|
||||
{ x: 710, y: 298, color: '#CF8917' },
|
||||
{ x: 280, y: 206, color: '#CF8917' },
|
||||
{ x: 478, y: 156, color: '#CF8917' },
|
||||
],
|
||||
|
||||
enemies: [
|
||||
{ x: 380, y: 290, patrol: 45 },
|
||||
{ x: 560, y: 230, patrol: 40 },
|
||||
{ x: 360, y: 293, patrol: 45 },
|
||||
{ x: 560, y: 235, patrol: 40 },
|
||||
],
|
||||
|
||||
tar: [
|
||||
{ x: 150, y: 432 },
|
||||
{ x: 490, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 3: YELLOW ───────────────────────────────────────────────────────
|
||||
// two wide wings — left and right — converging at the upper center
|
||||
{
|
||||
id: 3,
|
||||
name: 'Golden',
|
||||
color: '#E3D214',
|
||||
bg: '/backgrounds/level3.png',
|
||||
playerImg: '/assets/player_level3.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 140, y: 372, w: 150, h: 14 }, // left start
|
||||
{ x: 575, y: 365, w: 150, h: 14 }, // right start (enemy guards)
|
||||
{ x: 280, y: 302, w: 120, h: 14 }, // left mid
|
||||
{ x: 510, y: 295, w: 120, h: 14 }, // right mid
|
||||
{ x: 155, y: 238, w: 110, h: 14 }, // upper left
|
||||
{ x: 420, y: 228, w: 130, h: 14 }, // upper center (wider, enemy)
|
||||
{ x: 660, y: 220, w: 110, h: 14 }, // upper right
|
||||
],
|
||||
fragments: [
|
||||
{ x: 280, y: 270, color: '#E3D214' }, // left mid — easy
|
||||
{ x: 660, y: 188, color: '#E3D214' }, // upper right
|
||||
{ x: 155, y: 206, color: '#E3D214' }, // upper left
|
||||
{ x: 420, y: 196, color: '#E3D214' }, // upper center — guarded
|
||||
],
|
||||
enemies: [
|
||||
{ x: 575, y: 340, patrol: 52 },
|
||||
{ x: 420, y: 203, patrol: 45 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 370, y: 432 },
|
||||
{ x: 700, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 4: GREEN ────────────────────────────────────────────────────────
|
||||
// two vertical columns with connecting bridges — forest canopy feel
|
||||
{
|
||||
id: 4,
|
||||
name: 'Greenery',
|
||||
color: '#39BD1C',
|
||||
bg: '/backgrounds/level4.png',
|
||||
playerImg: '/assets/player_level4.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 140, y: 372, w: 130, h: 14 }, // left start (enemy guards)
|
||||
{ x: 665, y: 365, w: 130, h: 14 }, // right — separated by tar
|
||||
{ x: 270, y: 305, w: 120, h: 14 }, // center-left mid (enemy guards)
|
||||
{ x: 510, y: 298, w: 120, h: 14 }, // center-right mid
|
||||
{ x: 165, y: 242, w: 110, h: 14 }, // upper left
|
||||
{ x: 380, y: 232, w: 120, h: 14 }, // upper center
|
||||
{ x: 610, y: 225, w: 110, h: 14 }, // upper right (enemy guards)
|
||||
{ x: 290, y: 175, w: 95, h: 14 }, // top left
|
||||
{ x: 520, y: 168, w: 95, h: 14 }, // top right — hardest
|
||||
],
|
||||
fragments: [
|
||||
{ x: 270, y: 273, color: '#39BD1C' }, // center-left mid
|
||||
{ x: 510, y: 266, color: '#39BD1C' }, // center-right mid
|
||||
{ x: 380, y: 200, color: '#39BD1C' }, // upper center
|
||||
{ x: 520, y: 136, color: '#39BD1C' }, // top right — hardest
|
||||
],
|
||||
enemies: [
|
||||
{ x: 140, y: 347, patrol: 40 },
|
||||
{ x: 270, y: 280, patrol: 38 },
|
||||
{ x: 610, y: 200, patrol: 36 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 395, y: 432 },
|
||||
{ x: 610, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 5: CYAN ─────────────────────────────────────────────────────────
|
||||
// zigzag flow — dips and rises like tide pools
|
||||
{
|
||||
id: 5,
|
||||
name: 'Tidal',
|
||||
color: '#12B6C8',
|
||||
bg: '/backgrounds/level5.png',
|
||||
playerImg: '/assets/player_level5.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 145, y: 378, w: 140, h: 14 }, // left start
|
||||
{ x: 355, y: 395, w: 115, h: 14 }, // dips down — zigzag
|
||||
{ x: 540, y: 368, w: 125, h: 14 }, // center right
|
||||
{ x: 705, y: 348, w: 115, h: 14 }, // far right
|
||||
{ x: 235, y: 308, w: 110, h: 14 }, // upper left
|
||||
{ x: 445, y: 295, w: 110, h: 14 }, // upper center (enemy)
|
||||
{ x: 640, y: 278, w: 115, h: 14 }, // upper right
|
||||
{ x: 125, y: 248, w: 95, h: 14 }, // high far left — isolated
|
||||
{ x: 365, y: 235, w: 90, h: 14 }, // high center
|
||||
{ x: 590, y: 220, w: 90, h: 14 }, // high right (enemy)
|
||||
{ x: 755, y: 202, w: 80, h: 14 }, // top far right — narrow
|
||||
],
|
||||
fragments: [
|
||||
{ x: 235, y: 276, color: '#12B6C8' }, // upper left
|
||||
{ x: 640, y: 246, color: '#12B6C8' }, // upper right
|
||||
{ x: 125, y: 216, color: '#12B6C8' }, // high far left — isolated
|
||||
{ x: 365, y: 203, color: '#12B6C8' }, // high center
|
||||
{ x: 755, y: 170, color: '#12B6C8' }, // top far right — hardest
|
||||
],
|
||||
enemies: [
|
||||
{ x: 355, y: 370, patrol: 35 },
|
||||
{ x: 445, y: 270, patrol: 35 },
|
||||
{ x: 590, y: 195, patrol: 28 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 265, y: 432 },
|
||||
{ x: 480, y: 432 },
|
||||
{ x: 680, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 6: DEEP BLUE ────────────────────────────────────────────────────
|
||||
// cold, sparse — wider gaps, deliberate platform placement
|
||||
{
|
||||
id: 6,
|
||||
name: 'The Abyss',
|
||||
color: '#170CB7',
|
||||
bg: '/backgrounds/level6.png',
|
||||
playerImg: '/assets/player_level6.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 130, y: 382, w: 125, h: 14 }, // left start
|
||||
{ x: 675, y: 370, w: 130, h: 14 }, // far right — requires commitment
|
||||
{ x: 308, y: 355, w: 108, h: 14 }, // center step
|
||||
{ x: 510, y: 385, w: 105, h: 14 }, // dip right
|
||||
{ x: 220, y: 302, w: 100, h: 14 }, // upper left
|
||||
{ x: 455, y: 290, w: 100, h: 14 }, // upper center
|
||||
{ x: 660, y: 275, w: 110, h: 14 }, // upper right (enemy)
|
||||
{ x: 105, y: 245, w: 88, h: 14 }, // high far left — isolated
|
||||
{ x: 358, y: 232, w: 85, h: 14 }, // high center
|
||||
{ x: 580, y: 215, w: 85, h: 14 }, // high right
|
||||
{ x: 745, y: 198, w: 80, h: 14 }, // narrow top right
|
||||
{ x: 268, y: 175, w: 80, h: 14 }, // top left
|
||||
{ x: 480, y: 165, w: 80, h: 14 }, // very top — hardest
|
||||
],
|
||||
fragments: [
|
||||
{ x: 308, y: 323, color: '#170CB7' }, // center step — warmup
|
||||
{ x: 220, y: 270, color: '#170CB7' }, // upper left
|
||||
{ x: 660, y: 243, color: '#170CB7' }, // upper right
|
||||
{ x: 358, y: 200, color: '#170CB7' }, // high center
|
||||
{ x: 480, y: 133, color: '#170CB7' }, // very top — hardest
|
||||
],
|
||||
enemies: [
|
||||
{ x: 510, y: 360, patrol: 33 },
|
||||
{ x: 455, y: 265, patrol: 30 },
|
||||
{ x: 660, y: 250, patrol: 37 },
|
||||
{ x: 358, y: 207, patrol: 25 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 230, y: 432 },
|
||||
{ x: 460, y: 432 },
|
||||
{ x: 640, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 7: PURPLE ───────────────────────────────────────────────────────
|
||||
// narrow platforms spiraling up — precision required
|
||||
{
|
||||
id: 7,
|
||||
name: 'Twilight Spire',
|
||||
color: '#6613BA',
|
||||
bg: '/backgrounds/level7.png',
|
||||
playerImg: '/assets/player_level7.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 130, y: 385, w: 115, h: 14 }, // left start
|
||||
{ x: 308, y: 365, w: 100, h: 14 }, // small step
|
||||
{ x: 483, y: 390, w: 100, h: 14 }, // dip
|
||||
{ x: 652, y: 358, w: 115, h: 14 }, // right mid
|
||||
{ x: 768, y: 300, w: 75, h: 14 }, // narrow far right
|
||||
{ x: 578, y: 272, w: 88, h: 14 }, // upper right
|
||||
{ x: 400, y: 295, w: 85, h: 14 }, // upper center
|
||||
{ x: 220, y: 285, w: 90, h: 14 }, // upper left
|
||||
{ x: 90, y: 248, w: 80, h: 14 }, // narrow far left
|
||||
{ x: 320, y: 232, w: 80, h: 14 }, // high center
|
||||
{ x: 518, y: 215, w: 80, h: 14 }, // high right
|
||||
{ x: 698, y: 190, w: 78, h: 14 }, // top right — narrow
|
||||
],
|
||||
fragments: [
|
||||
{ x: 652, y: 326, color: '#6613BA' }, // right mid
|
||||
{ x: 220, y: 253, color: '#6613BA' }, // upper left
|
||||
{ x: 320, y: 200, color: '#6613BA' }, // high center
|
||||
{ x: 518, y: 183, color: '#6613BA' }, // high right
|
||||
{ x: 698, y: 158, color: '#6613BA' }, // top right — hardest
|
||||
],
|
||||
enemies: [
|
||||
{ x: 308, y: 340, patrol: 28 },
|
||||
{ x: 400, y: 270, patrol: 25 },
|
||||
{ x: 578, y: 247, patrol: 26 },
|
||||
{ x: 90, y: 223, patrol: 22 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 200, y: 432 },
|
||||
{ x: 408, y: 432 },
|
||||
{ x: 618, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 8: MAGENTA ──────────────────────────────────────────────────────
|
||||
// energetic grid-like layout with strategic gaps
|
||||
{
|
||||
id: 8,
|
||||
name: 'Neon Bloom',
|
||||
color: '#C71287',
|
||||
bg: '/backgrounds/level8.png',
|
||||
playerImg: '/assets/player_level8.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 155, y: 380, w: 130, h: 14 },
|
||||
{ x: 338, y: 360, w: 118, h: 14 },
|
||||
{ x: 513, y: 380, w: 110, h: 14 }, // dip
|
||||
{ x: 688, y: 358, w: 120, h: 14 },
|
||||
{ x: 165, y: 308, w: 100, h: 14 }, // upper left
|
||||
{ x: 365, y: 300, w: 90, h: 14 }, // upper center
|
||||
{ x: 553, y: 308, w: 100, h: 14 }, // upper right
|
||||
{ x: 728, y: 278, w: 78, h: 14 }, // narrow far right
|
||||
{ x: 260, y: 248, w: 90, h: 14 }, // high left
|
||||
{ x: 448, y: 238, w: 90, h: 14 }, // high center
|
||||
{ x: 636, y: 222, w: 90, h: 14 }, // high right
|
||||
{ x: 368, y: 178, w: 80, h: 14 }, // near top
|
||||
{ x: 553, y: 165, w: 80, h: 14 }, // top — hardest
|
||||
],
|
||||
fragments: [
|
||||
{ x: 165, y: 276, color: '#C71287' }, // upper left
|
||||
{ x: 728, y: 246, color: '#C71287' }, // narrow far right
|
||||
{ x: 448, y: 206, color: '#C71287' }, // high center
|
||||
{ x: 368, y: 146, color: '#C71287' }, // near top
|
||||
{ x: 553, y: 133, color: '#C71287' }, // top — hardest
|
||||
],
|
||||
enemies: [
|
||||
{ x: 338, y: 335, patrol: 38 },
|
||||
{ x: 688, y: 333, patrol: 40 },
|
||||
{ x: 553, y: 283, patrol: 32 },
|
||||
{ x: 365, y: 275, patrol: 25 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 178, y: 432 },
|
||||
{ x: 378, y: 432 },
|
||||
{ x: 558, y: 432 },
|
||||
{ x: 738, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 9: BROWN ────────────────────────────────────────────────────────
|
||||
// dense cave — most enemies, hardest single-color level
|
||||
{
|
||||
id: 9,
|
||||
name: 'Deep Caves',
|
||||
color: '#753F16',
|
||||
bg: '/backgrounds/level9.png',
|
||||
playerImg: '/assets/player_level9.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
{ x: 135, y: 382, w: 128, h: 14 },
|
||||
{ x: 315, y: 365, w: 112, h: 14 },
|
||||
{ x: 496, y: 388, w: 108, h: 14 }, // dip
|
||||
{ x: 660, y: 358, w: 118, h: 14 },
|
||||
{ x: 200, y: 322, w: 100, h: 14 },
|
||||
{ x: 400, y: 312, w: 93, h: 14 },
|
||||
{ x: 578, y: 305, w: 100, h: 14 },
|
||||
{ x: 738, y: 288, w: 78, h: 14 }, // narrow
|
||||
{ x: 118, y: 262, w: 88, h: 14 },
|
||||
{ x: 310, y: 255, w: 88, h: 14 },
|
||||
{ x: 492, y: 245, w: 88, h: 14 },
|
||||
{ x: 658, y: 232, w: 88, h: 14 },
|
||||
{ x: 232, y: 195, w: 78, h: 14 },
|
||||
{ x: 425, y: 185, w: 78, h: 14 },
|
||||
{ x: 608, y: 178, w: 78, h: 14 },
|
||||
],
|
||||
fragments: [
|
||||
{ x: 578, y: 273, color: '#753F16' },
|
||||
{ x: 310, y: 223, color: '#753F16' },
|
||||
{ x: 658, y: 200, color: '#753F16' },
|
||||
{ x: 425, y: 153, color: '#753F16' },
|
||||
{ x: 608, y: 146, color: '#753F16' },
|
||||
],
|
||||
enemies: [
|
||||
{ x: 315, y: 340, patrol: 35 },
|
||||
{ x: 660, y: 333, patrol: 40 },
|
||||
{ x: 400, y: 287, patrol: 28 },
|
||||
{ x: 492, y: 220, patrol: 28 },
|
||||
{ x: 738, y: 263, patrol: 22 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 152, y: 432 },
|
||||
{ x: 348, y: 432 },
|
||||
{ x: 540, y: 432 },
|
||||
{ x: 712, y: 432 },
|
||||
],
|
||||
},
|
||||
|
||||
// ── LEVEL 10: THE COLOR REALM — final ────────────────────────────────────
|
||||
// one fragment per level color — completing this leads to the win screen
|
||||
{
|
||||
id: 10,
|
||||
name: 'The Color Realm',
|
||||
color: '#FFD700',
|
||||
bg: '/backgrounds/level10.png',
|
||||
playerImg: '/assets/player_level10.png',
|
||||
spawnX: 60,
|
||||
spawnY: 400,
|
||||
platforms: [
|
||||
{ x: 400, y: 440, w: 800, h: 12 },
|
||||
// row 1 — low
|
||||
{ x: 120, y: 380, w: 140, h: 14 },
|
||||
{ x: 310, y: 362, w: 120, h: 14 },
|
||||
{ x: 510, y: 382, w: 120, h: 14 },
|
||||
{ x: 700, y: 365, w: 130, h: 14 },
|
||||
// row 2 — mid
|
||||
{ x: 210, y: 305, w: 120, h: 14 },
|
||||
{ x: 420, y: 298, w: 110, h: 14 },
|
||||
{ x: 628, y: 288, w: 120, h: 14 },
|
||||
// row 3 — high
|
||||
{ x: 115, y: 248, w: 100, h: 14 },
|
||||
{ x: 315, y: 238, w: 95, h: 14 },
|
||||
{ x: 515, y: 228, w: 95, h: 14 },
|
||||
{ x: 715, y: 215, w: 90, h: 14 },
|
||||
// row 4 — top
|
||||
{ x: 215, y: 178, w: 90, h: 14 },
|
||||
{ x: 415, y: 168, w: 90, h: 14 },
|
||||
{ x: 615, y: 160, w: 90, h: 14 },
|
||||
// row 5 — very top (brown fragment)
|
||||
{ x: 415, y: 128, w: 80, h: 14 },
|
||||
],
|
||||
fragments: [
|
||||
{ x: 310, y: 330, color: '#970505' }, // crimson
|
||||
{ x: 700, y: 333, color: '#CF8917' }, // amber
|
||||
{ x: 210, y: 273, color: '#E3D214' }, // yellow
|
||||
{ x: 628, y: 256, color: '#39BD1C' }, // green
|
||||
{ x: 315, y: 206, color: '#12B6C8' }, // cyan
|
||||
{ x: 715, y: 183, color: '#170CB7' }, // deep blue
|
||||
{ x: 215, y: 146, color: '#6613BA' }, // purple
|
||||
{ x: 615, y: 128, color: '#C71287' }, // magenta
|
||||
{ x: 415, y: 96, color: '#753F16' }, // brown — very top
|
||||
],
|
||||
enemies: [
|
||||
{ x: 120, y: 355, patrol: 45 },
|
||||
{ x: 510, y: 357, patrol: 40 },
|
||||
{ x: 420, y: 273, patrol: 35 },
|
||||
{ x: 628, y: 263, patrol: 42 },
|
||||
{ x: 515, y: 203, patrol: 30 },
|
||||
],
|
||||
tar: [
|
||||
{ x: 225, y: 432 },
|
||||
{ x: 390, y: 432 },
|
||||
{ x: 540, y: 432 },
|
||||
{ x: 665, y: 432 },
|
||||
{ x: 760, 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)
|
||||
}
|
||||
export function getLevel(id) {
|
||||
return LEVELS.find(level => level.id === id);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
<script>
|
||||
import {push} from 'svelte-spa-router';
|
||||
import {querystring} from 'svelte-spa-router';
|
||||
import {resetLevel} from '../stores/colorStore.js';
|
||||
import {getLevel} from '../game/levelData.js';
|
||||
<script>
|
||||
import { push, querystring } from 'svelte-spa-router';
|
||||
import { resetLevel } from '../stores/colorStore.js';
|
||||
import { getLevel } from '../game/levelData.js';
|
||||
|
||||
$: levelNum = parseInt(new URLSearchParams($querystring).get('level')) || 1;
|
||||
$: levelNum = $querystring
|
||||
? parseInt(new URLSearchParams($querystring).get('level')) || 1
|
||||
: 1;
|
||||
|
||||
function retry(){
|
||||
function retry() {
|
||||
const level = getLevel(levelNum);
|
||||
resetLevel(levelNum, level.color);
|
||||
push(`/game/${levelNum}`);
|
||||
push(`/game?level=${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>
|
||||
<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;
|
||||
.screen {
|
||||
width: 800px;
|
||||
height: 450px;
|
||||
margin: 0 auto;
|
||||
background: #0a0a0a;
|
||||
@@ -35,33 +36,31 @@
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
h1{
|
||||
font-family:'Courier New', Courier, monospace;
|
||||
font-size: 48px;
|
||||
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;
|
||||
p {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 20px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.buttons{
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px
|
||||
margin-top: 8px;
|
||||
}
|
||||
button{
|
||||
button {
|
||||
padding: 10px 32px;
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
font-family:'Courier New', Courier, monospace;
|
||||
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);
|
||||
}
|
||||
button:hover { background: rgba(255,255,255,0.18); }
|
||||
</style>
|
||||
@@ -48,18 +48,21 @@
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
h1{
|
||||
font-family:'Courier New', Courier, monospace;
|
||||
font-size: 42px;
|
||||
h1 {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 34px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.level-grid{
|
||||
.level-grid {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
max-width: 560px;
|
||||
}
|
||||
.level-card{
|
||||
width: 120px;
|
||||
height: 140px;
|
||||
.level-card {
|
||||
width: 100px;
|
||||
height: 118px;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
border-radius: 12px;
|
||||
@@ -68,24 +71,32 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
color: white;
|
||||
transition: transform 0.15s, border-color 0.15s;
|
||||
}
|
||||
.level-card:hover:not(.locked){
|
||||
.level-card:hover:not(.locked) {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--c);
|
||||
}
|
||||
.level-card.locked{
|
||||
.level-card.locked {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.level-card.locked .swatch{
|
||||
.swatch {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: var(--c);
|
||||
}
|
||||
.level-card.locked .swatch {
|
||||
background: #555;
|
||||
}
|
||||
.name{
|
||||
font-family:'Courier New', Courier, monospace;
|
||||
font-size: 18px;
|
||||
.name {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.back{
|
||||
background: none;
|
||||
|
||||
77
src/routes/Win.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script>
|
||||
import { push } from 'svelte-spa-router';
|
||||
</script>
|
||||
|
||||
<div class="screen">
|
||||
<h1 class="title">color restored</h1>
|
||||
<p class="line1">every fragment found. every hue returned to the world.</p>
|
||||
<p class="line2">the little painter has done it.</p>
|
||||
<button on:click={() => push('/')}>back to home</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screen {
|
||||
width: 800px;
|
||||
height: 450px;
|
||||
margin: 0 auto;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 52px;
|
||||
font-weight: 400;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#FF4136, #FF851B, #FFDC00, #2ECC40,
|
||||
#0074D9, #B10DC9, #FF69B4, #FF4136
|
||||
);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: shimmer 4s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
from { background-position: 0% center; }
|
||||
to { background-position: 200% center; }
|
||||
}
|
||||
|
||||
.line1 {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 18px;
|
||||
color: #ccc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.line2 {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 36px;
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 18px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
</style>
|
||||