adding storytelling elements

This commit is contained in:
2026-05-09 14:56:16 +09:00
parent d4b3a3842c
commit 0253789ea4
10 changed files with 389 additions and 156 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.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -3,6 +3,7 @@
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 } 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,19 +62,27 @@
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);
} }
} }

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

@@ -1,13 +1,11 @@
// 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: 'Eruption',
@@ -17,11 +15,11 @@ export const LEVELS = [
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,10 +31,16 @@ export const LEVELS = [
{ x: 390, y: 283, patrol: 50 }, { x: 390, y: 283, patrol: 50 },
], ],
tar: [], tar: [],
fragmentQuotes: [
'Red is the first color infants learn to see.',
'Anger, passion, urgency — red holds them all.',
'To feel intensely is not weakness. It is aliveness.',
'The heart has always beaten in red.',
],
completeQuote: 'Red is the color of urgency and passion. Of love at its most raw, and fury at its most honest. You do not need to be calm to be whole — sometimes the world needs your fire.',
}, },
// ── LEVEL 2: AMBER ──────────────────────────────────────────────────────── // ── LEVEL 2: AMBER ────────────────────────────────────────────────────────
// platforms and enemies unchanged from original design
{ {
id: 2, id: 2,
name: 'Sunset', name: 'Sunset',
@@ -47,12 +51,12 @@ export const LEVELS = [
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,10 +71,16 @@ export const LEVELS = [
tar: [ tar: [
{ x: 490, y: 432 }, { x: 490, y: 432 },
], ],
fragmentQuotes: [
'Orange reaches toward others.',
'Warmth is a form of courage.',
'Creativity begins where comfort ends.',
'You were built to connect.',
],
completeQuote: 'Amber glows like a hearthfire — the color of gathering, of sharing, of laughter that carries across a room. Enthusiasm is not naivety. 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: 'Golden',
@@ -81,19 +91,19 @@ export const LEVELS = [
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,10 +113,16 @@ export const LEVELS = [
{ x: 370, y: 432 }, { x: 370, y: 432 },
{ x: 700, y: 432 }, { x: 700, y: 432 },
], ],
fragmentQuotes: [
'Yellow is the color of a question.',
'Clarity costs something — it asks you to really look.',
'Anxiety and curiosity live in the same color.',
'The brightest light also casts the sharpest shadow.',
],
completeQuote: 'Yellow is the mind\'s color — quick, restless, brilliant. It carries both hope and anxiety in equal measure. Your nervous energy is not a flaw. It is the same thing as your intelligence.',
}, },
// ── LEVEL 4: GREEN ──────────────────────────────────────────────────────── // ── LEVEL 4: GREEN ────────────────────────────────────────────────────────
// two vertical columns with connecting bridges — forest canopy feel
{ {
id: 4, id: 4,
name: 'Greenery', name: 'Greenery',
@@ -117,21 +133,21 @@ export const LEVELS = [
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,10 +158,16 @@ 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.',
'Balance is not a destination. It is a practice.',
'Every root is also a reach.',
],
completeQuote: 'Green is the color of becoming. It does not rush, does not announce itself. It simply keeps going — through concrete, through drought, through winter. 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: 'Tidal',
@@ -156,24 +178,24 @@ export const LEVELS = [
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,10 +207,17 @@ export const LEVELS = [
{ x: 480, y: 432 }, { x: 480, y: 432 },
{ x: 680, y: 432 }, { x: 680, y: 432 },
], ],
fragmentQuotes: [
'Teal lives between blue\'s depth and green\'s life.',
'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. It asks you to say what you mean, and listen without armor. Clarity is a gift you can give.',
}, },
// ── LEVEL 6: DEEP BLUE ──────────────────────────────────────────────────── // ── LEVEL 6: DEEP BLUE ────────────────────────────────────────────────────
// cold, sparse — wider gaps, deliberate platform placement
{ {
id: 6, id: 6,
name: 'The Abyss', name: 'The Abyss',
@@ -199,26 +228,26 @@ export const LEVELS = [
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,10 +260,17 @@ export const LEVELS = [
{ x: 460, y: 432 }, { x: 460, y: 432 },
{ x: 640, y: 432 }, { x: 640, y: 432 },
], ],
fragmentQuotes: [
'Blue is the color humans most often call their favorite.',
'There is beauty in melancholy — it means you loved something.',
'Depth and heaviness are not the same thing.',
'Trust is built in the dark, not the light.',
'Some truths only surface in the quiet.',
],
completeQuote: 'Blue is the color of depth and devotion. It holds grief and loyalty in the same hand without flinching. Do not be afraid to go deep. That is where the real things live — 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: 'Twilight Spire',
@@ -245,25 +281,25 @@ export const LEVELS = [
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,10 +312,17 @@ export const LEVELS = [
{ x: 408, y: 432 }, { x: 408, y: 432 },
{ x: 618, y: 432 }, { x: 618, y: 432 },
], ],
fragmentQuotes: [
'Purple is the rarest color found in nature.',
'Mystery is an invitation, not a threat.',
'Imagination is how you escape — and return changed.',
'Wisdom is just questions that have aged well.',
'Transformation is always a little uncomfortable.',
],
completeQuote: 'Purple is the color of the in-between — twilight, magic, the moment before understanding arrives. You do not need everything figured out. Some things are only ever felt, never fully explained. Trust the mystery.',
}, },
// ── LEVEL 8: MAGENTA ────────────────────────────────────────────────────── // ── LEVEL 8: MAGENTA ──────────────────────────────────────────────────────
// energetic grid-like layout with strategic gaps
{ {
id: 8, id: 8,
name: 'Neon Bloom', name: 'Neon Bloom',
@@ -292,24 +335,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,10 +366,17 @@ 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.',
'To love boldly in a hard world is a radical act.',
'Playfulness is not childishness — it is aliveness.',
'You are allowed to bloom loudly.',
],
completeQuote: 'Magenta doesn\'t apologize for being bright. It is the color of unconditional love — love that does not shrink itself to be acceptable. Tenderness is not weakness. You can be soft and still be powerful.',
}, },
// ── LEVEL 9: BROWN ──────────────────────────────────────────────────────── // ── LEVEL 9: BROWN ────────────────────────────────────────────────────────
// dense cave — most enemies, hardest single-color level
{ {
id: 9, id: 9,
name: 'Deep Caves', name: 'Deep Caves',
@@ -339,12 +389,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,10 +423,17 @@ export const LEVELS = [
{ x: 540, y: 432 }, { x: 540, y: 432 },
{ x: 712, y: 432 }, { x: 712, y: 432 },
], ],
fragmentQuotes: [
'Brown is the color of everything that holds.',
'Stability is not glamorous. It is essential.',
'The earth holds everything without complaint.',
'Reliability is its own kind of love.',
'Roots make the reaching possible.',
],
completeQuote: 'Brown is the color of the ground beneath you — steady, patient, without performance. It does not need to be seen to do its work. In a world obsessed with brightness, groundedness is a radical act.',
}, },
// ── 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 Color Realm',
@@ -387,37 +444,32 @@ export const LEVELS = [
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 +485,18 @@ export const LEVELS = [
{ x: 665, y: 432 }, { x: 665, y: 432 },
{ x: 760, y: 432 }, { x: 760, y: 432 },
], ],
fragmentQuotes: [
'Red — the first color you ever saw.',
'Amber — the warmth you learned to offer.',
'Yellow — the light that made you curious.',
'Green — the slow courage to keep growing.',
'Cyan — the voice you learned to trust.',
'Blue — the depths you survived.',
'Purple — the mystery you stopped running from.',
'Magenta — the love you let yourself deserve.',
'Brown — the ground that held you all along.',
],
completeQuote: 'Every color is an emotion. Every emotion is a part of you. To be fully human is to carry them all — the burning and the quiet, the growing and the grief. You are not broken. You are a spectrum. And you are whole.',
}, },
]; ];

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 full color</p>
<button on:click={startGame}>begin</button> <button on:click={startGame}>begin</button>
</div> </div>
</div> </div>

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