Compare commits

..

3 Commits

Author SHA1 Message Date
6fbac92d71 bounds and refine the storytelling 2026-05-09 17:30:07 +09:00
6f173e93c9 level backgrounds 2026-05-09 16:23:41 +09:00
0253789ea4 adding storytelling elements 2026-05-09 14:56:16 +09:00
28 changed files with 448 additions and 185 deletions

View File

@@ -1,5 +1,8 @@
**Name**: Samantha Lopez **Name**: Samantha Lopez
**ID**: 20266142 **ID**: 20266142
**Email**: samantha@kaist.ac.kr **Email**: samantha@kaist.ac.kr
**Gittea Repo**: [https://git.prototyping.id/20266142/The-Full-Hue](https://git.prototyping.id/20266142/The-Full-Hue) **Gittea Repo**: [https://git.prototyping.id/20266142/The-Full-Hue](https://git.prototyping.id/20266142/The-Full-Hue)

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -2,7 +2,8 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { Player } from '../game/Player.js'; import { Player } from '../game/Player.js';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { lives, colorOpacity, levelColor, onFragmentCollection, resetLevel, completeLevel } from '../stores/colorStore.js'; import { lives, colorOpacity, levelColor, onFragmentCollection, resetLevel, completeLevel, gameCompleted } from '../stores/colorStore.js';
import { showFragmentQuote, showCompleteQuote } from '../stores/quoteStore.js';
import { push } from 'svelte-spa-router'; import { push } from 'svelte-spa-router';
import { Enemy } from '../game/Enemy.js'; import { Enemy } from '../game/Enemy.js';
import { TarPuddle } from '../game/TarPuddle.js'; import { TarPuddle } from '../game/TarPuddle.js';
@@ -18,6 +19,7 @@
const keysDown = { left: false, right: false, jump: false }; const keysDown = { left: false, right: false, jump: false };
const onKeyDown = (e) => { const onKeyDown = (e) => {
if (e.key === 'Escape') { push('/levelselect'); return; }
if (e.key === 'ArrowLeft' || e.key === 'a') { keysDown.left = true; e.preventDefault(); } if (e.key === 'ArrowLeft' || e.key === 'a') { keysDown.left = true; e.preventDefault(); }
if (e.key === 'ArrowRight' || e.key === 'd') { keysDown.right = 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 // e.repeat blocks the browser auto-repeat from re-queuing a jump while held
@@ -60,26 +62,37 @@
let playerImgLoaded; let playerImgLoaded;
let splatImg; let splatImg;
// fragment collection callback
// defined here at sketch level so both setup and draw can see it
const LAST_LEVEL = 10; const LAST_LEVEL = 10;
let collectedCount = 0; // tracks which quote to show next
function onFragmentCollected(hexColor, x, y) { function onFragmentCollected(hexColor, x, y) {
player.collectFragment(hexColor); player.collectFragment(hexColor);
onFragmentCollection(fragments.length, hexColor); // update the color store onFragmentCollection(fragments.length, hexColor);
splats.push({ x, y, alpha: 200, size: 30 }); // add paint splash splats.push({ x, y, alpha: 200, size: 30 });
if (fragments.every(f => f.collected)) {
const isLast = fragments.every(f => f.collected);
// skip the toast on the last fragment — the level-complete overlay takes over
if (!isLast) {
showFragmentQuote(levelData.fragmentQuotes?.[collectedCount], hexColor);
}
collectedCount++;
if (isLast) {
gameState = 'levelcomplete'; gameState = 'levelcomplete';
completeLevel(levelData.color); completeLevel(levelData.color);
const dest = levelNumber === LAST_LEVEL ? '/win' : '/levelselect'; const dest = levelNumber === LAST_LEVEL ? '/win' : `/game?level=${levelNumber + 1}`;
setTimeout(() => push(dest), 2500); showCompleteQuote(levelData.completeQuote, levelData.color, dest);
} }
} }
p.preload = () => { p.preload = () => {
const data = getLevel(levelNumber); const data = getLevel(levelNumber);
if (data?.bg) bgImg = p.loadImage(data.bg); const isComplete = get(gameCompleted);
if (data?.playerImg) playerImgLoaded = p.loadImage(data.playerImg); const bgPath = isComplete && data?.bgComplete ? data.bgComplete : data?.bg;
if (bgPath) bgImg = p.loadImage(bgPath);
const playerPath = isComplete ? '/assets/player_level10.png' : data?.playerImg;
if (playerPath) playerImgLoaded = p.loadImage(playerPath);
splatImg = p.loadImage('/assets/splat.png'); splatImg = p.loadImage('/assets/splat.png');
}; };
@@ -149,20 +162,20 @@
p.allSprites.update(); p.allSprites.update();
// ── 2. COLOR TINT OVERLAY ───────────────────────────────────── // ── 2. COLOR TINT OVERLAY ─────────────────────────────────────
// as fragments are collected, the world gradually gains color // skipped after game completion — full-color backgrounds need no tint
// reveal goes from 0.0 (gray) to 1.0 (full color) if (!get(gameCompleted)) {
const reveal = get(colorOpacity); const reveal = get(colorOpacity);
const hex = get(levelColor); const hex = get(levelColor);
if (reveal > 0 && hex) { if (reveal > 0 && hex) {
const col = p.color(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 r = p.lerp(128, p.red(col), reveal);
const g = p.lerp(128, p.green(col), reveal); const g = p.lerp(128, p.green(col), reveal);
const b = p.lerp(128, p.blue(col), reveal); const b = p.lerp(128, p.blue(col), reveal);
p.noStroke(); p.noStroke();
p.fill(r, g, b, reveal * 120); // max alpha 120 so its a subtle tint p.fill(r, g, b, reveal * 120);
p.rect(0, 0, p.width, p.height); p.rect(0, 0, p.width, p.height);
} }
}
// ── 3. PLAYER INPUT ─────────────────────────────────────────── // ── 3. PLAYER INPUT ───────────────────────────────────────────
// update runs AFTER allSprites.update() so touching.bottom is accurate // update runs AFTER allSprites.update() so touching.bottom is accurate

View File

@@ -1,16 +1,14 @@
<script> <script>
import {lives, fragmentsCollected, levelColor} from '../stores/colorStore.js' import { lives, fragmentsCollected, levelColor } from '../stores/colorStore.js';
import { LEVELS } from '../game/levelData.js' import { LEVELS } from '../game/levelData.js';
export let levelNumber = 1; export let levelNumber = 1;
// get total fragments for the level
$: totalFragments = LEVELS.find(l => l.id === levelNumber)?.fragments.length ?? 0; $: totalFragments = LEVELS.find(l => l.id === levelNumber)?.fragments.length ?? 0;
</script> </script>
<div class="hud"> <div class="hud">
<!-- lives counter displayed on the left--> <div class="lives">
<div class = "lives">
{#each {length: 3} as _, i} {#each {length: 3} as _, i}
<img <img
src={i < $lives ? '/assets/heart_full.png' : '/assets/heart_empty.png'} src={i < $lives ? '/assets/heart_full.png' : '/assets/heart_empty.png'}
@@ -21,7 +19,6 @@
{/each} {/each}
</div> </div>
<!-- fragment collections shown on the right-->
<div class="frags" style="color: {$levelColor}"> <div class="frags" style="color: {$levelColor}">
{$fragmentsCollected} / {totalFragments} {$fragmentsCollected} / {totalFragments}
</div> </div>
@@ -36,8 +33,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
pointer-events: none; /* this allows the clicks to pass through to the game */ pointer-events: none;
z-index: 10; /* shows on top */ z-index: 10;
} }
.lives { .lives {
@@ -45,10 +42,11 @@
gap: 6px; gap: 6px;
} }
.frags{ .frags {
font-size: 22px; font-size: 22px;
font-weight: 500; font-weight: 500;
font-family:'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
text-shadow: 0 1px 4px rgba(0,0,0,0.8); text-shadow: 0 1px 4px rgba(0,0,0,0.8);
} }
</style> </style>

View File

@@ -0,0 +1,76 @@
<script>
import { push } from 'svelte-spa-router';
import { completeQuoteData, clearCompleteQuote } from '../stores/quoteStore.js';
function proceed() {
const dest = $completeQuoteData?.nextDest;
clearCompleteQuote();
if (dest) push(dest);
}
</script>
{#if $completeQuoteData}
<div class="overlay">
<div class="bar" style="background: {$completeQuoteData.color}"></div>
<p class="quote">{$completeQuoteData.text}</p>
<button
class="continue"
style="border-color: {$completeQuoteData.color}; color: {$completeQuoteData.color}"
on:click={proceed}
>
{$completeQuoteData.nextDest === '/win' ? 'finish →' : 'next level →'}
</button>
</div>
{/if}
<style>
.overlay {
position: absolute;
inset: 0;
background: rgba(6, 6, 6, 0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 0 80px;
z-index: 30;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.bar {
width: 48px;
height: 3px;
border-radius: 2px;
}
.quote {
font-family: 'Courier New', Courier, monospace;
font-size: 17px;
color: #ddd;
text-align: center;
line-height: 1.75;
font-weight: 400;
}
.continue {
margin-top: 8px;
padding: 9px 30px;
background: transparent;
border: 1px solid;
border-radius: 22px;
font-family: 'Courier New', Courier, monospace;
font-size: 15px;
cursor: pointer;
transition: background 0.15s;
}
.continue:hover {
background: rgba(255, 255, 255, 0.07);
}
</style>

View File

@@ -0,0 +1,40 @@
<script>
import { fragmentQuote } from '../stores/quoteStore.js';
</script>
{#if $fragmentQuote}
{#key $fragmentQuote.key}
<div
class="toast"
style="border-color: {$fragmentQuote.color}; --accent: {$fragmentQuote.color}"
>
<p>{$fragmentQuote.text}</p>
</div>
{/key}
{/if}
<style>
.toast {
position: absolute;
bottom: 22px;
right: 18px;
width: 230px;
padding: 10px 13px;
background: rgba(8, 8, 8, 0.88);
border-left: 3px solid;
color: #ccc;
font-family: 'Courier New', Courier, monospace;
font-size: 12.5px;
line-height: 1.55;
pointer-events: none;
animation: toastIn 4.2s ease forwards;
z-index: 20;
}
@keyframes toastIn {
0% { opacity: 0; transform: translateX(14px); }
12% { opacity: 1; transform: translateX(0); }
78% { opacity: 1; }
100% { opacity: 0; }
}
</style>

View File

@@ -43,6 +43,16 @@ export class Player {
keysDown.jump = false; // consume so it can't re-fire until the next keydown keysDown.jump = false; // consume so it can't re-fire until the next keydown
} }
// clamp to canvas left/right edges
const halfW = this.sprite.w / 2;
if (this.sprite.x < halfW) {
this.sprite.x = halfW;
this.sprite.vel.x = 0;
} else if (this.sprite.x > this.p.width - halfW) {
this.sprite.x = this.p.width - halfW;
this.sprite.vel.x = 0;
}
// count down invincibility frames // count down invincibility frames
if (this.isInvincible) { if (this.isInvincible) {
this.invincibleTimer--; this.invincibleTimer--;

View File

@@ -1,27 +1,26 @@
// level config — one entry per level // level config — one entry per level
// x,y = center w,h = dimensions // x,y = center w,h = dimensions
// fragment colors match the level color (updated hex values)
// bg → put your PNG in public/backgrounds/levelN.png // bg → put your PNG in public/backgrounds/levelN.png
// playerImg → put your PNG in public/assets/player_levelN.png // playerImg → put your PNG in public/assets/player_levelN.png
export const LEVELS = [ export const LEVELS = [
// ── LEVEL 1: CRIMSON ────────────────────────────────────────────────────── // ── LEVEL 1: CRIMSON ──────────────────────────────────────────────────────
// platforms and enemies unchanged from original design
{ {
id: 1, id: 1,
name: 'Eruption', name: 'A Burning Heart',
color: '#970505', color: '#970505',
bg: '/backgrounds/level1.png', bg: '/backgrounds/level1.png',
bgComplete: '/backgrounds/level1_complete.png',
playerImg: '/assets/player_level1.png', playerImg: '/assets/player_level1.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, // ground { x: 400, y: 440, w: 800, h: 12 },
{ x: 170, y: 370, w: 160, h: 14 }, // left starter { x: 170, y: 370, w: 160, h: 14 },
{ x: 390, y: 308, w: 150, h: 14 }, // middle step { x: 390, y: 308, w: 150, h: 14 },
{ x: 610, y: 248, w: 150, h: 14 }, // upper right { x: 610, y: 248, w: 150, h: 14 },
{ x: 395, y: 188, w: 140, h: 14 }, // top center { x: 395, y: 188, w: 140, h: 14 },
], ],
fragments: [ fragments: [
{ x: 170, y: 338, color: '#970505' }, { x: 170, y: 338, color: '#970505' },
@@ -33,26 +32,33 @@ export const LEVELS = [
{ x: 390, y: 283, patrol: 50 }, { x: 390, y: 283, patrol: 50 },
], ],
tar: [], tar: [],
fragmentQuotes: [
'Red is the first color you learn to see.',
'You felt it first, before you had words for it',
'To feel intensely is not weakness. It is aliveness.',
'The heart has always beaten in red.',
],
completeQuote: 'Red is the color of being alive. It asks nothing of you except honesty. Red is rage, passion, urgency, and love; The most viceral emotion, it demands to be felt.',
}, },
// ── LEVEL 2: AMBER ──────────────────────────────────────────────────────── // ── LEVEL 2: AMBER ────────────────────────────────────────────────────────
// platforms and enemies unchanged from original design
{ {
id: 2, id: 2,
name: 'Sunset', name: 'Warm Hands',
color: '#CF8917', color: '#CF8917',
bg: '/backgrounds/level2.png', bg: '/backgrounds/level2.png',
bgComplete: '/backgrounds/level2_complete.png',
playerImg: '/assets/player_level2.png', playerImg: '/assets/player_level2.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 160, y: 375, w: 150, h: 14 }, // left low { x: 160, y: 375, w: 150, h: 14 },
{ x: 360, y: 318, w: 140, h: 14 }, // center { x: 360, y: 318, w: 140, h: 14 },
{ x: 560, y: 260, w: 130, h: 14 }, // right mid { x: 560, y: 260, w: 130, h: 14 },
{ x: 710, y: 330, w: 110, h: 14 }, // right island { x: 710, y: 330, w: 110, h: 14 },
{ x: 280, y: 238, w: 110, h: 14 }, // upper left — backtrack { x: 280, y: 238, w: 110, h: 14 },
{ x: 478, y: 188, w: 110, h: 14 }, // top center { x: 478, y: 188, w: 110, h: 14 },
], ],
fragments: [ fragments: [
{ x: 160, y: 343, color: '#CF8917' }, { x: 160, y: 343, color: '#CF8917' },
@@ -67,33 +73,40 @@ export const LEVELS = [
tar: [ tar: [
{ x: 490, y: 432 }, { x: 490, y: 432 },
], ],
fragmentQuotes: [
'Not every fire burns, some just keep you warm',
'Warmth is a form of courage.',
'You made something today.That matters.',
'You were built to connect and create.',
],
completeQuote: 'Orange reminds you that making things is an act of hope. It is the color of warmth, creativity, enthusiams, and connection. It is a choice to stay open. Let yourself be warm.',
}, },
// ── LEVEL 3: YELLOW ─────────────────────────────────────────────────────── // ── LEVEL 3: YELLOW ───────────────────────────────────────────────────────
// two wide wings — left and right — converging at the upper center
{ {
id: 3, id: 3,
name: 'Golden', name: 'A Bright Ache',
color: '#E3D214', color: '#E3D214',
bg: '/backgrounds/level3.png', bg: '/backgrounds/level3.png',
bgComplete: '/backgrounds/level3_complete.png',
playerImg: '/assets/player_level3.png', playerImg: '/assets/player_level3.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 140, y: 372, w: 150, h: 14 }, // left start { x: 140, y: 372, w: 150, h: 14 },
{ x: 575, y: 365, w: 150, h: 14 }, // right start (enemy guards) { x: 575, y: 365, w: 150, h: 14 },
{ x: 280, y: 302, w: 120, h: 14 }, // left mid { x: 280, y: 302, w: 120, h: 14 },
{ x: 510, y: 295, w: 120, h: 14 }, // right mid { x: 510, y: 295, w: 120, h: 14 },
{ x: 155, y: 238, w: 110, h: 14 }, // upper left { x: 155, y: 238, w: 110, h: 14 },
{ x: 420, y: 228, w: 130, h: 14 }, // upper center (wider, enemy) { x: 420, y: 228, w: 130, h: 14 },
{ x: 660, y: 220, w: 110, h: 14 }, // upper right { x: 660, y: 220, w: 110, h: 14 },
], ],
fragments: [ fragments: [
{ x: 280, y: 270, color: '#E3D214' }, // left mid — easy { x: 280, y: 270, color: '#E3D214' },
{ x: 660, y: 188, color: '#E3D214' }, // upper right { x: 660, y: 188, color: '#E3D214' },
{ x: 155, y: 206, color: '#E3D214' }, // upper left { x: 155, y: 206, color: '#E3D214' },
{ x: 420, y: 196, color: '#E3D214' }, // upper center — guarded { x: 420, y: 196, color: '#E3D214' },
], ],
enemies: [ enemies: [
{ x: 575, y: 340, patrol: 52 }, { x: 575, y: 340, patrol: 52 },
@@ -103,35 +116,42 @@ export const LEVELS = [
{ x: 370, y: 432 }, { x: 370, y: 432 },
{ x: 700, y: 432 }, { x: 700, y: 432 },
], ],
fragmentQuotes: [
'Joy is allowed to be simple',
'Clarity costs something, it asks you to really look.',
'Anxiety and curiosity live in the same color.',
'Your mind runs fast because it cares deeply',
],
completeQuote: 'Yellow carries both hope and anxiety in equal measure. It is the color of joy but also of anxeity. Your nervous energy is not a flaw. It is the same thing as your intelligence, it goes hand in hand with your joy\'s .',
}, },
// ── LEVEL 4: GREEN ──────────────────────────────────────────────────────── // ── LEVEL 4: GREEN ────────────────────────────────────────────────────────
// two vertical columns with connecting bridges — forest canopy feel
{ {
id: 4, id: 4,
name: 'Greenery', name: 'Growth',
color: '#39BD1C', color: '#39BD1C',
bg: '/backgrounds/level4.png', bg: '/backgrounds/level4.png',
bgComplete: '/backgrounds/level4_complete.png',
playerImg: '/assets/player_level4.png', playerImg: '/assets/player_level4.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 140, y: 372, w: 130, h: 14 }, // left start (enemy guards) { x: 140, y: 372, w: 130, h: 14 },
{ x: 665, y: 365, w: 130, h: 14 }, // right — separated by tar { x: 665, y: 365, w: 130, h: 14 },
{ x: 270, y: 305, w: 120, h: 14 }, // center-left mid (enemy guards) { x: 270, y: 305, w: 120, h: 14 },
{ x: 510, y: 298, w: 120, h: 14 }, // center-right mid { x: 510, y: 298, w: 120, h: 14 },
{ x: 165, y: 242, w: 110, h: 14 }, // upper left { x: 165, y: 242, w: 110, h: 14 },
{ x: 380, y: 232, w: 120, h: 14 }, // upper center { x: 380, y: 232, w: 120, h: 14 },
{ x: 610, y: 225, w: 110, h: 14 }, // upper right (enemy guards) { x: 610, y: 225, w: 110, h: 14 },
{ x: 290, y: 175, w: 95, h: 14 }, // top left { x: 290, y: 175, w: 95, h: 14 },
{ x: 520, y: 168, w: 95, h: 14 }, // top right — hardest { x: 520, y: 168, w: 95, h: 14 },
], ],
fragments: [ fragments: [
{ x: 270, y: 273, color: '#39BD1C' }, // center-left mid { x: 270, y: 273, color: '#39BD1C' },
{ x: 510, y: 266, color: '#39BD1C' }, // center-right mid { x: 510, y: 266, color: '#39BD1C' },
{ x: 380, y: 200, color: '#39BD1C' }, // upper center { x: 380, y: 200, color: '#39BD1C' },
{ x: 520, y: 136, color: '#39BD1C' }, // top right — hardest { x: 520, y: 136, color: '#39BD1C' },
], ],
enemies: [ enemies: [
{ x: 140, y: 347, patrol: 40 }, { x: 140, y: 347, patrol: 40 },
@@ -142,38 +162,45 @@ export const LEVELS = [
{ x: 395, y: 432 }, { x: 395, y: 432 },
{ x: 610, y: 432 }, { x: 610, y: 432 },
], ],
fragmentQuotes: [
'Growth rarely feels like growth while it\'s happening.',
'Green is the slowest and most stubborn color.',
'Healing is not linear',
'Every root is also a reach.',
],
completeQuote: 'Green is the color of becoming. It does not rush, does not announce itself. It doesn\'t ask you to heal, only to keep growing. Green represents growth, healing, balance, the slow work of becoming. You are allowed to grow quietly, at your own pace.',
}, },
// ── LEVEL 5: CYAN ───────────────────────────────────────────────────────── // ── LEVEL 5: CYAN ─────────────────────────────────────────────────────────
// zigzag flow — dips and rises like tide pools
{ {
id: 5, id: 5,
name: 'Tidal', name: 'Open Water',
color: '#12B6C8', color: '#12B6C8',
bg: '/backgrounds/level5.png', bg: '/backgrounds/level5.png',
bgComplete: '/backgrounds/level5_complete.png',
playerImg: '/assets/player_level5.png', playerImg: '/assets/player_level5.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 145, y: 378, w: 140, h: 14 }, // left start { x: 145, y: 378, w: 140, h: 14 },
{ x: 355, y: 395, w: 115, h: 14 }, // dips down — zigzag { x: 355, y: 395, w: 115, h: 14 },
{ x: 540, y: 368, w: 125, h: 14 }, // center right { x: 540, y: 368, w: 125, h: 14 },
{ x: 705, y: 348, w: 115, h: 14 }, // far right { x: 705, y: 348, w: 115, h: 14 },
{ x: 235, y: 308, w: 110, h: 14 }, // upper left { x: 235, y: 308, w: 110, h: 14 },
{ x: 445, y: 295, w: 110, h: 14 }, // upper center (enemy) { x: 445, y: 295, w: 110, h: 14 },
{ x: 640, y: 278, w: 115, h: 14 }, // upper right { x: 640, y: 278, w: 115, h: 14 },
{ x: 125, y: 248, w: 95, h: 14 }, // high far left — isolated { x: 125, y: 248, w: 95, h: 14 },
{ x: 365, y: 235, w: 90, h: 14 }, // high center { x: 365, y: 235, w: 90, h: 14 },
{ x: 590, y: 220, w: 90, h: 14 }, // high right (enemy) { x: 590, y: 220, w: 90, h: 14 },
{ x: 755, y: 202, w: 80, h: 14 }, // top far right — narrow { x: 755, y: 202, w: 80, h: 14 },
], ],
fragments: [ fragments: [
{ x: 235, y: 276, color: '#12B6C8' }, // upper left { x: 235, y: 276, color: '#12B6C8' },
{ x: 640, y: 246, color: '#12B6C8' }, // upper right { x: 640, y: 246, color: '#12B6C8' },
{ x: 125, y: 216, color: '#12B6C8' }, // high far left — isolated { x: 125, y: 216, color: '#12B6C8' },
{ x: 365, y: 203, color: '#12B6C8' }, // high center { x: 365, y: 203, color: '#12B6C8' },
{ x: 755, y: 170, color: '#12B6C8' }, // top far right — hardest { x: 755, y: 170, color: '#12B6C8' },
], ],
enemies: [ enemies: [
{ x: 355, y: 370, patrol: 35 }, { x: 355, y: 370, patrol: 35 },
@@ -185,40 +212,48 @@ export const LEVELS = [
{ x: 480, y: 432 }, { x: 480, y: 432 },
{ x: 680, y: 432 }, { x: 680, y: 432 },
], ],
fragmentQuotes: [
'Clarity comes after, not before',
'Clear water still has a bottom.',
'Calm is not the absence of feeling, it\'s feeling without drowning.',
'To speak honestly is an act of trust.',
'You can be still and still be powerful.',
],
completeQuote: 'Cyan is the color of honest water, clear enough to see through, deep enough to matter, and standing at a calming still. Cyan sits between calmness and clarity. It asks you to say what you mean, and listen with clarity. ',
}, },
// ── LEVEL 6: DEEP BLUE ──────────────────────────────────────────────────── // ── LEVEL 6: DEEP BLUE ────────────────────────────────────────────────────
// cold, sparse — wider gaps, deliberate platform placement
{ {
id: 6, id: 6,
name: 'The Abyss', name: 'A Long Quiet',
color: '#170CB7', color: '#170CB7',
bg: '/backgrounds/level6.png', bg: '/backgrounds/level6.png',
bgComplete: '/backgrounds/level6_complete.png',
playerImg: '/assets/player_level6.png', playerImg: '/assets/player_level6.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 130, y: 382, w: 125, h: 14 }, // left start { x: 130, y: 382, w: 125, h: 14 },
{ x: 675, y: 370, w: 130, h: 14 }, // far right — requires commitment { x: 675, y: 370, w: 130, h: 14 },
{ x: 308, y: 355, w: 108, h: 14 }, // center step { x: 308, y: 355, w: 108, h: 14 },
{ x: 510, y: 385, w: 105, h: 14 }, // dip right { x: 510, y: 385, w: 105, h: 14 },
{ x: 220, y: 302, w: 100, h: 14 }, // upper left { x: 220, y: 302, w: 100, h: 14 },
{ x: 455, y: 290, w: 100, h: 14 }, // upper center { x: 455, y: 290, w: 100, h: 14 },
{ x: 660, y: 275, w: 110, h: 14 }, // upper right (enemy) { x: 660, y: 275, w: 110, h: 14 },
{ x: 105, y: 245, w: 88, h: 14 }, // high far left — isolated { x: 105, y: 245, w: 88, h: 14 },
{ x: 358, y: 232, w: 85, h: 14 }, // high center { x: 358, y: 232, w: 85, h: 14 },
{ x: 580, y: 215, w: 85, h: 14 }, // high right { x: 580, y: 215, w: 85, h: 14 },
{ x: 745, y: 198, w: 80, h: 14 }, // narrow top right { x: 745, y: 198, w: 80, h: 14 },
{ x: 268, y: 175, w: 80, h: 14 }, // top left { x: 268, y: 175, w: 80, h: 14 },
{ x: 480, y: 165, w: 80, h: 14 }, // very top — hardest { x: 480, y: 165, w: 80, h: 14 },
], ],
fragments: [ fragments: [
{ x: 308, y: 323, color: '#170CB7' }, // center step — warmup { x: 308, y: 323, color: '#170CB7' },
{ x: 220, y: 270, color: '#170CB7' }, // upper left { x: 220, y: 270, color: '#170CB7' },
{ x: 660, y: 243, color: '#170CB7' }, // upper right { x: 660, y: 243, color: '#170CB7' },
{ x: 358, y: 200, color: '#170CB7' }, // high center { x: 358, y: 200, color: '#170CB7' },
{ x: 480, y: 133, color: '#170CB7' }, // very top — hardest { x: 480, y: 133, color: '#170CB7' },
], ],
enemies: [ enemies: [
{ x: 510, y: 360, patrol: 33 }, { x: 510, y: 360, patrol: 33 },
@@ -231,39 +266,47 @@ export const LEVELS = [
{ x: 460, y: 432 }, { x: 460, y: 432 },
{ x: 640, y: 432 }, { x: 640, y: 432 },
], ],
fragmentQuotes: [
'Some feelings don\'t have names, they just have weight',
'There is beauty in melancholy, it means you loved something.',
'Depth and darkness are not the same thing.',
'You are allowed to sit with it.',
'Some truths only surface in the quiet.',
],
completeQuote: 'Blue is the color of depth, sadness, and introspection, the most universally felt emotions. It does not ask you to feel better. It asks you to feel, that is enough. Do not be afraid to go deep. That is where you find out who you actually are.',
}, },
// ── LEVEL 7: PURPLE ─────────────────────────────────────────────────────── // ── LEVEL 7: PURPLE ───────────────────────────────────────────────────────
// narrow platforms spiraling up — precision required
{ {
id: 7, id: 7,
name: 'Twilight Spire', name: 'In-Between',
color: '#6613BA', color: '#6613BA',
bg: '/backgrounds/level7.png', bg: '/backgrounds/level7.png',
bgComplete: '/backgrounds/level7_complete.png',
playerImg: '/assets/player_level7.png', playerImg: '/assets/player_level7.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 130, y: 385, w: 115, h: 14 }, // left start { x: 130, y: 385, w: 115, h: 14 },
{ x: 308, y: 365, w: 100, h: 14 }, // small step { x: 308, y: 365, w: 100, h: 14 },
{ x: 483, y: 390, w: 100, h: 14 }, // dip { x: 483, y: 390, w: 100, h: 14 },
{ x: 652, y: 358, w: 115, h: 14 }, // right mid { x: 652, y: 358, w: 115, h: 14 },
{ x: 768, y: 300, w: 75, h: 14 }, // narrow far right { x: 768, y: 300, w: 75, h: 14 },
{ x: 578, y: 272, w: 88, h: 14 }, // upper right { x: 578, y: 272, w: 88, h: 14 },
{ x: 400, y: 295, w: 85, h: 14 }, // upper center { x: 400, y: 295, w: 85, h: 14 },
{ x: 220, y: 285, w: 90, h: 14 }, // upper left { x: 220, y: 285, w: 90, h: 14 },
{ x: 90, y: 248, w: 80, h: 14 }, // narrow far left { x: 90, y: 248, w: 80, h: 14 },
{ x: 320, y: 232, w: 80, h: 14 }, // high center { x: 320, y: 232, w: 80, h: 14 },
{ x: 518, y: 215, w: 80, h: 14 }, // high right { x: 518, y: 215, w: 80, h: 14 },
{ x: 698, y: 190, w: 78, h: 14 }, // top right — narrow { x: 698, y: 190, w: 78, h: 14 },
], ],
fragments: [ fragments: [
{ x: 652, y: 326, color: '#6613BA' }, // right mid { x: 652, y: 326, color: '#6613BA' },
{ x: 220, y: 253, color: '#6613BA' }, // upper left { x: 220, y: 253, color: '#6613BA' },
{ x: 320, y: 200, color: '#6613BA' }, // high center { x: 320, y: 200, color: '#6613BA' },
{ x: 518, y: 183, color: '#6613BA' }, // high right { x: 518, y: 183, color: '#6613BA' },
{ x: 698, y: 158, color: '#6613BA' }, // top right — hardest { x: 698, y: 158, color: '#6613BA' },
], ],
enemies: [ enemies: [
{ x: 308, y: 340, patrol: 28 }, { x: 308, y: 340, patrol: 28 },
@@ -276,15 +319,23 @@ export const LEVELS = [
{ x: 408, y: 432 }, { x: 408, y: 432 },
{ x: 618, y: 432 }, { x: 618, y: 432 },
], ],
fragmentQuotes: [
'Not everything needs an explanation.',
'Mystery is an invitation, not a threat.',
'Your contradictions are not flaws, ther are complexity.',
'The unknown is not something to fix.',
'Transformation is always a little uncomfortable.',
],
completeQuote: 'Purple is the color of the in-betweens it represents mystery and intuition. Purple lives in the questions. You do not need everything figured out. Some things are only ever felt, never fully explained.',
}, },
// ── LEVEL 8: MAGENTA ────────────────────────────────────────────────────── // ── LEVEL 8: MAGENTA ──────────────────────────────────────────────────────
// energetic grid-like layout with strategic gaps
{ {
id: 8, id: 8,
name: 'Neon Bloom', name: 'Tenderness',
color: '#C71287', color: '#C71287',
bg: '/backgrounds/level8.png', bg: '/backgrounds/level8.png',
bgComplete: '/backgrounds/level8_complete.png',
playerImg: '/assets/player_level8.png', playerImg: '/assets/player_level8.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
@@ -292,24 +343,24 @@ export const LEVELS = [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 155, y: 380, w: 130, h: 14 }, { x: 155, y: 380, w: 130, h: 14 },
{ x: 338, y: 360, w: 118, h: 14 }, { x: 338, y: 360, w: 118, h: 14 },
{ x: 513, y: 380, w: 110, h: 14 }, // dip { x: 513, y: 380, w: 110, h: 14 },
{ x: 688, y: 358, w: 120, h: 14 }, { x: 688, y: 358, w: 120, h: 14 },
{ x: 165, y: 308, w: 100, h: 14 }, // upper left { x: 165, y: 308, w: 100, h: 14 },
{ x: 365, y: 300, w: 90, h: 14 }, // upper center { x: 365, y: 300, w: 90, h: 14 },
{ x: 553, y: 308, w: 100, h: 14 }, // upper right { x: 553, y: 308, w: 100, h: 14 },
{ x: 728, y: 278, w: 78, h: 14 }, // narrow far right { x: 728, y: 278, w: 78, h: 14 },
{ x: 260, y: 248, w: 90, h: 14 }, // high left { x: 260, y: 248, w: 90, h: 14 },
{ x: 448, y: 238, w: 90, h: 14 }, // high center { x: 448, y: 238, w: 90, h: 14 },
{ x: 636, y: 222, w: 90, h: 14 }, // high right { x: 636, y: 222, w: 90, h: 14 },
{ x: 368, y: 178, w: 80, h: 14 }, // near top { x: 368, y: 178, w: 80, h: 14 },
{ x: 553, y: 165, w: 80, h: 14 }, // top — hardest { x: 553, y: 165, w: 80, h: 14 },
], ],
fragments: [ fragments: [
{ x: 165, y: 276, color: '#C71287' }, // upper left { x: 165, y: 276, color: '#C71287' },
{ x: 728, y: 246, color: '#C71287' }, // narrow far right { x: 728, y: 246, color: '#C71287' },
{ x: 448, y: 206, color: '#C71287' }, // high center { x: 448, y: 206, color: '#C71287' },
{ x: 368, y: 146, color: '#C71287' }, // near top { x: 368, y: 146, color: '#C71287' },
{ x: 553, y: 133, color: '#C71287' }, // top — hardest { x: 553, y: 133, color: '#C71287' },
], ],
enemies: [ enemies: [
{ x: 338, y: 335, patrol: 38 }, { x: 338, y: 335, patrol: 38 },
@@ -323,15 +374,23 @@ export const LEVELS = [
{ x: 558, y: 432 }, { x: 558, y: 432 },
{ x: 738, y: 432 }, { x: 738, y: 432 },
], ],
fragmentQuotes: [
'Pink is strength dressed in softness.',
'Compassion begins with yourself.',
'You would never speak to a friend the way you speak to yourself.',
'Playfulness is not childishness, it is aliveness.',
'You are allowed to bloom loudly.',
],
completeQuote: 'Magenta doesn\'t apologize for being bright. Magenta represents compassion, self-love, and softness. It aks you to be as gentel with youself as you are with the people you love the most.',
}, },
// ── LEVEL 9: BROWN ──────────────────────────────────────────────────────── // ── LEVEL 9: BROWN ────────────────────────────────────────────────────────
// dense cave — most enemies, hardest single-color level
{ {
id: 9, id: 9,
name: 'Deep Caves', name: 'What holds you',
color: '#753F16', color: '#753F16',
bg: '/backgrounds/level9.png', bg: '/backgrounds/level9.png',
bgComplete: '/backgrounds/level9_complete.png',
playerImg: '/assets/player_level9.png', playerImg: '/assets/player_level9.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
@@ -339,12 +398,12 @@ export const LEVELS = [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
{ x: 135, y: 382, w: 128, h: 14 }, { x: 135, y: 382, w: 128, h: 14 },
{ x: 315, y: 365, w: 112, h: 14 }, { x: 315, y: 365, w: 112, h: 14 },
{ x: 496, y: 388, w: 108, h: 14 }, // dip { x: 496, y: 388, w: 108, h: 14 },
{ x: 660, y: 358, w: 118, h: 14 }, { x: 660, y: 358, w: 118, h: 14 },
{ x: 200, y: 322, w: 100, h: 14 }, { x: 200, y: 322, w: 100, h: 14 },
{ x: 400, y: 312, w: 93, h: 14 }, { x: 400, y: 312, w: 93, h: 14 },
{ x: 578, y: 305, w: 100, h: 14 }, { x: 578, y: 305, w: 100, h: 14 },
{ x: 738, y: 288, w: 78, h: 14 }, // narrow { x: 738, y: 288, w: 78, h: 14 },
{ x: 118, y: 262, w: 88, h: 14 }, { x: 118, y: 262, w: 88, h: 14 },
{ x: 310, y: 255, w: 88, h: 14 }, { x: 310, y: 255, w: 88, h: 14 },
{ x: 492, y: 245, w: 88, h: 14 }, { x: 492, y: 245, w: 88, h: 14 },
@@ -373,51 +432,54 @@ export const LEVELS = [
{ x: 540, y: 432 }, { x: 540, y: 432 },
{ x: 712, y: 432 }, { x: 712, y: 432 },
], ],
fragmentQuotes: [
'Some things stay so that other things can move.',
'Stability is essential.',
'The earth holds everything without complaint.',
'Steadiness is a kind of strenght.',
'Your roors are not holding you back.',
],
completeQuote: 'Brown is groundedness and stability. It is the earth beneath everything, often overlooked but everything grows from it. It does not need to be seen to do its work.',
}, },
// ── LEVEL 10: THE COLOR REALM — final ──────────────────────────────────── // ── LEVEL 10: THE COLOR REALM — final ────────────────────────────────────
// one fragment per level color — completing this leads to the win screen
{ {
id: 10, id: 10,
name: 'The Color Realm', name: 'The Whole of You',
color: '#FFD700', color: '#FFD700',
bg: '/backgrounds/level10.png', bg: '/backgrounds/level10.png',
bgComplete: '/backgrounds/level10_complete.png',
playerImg: '/assets/player_level10.png', playerImg: '/assets/player_level10.png',
spawnX: 60, spawnX: 60,
spawnY: 400, spawnY: 400,
platforms: [ platforms: [
{ x: 400, y: 440, w: 800, h: 12 }, { x: 400, y: 440, w: 800, h: 12 },
// row 1 — low
{ x: 120, y: 380, w: 140, h: 14 }, { x: 120, y: 380, w: 140, h: 14 },
{ x: 310, y: 362, w: 120, h: 14 }, { x: 310, y: 362, w: 120, h: 14 },
{ x: 510, y: 382, w: 120, h: 14 }, { x: 510, y: 382, w: 120, h: 14 },
{ x: 700, y: 365, w: 130, h: 14 }, { x: 700, y: 365, w: 130, h: 14 },
// row 2 — mid
{ x: 210, y: 305, w: 120, h: 14 }, { x: 210, y: 305, w: 120, h: 14 },
{ x: 420, y: 298, w: 110, h: 14 }, { x: 420, y: 298, w: 110, h: 14 },
{ x: 628, y: 288, w: 120, h: 14 }, { x: 628, y: 288, w: 120, h: 14 },
// row 3 — high
{ x: 115, y: 248, w: 100, h: 14 }, { x: 115, y: 248, w: 100, h: 14 },
{ x: 315, y: 238, w: 95, h: 14 }, { x: 315, y: 238, w: 95, h: 14 },
{ x: 515, y: 228, w: 95, h: 14 }, { x: 515, y: 228, w: 95, h: 14 },
{ x: 715, y: 215, w: 90, h: 14 }, { x: 715, y: 215, w: 90, h: 14 },
// row 4 — top
{ x: 215, y: 178, w: 90, h: 14 }, { x: 215, y: 178, w: 90, h: 14 },
{ x: 415, y: 168, w: 90, h: 14 }, { x: 415, y: 168, w: 90, h: 14 },
{ x: 615, y: 160, 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 }, { x: 415, y: 128, w: 80, h: 14 },
], ],
fragments: [ fragments: [
{ x: 310, y: 330, color: '#970505' }, // crimson { x: 310, y: 330, color: '#970505' },
{ x: 700, y: 333, color: '#CF8917' }, // amber { x: 700, y: 333, color: '#CF8917' },
{ x: 210, y: 273, color: '#E3D214' }, // yellow { x: 210, y: 273, color: '#E3D214' },
{ x: 628, y: 256, color: '#39BD1C' }, // green { x: 628, y: 256, color: '#39BD1C' },
{ x: 315, y: 206, color: '#12B6C8' }, // cyan { x: 315, y: 206, color: '#12B6C8' },
{ x: 715, y: 183, color: '#170CB7' }, // deep blue { x: 715, y: 183, color: '#170CB7' },
{ x: 215, y: 146, color: '#6613BA' }, // purple { x: 215, y: 146, color: '#6613BA' },
{ x: 615, y: 128, color: '#C71287' }, // magenta { x: 615, y: 128, color: '#C71287' },
{ x: 415, y: 96, color: '#753F16' }, // brown — very top { x: 415, y: 96, color: '#753F16' },
], ],
enemies: [ enemies: [
{ x: 120, y: 355, patrol: 45 }, { x: 120, y: 355, patrol: 45 },
@@ -433,6 +495,18 @@ export const LEVELS = [
{ x: 665, y: 432 }, { x: 665, y: 432 },
{ x: 760, y: 432 }, { x: 760, y: 432 },
], ],
fragmentQuotes: [
'you burned',
'you made',
'you hoped and worried',
'you grew',
'you let go',
'you felt deeply',
'you wondered',
'you loved',
'you stayed',
],
completeQuote: 'Every emotion is a part of you. You needed all of it. The rage and the tenderness. The confusion and the clarity. The grief and the joy. A world with only one color is not a world at all. Neither are you. You are the whole spectrum. The full Hue.',
}, },
]; ];

View File

@@ -2,6 +2,8 @@
import { querystring } from 'svelte-spa-router'; import { querystring } from 'svelte-spa-router';
import GameCanvas from '../components/GameCanvas.svelte'; import GameCanvas from '../components/GameCanvas.svelte';
import HUD from '../components/HUD.svelte'; import HUD from '../components/HUD.svelte';
import QuoteToast from '../components/QuoteToast.svelte';
import LevelCompleteOverlay from '../components/LevelCompleteOverlay.svelte';
// querystring is the part after ? in the URL e.g. "level=2" // querystring is the part after ? in the URL e.g. "level=2"
// we parse it safely — if missing, default to level 1 // we parse it safely — if missing, default to level 1
@@ -11,8 +13,12 @@
</script> </script>
<div class="game-wrapper"> <div class="game-wrapper">
{#key levelNum}
<GameCanvas levelNumber={levelNum}/> <GameCanvas levelNumber={levelNum}/>
{/key}
<HUD levelNumber={levelNum}/> <HUD levelNumber={levelNum}/>
<QuoteToast />
<LevelCompleteOverlay />
</div> </div>
<style> <style>

View File

@@ -11,8 +11,8 @@
<!--<img src="/backgrounds/title_bg.png" class="bg" alt="title background"/>--> <!--<img src="/backgrounds/title_bg.png" class="bg" alt="title background"/>-->
<div class="content"> <div class="content">
<h1>ColorQuest</h1> <h1>The Full Hue</h1>
<p>Bring color back to your world</p> <p>Bring back your color</p>
<button on:click={startGame}>begin</button> <button on:click={startGame}>begin</button>
</div> </div>
</div> </div>

View File

@@ -4,8 +4,8 @@
<div class="screen"> <div class="screen">
<h1 class="title">color restored</h1> <h1 class="title">color restored</h1>
<p class="line1">every fragment found. every hue returned to the world.</p> <p class="line1">every fragment found. every hue returned </p>
<p class="line2">the little painter has done it.</p> <p class="line2">Go forth and expereince the world in full color.</p>
<button on:click={() => push('/')}>back to home</button> <button on:click={() => push('/')}>back to home</button>
</div> </div>

View File

@@ -12,7 +12,8 @@
// -- so these are the global variables ---- // -- so these are the global variables ----
import { writable } from 'svelte/store'; import { writable, derived } from 'svelte/store';
import { LEVELS } from '../game/levelData.js';
// 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([]);
@@ -33,6 +34,12 @@ export const fragmentsCollected = writable(0);
//player lives //player lives
export const lives = writable(3); export const lives = writable(3);
// true once every level's color has been unlocked (game beaten)
export const gameCompleted = derived(
unlockedColors,
$colors => LEVELS.every(level => $colors.includes(level.color))
);
// --- these are the global functions ----- // --- these are the global functions -----
// for collecting a fragment // for collecting a fragment

36
src/stores/quoteStore.js Normal file
View File

@@ -0,0 +1,36 @@
import { writable } from 'svelte/store';
// ── Fragment quote toast ──────────────────────────────────────────────────────
// Shows a short sentence in the bottom-right when a fragment is collected.
// Auto-clears after 4 seconds; cancels any previous timer so rapid collection
// always shows the newest quote cleanly.
export const fragmentQuote = writable(null); // { text, color, key } | null
let toastTimer = null;
let quoteKey = 0;
export function showFragmentQuote(text, color) {
if (!text) return;
if (toastTimer) clearTimeout(toastTimer);
quoteKey++;
fragmentQuote.set({ text, color, key: quoteKey });
toastTimer = setTimeout(() => {
fragmentQuote.set(null);
toastTimer = null;
}, 4200);
}
// ── Level-complete overlay ────────────────────────────────────────────────────
// Shows the color-psychology takeaway before the level-select redirect.
export const completeQuoteData = writable(null); // { text, color } | null
export function showCompleteQuote(text, color, nextDest) {
if (!text) return;
completeQuoteData.set({ text, color, nextDest });
}
export function clearCompleteQuote() {
completeQuoteData.set(null);
}