feat: apply pixel-art design assets across sprites, tiles, hud, and scenes

This commit is contained in:
tiennm99 committed 2026-04-20 22:03:16 +07:00
1 parent 6a1b3ec572
commit fb2f72f85b
14 files changed
+1207 -59

No files matched your search

+37 -12
View File
@@ -1,4 +1,13 @@
<script>
import Pixel from '../lib/pixel/Pixel.svelte';
import {
TILE_EMPTY, TILE_EMPTY_PAL,
TILE_WALL, TILE_WALL_PAL,
TILE_GOAL, TILE_GOAL_PAL,
TILE_LIT, TILE_LIT_PAL,
TILE_PREVIEW, TILE_PREVIEW_PAL,
} from '../lib/pixel/art-tiles.js';
let { cells = [], rows = 6, cols = 6, cellSize = 50, previewCells = new Set(),
detectedCell = null, oncellclick } = $props();
@@ -13,6 +22,14 @@
function isDetected(cell) {
return detectedCell && detectedCell.row === cell.row && detectedCell.col === cell.col;
}
// Resolve the tile art/palette for a cell. Priority: lit > goal > wall > empty.
function tileFor(cell) {
if (cell.isLight) return { art: TILE_LIT, pal: TILE_LIT_PAL };
if (cell.isGoal) return { art: TILE_GOAL, pal: TILE_GOAL_PAL };
if (cell.isWall) return { art: TILE_WALL, pal: TILE_WALL_PAL };
return { art: TILE_EMPTY, pal: TILE_EMPTY_PAL };
}
</script>
<div
@@ -22,18 +39,25 @@
style="grid-template-columns: repeat({cols}, {cellSize}px); grid-template-rows: repeat({rows}, {cellSize}px);"
>
{#each cells as cell (cell.row * cols + cell.col)}
{@const tile = tileFor(cell)}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions a11y_interactive_supports_focus -->
<div
tabindex="-1"
class="cell"
class:wall={cell.isWall}
class:goal={cell.isGoal}
class:lit={cell.isLight}
class:preview={previewCells.has(`${cell.row},${cell.col}`) && !cell.isLight}
class:detected-flash={isDetected(cell)}
role="gridcell"
aria-label={cellLabel(cell)}
onclick={() => oncellclick?.(cell.row, cell.col)}
></div>
>
<Pixel art={tile.art} palette={tile.pal} width={cellSize} height={cellSize} />
{#if previewCells.has(`${cell.row},${cell.col}`) && !cell.isLight}
<div class="preview-overlay">
<Pixel art={TILE_PREVIEW} palette={TILE_PREVIEW_PAL} width={cellSize} height={cellSize} />
</div>
{/if}
</div>
{/each}
</div>
@@ -44,22 +68,23 @@
position: relative;
}
.cell {
position: relative;
width: 100%;
height: 100%;
background: var(--grid-empty);
border: 1px solid var(--grid-border);
cursor: pointer;
transition: background 0.2s ease;
}
.cell.wall { background: var(--grid-wall); cursor: default; }
.cell.goal { background: var(--grid-goal); }
.cell.lit { background: var(--grid-lit); }
.cell.preview { background: rgba(255, 234, 0, 0.15); border-color: rgba(255, 234, 0, 0.3); }
.cell.wall { cursor: default; }
.preview-overlay {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.7;
}
.cell.detected-flash {
animation: cell-flash 0.4s ease-out;
}
@keyframes cell-flash {
0% { background: #ff0000; }
100% { background: var(--grid-lit); }
0% { filter: brightness(2) hue-rotate(-60deg); }
100% { filter: none; }
}
</style>
+41 -5
View File
@@ -2,6 +2,9 @@
import { getText } from '../lib/localization.js';
import { isMuted, setMuted, playClick } from '../lib/audio.js';
import Button from './Button.svelte';
import Pixel from '../lib/pixel/Pixel.svelte';
import { HEART_ART, HEART_ART_EMPTY, HEART_PAL,
ICON_UNDO, ICON_EYE, ICON_PAUSE, ICON_PAL } from '../lib/pixel/art-ui.js';
let { lives = 3, level = 1, turns = 0, showPreview = false, canUndo = false,
ontogglepreview, onpause, onmenu, onundo, onshowcontrols } = $props();
let muted = $state(isMuted());
@@ -11,20 +14,33 @@
setMuted(muted);
if (!muted) playClick();
}
const MAX_LIVES = 3;
let hearts = $derived(Array.from({ length: MAX_LIVES }, (_, i) => i < lives));
</script>
<div class="hud">
<div class="hud-left">
<span>{getText('lives')}{lives}</span>
<div class="lives" aria-label="{getText('lives')}{lives}">
{#each hearts as full}
<Pixel art={full ? HEART_ART : HEART_ART_EMPTY} palette={HEART_PAL} width={20} height={20} />
{/each}
</div>
<span class="turns">Turns: {turns}</span>
</div>
<div class="hud-right">
<span>{getText('level')}{level}</span>
<Button text={canUndo ? 'Z' : '-'} onclick={onundo} small disabled={!canUndo} />
<span class="level-label">{getText('level')}{level}</span>
<button class="icon-btn" onclick={onundo} disabled={!canUndo} aria-label="Undo">
<Pixel art={ICON_UNDO} palette={ICON_PAL} width={20} height={20} />
</button>
<Button text={muted ? 'MUTE' : 'SND'} onclick={toggleMute} small />
<Button text={showPreview ? 'V:ON' : 'V:OFF'} onclick={ontogglepreview} small />
<button class="icon-btn" class:active={showPreview} onclick={ontogglepreview} aria-label="Toggle preview">
<Pixel art={ICON_EYE} palette={ICON_PAL} width={20} height={20} />
</button>
<Button text="?" onclick={onshowcontrols} small />
<Button text={getText('pause')} onclick={onpause} small />
<button class="icon-btn" onclick={onpause} aria-label={getText('pause')}>
<Pixel art={ICON_PAUSE} palette={ICON_PAL} width={20} height={20} />
</button>
<Button text={getText('menu')} onclick={onmenu} small />
</div>
</div>
@@ -43,5 +59,25 @@
align-items: center;
gap: 12px;
}
.lives {
display: flex;
gap: 2px;
align-items: center;
}
.turns { color: var(--text-secondary); font: var(--font-small); }
.level-label { font: var(--font-ui); color: var(--text-primary); }
.icon-btn {
background: var(--btn-default);
border: 2px solid var(--btn-border);
border-radius: 4px;
padding: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
.icon-btn:hover:not(:disabled) { background: var(--btn-hover); }
.icon-btn:disabled { opacity: 0.4; cursor: default; }
.icon-btn.active { background: var(--btn-hover); border-color: var(--text-accent); }
</style>
+25 -15
View File
@@ -1,10 +1,19 @@
<script>
import Pixel from '../lib/pixel/Pixel.svelte';
import { GUARD_SPRITES } from '../lib/pixel/art-characters.js';
let { guard, cellSize = 50 } = $props();
let top = $derived(guard.row * cellSize + cellSize / 2);
let left = $derived(guard.col * cellSize + cellSize / 2);
let size = $derived(Math.floor(cellSize / 2));
let size = $derived(Math.floor(cellSize * 0.95));
let rotation = $derived(guard.direction * 90);
let hasDirection = $derived(guard.type === 'rotating' || guard.type === 'patrolling' || guard.type === 'chaser');
let hasDirection = $derived(
guard.type === 'rotating' || guard.type === 'patrolling' || guard.type === 'chaser'
);
let sprite = $derived(GUARD_SPRITES[guard.type]);
let activePal = $derived(
guard.type === 'blinking' && !guard.isOn ? sprite.palOff : sprite.pal
);
</script>
<div
@@ -13,6 +22,9 @@
class:alert={guard.type === 'chaser' && guard.isChasing}
style="top: {top}px; left: {left}px; width: {size}px; height: {size}px;"
>
{#if sprite}
<Pixel art={sprite.art} palette={activePal} width={size} height={size} />
{/if}
{#if hasDirection}
<div class="direction-indicator" style="transform: rotate({rotation}deg);"></div>
{/if}
@@ -21,28 +33,26 @@
<style>
.guard {
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%);
transition: top 100ms linear, left 100ms linear;
z-index: 5;
pointer-events: none;
}
.guard.static { background: var(--guard-static); }
.guard.rotating { background: var(--guard-rotating); }
.guard.blinking { background: var(--guard-blinking); }
.guard.blinking.off { background: var(--guard-blinking-off); }
.guard.patrolling { background: var(--guard-patrolling); }
.guard.chaser { background: var(--guard-chaser, #ff6622); }
.guard.chaser.alert { background: #ff2200; box-shadow: 0 0 8px #ff2200; }
.guard.mirror { background: var(--guard-mirror); border-radius: 4px; transform: translate(-50%, -50%) rotate(45deg); }
.guard.alert {
filter: drop-shadow(0 0 6px #ff2200);
}
.guard.off {
opacity: 0.85;
}
.direction-indicator {
position: absolute;
top: 0;
top: 6%;
left: 50%;
width: 2px;
height: 50%;
background: white;
width: 3px;
height: 40%;
background: var(--cream, #fff4d6);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
transform-origin: bottom center;
translate: -50% 0;
}
+6 -4
View File
@@ -1,21 +1,23 @@
<script>
import Pixel from '../lib/pixel/Pixel.svelte';
import { RABBIT_ART, RABBIT_PAL } from '../lib/pixel/art-characters.js';
let { row = 0, col = 0, cellSize = 50, shake = false } = $props();
let top = $derived(row * cellSize + cellSize / 2);
let left = $derived(col * cellSize + cellSize / 2);
let size = $derived(Math.floor(cellSize / 1.5));
let size = $derived(Math.floor(cellSize * 0.9));
</script>
<div
class="player"
class:shake
style="top: {top}px; left: {left}px; width: {size}px; height: {size}px;"
></div>
>
<Pixel art={RABBIT_ART} palette={RABBIT_PAL} width={size} height={size} />
</div>
<style>
.player {
position: absolute;
background: var(--player-color);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: top 100ms linear, left 100ms linear;
z-index: 10;
+27 -6
View File
@@ -2,6 +2,8 @@
import { fade } from 'svelte/transition';
import { getText } from '../lib/localization.js';
import Button from './Button.svelte';
import Pixel from '../lib/pixel/Pixel.svelte';
import { ICON_UNDO, ICON_REDO, ICON_EYE, ICON_ARROW, ICON_PAL } from '../lib/pixel/art-ui.js';
let { onclose } = $props();
</script>
@@ -9,11 +11,25 @@
<div class="popup">
<h2>{getText('controlsTitle')}</h2>
<div class="controls-list">
<div class="control-row"><kbd>Arrow Keys</kbd><span>/ WASD — {getText('controlMove')}</span></div>
<div class="control-row"><kbd>Space</kbd><span>— {getText('controlWait')}</span></div>
<div class="control-row"><kbd>V</kbd><span>— {getText('controlPreview')}</span></div>
<div class="control-row"><kbd>Z</kbd><span>— {getText('controlUndo')}</span></div>
<div class="control-row"><kbd>Y</kbd><span>— {getText('controlRedo')}</span></div>
<div class="control-row">
<span class="icon"><Pixel art={ICON_ARROW} palette={ICON_PAL} width={20} height={20} /></span>
<kbd>Arrow Keys</kbd><span>/ WASD — {getText('controlMove')}</span>
</div>
<div class="control-row">
<kbd>Space</kbd><span>— {getText('controlWait')}</span>
</div>
<div class="control-row">
<span class="icon"><Pixel art={ICON_EYE} palette={ICON_PAL} width={20} height={20} /></span>
<kbd>V</kbd><span>— {getText('controlPreview')}</span>
</div>
<div class="control-row">
<span class="icon"><Pixel art={ICON_UNDO} palette={ICON_PAL} width={20} height={20} /></span>
<kbd>Z</kbd><span>— {getText('controlUndo')}</span>
</div>
<div class="control-row">
<span class="icon"><Pixel art={ICON_REDO} palette={ICON_PAL} width={20} height={20} /></span>
<kbd>Y</kbd><span>— {getText('controlRedo')}</span>
</div>
<div class="control-row"><kbd>Click</kbd><span>/ Tap — {getText('controlTap')}</span></div>
<div class="control-row"><kbd>Swipe</kbd><span>— {getText('controlSwipe')}</span></div>
</div>
@@ -40,7 +56,7 @@
background: var(--bg-panel);
padding: 24px 32px;
border-radius: 8px;
max-width: 360px;
max-width: 400px;
}
h2 {
font: var(--font-heading);
@@ -60,6 +76,11 @@
font: var(--font-small);
color: var(--text-secondary);
}
.icon {
display: inline-flex;
width: 24px;
justify-content: center;
}
kbd {
background: var(--btn-default);
border: 1px solid var(--btn-border);
+44
View File
@@ -0,0 +1,44 @@
<script>
// Pixel-art renderer — strings+palette → SVG <rect>s with run-length merge.
// `art`: array of equal-length strings. `palette`: { [char]: hexColor }. '.' / ' ' = transparent.
let { art, palette, scale = 6, bg = null, width = null, height = null, pixelated = true, style = '' } = $props();
let rows = $derived(art?.length ?? 0);
let cols = $derived(art?.[0]?.length ?? 0);
let rects = $derived.by(() => {
if (!art || !palette) return [];
const out = [];
for (let r = 0; r < rows; r++) {
let c = 0;
const row = art[r];
while (c < cols) {
const ch = row[c];
if (ch === '.' || ch === ' ') { c++; continue; }
let run = 1;
while (c + run < cols && row[c + run] === ch) run++;
const color = palette[ch];
if (color) out.push({ x: c, y: r, w: run, color });
c += run;
}
}
return out;
});
let w = $derived(width ?? cols * scale);
let h = $derived(height ?? rows * scale);
</script>
<svg
width={w}
height={h}
viewBox="0 0 {cols} {rows}"
shape-rendering="crispEdges"
style="display:block; image-rendering: {pixelated ? 'pixelated' : 'auto'}; {style}"
preserveAspectRatio="none"
>
{#if bg}<rect x="0" y="0" width={cols} height={rows} fill={bg} />{/if}
{#each rects as r}
<rect x={r.x} y={r.y} width={r.w} height="1" fill={r.color} />
{/each}
</svg>
+330
View File
@@ -0,0 +1,330 @@
// 32×32 pixel-art sprites for player, guards (veggies), and princess.
// Primary colors mirror theme.css guard colors for gameplay readability.
import { NNTV } from './palette.js';
export const RABBIT_ART = [
'................................',
'................................',
'.......KK........KKKK...........',
'......KFFK......KFFFFK..........',
'......KFFK......KFFFFK..........',
'......KFCK......KFFCFK..........',
'......KFCK......KFFCFK..........',
'.......KFK......KFFFK...........',
'.......KKKKKKKKKFFFFK...........',
'......KFFFFFFFFFFFFFFK..........',
'.....KFFFFFFFFFFFFFFFFK.........',
'....KFSSSSSSSSSSSSSSSSFK........',
'...KFSSSSSSSSSSSSSSSSSSFK.......',
'...KFSKKKKSSSSSSSSKKKKSFK.......',
'...KFSKWWKSSSSSSSSKWWKSFK.......',
'...KFSKWEKSSSSSSSSKWEKSFK.......',
'...KFSKKKKSSSSSSSSKKKKSFK.......',
'...KFSSSSSSSKRRKSSSSSSSFK.......',
'...KFSSSSSSKRRRRKSSSSSSFK.......',
'....KFSSSSSSSRRSSSSSSSFK........',
'.....KFSSSSSSSSSSSSSSFK.........',
'......KFFSSSSSSSSSSFFK..........',
'.......KKFFSSSSSSFFKK...........',
'.........KFKKKKKKFK.............',
'.........KFK....KFK.............',
'........KFFK....KFFK............',
'.......KFMMK....KMMFK...........',
'.......KMMMK....KMMMK...........',
'........KKK......KKK............',
'................................',
'................................',
'................................',
];
export const RABBIT_PAL = {
K: NNTV.ink, F: NNTV.furLight, C: NNTV.furMid,
S: NNTV.cloth, W: NNTV.eyeShine, E: NNTV.ink,
R: NNTV.scarfRed, M: NNTV.furMid,
};
export const TOMATO_ART = [
'................................',
'................................',
'................................',
'..............GGG...............',
'.............GLLLG..............',
'............GLDDLG..............',
'...........GLDLLDLG.............',
'..........GGLLLLLLGG............',
'.........KKKKKKKKKKKK...........',
'.......KKRRRRRRRRRRRRKK.........',
'......KRRHHHHRRRRHHHHRRK........',
'.....KRHHHHHHRRRRHHHHHRRK.......',
'....KRRHWWHHRRRRRRHWWHRRRK......',
'....KRRHWWHHRRRRRRHWWHRRRK......',
'...KRRRHHHHRRRRRRRRHHHHRRRK.....',
'...KRRRRRRRRRRRDDRRRRRRRRRRK....',
'...KRRRRRRRRRRDDDDRRRRRRRRRK....',
'...KRRRRRRRRRRRRRRRRRRRRRRRK....',
'...KRRRRHHRRRRRRRRRRRRHHRRRK....',
'....KRRRHHHRRRRRRRRRRHHHRRK.....',
'....KRRRRHHHHRRRRRRHHHHRRRK.....',
'.....KRRRRRHHHHHHHHHHRRRRRK.....',
'......KRRRRRRRRRRRRRRRRRRK......',
'.......KKRRRRRRRRRRRRRRKK.......',
'.........KKRRRRRRRRRRKK.........',
'...........KKKKKKKKKK...........',
'................................',
'................................',
'................................',
'................................',
'................................',
'................................',
];
export const TOMATO_PAL = {
K: NNTV.ink, R: NNTV.tomatoRed, H: NNTV.tomatoDark,
W: NNTV.eyeShine, D: NNTV.cream, G: NNTV.leafGreen, L: NNTV.leafDark,
};
export const BLUEBERRY_ART = [
'................................',
'................................',
'................................',
'..............GGG...............',
'.............GGSG...............',
'..............GGG...............',
'.........KKKKKKKKKKKK...........',
'.......KKBBBBBBBBBBBBKK.........',
'......KBBBLLLBBBBLLLBBBK........',
'.....KBBLLLLBBBBBBLLLLBBK.......',
'....KBBBLLLLBBBBBBBLLLLBBBK.....',
'....KBBBBBBBBBBBBBBBBBBBBBK.....',
'....KBBBBBBBBBBBBBBBBBBBBBK.....',
'...KBBBBHHHBBBBBBBBBHHHBBBBK....',
'...KBBBHWWHBBBBBBBBHWWHBBBBK....',
'...KBBBHWWHBBBBBBBBHWWHBBBBK....',
'...KBBBBHHHBBBBBBBBBHHHBBBBK....',
'...KBBBBBBBBBBBBBBBBBBBBBBBK....',
'...KBBBBBBBBBBBBBBBBBBBBBBBK....',
'...KBBBBBBBBBBNNNBBBBBBBBBBK....',
'....KBBBBBBBBBNNNBBBBBBBBBK.....',
'....KBBBBBBBBBBNBBBBBBBBBBK.....',
'....KBBBBBBBBBBBBBBBBBBBBBK.....',
'.....KBBBBBBBBBBBBBBBBBBBK......',
'......KBBBBBBBBBBBBBBBBBK.......',
'.......KKBBBBBBBBBBBBBKK........',
'.........KKBBBBBBBBBKK..........',
'...........KKKKKKKKK............',
'................................',
'................................',
'................................',
'................................',
];
export const BLUEBERRY_PAL = {
K: NNTV.ink, B: NNTV.guardRotating, L: NNTV.cream,
H: NNTV.blueberryDark, W: NNTV.eyeShine, N: NNTV.cream,
G: NNTV.leafGreen, S: NNTV.leafDark,
};
export const CORN_ART = [
'................................',
'................................',
'...........GGG....GGG...........',
'..........GHHHG..GHHHG..........',
'..........GHHHGGGHHHG...........',
'...........GHHHGHHHG............',
'............KGGKGGK.............',
'..........KKCCCCCCCCKK..........',
'........KKCCYCCYCCYCCKK.........',
'........KCYYCCYCCYYCCYCK........',
'.......KCCYYCCYCCYYCCYCCK.......',
'.......KCYYYYCCYCCYYYYYCK.......',
'.......KCCHHCCYYCCCHHCCCCK......',
'.......KCHWWHCCYCCHWWHCCCK......',
'.......KCHWWHCCYCCHWWHCCCK......',
'.......KCCHHCCYCCCHHCCCCCK......',
'.......KCYYYYCCYCCYYYYCCCK......',
'.......KCCYYCCYCCCCYCCYCCK......',
'.......KCYYYCYYCYCYYCCYYCK......',
'.......KCCYCCYCCCYCCYYYCCK......',
'........KCYYYNNNNCYYYCCCK.......',
'........KCCYCNNNNCCYCCYCK.......',
'........KCYYCCNNCCCYCYYCK.......',
'........KCCYYCCCCYYYCCCCK.......',
'.........KCCYYYYYCYYYCCK........',
'.........KKCCCCCCCCCCCKK........',
'...........KKCCCCCCCKK..........',
'.............KKKKKKKK...........',
'................................',
'................................',
'................................',
'................................',
];
export const CORN_PAL = {
K: NNTV.ink, C: NNTV.guardBlinking, Y: NNTV.cornDark,
H: NNTV.cornDark, W: NNTV.eyeShine, N: NNTV.ink, G: NNTV.cornHusk,
};
export const CORN_OFF_PAL = { ...CORN_PAL, C: NNTV.guardBlinkingOff, Y: '#5a4410' };
export const EGGPLANT_ART = [
'................................',
'................................',
'..............GG................',
'.............GLLG...............',
'............GLLLG...............',
'...........GLLLLG...............',
'...........KLLLLK...............',
'..........KKKLLKKK..............',
'.........KKPPPPPPKK.............',
'........KPPHHPPPPPPK............',
'.......KPPPHPPPPPHPPK...........',
'......KPPPPPPPPPPPPPPK..........',
'......KPPPPPPPPPPPPPPK..........',
'.....KPPHHPPPPPPPPHHPPK.........',
'.....KPHWWHPPPPPPHWWHPK.........',
'.....KPHWWHPPPPPPHWWHPK.........',
'.....KPPHHPPPPPPPPHHPPK.........',
'.....KPPPPPPPPPPPPPPPPK.........',
'.....KPPPPPPPPPPPPPPPPK.........',
'......KPPPPPPNNNNPPPPK..........',
'......KPPPPPPNNNNPPPPK..........',
'......KPPPPPPPPPPPPPPK..........',
'.......KPPPPPPPPPPPPK...........',
'.......KPPPPPPPPPPPPK...........',
'........KPPPPPPPPPPK............',
'........KKPPPPPPPPKK............',
'.........KKPPPPPPKK.............',
'..........KKPPPPKK..............',
'...........KKKKKK...............',
'................................',
'................................',
'................................',
];
export const EGGPLANT_PAL = {
K: NNTV.ink, P: NNTV.guardPatrolling, H: NNTV.eggplantDark,
W: NNTV.eyeShine, N: NNTV.cream, G: NNTV.leafGreen, L: NNTV.leafDark,
};
export const LETTUCE_ART = [
'................................',
'................................',
'................................',
'..............KKK...............',
'............KKMMMKK.............',
'..........KKMMMMMMMKK...........',
'.........KMMMMLLLMMMMK..........',
'........KMMLLLLLLLLMMMK.........',
'.......KMMLLLLLLLLLLLMMK........',
'......KMMLLLMMMMMMMLLLMMK.......',
'.....KMMLLMMLLLLLLLMMLLMMK......',
'....KMMLLMLLLLLLLLLLLMLLMMK.....',
'....KMMLMLLLHHLLLLHHLLMLMMK.....',
'...KMMLLMLHWWHLLHWWHLMLLMMK.....',
'...KMLLMMLHWWHLLHWWHLMMLLMK.....',
'...KMLLMMLLHHLLLLHHLLMMLLMK.....',
'....KMMLMLLLLLLLLLLLLMLMMK......',
'....KMMLLMLLLNNNNNLLLMLLMMK.....',
'.....KMMLLMLLNNNNNLLMLLMMK......',
'......KMMLLMMLLLLLMMLLMMK.......',
'.......KMMLLLMMMMMMMLLMMK.......',
'........KMMLLLLLLLLLLMMK........',
'.........KMMLLLLLLLLMMK.........',
'..........KKMMMMMMMMKK..........',
'............KKMMMMKK............',
'..............KKKK..............',
'................................',
'................................',
'................................',
'................................',
'................................',
'................................',
];
export const LETTUCE_PAL = {
K: NNTV.ink, L: NNTV.guardMirror, M: NNTV.lettuceDark,
H: NNTV.lettuceDark, W: NNTV.eyeShine, N: NNTV.cream,
};
export const PUMPKIN_ART = [
'................................',
'................................',
'................................',
'..............SS................',
'.............SSSS...............',
'.............SSSS...............',
'...........GGGSSGGG.............',
'...........KGGSSGGK.............',
'.........KKPPKKKKPPKK...........',
'........KPPOOPPPPOOPPK..........',
'.......KPPOOOPPPPOOOPPK.........',
'......KPPOOOPPPPPPOOOPPK........',
'.....KPOOOPPPPPPPPPPOOOPK.......',
'.....KPOOPPPPPPPPPPPPOOPK.......',
'....KPPOOPHHPPPPPPHHPPOOPPK.....',
'....KPOOPPHWHPPPPPHWHPPOOOPK....',
'....KPOOPPHWHPPPPPHWHPPOOOPK....',
'....KPOOPPPHHPPPPPPHHPPPOOPK....',
'....KPOOPPPPPPPPPPPPPPPPOOPK....',
'....KPOOPPPPKKPPPPKKPPPPOOPK....',
'....KPPOOPPKNNKPPKNNKPPPOOPK....',
'.....KPOOPKNNNNKKNNNNKPPOOPK....',
'.....KPPOOPKNNNNNNNNKPPOOPPK....',
'......KPOOOPKKNNNNKKPOOOPK......',
'.......KPOOOOPPPPPPOOOOPK.......',
'........KPPOOOOOOOOOOPPK........',
'.........KKPPOOOOOOPPKK.........',
'...........KKPPPPPPKK...........',
'.............KKKKKK.............',
'................................',
'................................',
'................................',
];
export const PUMPKIN_PAL = {
K: NNTV.ink, P: NNTV.guardChaser, O: NNTV.pumpkinDark,
H: NNTV.eyeRed, W: NNTV.cream, N: NNTV.ink,
G: NNTV.leafGreen, S: NNTV.leafDark,
};
export const PRINCESS_ART = [
'................................',
'................................',
'............SSSSS...............',
'...........SLLLLLS..............',
'..........SLLHLLLLS.............',
'..........SLLLHLLLS.............',
'.........SSLLLLHLLSS............',
'........KKKGGGGGGGKK............',
'.......KYYYYCCCCCYYYK...........',
'......KYYCCCDCCDCCCYYK..........',
'......KYCCCCCDCDCCCCYK..........',
'......KYCCCCCCCCCCCCYK..........',
'......KYCCHHCCCCCHHCYK..........',
'......KYCHWWHCCCHWWHCK..........',
'......KYCHWEHCCCHWEHCK..........',
'......KYCCHHCCCCCHHCCK..........',
'......KYCCCCCCKKCCCCCK..........',
'......KYCCCCCKPPKCCCCK..........',
'......KYCCCCCKKKKCCCCK..........',
'......KYCCCCCCCCCCCCCK..........',
'.......KYCCCCCCCCCCCK...........',
'........KYCCCCCCCCCK............',
'.........KYCCCCCCCK.............',
'..........KYCCCCCK..............',
'...........KYCCCK...............',
'............KYCK................',
'.............KK.................',
'................................',
'................................',
'................................',
'................................',
'................................',
];
export const PRINCESS_PAL = {
K: NNTV.ink, C: NNTV.carrot, D: NNTV.cream, H: NNTV.carrotDark,
W: NNTV.eyeShine, E: NNTV.ink, P: NNTV.scarfRed,
Y: NNTV.carrotDark, L: NNTV.leafGreen, S: NNTV.leafDark, G: NNTV.gold,
};
// Map game guard types → sprite data. Blinking uses on/off variants.
export const GUARD_SPRITES = {
static: { art: TOMATO_ART, pal: TOMATO_PAL },
rotating: { art: BLUEBERRY_ART, pal: BLUEBERRY_PAL },
blinking: { art: CORN_ART, pal: CORN_PAL, palOff: CORN_OFF_PAL },
patrolling: { art: EGGPLANT_ART, pal: EGGPLANT_PAL },
mirror: { art: LETTUCE_ART, pal: LETTUCE_PAL },
chaser: { art: PUMPKIN_ART, pal: PUMPKIN_PAL },
};
+214
View File
@@ -0,0 +1,214 @@
// 80×N pixel-art backdrops for each act (Levels 1-2, 3-4, 5-6, 7-8, 9-11, 12).
import { NNTV } from './palette.js';
// Normalize helper — ensure every row is exactly 80 chars.
const norm = (rows) => rows.map(r => (r.length >= 80 ? r.slice(0, 80) : r.padEnd(80, r.at(-1) || '.')));
// ── ACT 1: GARDEN ──────────────────────────────────────────────────────
export const BG_GARDEN = norm([
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmm........smmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmm....OOOOO....smmmmmmmmmmmmmmmmmmmmmm..s..s...mmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmm...OO..........OOmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmm..OO............OOmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmm..O..............Omm.s.mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmm..O..............Ommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmm..OO..........OOmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmm..OOOOOOOOOOOOmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmm........smmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'..LL......LL......LL......LL......LL......LL......LL......LL......LL......LL...',
'.LLLL....LLLL....LLLL....LLLL....LLLL....LLLL....LLLL....LLLL....LLLL....LLLL...',
'.LDDL....LDDL....LDDL....LDDL....LDDL....LDDL....LDDL....LDDL....LDDL....LDDL...',
'gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg',
'GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG',
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'...CC..........CC..........CC..........CC..........CC..........CC..........CC..',
'..CCCC........CCCC........CCCC........CCCC........CCCC........CCCC........CCCC.',
'..CCCC........CCCC........CCCC........CCCC........CCCC........CCCC........CCCC.',
'..CCCC........CCCC........CCCC........CCCC........CCCC........CCCC........CCCC.',
'..OOOO........OOOO........OOOO........OOOO........OOOO........OOOO........OOOO.',
'..OOOO........OOOO........OOOO........OOOO........OOOO........OOOO........OOOO.',
'..ooooo......ooooo........oooo........ooooo.......oooo.......ooooo........ooo..',
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
]);
export const BG_GARDEN_PAL = {
m: NNTV.midnight, s: NNTV.moonlight, O: NNTV.cream, o: NNTV.moonGlow,
L: NNTV.leafDark, D: NNTV.mossGreen, g: NNTV.grass, G: NNTV.grassDark,
d: NNTV.dirt, C: NNTV.leafGreen, f: NNTV.dirtDark,
};
// ── ACT 2: GARDEN WALLS ────────────────────────────────────────────────
export const BG_WALLS = norm([
'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp',
'ppppppppppppppppp..sppppppppppppppppppppppppps.ppppppppppppppppppp..spppppppppp',
'pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBB',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbb',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBB',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbb',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBB',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbbBBBBbbb',
'..vvbbb.bbbvvbbbb..vvbb.bbbvv..bbbvvb..bbbbvvb..vvbbbb..vvbbbbb..vvbbbb..vvbbbbb',
'vvllvvvvbbbllvvvbbllvvbbbbllv..bllvvbbvvllvvbvvvllvbvvvvllvbvvvllvvbbvvllvvvbbbb',
'lllllvvvvvlllllvvvlllvvvvllll.lllllvllllllvllllllvllllllvllllllvlllllllvllllllv',
'gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg',
'GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG',
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
]);
export const BG_WALLS_PAL = {
p: NNTV.midnight, s: NNTV.cream, b: NNTV.brick, B: NNTV.brickDark,
v: NNTV.leafDark, l: NNTV.leafGreen, g: NNTV.grass, G: NNTV.grassDark,
d: NNTV.dirt, f: NNTV.dirtDark,
};
// ── ACT 3: FORTRESS ────────────────────────────────────────────────────
export const BG_FORTRESS = norm([
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmsmmmmmmmmmmmsmmmmmmmmmmmmmmmmmmmmmsmmmmmmmmmmmmmmsmmmmmmmmmmmmmmmmmmmmmmmm',
'sssssssssssssssssssssssssssssssssssssssss.....ssssssssssssssssssssssssssssssssss',
's.....ss.....ss.....ss.....ss.....ss...ss.....ss...ss.....ss.....ss.....ss....ss',
's.....ss.....ss.....ss.....ss.....ss...ss.....ss...ss.....ss.....ss.....ss....ss',
'SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS',
'sSSsSSssSsSSssSSsSSssSsSSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSS',
'SssSSsSSssSsSSssSSsSSssSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSS',
'sSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSssSsSS',
'SssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSS',
'sSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSssSsSS',
'ssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSss',
'sSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSssSsSS',
'ssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSss',
'sSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSsSSssSSsSSssSSsSSssSsSSssSSsSSssSsSS',
'..........ff..................................ff...........................ff..',
'.........fFFf................................fFFf.........................fFFf.',
'........fFFFFf..............................fFFFFf........................fFFFFf',
'.........ffff................................ffff..........................ffff',
'..........kk..................................kk...........................kk..',
'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss',
'SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS',
'ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.',
]);
export const BG_FORTRESS_PAL = {
m: NNTV.midnight, s: NNTV.stone, S: NNTV.stoneDark,
f: NNTV.guardBlinking, F: NNTV.pumpkinOrange, k: NNTV.ink, d: NNTV.dirtDark,
};
// ── ACT 4: UNDERGROUND ─────────────────────────────────────────────────
export const BG_UNDERGROUND = norm([
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'dDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDddd',
'dddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDd',
'dDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDddd',
'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss',
'sSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmmfmmmmmmmmmmmmmmmmmmmmmmmfmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmFFmmmmmmmmmmmmmmmmmmmmmmFFmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmffmmmmmmmmmmmmmmmmmmmmmmffmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs',
'sSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSs',
'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss',
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'dDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDddd',
'dddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDd',
]);
export const BG_UNDERGROUND_PAL = {
d: NNTV.dirt, D: NNTV.dirtDark, s: NNTV.stoneDark, S: NNTV.stoneDark,
m: NNTV.midnight, f: NNTV.guardBlinking, F: NNTV.pumpkinOrange,
};
// ── ACT 5: PALACE ──────────────────────────────────────────────────────
export const BG_PALACE = norm([
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmsmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGmmm',
'mmGCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGmm',
'mGC..........pp.......................pp.......................pp............CGm',
'mGC.........pPPp.....................pPPp.....................pPPp...........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGC........pPPPPp...................pPPPPp...................pPPPPp..........CGm',
'mGCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGm',
'mmGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGmm.',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
]);
export const BG_PALACE_PAL = {
m: NNTV.midnight, s: NNTV.cream, G: NNTV.gold, C: NNTV.goldDark,
p: NNTV.moonlight, P: NNTV.silver,
};
// ── ACT 6: CHAMBER ─────────────────────────────────────────────────────
export const BG_CHAMBER = norm([
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyYYYYYYymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYCCYYYymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYCCCCYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYCCCCCYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYYYCCCCCYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYYYYYCCCCYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYYYYYYYCCCCYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmyyyyyYYYYYYYYYYYYYCCYYYYYYYYyyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmyyyyYYYYYYYYYYYYYYYYCCYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmyyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmyyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyyymmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmyyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmmm',
'mmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmm',
'mmmyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmm',
]);
export const BG_CHAMBER_PAL = {
m: NNTV.shadow, y: NNTV.guardBlinkingOff, Y: NNTV.gridLit, C: NNTV.carrot,
};
// Map level (1-indexed) → act scene data. Keeps UI code simple.
export const SCENE_BY_LEVEL = [
// 1-2 garden, 3-4 walls, 5-6 fortress, 7-8 underground, 9-11 palace, 12 chamber
BG_GARDEN, BG_GARDEN, BG_WALLS, BG_WALLS, BG_FORTRESS, BG_FORTRESS,
BG_UNDERGROUND, BG_UNDERGROUND, BG_PALACE, BG_PALACE, BG_PALACE, BG_CHAMBER,
];
export const SCENE_PAL_BY_LEVEL = [
BG_GARDEN_PAL, BG_GARDEN_PAL, BG_WALLS_PAL, BG_WALLS_PAL,
BG_FORTRESS_PAL, BG_FORTRESS_PAL, BG_UNDERGROUND_PAL, BG_UNDERGROUND_PAL,
BG_PALACE_PAL, BG_PALACE_PAL, BG_PALACE_PAL, BG_CHAMBER_PAL,
];
export function sceneForLevel(level) {
const idx = Math.max(0, Math.min(SCENE_BY_LEVEL.length - 1, level - 1));
return { art: SCENE_BY_LEVEL[idx], pal: SCENE_PAL_BY_LEVEL[idx] };
}
+125
View File
@@ -0,0 +1,125 @@
// 16×16 pixel-art tiles for the game board.
import { NNTV } from './palette.js';
export const TILE_EMPTY = [
'aaaaaaaaaaaaaaaa',
'abbbbbbbbbbbbbba',
'abccccccccccccba',
'abcccccccccccccb',
'abcccdddccccdccb',
'abccdcccccccccbb',
'abcccccccccccccb',
'abcccccccccddcbb',
'abccdddcccccccbb',
'abccccccccccccbb',
'abccccdcccccccbb',
'abcccccccccccccb',
'abccccccccdccccb',
'abbbbbbbbbbbbbbb',
'aaaaaaaaaaaaaaaa',
'aaaaaaaaaaaaaaaa',
];
export const TILE_EMPTY_PAL = { a: NNTV.gridBorder, b: NNTV.twilight, c: NNTV.gridEmpty, d: NNTV.dusk };
export const TILE_WALL = [
'kkkkkkkkkkkkkkkk',
'kssssssstsssssrk',
'ksllllllstlllsrk',
'kslmmmmlstlmmsrk',
'ksllllllstlllsrk',
'ksssssssstssssrk',
'ktttttttttttttrk',
'kssssstsssssssrk',
'kslmmstlmmmlssrk',
'kslllstllllllsrk',
'kssssstsssssssrk',
'ksssssstssssssrk',
'kslmmlstmmlssrkk',
'ksllllstlllssrkk',
'kssssstsssssrrkk',
'kkkkkkkkkkkkkkkk',
];
export const TILE_WALL_PAL = {
k: NNTV.ink, s: NNTV.stone, l: NNTV.stoneLight,
m: NNTV.stoneDark, r: NNTV.stoneDark, t: NNTV.ink,
};
export const TILE_GOAL = [
'kkkkkkkkkkkkkkkk',
'kggggggggggggggk',
'kghhghhhghhhgggk',
'kgghghhhghhhhggk',
'kgggghhghhhhhggk',
'kgghghhhghhhhggk',
'kggghhhhhhhhhggk',
'kggghhwhwhhhhggk',
'kggghhhhhwhhhggk',
'kggghwhhhhhhhggk',
'kgghghhhwhhhhggk',
'kggghhhhhhhhhggk',
'kgghghhhhhhhhggk',
'kggghwhhhhwhhggk',
'kggggggggggggggk',
'kkkkkkkkkkkkkkkk',
];
export const TILE_GOAL_PAL = { k: NNTV.ink, g: NNTV.gridGoal, h: '#00e763', w: '#7affb0' };
export const TILE_LIT = [
'yyyyyyyyyyyyyyyy',
'ywwwwwwwwwwwwwwy',
'ywyyyyyyyyyyyywy',
'ywyyfffffffffywy',
'ywyfyyyyyyyyfywy',
'ywyfyffffffyfywy',
'ywyfyfyyyyyfyfwy',
'ywyfyfyffyyfyfwy',
'ywyfyfyffyyfyfwy',
'ywyfyfyyyyyfyfwy',
'ywyfyffffffyfywy',
'ywyfyyyyyyyyfywy',
'ywyyfffffffffywy',
'ywyyyyyyyyyyyywy',
'ywwwwwwwwwwwwwwy',
'yyyyyyyyyyyyyyyy',
];
export const TILE_LIT_PAL = { y: NNTV.gridLit, w: '#fff88a', f: NNTV.gridLit };
export const TILE_MIRROR = [
'........kk......',
'.......kmmk.....',
'......kmllmk....',
'.....kmllllmk...',
'....kmllwwllmk..',
'...kmllwllwllmk.',
'..kmllwlllwllmk.',
'.kmllwlllllwllmk',
'kmlwlllllllwllmk',
'.kmllwlllllwllmk',
'..kmllwlllwllmk.',
'...kmllwwwllmk..',
'....kmllllmk....',
'.....kmllmk.....',
'......kmmk......',
'.......kk.......',
];
export const TILE_MIRROR_PAL = { k: NNTV.ink, l: NNTV.guardMirror, m: NNTV.lettuceDark, w: NNTV.cream };
export const TILE_PREVIEW = [
'yyyy........yyyy',
'y..y........y..y',
'y..y........y..y',
'yyyy........yyyy',
'................',
'................',
'................',
'................',
'................',
'................',
'................',
'................',
'yyyy........yyyy',
'y..y........y..y',
'y..y........y..y',
'yyyy........yyyy',
];
export const TILE_PREVIEW_PAL = { y: NNTV.gridLit };
+194
View File
@@ -0,0 +1,194 @@
// Pixel-art UI elements — hearts, logo, moon, icons.
import { NNTV } from './palette.js';
export const HEART_ART = [
'................',
'..kkkk....kkkk..',
'.krrrhk..krrrhk.',
'krrwrrrkkrrrrhk.',
'krwrrrrrrrrrrhk.',
'krrrrrrrrrrrrhk.',
'krrrrrrrrrrrhhk.',
'krrrrrrrrrrrhhk.',
'.krrrrrrrrrrhk..',
'.khrrrrrrrrhhk..',
'..khrrrrrrhhk...',
'...khrrrrhhk....',
'....khrrhhk.....',
'.....khhhk......',
'......khk.......',
'.......k........',
];
export const HEART_ART_EMPTY = HEART_ART.map(r => r.replace(/[rwh]/g, 'e'));
export const HEART_PAL = {
k: NNTV.ink, r: NNTV.scarfRed, h: '#8a1a22',
w: NNTV.cream, e: NNTV.dusk,
};
export const MOON_ART = [
'........kkkkk.......',
'......kkggghhk......',
'.....kgggghhhhk.....',
'....kggghhhhhhhk....',
'...kggghhhhhhhhhk...',
'...kgghhhhhhhhhhk...',
'..kggghhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kggghhhhhhhhhhhk..',
'...kgghhhhhhhhhhk...',
'...kggghhhhhhhhhk...',
'....kggghhhhhhhk....',
'.....kgggghhhhk.....',
'......kkggghhk......',
'........kkkkk.......',
'....................',
];
export const MOON_PAL = { k: NNTV.ink, g: NNTV.cream, h: NNTV.moonGlow };
// 16-wide icons — 'k' outline, 'w' fill.
const padIcon = (s) => s.split('\n').filter(Boolean).map(r => r.padEnd(16, '.').slice(0, 16));
export const ICON_PAL = { k: NNTV.ink, w: NNTV.moonlight, a: NNTV.scarfRed };
export const ICON_UNDO = padIcon(`
................
................
.....wwww.......
....w....w......
...w......w.....
..w...wwww.w....
..w..w.....w....
..w..w..........
.wwww.w.........
.wwwww..........
..www...........
...w............
................
................
................
................`);
export const ICON_REDO = padIcon(`
................
................
.......wwww.....
......w....w....
.....w......w...
....w.wwww...w..
....w.....w..w..
..........w..w..
.........w.wwww.
..........wwwww.
...........www..
............w...
................
................
................
................`);
export const ICON_EYE = padIcon(`
................
................
...wwwwwwwww....
..w.........w...
.w...wwwww...w..
w...w.....w...w.
w...w.www.w...w.
w...w.www.w...w.
w...w.....w...w.
.w...wwwww...w..
..w.........w...
...wwwwwwwww....
................
................
................
................`);
export const ICON_PAUSE = padIcon(`
................
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
................
................
................
................`);
export const ICON_SETTINGS = padIcon(`
................
.....wwww.......
....w....w......
...w..ww..w.....
..w..wwww..w....
..w..wwww..w....
.w....ww....w...
.w..........w...
.w..........w...
.w....ww....w...
..w..wwww..w....
..w..wwww..w....
...w..ww..w.....
....w....w......
.....wwww.......
................`);
export const ICON_LANG = padIcon(`
................
.....wwww.......
....wwwwww......
...ww....ww.....
..ww..ww..ww....
.ww..w..w..ww...
.w..ww..ww..w...
.w..ww..ww..w...
.w..ww..ww..w...
.ww..w..w..ww...
..ww..ww..ww....
...ww....ww.....
....wwwwww......
.....wwww.......
................
................`);
export const ICON_ARROW = padIcon(`
................
................
.......w........
......ww........
.....www........
....wwww........
...wwwww........
..wwwwwwwwww....
...wwwww........
....wwww........
.....www........
......ww........
.......w........
................
................
................`);
// Simple "NNTV" wordmark — compact 36×9 to fit menu header.
export const LOGO_ART = [
'....................................',
'.k..k.k...k.kkkk.k...k..kkk.k...k...',
'.kk.k.kk..k.k.k..kk..k.k....kk..k...',
'.k.kk.k.k.k.k.k..k.k.k.k....k.k.k...',
'.k..k.k..kk.k.k..k..kk.k....k..kk...',
'.k..k.k...k.kkkk.k...k..kkk.k...k...',
'....................................',
'....................................',
'....................................',
];
export const LOGO_PAL = { k: NNTV.moonlight };
+80
View File
@@ -0,0 +1,80 @@
// NNTV pixel-art palette — shared across sprite/tile/scene art.
// Semantic guard colors mirror theme.css variables to preserve gameplay readability.
export const NNTV = {
// Gameplay-critical (must match theme.css guard colors)
guardStatic: '#ff4444',
guardRotating: '#4488ff',
guardBlinking: '#ffdd44',
guardBlinkingOff: '#887722',
guardPatrolling: '#bb44ff',
guardMirror: '#44ddaa',
guardChaser: '#ff6622',
gridEmpty: '#1a1a2e',
gridWall: '#4a4a5e',
gridGoal: '#00c853',
gridLit: '#ffea00',
gridBorder: '#2a2a3e',
playerInk: '#111111',
// Extended palette — atmosphere & pixel shading
midnight: '#0a0a1a',
twilight: '#141428',
dusk: '#1f1b3a',
plum: '#2d2150',
moonlight: '#e8e4ff',
silver: '#9a9ac0',
shadow: '#05050f',
ink: '#0b0a18',
cream: '#fff4d6',
moonGlow: '#ffe88a',
carrot: '#ff8844',
carrotDark: '#cc5a1f',
leafGreen: '#3ea85c',
leafDark: '#1e6b38',
// Rabbit fur/cloth
furLight: '#f5eeff',
furMid: '#b8b0d0',
furShadow: '#6a607a',
cloth: '#1a1a2e',
clothShadow: '#05050f',
eyeShine: '#ffffff',
eyeRed: '#ff3355',
scarfRed: '#c73e3a',
// Veggie tones
tomatoRed: '#e23a3a',
tomatoDark: '#991f1f',
tomatoLeaf: '#3ea85c',
eggplantPurp: '#8a3fc9',
eggplantDark: '#4a1a6e',
cornYellow: '#ffd84a',
cornDark: '#b88a1a',
cornHusk: '#6aa84a',
lettuceGreen: '#8fd66a',
lettuceDark: '#3e7a3e',
pumpkinOrange: '#ff7722',
pumpkinDark: '#aa4411',
blueberry: '#4a7acc',
blueberryDark: '#2a4a8c',
onionPurp: '#a968c9',
onionDark: '#5a2a7a',
// Tile tones
stone: '#5a5a6e',
stoneDark: '#2a2a3e',
stoneLight: '#7a7a9a',
dirt: '#4a3a2a',
dirtDark: '#2a1a1a',
grass: '#3e7a3e',
grassDark: '#2a5a2a',
grassLight: '#5aaa5a',
mossGreen: '#2a5a3a',
brick: '#6a3a2a',
brickDark: '#3a1a0a',
wood: '#7a5a2a',
woodDark: '#3a2a1a',
gold: '#ffcc44',
goldDark: '#aa7722',
};
+26 -1
View File
@@ -17,6 +17,8 @@
import LevelCompletePopup from '../components/LevelCompletePopup.svelte';
import PauseMenu from '../components/PauseMenu.svelte';
import ControlsOverlay from '../components/controls-overlay.svelte';
import Pixel from '../lib/pixel/Pixel.svelte';
import { sceneForLevel } from '../lib/pixel/art-scenes.js';
let { navigate, level = 1, lives = 3 } = $props();
@@ -64,6 +66,7 @@
? turnManager.previewNextTurn(grid, player, guards)
: new Set()));
let canUndo = $derived((renderVersion, history.canUndo()));
let scene = $derived(sceneForLevel(currentLevel));
// Initialize level
function initLevel() {
@@ -266,6 +269,9 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="game-scene" class:flash={showFlash}
ontouchstart={onTouchStart} ontouchend={onTouchEnd}>
<div class="scene-backdrop">
<Pixel art={scene.art} palette={scene.pal} width={1024} height={384} />
</div>
<GameHud
lives={livesRemaining}
level={currentLevel}
@@ -337,6 +343,17 @@
display: flex; flex-direction: column;
background: var(--bg-dark);
position: relative;
overflow: hidden;
}
.scene-backdrop {
position: absolute;
inset: 0;
opacity: 0.22;
pointer-events: none;
z-index: 0;
display: flex;
align-items: flex-start;
justify-content: center;
}
.game-scene.flash {
animation: level-flash 0.4s ease-out;
@@ -351,8 +368,16 @@
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 1;
}
.board-container {
position: relative;
background: rgba(10, 10, 26, 0.55);
padding: 8px;
border-radius: 6px;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.6);
}
.board-container { position: relative; }
.final-message {
position: absolute;
top: 60px; left: 50%;
+37 -7
View File
@@ -2,29 +2,57 @@
import { getText } from '../lib/localization.js';
import { LEVELS } from '../lib/levels/levels.js';
import Button from '../components/Button.svelte';
import Pixel from '../lib/pixel/Pixel.svelte';
import { sceneForLevel } from '../lib/pixel/art-scenes.js';
let { navigate, level = 1, lives = 3 } = $props();
let levelData = $derived(LEVELS[level - 1]);
let levelName = $derived(levelData?.name || `Level ${level}`);
let storyText = $derived(levelData?.storyKey ? getText(levelData.storyKey) : '');
let scene = $derived(sceneForLevel(level));
</script>
<div class="intro">
<span class="level-num">{getText('level')}{level}</span>
<h1>{levelName}</h1>
{#if storyText}
<p class="story">{storyText}</p>
{/if}
<Button text={getText('continue')} onclick={() => navigate('Game', { level, lives })} />
<div class="backdrop">
<Pixel art={scene.art} palette={scene.pal} width={1024} height={480} />
</div>
<div class="foreground">
<span class="level-num">{getText('level')}{level}</span>
<h1>{levelName}</h1>
{#if storyText}
<p class="story">{storyText}</p>
{/if}
<Button text={getText('continue')} onclick={() => navigate('Game', { level, lives })} />
</div>
</div>
<style>
.intro {
width: 100%; height: 100%;
position: relative;
background: var(--bg-dark);
overflow: hidden;
}
.backdrop {
position: absolute;
inset: 0;
opacity: 0.55;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
}
.foreground {
position: relative;
width: 100%; height: 100%;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
gap: 20px;
background: var(--bg-dark);
background: linear-gradient(180deg,
rgba(10,10,26,0.65) 0%,
rgba(10,10,26,0.3) 40%,
rgba(10,10,26,0.3) 60%,
rgba(10,10,26,0.75) 100%);
}
.level-num {
font: var(--font-small);
@@ -33,6 +61,7 @@
h1 {
font: var(--font-title);
color: var(--text-title);
text-shadow: 0 0 12px rgba(0, 0, 0, 0.8);
}
.story {
font: var(--font-body);
@@ -41,5 +70,6 @@
max-width: 70%;
line-height: 1.6;
white-space: pre-line;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
</style>
+21 -9
View File
@@ -1,13 +1,15 @@
<script>
import { getText } from '../lib/localization.js';
import Button from '../components/Button.svelte';
import Pixel from '../lib/pixel/Pixel.svelte';
import { MOON_ART, MOON_PAL } from '../lib/pixel/art-ui.js';
import { RABBIT_ART, RABBIT_PAL } from '../lib/pixel/art-characters.js';
let { navigate } = $props();
// Generate starfield dots
const stars = Array.from({ length: 40 }, () => ({
x: Math.random() * 100,
y: Math.random() * 100,
opacity: 0.1 + Math.random() * 0.4,
opacity: 0.1 + Math.random() * 0.5,
size: 1 + Math.floor(Math.random() * 2),
}));
</script>
@@ -20,8 +22,14 @@
></div>
{/each}
<div class="moon">
<Pixel art={MOON_ART} palette={MOON_PAL} width={100} height={100} />
</div>
<h1>{getText('gameTitle')}</h1>
<div class="ninja-icon"></div>
<div class="hero">
<Pixel art={RABBIT_ART} palette={RABBIT_PAL} width={96} height={96} />
</div>
<div class="buttons">
<Button text={getText('startGame')} onclick={() => navigate('StoryIntro')} />
@@ -45,19 +53,23 @@
border-radius: 50%;
pointer-events: none;
}
.moon {
position: absolute;
top: 40px;
right: 60px;
filter: drop-shadow(0 0 24px rgba(255, 232, 138, 0.3));
}
h1 {
font: var(--font-title);
color: var(--text-title);
margin-bottom: 16px;
z-index: 1;
text-shadow: 0 0 12px rgba(187, 134, 252, 0.4);
}
.ninja-icon {
width: 36px; height: 36px;
background: var(--player-color);
border-radius: 50%;
margin-bottom: 32px;
box-shadow: 0 0 20px rgba(83, 52, 131, 0.3);
.hero {
margin-bottom: 24px;
z-index: 1;
filter: drop-shadow(0 4px 12px rgba(83, 52, 131, 0.6));
}
.buttons {
display: flex; flex-direction: column;