refining game details and continuing documentation
This commit is contained in:
93
README.md
93
README.md
@@ -5,4 +5,95 @@
|
|||||||
**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)
|
||||||
|
|
||||||
|
**Video Demo**: []()
|
||||||
|
|
||||||
|
# Game Description
|
||||||
|
_The Full Hue_ is a 2D platformer game based around the themes of color and emotion. The game begins completely gray. Each level is themed around a specific color and its corresponding meaning: orange is warmth and creativity, yellow is joy and anxiety, blue is depth, and so on. The goal is to collect the color fragments scattered across each levels platforms, restoring color to the world one hue at a time.
|
||||||
|
|
||||||
|
### How to play:
|
||||||
|
Move utilizing the arrow keys or WASD
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|--------|-----|
|
||||||
|
| `← →` or `A D` | Move left/right |
|
||||||
|
| `Space`, ` ↑`, or `w`| Jump|
|
||||||
|
|
||||||
|
**Collecting Fragments:** small glowing fragments are placed on the platforms aross each level, to collect them simply walk into them. As they are picked up a small quote appears at the top of the screen and the level's background is gradually tinted to its corresponding color.
|
||||||
|
|
||||||
|
**Avoid Hazards:** Enemies walk back and forth on platform. Tar puddles sit on the ground floor. Coming into contact with either costs the player a life, there are 3 lives per level. Taking damage triggers a short invincibility period with a blinking effect so that the player can recover and reset.
|
||||||
|
|
||||||
|
**Compleating the level:** You successfully complete a level by collecting every fragment at that stage. An overaly with a short reflection will be shown upon level compleation which will allow you to move to the next level.
|
||||||
|
|
||||||
|
**Game progess:** Levels unlock sequentially as they are completed. Compleating a level permanently unlock the next one, making it visible on the level select screen. Locked levels show a `?` symbol.
|
||||||
|
|
||||||
|
### After beating all levels
|
||||||
|
|
||||||
|
Once all 10 levels are compleated the game enters a free explore mode. The home screen changes to a new message inviting the player to go through the game again now in full color and without obstacles. Re-entering the game levels removes the tar puddles and enemies and the backgrounds are shown in full color. The game becomes a sort of gallery inviting the player to walk through each level again appreciating the art and reading the messages at a more leisurely pace.
|
||||||
|
|
||||||
|
# Code organization
|
||||||
|
|
||||||
|
### The file structure
|
||||||
|
|
||||||
|
My files are organized as illustrated below:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Game Screen
|
||||||
|
|
||||||
|
using `Game.svelte` I layer four compnents in a single `800x450` container. The organization is as follows:
|
||||||
|
|
||||||
|
| component | contents | z-index|
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| GameCanvas | p5 canvas | 0 |
|
||||||
|
| HUD overlay | lives and fragment info display | 10 |
|
||||||
|
| QuoteToast | quote displayed on fragment collection | 20 |
|
||||||
|
| LevelCompleteOverlay | Final level quote & next level button | 30 |
|
||||||
|
|
||||||
|
### How GameCanvas.svelte runs the game loop
|
||||||
|
|
||||||
|
The p5 `draw()` function is going at 60fps and doing the following each frame:
|
||||||
|
|
||||||
|
- Draw the background image
|
||||||
|
- `allSprites.update()` - checks for collisions & handles p5play physics
|
||||||
|
- updates the color tint overlay
|
||||||
|
- `fragment.drawGlow() and update()` - fragment animation and callback in case of collection
|
||||||
|
- `enemy.update()` and overlap check - enemy animation and updates lives in case of overlap
|
||||||
|
- tar overlap check - updates lives on overlap
|
||||||
|
- splat effect called when triggered
|
||||||
|
- fall-off-screen check for avatar
|
||||||
|
- `allSprites.draw()` - render all the sprites using p5play
|
||||||
|
|
||||||
|
### LevelData.js Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
bg,
|
||||||
|
bgcomplete,
|
||||||
|
playerImg,
|
||||||
|
spawnX,
|
||||||
|
spawnY,
|
||||||
|
platforms: [{x,y,w,h},...],
|
||||||
|
fragments: [{x,y,color},...],
|
||||||
|
enemies: [{x,y,patrol},...],
|
||||||
|
tar: [{x,y},...],
|
||||||
|
fragmentQuotes: [{'...','...'}],
|
||||||
|
completeQuote: '...',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`gameCanvas.setup()` reads this information and draws the game objects from it. This makes it easier to create new levels, change platform positions, text, etc. by only having to update the information on this file.
|
||||||
|
|
||||||
|
### Svelte stores (An observer pattern?) - shared states
|
||||||
|
|
||||||
|
stores essentially hold all the global variables and states. If the values are changed all files are able to see this and automatically re-render. We are able to do this with Svelte's `$store` sytanx.
|
||||||
|
|
||||||
|
colorStore.js
|
||||||
|
|
||||||
|
| Function | purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `lives` | player lives (0-3)|
|
||||||
|
| `colorOpacity` |
|
||||||
BIN
public/documentation/File organization.png
Normal file
BIN
public/documentation/File organization.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
@@ -19,7 +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 === 'Escape' || e.key === 'e' || e.key === 'E') { 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
|
||||||
|
|||||||
@@ -1,30 +1,249 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { push } from 'svelte-spa-router';
|
import { push } from 'svelte-spa-router';
|
||||||
import { gameCompleted } from '../stores/colorStore.js';
|
import { gameCompleted, hasSeenControls } from '../stores/colorStore.js';
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'#970505', '#CF8917', '#E3D214', '#39BD1C',
|
||||||
|
'#12B6C8', '#170CB7', '#6613BA', '#C71287',
|
||||||
|
'#753F16', '#FFD700',
|
||||||
|
];
|
||||||
|
|
||||||
|
let sparks = [];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
sparks = Array.from({ length: 140 }, () => ({
|
||||||
|
x: Math.random() * 100,
|
||||||
|
y: Math.random() * 100,
|
||||||
|
size: Math.random() * 3 + 1,
|
||||||
|
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||||
|
delay: -(Math.random() * 4),
|
||||||
|
duration: Math.random() * 2 + 1,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
let showControls = false;
|
||||||
|
const miniKeys = { left: false, right: false, jump: false };
|
||||||
|
|
||||||
function startGame() {
|
function startGame() {
|
||||||
|
if (!$gameCompleted && !$hasSeenControls) {
|
||||||
|
showControls = true;
|
||||||
|
} else {
|
||||||
|
push('/levelselect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goReady() {
|
||||||
|
hasSeenControls.set(true);
|
||||||
push('/levelselect');
|
push('/levelselect');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
if (!showControls) return;
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'a') { miniKeys.left = true; e.preventDefault(); }
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'd') { miniKeys.right = true; e.preventDefault(); }
|
||||||
|
if ((e.key === 'ArrowUp' || e.key === 'w' || e.key === ' ') && !e.repeat) {
|
||||||
|
miniKeys.jump = true;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (e) => {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'a') miniKeys.left = false;
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'd') miniKeys.right = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
miniKeys.left = false;
|
||||||
|
miniKeys.right = false;
|
||||||
|
miniKeys.jump = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
window.addEventListener('blur', onBlur);
|
||||||
|
|
||||||
|
function initMiniCanvas(node) {
|
||||||
|
const CANVAS_W = 680;
|
||||||
|
const CANVAS_H = 124;
|
||||||
|
const PLAT_H = 16;
|
||||||
|
|
||||||
|
const sketch = (p) => {
|
||||||
|
let playerSprite;
|
||||||
|
let playerImg;
|
||||||
|
let canJump = true;
|
||||||
|
let peakReached = false;
|
||||||
|
|
||||||
|
p.preload = () => {
|
||||||
|
playerImg = p.loadImage('/assets/player.png');
|
||||||
|
};
|
||||||
|
|
||||||
|
p.setup = () => {
|
||||||
|
const canvas = p.createCanvas(CANVAS_W, CANVAS_H);
|
||||||
|
canvas.parent(node);
|
||||||
|
p.world.gravity.y = 30;
|
||||||
|
|
||||||
|
// Ground platform
|
||||||
|
const plat = new p.Sprite(CANVAS_W / 2, CANVAS_H - PLAT_H / 2, CANVAS_W, PLAT_H);
|
||||||
|
plat.collider = 'static';
|
||||||
|
const g = p.createGraphics(CANVAS_W, PLAT_H);
|
||||||
|
g.clear();
|
||||||
|
g.noStroke();
|
||||||
|
g.fill('#888888');
|
||||||
|
g.rect(0, 0, CANVAS_W, PLAT_H, 4);
|
||||||
|
plat.img = g;
|
||||||
|
|
||||||
|
// Player centered above platform
|
||||||
|
playerSprite = new p.Sprite(CANVAS_W / 2, CANVAS_H - PLAT_H - 22, 28, 28);
|
||||||
|
playerSprite.rotationLock = true;
|
||||||
|
playerSprite.bounciness = 0;
|
||||||
|
if (playerImg) playerSprite.img = playerImg;
|
||||||
|
};
|
||||||
|
|
||||||
|
p.draw = () => {
|
||||||
|
p.background(18);
|
||||||
|
p.allSprites.update();
|
||||||
|
|
||||||
|
if (playerSprite) {
|
||||||
|
// Jump state machine — mirrors Player.js logic
|
||||||
|
const vy = playerSprite.vel.y;
|
||||||
|
if (vy > 2) peakReached = true;
|
||||||
|
if (peakReached && vy > -0.5 && vy < 1.5) {
|
||||||
|
canJump = true;
|
||||||
|
peakReached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal movement with friction
|
||||||
|
if (miniKeys.left) playerSprite.vel.x = -5;
|
||||||
|
if (miniKeys.right) playerSprite.vel.x = 5;
|
||||||
|
if (!miniKeys.left && !miniKeys.right) playerSprite.vel.x *= 0.78;
|
||||||
|
|
||||||
|
// Jump
|
||||||
|
if (miniKeys.jump && canJump) {
|
||||||
|
playerSprite.vel.y = -11;
|
||||||
|
canJump = false;
|
||||||
|
miniKeys.jump = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to canvas edges
|
||||||
|
if (playerSprite.x < 14) { playerSprite.x = 14; playerSprite.vel.x = 0; }
|
||||||
|
if (playerSprite.x > CANVAS_W - 14) { playerSprite.x = CANVAS_W - 14; playerSprite.vel.x = 0; }
|
||||||
|
|
||||||
|
// Safety respawn
|
||||||
|
if (playerSprite.y > CANVAS_H + 40) {
|
||||||
|
playerSprite.x = CANVAS_W / 2;
|
||||||
|
playerSprite.y = CANVAS_H - PLAT_H - 22;
|
||||||
|
playerSprite.vel.x = 0;
|
||||||
|
playerSprite.vel.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft white glow under the player
|
||||||
|
p.push();
|
||||||
|
p.noStroke();
|
||||||
|
for (let r = 38; r > 0; r -= 6) {
|
||||||
|
p.fill(255, 255, 255, (r / 38) * 18);
|
||||||
|
p.circle(playerSprite.x, playerSprite.y, r * 2);
|
||||||
|
}
|
||||||
|
p.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
p.allSprites.draw();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const inst = new p5(sketch);
|
||||||
|
return { destroy() { inst.remove(); } };
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
window.removeEventListener('blur', onBlur);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title-screen">
|
<div class="title-screen">
|
||||||
|
|
||||||
{#if $gameCompleted}
|
{#if $gameCompleted}
|
||||||
|
|
||||||
|
<!-- ── NG+ sparkle layer ── -->
|
||||||
|
<div class="sparkfield" aria-hidden="true">
|
||||||
|
{#each sparks as s}
|
||||||
|
<span
|
||||||
|
class="spark"
|
||||||
|
style="
|
||||||
|
left: {s.x}%;
|
||||||
|
top: {s.y}%;
|
||||||
|
width: {s.size}px;
|
||||||
|
height: {s.size}px;
|
||||||
|
background: {s.color};
|
||||||
|
box-shadow: 0 0 {s.size * 2}px {s.color};
|
||||||
|
animation-duration: {s.duration}s;
|
||||||
|
animation-delay: {s.delay}s;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── NG+ home screen ── -->
|
<!-- ── NG+ home screen ── -->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1 class="rainbow-title">The Full Hue</h1>
|
<h1 class="rainbow-title">The Full Hue</h1>
|
||||||
<p class="ng-lead">you've brought all the color back.</p>
|
<p class="ng-lead">you've brought all the color back.</p>
|
||||||
<p class="ng-body">
|
<p class="ng-body">
|
||||||
go through the world again — now in full color and without sorrows.<br>
|
go through the world again, now in full color and without sorrows.<br>
|
||||||
take your time. view the scenery. learn about your hues.
|
take your time. view the scenery. learn about your hues.
|
||||||
</p>
|
</p>
|
||||||
<button on:click={startGame}>go again</button>
|
<button on:click={startGame}>go again</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{:else if showControls}
|
||||||
|
|
||||||
|
<!-- ── Controls screen ── -->
|
||||||
|
<div class="content controls-content">
|
||||||
|
<h2>how to play</h2>
|
||||||
|
<p class="ctrl-sub">a short guide before your journey begins</p>
|
||||||
|
|
||||||
|
<div class="key-table">
|
||||||
|
<div class="key-row">
|
||||||
|
<div class="keys">
|
||||||
|
<span class="key">←</span>
|
||||||
|
<span class="key">→</span>
|
||||||
|
<span class="or">or</span>
|
||||||
|
<span class="key">A</span>
|
||||||
|
<span class="key">D</span>
|
||||||
|
</div>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span class="key-desc">move left / right</span>
|
||||||
|
</div>
|
||||||
|
<div class="key-row">
|
||||||
|
<div class="keys">
|
||||||
|
<span class="key">↑</span>
|
||||||
|
<span class="or">or</span>
|
||||||
|
<span class="key">W</span>
|
||||||
|
<span class="or">or</span>
|
||||||
|
<span class="key wide">SPACE</span>
|
||||||
|
</div>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span class="key-desc">jump</span>
|
||||||
|
</div>
|
||||||
|
<div class="key-row">
|
||||||
|
<div class="keys">
|
||||||
|
<span class="key">E</span>
|
||||||
|
</div>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span class="key-desc">open level select</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="try-label">try it out ↓</p>
|
||||||
|
<div class="mini-canvas-wrap" use:initMiniCanvas></div>
|
||||||
|
|
||||||
|
<button on:click={goReady}>i'm ready →</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
|
|
||||||
<!-- ── original home screen ── -->
|
<!-- ── Original home screen ── -->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>The Full Hue</h1>
|
<h1>The Full Hue</h1>
|
||||||
<p class="tagline">Bring back your color</p>
|
<p class="tagline">Bring back your color</p>
|
||||||
@@ -126,4 +345,126 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:hover { background: rgba(255, 255, 255, 0.3); }
|
button:hover { background: rgba(255, 255, 255, 0.3); }
|
||||||
|
|
||||||
|
/* ── NG+ sparkle layer ── */
|
||||||
|
.sparkfield {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spark {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: sparkle linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.05; transform: scale(0.4); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── controls screen ── */
|
||||||
|
.controls-content {
|
||||||
|
gap: 9px;
|
||||||
|
padding: 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-sub {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.38);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
margin: 4px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 218px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 28px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.28);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key.wide {
|
||||||
|
min-width: 62px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.or {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.22);
|
||||||
|
padding: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
color: rgba(255, 255, 255, 0.18);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-desc {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.try-label {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.22);
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-canvas-wrap {
|
||||||
|
width: 680px;
|
||||||
|
height: 124px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* controls-content button gets less top margin */
|
||||||
|
.controls-content button {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 10px 36px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { push } from 'svelte-spa-router';
|
import { push } from 'svelte-spa-router';
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'#970505', '#CF8917', '#E3D214', '#39BD1C',
|
||||||
|
'#12B6C8', '#170CB7', '#6613BA', '#C71287',
|
||||||
|
'#753F16', '#FFD700',
|
||||||
|
];
|
||||||
|
|
||||||
|
let sparks = [];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
sparks = Array.from({ length: 140 }, () => ({
|
||||||
|
x: Math.random() * 100,
|
||||||
|
y: Math.random() * 100,
|
||||||
|
size: Math.random() * 3 + 1,
|
||||||
|
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||||
|
delay: -(Math.random() * 4),
|
||||||
|
duration: Math.random() * 2 + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const timer = setTimeout(() => push('/'), 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="screen">
|
<div class="screen">
|
||||||
|
<div class="sparkfield" aria-hidden="true">
|
||||||
|
{#each sparks as s}
|
||||||
|
<span
|
||||||
|
class="spark"
|
||||||
|
style="
|
||||||
|
left: {s.x}%;
|
||||||
|
top: {s.y}%;
|
||||||
|
width: {s.size}px;
|
||||||
|
height: {s.size}px;
|
||||||
|
background: {s.color};
|
||||||
|
box-shadow: 0 0 {s.size * 2}px {s.color};
|
||||||
|
animation-duration: {s.duration}s;
|
||||||
|
animation-delay: {s.delay}s;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="title">color restored</h1>
|
<h1 class="title">color restored</h1>
|
||||||
<p class="line1">every fragment found. every hue returned </p>
|
<p class="line1">every fragment found. every hue returned</p>
|
||||||
<p class="line2">Go forth and experience the world in full color.</p>
|
<p class="line2">Go forth and experience the world in all its colors.</p>
|
||||||
<button on:click={() => push('/')}>back to home</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -20,6 +60,25 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparkfield {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spark {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: sparkle linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.05; transform: scale(0.4); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -36,6 +95,8 @@
|
|||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
animation: shimmer 4s linear infinite;
|
animation: shimmer 4s linear infinite;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
@@ -48,6 +109,8 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line2 {
|
.line2 {
|
||||||
@@ -56,22 +119,7 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
}
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
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>
|
</style>
|
||||||
|
|||||||
77
src/routes/Win.svelte.bak
Normal file
77
src/routes/Win.svelte.bak
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 </p>
|
||||||
|
<p class="line2">Go forth and experience the world in full color.</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>
|
||||||
@@ -18,6 +18,9 @@ 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([]);
|
||||||
|
|
||||||
|
// true once the player has seen the controls screen (skip it on future visits)
|
||||||
|
export const hasSeenControls = writable(false);
|
||||||
|
|
||||||
// the current level number
|
// the current level number
|
||||||
export const currentLevel = writable(1);
|
export const currentLevel = writable(1);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user