feat: add design canvas assets from claude design

This commit is contained in:
tiennm99 committed 2026-04-20 21:46:21 +07:00
1 parent 286edc3c1d
commit 6a1b3ec572
8 files changed
+1828

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Night Ninja : Twilight Voyage — Assets</title>
<style>
html, body { margin: 0; padding: 0; background: #14131f; overflow: hidden; }
#root { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel" src="design-canvas.jsx"></script>
<script type="text/babel" src="src/pixel.jsx"></script>
<script type="text/babel" src="src/characters.jsx"></script>
<script type="text/babel" src="src/tiles-ui.jsx"></script>
<script type="text/babel" src="src/scenes.jsx"></script>
<script type="text/babel" src="src/canvas.jsx"></script>
</body>
</html>
+253
View File
@@ -0,0 +1,253 @@
// DesignCanvas.jsx — Figma-ish design canvas wrapper
// Warm gray grid bg + Sections + Artboards + PostIt notes.
// No assets, no deps.
const DC = {
bg: '#f0eee9',
grid: 'rgba(0,0,0,0.06)',
label: 'rgba(60,50,40,0.7)',
title: 'rgba(40,30,20,0.85)',
subtitle: 'rgba(60,50,40,0.6)',
postitBg: '#fef4a8',
postitText: '#5a4a2a',
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
};
// ─────────────────────────────────────────────────────────────
// Main canvas — transform-based pan/zoom viewport
//
// Input mapping (Figma-style):
// • trackpad pinch → zoom (ctrlKey wheel; Safari gesture* events)
// • trackpad scroll → pan (two-finger)
// • mouse wheel → zoom (notched; distinguished from trackpad scroll)
// • middle-drag / primary-drag-on-bg → pan
//
// Transform state lives in a ref and is written straight to the DOM
// (translate3d + will-change) so wheel ticks don't go through React —
// keeps pans at 60fps on dense canvases.
// ─────────────────────────────────────────────────────────────
function DesignCanvas({ children, minScale = 0.1, maxScale = 8, style = {} }) {
const vpRef = React.useRef(null);
const worldRef = React.useRef(null);
const tf = React.useRef({ x: 0, y: 0, scale: 1 });
const apply = React.useCallback(() => {
const { x, y, scale } = tf.current;
const el = worldRef.current;
if (el) el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
}, []);
React.useEffect(() => {
const vp = vpRef.current;
if (!vp) return;
const zoomAt = (cx, cy, factor) => {
const r = vp.getBoundingClientRect();
const px = cx - r.left, py = cy - r.top;
const t = tf.current;
const next = Math.min(maxScale, Math.max(minScale, t.scale * factor));
const k = next / t.scale;
// keep the world point under the cursor fixed
t.x = px - (px - t.x) * k;
t.y = py - (py - t.y) * k;
t.scale = next;
apply();
};
// Mouse-wheel vs trackpad-scroll heuristic. A physical wheel sends
// line-mode deltas (Firefox) or large integer pixel deltas with no X
// component (Chrome/Safari, typically multiples of 100/120). Trackpad
// two-finger scroll sends small/fractional pixel deltas, often with
// non-zero deltaX. ctrlKey is set by the browser for trackpad pinch.
const isMouseWheel = (e) =>
e.deltaMode !== 0 ||
(e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40);
const onWheel = (e) => {
e.preventDefault();
if (isGesturing) return; // Safari: gesture* owns the pinch — discard concurrent wheels
if (e.ctrlKey) {
// trackpad pinch (or explicit ctrl+wheel)
zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.01));
} else if (isMouseWheel(e)) {
// notched mouse wheel — fixed-ratio step per click
zoomAt(e.clientX, e.clientY, Math.exp(-Math.sign(e.deltaY) * 0.18));
} else {
// trackpad two-finger scroll — pan
tf.current.x -= e.deltaX;
tf.current.y -= e.deltaY;
apply();
}
};
// Safari sends native gesture* events for trackpad pinch with a smooth
// e.scale; preferring these over the ctrl+wheel fallback gives a much
// better feel there. No-ops on other browsers. Safari also fires
// ctrlKey wheel events during the same pinch — isGesturing makes
// onWheel drop those entirely so they neither zoom nor pan.
let gsBase = 1;
let isGesturing = false;
const onGestureStart = (e) => { e.preventDefault(); isGesturing = true; gsBase = tf.current.scale; };
const onGestureChange = (e) => {
e.preventDefault();
zoomAt(e.clientX, e.clientY, (gsBase * e.scale) / tf.current.scale);
};
const onGestureEnd = (e) => { e.preventDefault(); isGesturing = false; };
// Drag-pan: middle button anywhere, or primary button starting on the
// canvas background (not inside an artboard).
let drag = null;
const onPointerDown = (e) => {
const onBg = e.target === vp || e.target === worldRef.current;
if (!(e.button === 1 || (e.button === 0 && onBg))) return;
e.preventDefault();
vp.setPointerCapture(e.pointerId);
drag = { id: e.pointerId, lx: e.clientX, ly: e.clientY };
vp.style.cursor = 'grabbing';
};
const onPointerMove = (e) => {
if (!drag || e.pointerId !== drag.id) return;
tf.current.x += e.clientX - drag.lx;
tf.current.y += e.clientY - drag.ly;
drag.lx = e.clientX; drag.ly = e.clientY;
apply();
};
const onPointerUp = (e) => {
if (!drag || e.pointerId !== drag.id) return;
vp.releasePointerCapture(e.pointerId);
drag = null;
vp.style.cursor = '';
};
vp.addEventListener('wheel', onWheel, { passive: false });
vp.addEventListener('gesturestart', onGestureStart, { passive: false });
vp.addEventListener('gesturechange', onGestureChange, { passive: false });
vp.addEventListener('gestureend', onGestureEnd, { passive: false });
vp.addEventListener('pointerdown', onPointerDown);
vp.addEventListener('pointermove', onPointerMove);
vp.addEventListener('pointerup', onPointerUp);
vp.addEventListener('pointercancel', onPointerUp);
return () => {
vp.removeEventListener('wheel', onWheel);
vp.removeEventListener('gesturestart', onGestureStart);
vp.removeEventListener('gesturechange', onGestureChange);
vp.removeEventListener('gestureend', onGestureEnd);
vp.removeEventListener('pointerdown', onPointerDown);
vp.removeEventListener('pointermove', onPointerMove);
vp.removeEventListener('pointerup', onPointerUp);
vp.removeEventListener('pointercancel', onPointerUp);
};
}, [apply, minScale, maxScale]);
const gridSvg = `url("data:image/svg+xml,%3Csvg width='120' height='120' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M120 0H0v120' fill='none' stroke='${encodeURIComponent(DC.grid)}' stroke-width='1'/%3E%3C/svg%3E")`;
return (
<div
ref={vpRef}
className="design-canvas"
style={{
height: '100vh', width: '100vw',
background: DC.bg,
overflow: 'hidden',
overscrollBehavior: 'none',
touchAction: 'none',
position: 'relative',
fontFamily: DC.font,
boxSizing: 'border-box',
...style,
}}
>
<div
ref={worldRef}
style={{
position: 'absolute', top: 0, left: 0,
transformOrigin: '0 0',
willChange: 'transform',
width: 'max-content', minWidth: '100%',
minHeight: '100%',
padding: '60px 0 80px',
backgroundImage: gridSvg,
backgroundSize: '120px 120px',
}}
>
{children}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Section — title + subtitle + h-stack of artboards (no wrap)
// ─────────────────────────────────────────────────────────────
function DCSection({ title, subtitle, children, gap = 48 }) {
return (
<div style={{ marginBottom: 80, position: 'relative' }}>
<div style={{ padding: '0 60px 36px' }}>
<div style={{
fontSize: 22, fontWeight: 600, color: DC.title,
letterSpacing: -0.3, marginBottom: 4,
}}>{title}</div>
{subtitle && (
<div style={{
fontSize: 14, fontWeight: 400, color: DC.subtitle,
}}>{subtitle}</div>
)}
</div>
{/* h-stack — clips offscreen, never wraps */}
<div style={{
display: 'flex', gap, padding: '0 60px',
alignItems: 'flex-start', width: 'max-content',
}}>
{children}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Artboard — labeled card
// ─────────────────────────────────────────────────────────────
function DCArtboard({ label, children, width, height, style = {} }) {
return (
<div style={{ position: 'relative', flexShrink: 0 }}>
{label && (
<div style={{
position: 'absolute', bottom: '100%', left: 0,
paddingBottom: 8,
fontSize: 12, fontWeight: 500, color: DC.label,
whiteSpace: 'nowrap',
}}>{label}</div>
)}
<div style={{
borderRadius: 2,
boxShadow: '0 1px 3px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06)',
overflow: 'hidden',
width, height,
background: '#fff',
...style,
}}>
{children}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Post-it — absolute-positioned sticky note
// ─────────────────────────────────────────────────────────────
function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) {
return (
<div style={{
position: 'absolute', top, left, right, bottom, width,
background: DC.postitBg, padding: '14px 16px',
fontFamily: '"Comic Sans MS", "Marker Felt", "Segoe Print", cursive',
fontSize: 14, lineHeight: 1.4, color: DC.postitText,
boxShadow: '0 2px 8px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08)',
transform: `rotate(${rotate}deg)`,
zIndex: 5,
}}>{children}</div>
);
}
Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt });
@@ -0,0 +1,6 @@
{
"version": 1,
"created": "2026-04-20T14:24:41.949Z",
"modified": "2026-04-20T14:24:41.949Z",
"objects": []
}
+445
View File
@@ -0,0 +1,445 @@
// Main canvas — assembles all NNTV pixel assets onto a single design canvas.
const { useState } = React;
function Card({ title, subtitle, children, bg = NNTV.twilight, width = 'auto', pad = 20 }) {
return (
<div style={{
background: bg, borderRadius: 4, padding: pad,
border: `2px solid ${NNTV.shadow}`,
boxShadow: '0 2px 8px rgba(0,0,0,0.5)',
color: NNTV.moonlight, fontFamily: 'ui-monospace, Menlo, monospace',
width, display: 'flex', flexDirection: 'column', gap: 12,
}}>
{title && <div style={{ fontSize: 13, fontWeight: 700, letterSpacing: 1, textTransform: 'uppercase', color: NNTV.cream }}>{title}</div>}
{subtitle && <div style={{ fontSize: 11, color: NNTV.silver, marginTop: -8 }}>{subtitle}</div>}
{children}
</div>
);
}
function SpriteCard({ sprite, scale = 5, border = NNTV.shadow, colorDot }) {
return (
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
padding: 10, background: NNTV.midnight, border: `1px solid ${border}`,
}}>
<div style={{ background: NNTV.gridEmpty, padding: 4, border: `1px solid ${NNTV.gridBorder}` }}>
<Pixel art={sprite.art} palette={sprite.pal} scale={scale} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: NNTV.moonlight, fontFamily: 'ui-monospace, Menlo, monospace', fontWeight: 700 }}>
{colorDot && <span style={{ width: 8, height: 8, borderRadius: '50%', background: colorDot, border: '1px solid #000' }} />}
{sprite.label}
</div>
<div style={{ fontSize: 10, color: NNTV.silver, fontFamily: 'ui-monospace, Menlo, monospace', textAlign: 'center', maxWidth: 180 }}>{sprite.desc}</div>
</div>
);
}
// ── 1. LOGO + PALETTE ──────────────────────────────────────────────────
function LogoSection() {
return (
<DCSection title="01 · Identity" subtitle="Logo, wordmark, palette tokens">
<DCArtboard label="Logo (stacked)" width={720} height={360}>
<div style={{
width: '100%', height: '100%', background: NNTV.midnight,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 18,
position: 'relative', overflow: 'hidden',
}}>
{/* starfield */}
<div style={{ position: 'absolute', top: 20, left: 40 }}><Pixel art={MOON_ART} palette={MOON_PAL} scale={3} /></div>
{[[80,50],[600,40],[120,280],[680,260],[380,30]].map(([x,y],i)=>(
<div key={i} style={{ position: 'absolute', top: y, left: x, width: 3, height: 3, background: NNTV.cream }}/>
))}
<Pixel art={LOGO_ART} palette={LOGO_PAL} scale={4} />
<div style={{
fontFamily: 'ui-monospace, Menlo, monospace', color: NNTV.moonGlow,
fontSize: 14, letterSpacing: 6, fontWeight: 700,
}}>NIGHT NINJA : TWILIGHT VOYAGE</div>
<div style={{ fontFamily: 'ui-monospace, Menlo, monospace', color: NNTV.silver, fontSize: 10, letterSpacing: 2 }}>
— a turn-based stealth puzzle —
</div>
</div>
</DCArtboard>
<DCArtboard label="Palette tokens" width={420} height={360}>
<div style={{ background: NNTV.midnight, padding: 18, height: '100%', color: NNTV.moonlight, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 10 }}>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, color: NNTV.cream, marginBottom: 10 }}>GUARD COLORS · STRICT</div>
{[
['static', NNTV.guardStatic, 'tomato · red'],
['rotating', NNTV.guardRotating, 'blueberry · blue'],
['blinking', NNTV.guardBlinking, 'corn · yellow'],
['patrolling', NNTV.guardPatrolling, 'eggplant · purple'],
['mirror', NNTV.guardMirror, 'lettuce · green'],
['chaser', NNTV.guardChaser, 'pumpkin · orange'],
].map(([k, c, label]) => (
<div key={k} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '3px 0' }}>
<span style={{ width: 18, height: 18, background: c, border: '1px solid #000' }} />
<span style={{ width: 90 }}>{k}</span>
<span style={{ color: NNTV.silver }}>{c}</span>
<span style={{ color: NNTV.silver, marginLeft: 'auto' }}>{label}</span>
</div>
))}
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, color: NNTV.cream, marginTop: 14, marginBottom: 8 }}>WORLD · TWILIGHT</div>
{[
['midnight', NNTV.midnight],
['twilight', NNTV.twilight],
['dusk', NNTV.dusk],
['plum', NNTV.plum],
['moonlight', NNTV.moonlight],
['moonGlow', NNTV.moonGlow],
['cream', NNTV.cream],
['carrot', NNTV.carrot],
].map(([k, c]) => (
<div key={k} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '2px 0' }}>
<span style={{ width: 18, height: 18, background: c, border: '1px solid #000' }} />
<span style={{ width: 90 }}>{k}</span>
<span style={{ color: NNTV.silver }}>{c}</span>
</div>
))}
</div>
</DCArtboard>
<DCPostIt top={-8} left={1180} rotate={3} width={220}>
Guard colors match the engine's color tokens exactly — so a player reading the board sees the mechanic, not just the character.
</DCPostIt>
</DCSection>
);
}
// ── 2. CHARACTER SPRITES ───────────────────────────────────────────────
function CharactersSection() {
return (
<DCSection title="02 · Characters" subtitle="32×32 sprites · shown @ 5× · ink-outlined for contrast on dark cells">
<DCArtboard label="Player" width={220} height={260}>
<div style={{ background: NNTV.twilight, height: '100%', display: 'grid', placeItems: 'center' }}>
<SpriteCard sprite={SPRITES.rabbit} scale={5} />
</div>
</DCArtboard>
<DCArtboard label="Guards (6 types · strict color)" width={980} height={260}>
<div style={{ background: NNTV.twilight, height: '100%', display: 'flex', alignItems: 'center', gap: 8, padding: 8, overflow:'hidden' }}>
<SpriteCard sprite={SPRITES.tomato} scale={4} colorDot={NNTV.guardStatic} />
<SpriteCard sprite={SPRITES.blueberry} scale={4} colorDot={NNTV.guardRotating} />
<SpriteCard sprite={SPRITES.corn} scale={4} colorDot={NNTV.guardBlinking} />
<SpriteCard sprite={SPRITES.eggplant} scale={4} colorDot={NNTV.guardPatrolling} />
<SpriteCard sprite={SPRITES.lettuce} scale={4} colorDot={NNTV.guardMirror} />
<SpriteCard sprite={SPRITES.pumpkin} scale={4} colorDot={NNTV.guardChaser} />
</div>
</DCArtboard>
<DCArtboard label="Princess · Blinking (off)" width={360} height={260}>
<div style={{ background: NNTV.twilight, height: '100%', display: 'flex', alignItems: 'center', gap: 8, padding: 8 }}>
<SpriteCard sprite={SPRITES.princess} scale={4} />
<SpriteCard sprite={SPRITES.cornOff} scale={4} colorDot={NNTV.guardBlinkingOff} />
</div>
</DCArtboard>
<DCPostIt top={-8} left={1620} rotate={-2} width={220}>
Princess sits on a carrot throne color. Tiara echoes the gold palace trim — foreshadowing Act 5 before you ever reach it.
</DCPostIt>
</DCSection>
);
}
// ── 3. TILES ────────────────────────────────────────────────────────────
function TilesSection() {
const tiles = [
['Empty', TILE_EMPTY, TILE_EMPTY_PAL, 'dark safe cell with subtle dither'],
['Wall', TILE_WALL, TILE_WALL_PAL, 'dressed stone block'],
['Goal', TILE_GOAL, TILE_GOAL_PAL, 'grass patch — safe end'],
['Lit (danger)', TILE_LIT, TILE_LIT_PAL, 'pulsing inner rings'],
['Mirror', TILE_MIRROR, TILE_MIRROR_PAL, 'diamond — deflects beams'],
['Preview', TILE_PREVIEW, TILE_PREVIEW_PAL, 'press V · corner markers'],
];
return (
<DCSection title="03 · Tileset" subtitle="16×16 tiles · shown @ 8× · tiles sit flush; no rounded corners">
<DCArtboard label="Cells" width={1260} height={240}>
<div style={{ background: NNTV.midnight, height: '100%', display: 'flex', alignItems: 'center', gap: 18, padding: 18 }}>
{tiles.map(([n, a, p, d]) => (
<div key={n} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
<div style={{ border: '1px solid #000' }}><Pixel art={a} palette={p} scale={8} /></div>
<div style={{ fontSize: 11, color: NNTV.cream, fontFamily: 'ui-monospace, Menlo, monospace', fontWeight: 700 }}>{n}</div>
<div style={{ fontSize: 10, color: NNTV.silver, fontFamily: 'ui-monospace, Menlo, monospace', maxWidth: 140, textAlign: 'center' }}>{d}</div>
</div>
))}
</div>
</DCArtboard>
{/* live board mock */}
<DCArtboard label="Live board mock · Level 4 · The Searchlight" width={520} height={460}>
<div style={{ background: NNTV.midnight, width: '100%', height: '100%', display: 'grid', placeItems: 'center' }}>
<BoardMock />
</div>
</DCArtboard>
</DCSection>
);
}
function BoardMock() {
// 7x7 level layout. W=wall, .=empty, G=goal, L=lit, M=mirror
// guards/player rendered as overlays
const rows = [
'P......',
'.......',
'..WWW..',
'..W.W..',
'..WbW..', // b = blue guard inside wall box
'.......',
'......G',
];
const tileFor = (ch) => {
if (ch === 'W') return { art: TILE_WALL, pal: TILE_WALL_PAL };
if (ch === 'G') return { art: TILE_GOAL, pal: TILE_GOAL_PAL };
return { art: TILE_EMPTY, pal: TILE_EMPTY_PAL };
};
const SIZE = 48;
return (
<div style={{ position: 'relative', width: 7*SIZE, height: 7*SIZE }}>
{rows.map((row, r) => (
<div key={r} style={{ display: 'flex' }}>
{[...row].map((ch, c) => (
<div key={c} style={{ width: SIZE, height: SIZE }}>
<Pixel art={tileFor(ch).art} palette={tileFor(ch).pal} scale={SIZE/16} />
</div>
))}
</div>
))}
{/* lit cells — beam pointing right from blue guard */}
{[[4,3],[4,4]].map(([r,c],i)=>(
<div key={'l'+i} style={{ position: 'absolute', top: r*SIZE, left: c*SIZE, width: SIZE, height: SIZE, background: NNTV.gridLit, opacity: 0.85, mixBlendMode: 'screen' }}/>
))}
{/* player at 0,0 */}
<div style={{ position: 'absolute', top: 0, left: 0, width: SIZE, height: SIZE, display: 'grid', placeItems: 'center' }}>
<Pixel art={SPRITES.rabbit.art} palette={SPRITES.rabbit.pal} scale={SIZE/32} />
</div>
{/* blueberry guard at 4,3 */}
<div style={{ position: 'absolute', top: 4*SIZE, left: 3*SIZE, width: SIZE, height: SIZE, display: 'grid', placeItems: 'center' }}>
<Pixel art={SPRITES.blueberry.art} palette={SPRITES.blueberry.pal} scale={SIZE/32} />
</div>
</div>
);
}
// ── 4. UI / HUD / ICONS ────────────────────────────────────────────────
function UISection() {
const [livesState] = useState(2);
return (
<DCSection title="04 · UI · HUD · icons" subtitle="Game chrome. Buttons keep pixel ink outlines for cohesion.">
<DCArtboard label="HUD" width={640} height={180}>
<div style={{ background: NNTV.midnight, height: '100%', padding: 18, display: 'flex', alignItems: 'center', gap: 24, fontFamily: 'ui-monospace, Menlo, monospace', color: NNTV.moonlight }}>
{/* level */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div style={{ fontSize: 10, color: NNTV.silver, letterSpacing: 2 }}>LEVEL</div>
<div style={{ fontSize: 28, fontWeight: 700, color: NNTV.cream }}>07<span style={{ fontSize: 14, color: NNTV.silver }}>/12</span></div>
</div>
{/* lives */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ fontSize: 10, color: NNTV.silver, letterSpacing: 2 }}>LIVES</div>
<div style={{ display: 'flex', gap: 4 }}>
{[0,1,2].map(i => (
<Pixel key={i} art={i < livesState+1 ? HEART_ART : HEART_ART_EMPTY} palette={HEART_PAL} scale={3} />
))}
</div>
</div>
{/* turns */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div style={{ fontSize: 10, color: NNTV.silver, letterSpacing: 2 }}>TURN</div>
<div style={{ fontSize: 28, fontWeight: 700, color: NNTV.cream }}>014</div>
</div>
{/* divider */}
<div style={{ width: 1, alignSelf: 'stretch', background: NNTV.plum }}/>
{/* icon buttons */}
<div style={{ display: 'flex', gap: 8 }}>
{[['undo', ICON_UNDO, 'Z'], ['redo', ICON_REDO, 'Y'], ['preview', ICON_EYE, 'V'], ['pause', ICON_PAUSE, 'Esc']].map(([k, art, key]) => (
<div key={k} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<div style={{ padding: 6, background: NNTV.plum, border: `1px solid ${NNTV.shadow}` }}>
<Pixel art={art} palette={ICON_PAL} scale={2} />
</div>
<div style={{ fontSize: 9, color: NNTV.silver, letterSpacing: 1 }}>{key}</div>
</div>
))}
</div>
</div>
</DCArtboard>
<DCArtboard label="Buttons" width={400} height={180}>
<div style={{ background: NNTV.twilight, height: '100%', padding: 18, display: 'flex', flexDirection: 'column', gap: 10, fontFamily: 'ui-monospace, Menlo, monospace' }}>
{['PLAY', 'LEVEL SELECT', 'CONTINUE'].map((t, i) => (
<button key={t} style={{
fontFamily: 'inherit', fontSize: 13, letterSpacing: 2, fontWeight: 700,
background: i === 0 ? NNTV.plum : NNTV.twilight,
color: i === 0 ? NNTV.cream : NNTV.moonlight,
border: `2px solid ${i === 0 ? NNTV.moonGlow : NNTV.plum}`,
padding: '10px 18px', textAlign: 'left', cursor: 'pointer',
boxShadow: `3px 3px 0 ${NNTV.shadow}`,
}}>{t}</button>
))}
<button style={{
fontFamily: 'inherit', fontSize: 11, letterSpacing: 2,
background: 'transparent', color: NNTV.silver,
border: `1px dashed ${NNTV.plum}`, padding: '6px 14px',
}}>SETTINGS / GUIDE</button>
</div>
</DCArtboard>
<DCArtboard label="Icon library" width={480} height={180}>
<div style={{ background: NNTV.midnight, height: '100%', padding: 18, display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap', fontFamily: 'ui-monospace, Menlo, monospace' }}>
{[['undo', ICON_UNDO], ['redo', ICON_REDO], ['eye', ICON_EYE], ['pause', ICON_PAUSE], ['gear', ICON_SETTINGS], ['lang', ICON_LANG], ['move', ICON_ARROW]].map(([name, art]) => (
<div key={name} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<div style={{ padding: 4, background: NNTV.plum }}>
<Pixel art={art} palette={ICON_PAL} scale={3} />
</div>
<div style={{ fontSize: 9, color: NNTV.silver, letterSpacing: 1 }}>{name}</div>
</div>
))}
</div>
</DCArtboard>
</DCSection>
);
}
// ── 5. POPUPS / SCREENS ────────────────────────────────────────────────
function PopupsSection() {
return (
<DCSection title="05 · Popups & screens" subtitle="Detection, level complete, pause, game over, main menu">
<DCArtboard label="Main menu" width={460} height={520}>
<div style={{ background: NNTV.midnight, height: '100%', padding: 24, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 18, fontFamily: 'ui-monospace, Menlo, monospace', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 14, right: 18 }}><Pixel art={MOON_ART} palette={MOON_PAL} scale={2} /></div>
<Pixel art={LOGO_ART} palette={LOGO_PAL} scale={2.5} />
<div style={{ color: NNTV.moonGlow, fontSize: 11, letterSpacing: 4, marginTop: -6 }}>TWILIGHT VOYAGE</div>
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
<Pixel art={SPRITES.rabbit.art} palette={SPRITES.rabbit.pal} scale={3} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: 260, marginTop: 10 }}>
{['START GAME', 'LEVEL SELECT', 'GUIDE', 'SETTINGS'].map((t, i) => (
<button key={t} style={{
fontFamily: 'inherit', fontSize: 12, letterSpacing: 2, fontWeight: 700,
background: i === 0 ? NNTV.plum : 'transparent',
color: i === 0 ? NNTV.cream : NNTV.moonlight,
border: `2px solid ${i === 0 ? NNTV.moonGlow : NNTV.plum}`,
padding: '10px 14px', textAlign: 'left', cursor: 'pointer',
boxShadow: i === 0 ? `3px 3px 0 ${NNTV.shadow}` : 'none',
}}>› {t}</button>
))}
</div>
</div>
</DCArtboard>
<DCArtboard label="Detection popup" width={420} height={320}>
<div style={{ background: 'rgba(0,0,0,0.85)', height: '100%', display: 'grid', placeItems: 'center', fontFamily: 'ui-monospace, Menlo, monospace' }}>
<div style={{ background: NNTV.twilight, border: `3px solid ${NNTV.eyeRed}`, padding: '22px 28px', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center', boxShadow: `5px 5px 0 ${NNTV.shadow}` }}>
<div style={{ display: 'flex', gap: 10 }}>
<Pixel art={SPRITES.pumpkin.art} palette={SPRITES.pumpkin.pal} scale={2} />
<Pixel art={SPRITES.rabbit.art} palette={SPRITES.rabbit.pal} scale={2} />
</div>
<div style={{ color: NNTV.eyeRed, fontSize: 22, fontWeight: 700, letterSpacing: 4 }}>DETECTED!</div>
<div style={{ color: NNTV.silver, fontSize: 11 }}>– 1 life · restarting level</div>
<button style={{ fontFamily: 'inherit', fontSize: 12, letterSpacing: 2, background: NNTV.plum, color: NNTV.cream, border: `2px solid ${NNTV.moonGlow}`, padding: '8px 20px', marginTop: 6, cursor: 'pointer' }}>TRY AGAIN</button>
</div>
</div>
</DCArtboard>
<DCArtboard label="Level complete" width={420} height={320}>
<div style={{ background: 'rgba(0,0,0,0.85)', height: '100%', display: 'grid', placeItems: 'center', fontFamily: 'ui-monospace, Menlo, monospace' }}>
<div style={{ background: NNTV.twilight, border: `3px solid ${NNTV.gridGoal}`, padding: '22px 28px', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center', boxShadow: `5px 5px 0 ${NNTV.shadow}` }}>
<div style={{ display: 'flex', gap: 4 }}>
{[...'★★★'].map((s, i) => <span key={i} style={{ color: NNTV.moonGlow, fontSize: 32, textShadow: `2px 2px 0 ${NNTV.shadow}` }}>{s}</span>)}
</div>
<div style={{ color: NNTV.gridGoal, fontSize: 18, fontWeight: 700, letterSpacing: 3 }}>LEVEL CLEAR</div>
<div style={{ color: NNTV.silver, fontSize: 11 }}>14 turns · 0 detections</div>
<button style={{ fontFamily: 'inherit', fontSize: 12, letterSpacing: 2, background: NNTV.plum, color: NNTV.cream, border: `2px solid ${NNTV.moonGlow}`, padding: '8px 20px', cursor: 'pointer' }}>NEXT →</button>
</div>
</div>
</DCArtboard>
<DCArtboard label="Game over (level 12)" width={420} height={320}>
<div style={{ background: 'rgba(0,0,0,0.95)', height: '100%', display: 'grid', placeItems: 'center', fontFamily: 'ui-monospace, Menlo, monospace', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', inset: 0, background: `radial-gradient(circle at center, ${NNTV.guardBlinkingOff}22 0%, transparent 60%)` }}/>
<div style={{ background: NNTV.shadow, border: `2px solid ${NNTV.gridLit}`, padding: '24px 28px', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center', position: 'relative' }}>
<Pixel art={SPRITES.princess.art} palette={SPRITES.princess.pal} scale={2} />
<div style={{ color: NNTV.gridLit, fontSize: 13, fontWeight: 700, letterSpacing: 2, maxWidth: 280, lineHeight: 1.5 }}>"SOME RESCUES WERE NEVER MEANT TO BE…"</div>
<div style={{ color: NNTV.silver, fontSize: 10, fontStyle: 'italic' }}>— the princess remains beyond your reach</div>
</div>
</div>
</DCArtboard>
</DCSection>
);
}
// ── 6. ACT BACKGROUNDS ──────────────────────────────────────────────────
function BackgroundsSection() {
const acts = [
['garden', SCENES.garden],
['walls', SCENES.walls],
['fortress', SCENES.fortress],
['underground', SCENES.underground],
['palace', SCENES.palace],
['chamber', SCENES.chamber],
];
return (
<DCSection title="06 · Act backgrounds" subtitle="One per act · level intro banner behind story text">
{acts.map(([k, s]) => (
<DCArtboard key={k} label={s.label} width={640} height={300}>
<div style={{ background: NNTV.midnight, width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'hidden', display: 'grid', placeItems: 'center' }}>
<Pixel art={s.art} palette={s.pal} scale={8} />
</div>
<div style={{ padding: '8px 12px', background: NNTV.shadow, color: NNTV.silver, fontSize: 10, fontFamily: 'ui-monospace, Menlo, monospace' }}>{s.desc}</div>
</div>
</DCArtboard>
))}
</DCSection>
);
}
// ── 7. LEVEL INTRO MOCK ────────────────────────────────────────────────
function LevelIntroSection() {
const story = [
{ act: SCENES.garden, name: 'Level 1 · Garden Path', body: 'You slip past the outskirts of the Vegetable Kingdom. Moonlight silvers the hedges. Nothing stirs — yet.' },
{ act: SCENES.fortress, name: 'Level 5 · Fortress Gate', body: 'A corn sentry blinks its torch on and off. Count the beats. Move with the dark.' },
{ act: SCENES.chamber, name: 'Level 12 · The Princess Chamber', body: 'The air feels watchful. She already knows you are here…' },
];
return (
<DCSection title="07 · Level intro layouts" subtitle="Story beats with act background behind">
{story.map((s, i) => (
<DCArtboard key={i} label={s.name} width={640} height={360}>
<div style={{ position: 'relative', width: '100%', height: '100%', background: NNTV.midnight, overflow: 'hidden' }}>
<div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', opacity: 0.55 }}>
<Pixel art={s.act.art} palette={s.act.pal} scale={8} />
</div>
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, transparent 0%, rgba(10,10,26,0.3) 40%, rgba(10,10,26,0.95) 90%)' }}/>
<div style={{ position: 'absolute', left: 32, right: 32, bottom: 28, color: NNTV.moonlight, fontFamily: 'ui-monospace, Menlo, monospace' }}>
<div style={{ color: NNTV.moonGlow, fontSize: 10, letterSpacing: 4, marginBottom: 6 }}>{s.act.label.toUpperCase()}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: NNTV.cream, marginBottom: 8 }}>{s.name}</div>
<div style={{ fontSize: 12, color: NNTV.silver, lineHeight: 1.6, maxWidth: 440 }}>{s.body}</div>
<button style={{ marginTop: 12, fontFamily: 'inherit', fontSize: 11, letterSpacing: 2, background: NNTV.plum, color: NNTV.cream, border: `2px solid ${NNTV.moonGlow}`, padding: '6px 16px', cursor: 'pointer' }}>CONTINUE →</button>
</div>
</div>
</DCArtboard>
))}
</DCSection>
);
}
// ── APP ─────────────────────────────────────────────────────────────────
function App() {
return (
<DesignCanvas style={{ background: '#14131f', fontFamily: 'ui-monospace, Menlo, monospace' }}>
<div style={{ padding: '20px 60px 40px', color: NNTV.moonlight }}>
<div style={{ fontSize: 28, fontWeight: 700, color: NNTV.cream, letterSpacing: 2 }}>NIGHT NINJA : TWILIGHT VOYAGE</div>
<div style={{ fontSize: 13, color: NNTV.silver, marginTop: 6, maxWidth: 720 }}>
Pixel-art asset set for tiennm99/nntv. 32×32 characters, 16×16 tiles, 80×40 act backgrounds.
Strict guard-color readability preserved. Palette: evolution of the original indigo theme toward twilight plum + moonlight.
</div>
</div>
<LogoSection />
<CharactersSection />
<TilesSection />
<UISection />
<PopupsSection />
<BackgroundsSection />
<LevelIntroSection />
</DesignCanvas>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+391
View File
@@ -0,0 +1,391 @@
// Character sprites for NNTV — 32×32 pixel art.
// Design rules:
// • ink outline in rabbit/ink tones
// • each guard's primary color matches its mechanic (strict readability)
// • guards: tomato=static(red), blueberry=rotating(blue), corn=blinking(yellow),
// eggplant=patrolling(purple), lettuce=mirror(green), pumpkin=chaser(orange)
// • readable silhouette even at 32px
// ── NINJA RABBIT (32×32) ──────────────────────────────────────────────
// Scrappy apprentice. Dark ninja wrap, wide eyes, one ear flicked.
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............',
'................................',
'................................',
'................................',
];
const RABBIT_PAL = {
K: NNTV.ink,
F: NNTV.furLight,
C: NNTV.furMid, // inner ear
S: NNTV.cloth, // ninja wrap
W: NNTV.eyeShine, // eye white
E: NNTV.ink, // pupil
R: NNTV.scarfRed, // scarf knot
M: NNTV.furMid, // paw shadow
};
// ── TOMATO — Static Guard (RED, circle shape) 32×32 ───────────────────
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...........',
'................................',
'................................',
'................................',
'................................',
'................................',
'................................',
];
const TOMATO_PAL = {
K: NNTV.ink,
R: NNTV.tomatoRed, // primary = red (static guard color)
H: NNTV.tomatoDark, // shadow
W: NNTV.eyeShine,
D: NNTV.cream, // highlight
G: NNTV.leafGreen, // leaf
L: NNTV.leafDark, // leaf shadow
};
// ── BLUEBERRY — Rotating Guard (BLUE, circle + direction) 32×32 ────────
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............',
'................................',
'................................',
'................................',
'................................',
];
const BLUEBERRY_PAL = {
K: NNTV.ink,
B: NNTV.guardRotating, // strict guard blue
L: NNTV.cream, // dusting highlight
H: NNTV.blueberryDark,
W: NNTV.eyeShine,
N: NNTV.cream, // mouth/whisker
G: NNTV.leafGreen,
S: NNTV.leafDark,
};
// ── CORN — Blinking Guard (YELLOW, dims when off) 32×32 ───────────────
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...........',
'................................',
'................................',
'................................',
'................................',
];
const CORN_PAL = {
K: NNTV.ink,
C: NNTV.guardBlinking, // kernel yellow
Y: NNTV.cornDark, // kernel shadow
H: NNTV.cornDark,
W: NNTV.eyeShine,
N: NNTV.ink, // mouth
G: NNTV.cornHusk, // husk
};
// OFF variant: swap C→dim
const CORN_OFF_PAL = { ...CORN_PAL, C: NNTV.guardBlinkingOff, Y: '#5a4410' };
// ── EGGPLANT — Patrolling Guard (PURPLE, circle + direction) 32×32 ─────
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...............',
'................................',
'................................',
'................................',
];
const EGGPLANT_PAL = {
K: NNTV.ink,
P: NNTV.guardPatrolling, // strict purple
H: NNTV.eggplantDark,
W: NNTV.eyeShine,
N: NNTV.cream,
G: NNTV.leafGreen,
L: NNTV.leafDark,
};
// ── LETTUCE — Mirror Guard (GREEN, diamond-ish) 32×32 ──────────────────
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..............',
'................................',
'................................',
'................................',
'................................',
'................................',
'................................',
];
const LETTUCE_PAL = {
K: NNTV.ink,
L: NNTV.guardMirror, // strict green
M: NNTV.lettuceDark, // leaf vein
H: NNTV.lettuceDark,
W: NNTV.eyeShine,
N: NNTV.cream,
};
// ── PUMPKIN — Chaser Guard (ORANGE, angry) 32×32 ──────────────────────
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.............',
'................................',
'................................',
'................................',
];
const PUMPKIN_PAL = {
K: NNTV.ink,
P: NNTV.guardChaser, // strict orange
O: NNTV.pumpkinDark,
H: NNTV.eyeRed, // glowing angry eye
W: NNTV.cream,
N: NNTV.ink, // jagged mouth
G: NNTV.leafGreen, // stem leaf
S: NNTV.leafDark, // stem
};
// ── CARROT PRINCESS — 32×32 ────────────────────────────────────────────
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.................',
'................................',
'................................',
'................................',
'................................',
'................................',
];
const PRINCESS_PAL = {
K: NNTV.ink,
C: NNTV.carrot, // carrot body
D: NNTV.cream, // highlight streaks
H: NNTV.carrotDark,
W: NNTV.eyeShine,
E: NNTV.ink,
P: NNTV.scarfRed, // lips
Y: NNTV.carrotDark, // outline carrot
L: NNTV.leafGreen, // leafy crown
S: NNTV.leafDark,
G: NNTV.gold, // tiara band
};
// Export all
const SPRITES = {
rabbit: { art: RABBIT_ART, pal: RABBIT_PAL, label: 'Ninja Rabbit', desc: 'Player. Scrappy apprentice, dark wrap, red scarf.' },
tomato: { art: TOMATO_ART, pal: TOMATO_PAL, label: 'Tomato — Static', desc: 'Red · fixed light pattern', color: NNTV.guardStatic },
blueberry: { art: BLUEBERRY_ART, pal: BLUEBERRY_PAL, label: 'Blueberry — Rotating', desc: 'Blue · rotates beam 90° / turn', color: NNTV.guardRotating },
corn: { art: CORN_ART, pal: CORN_PAL, label: 'Corn — Blinking (on)', desc: 'Yellow · toggles lights on/off', color: NNTV.guardBlinking },
cornOff: { art: CORN_ART, pal: CORN_OFF_PAL, label: 'Corn — Blinking (off)', desc: 'Dimmed variant', color: NNTV.guardBlinkingOff },
eggplant: { art: EGGPLANT_ART, pal: EGGPLANT_PAL, label: 'Eggplant — Patrolling', desc: 'Purple · walks a path', color: NNTV.guardPatrolling },
lettuce: { art: LETTUCE_ART, pal: LETTUCE_PAL, label: 'Lettuce — Mirror', desc: 'Green · reflects beams 90°', color: NNTV.guardMirror },
pumpkin: { art: PUMPKIN_ART, pal: PUMPKIN_PAL, label: 'Pumpkin — Chaser', desc: 'Orange · hunts via pathfinding', color: NNTV.guardChaser },
princess: { art: PRINCESS_ART, pal: PRINCESS_PAL, label: 'Carrot Princess', desc: 'Objective. Royal. Unreachable.' },
};
Object.assign(window, { SPRITES });
+122
View File
@@ -0,0 +1,122 @@
// Pixel art renderer — converts string art into inline SVG <rect>s.
// Each character maps to a palette index; '.' or ' ' = transparent.
//
// Usage:
// <Pixel art={ART} palette={PAL} scale={6} />
// Pixel(art, palette, scale) → SVG element
//
// Palette keys are 1-char strings. Keep sprites small (≤64px) for readability.
const NNTV = {
// Game semantic colors (from theme.css) — preserved exactly for gameplay readability
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',
// Expanded palette for richer pixel work — evolution of current indigo theme
midnight: '#0a0a1a', // deepest bg
twilight: '#141428', // bg panel deep
dusk: '#1f1b3a', // panel mid
plum: '#2d2150', // panel warm
moonlight: '#e8e4ff', // light text / highlight
silver: '#9a9ac0', // mid text
shadow: '#05050f', // near-black
ink: '#0b0a18', // outline ink
cream: '#fff4d6', // moon / whites
moonGlow: '#ffe88a', // warm light halo
carrot: '#ff8844', // princess warm
carrotDark:'#cc5a1f', // princess shadow
leafGreen: '#3ea85c', // princess leaf
leafDark: '#1e6b38',
// Rabbit fur/cloth tones
furLight: '#f5eeff',
furMid: '#b8b0d0',
furShadow: '#6a607a',
cloth: '#1a1a2e',
clothShadow:'#05050f',
eyeShine: '#ffffff',
eyeRed: '#ff3355', // determined glint
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',
};
// ---- Pixel renderer ------------------------------------------------------
function Pixel({ art, palette, scale = 6, bg = null, style = {}, pixelated = true }) {
// art: array of strings, all same length. palette: { [char]: color }
const rows = art.length;
const cols = art[0]?.length ?? 0;
const rects = [];
for (let r = 0; r < rows; r++) {
let c = 0;
while (c < cols) {
const ch = art[r][c];
if (ch === '.' || ch === ' ') { c++; continue; }
// run-length merge same color horizontally
let run = 1;
while (c + run < cols && art[r][c + run] === ch) run++;
const color = palette[ch];
if (color) {
rects.push(<rect key={r+','+c} x={c} y={r} width={run} height={1} fill={color} shapeRendering="crispEdges" />);
}
c += run;
}
}
return (
<svg
width={cols * scale} height={rows * scale}
viewBox={`0 0 ${cols} ${rows}`}
style={{ display: 'block', imageRendering: pixelated ? 'pixelated' : 'auto', ...style }}
shapeRendering="crispEdges"
>
{bg && <rect x={0} y={0} width={cols} height={rows} fill={bg} />}
{rects}
</svg>
);
}
Object.assign(window, { NNTV, Pixel });
+250
View File
@@ -0,0 +1,250 @@
// Act backgrounds & level intro illustrations — wider pixel art scenes.
// Each act: 80×40 pixel strip evoking the environment.
// ── ACT 1: GARDEN (Levels 1-2) — cabbages, carrots in rows, moon ──────
const BG_GARDEN = [
'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',
'ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'...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',
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'.slice(0,80),
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'.slice(0,80),
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
];
const BG_GARDEN_PAL = {
m: NNTV.midnight, // sky
s: NNTV.moonlight, // stars
O: NNTV.cream, // moon edge
o: NNTV.moonGlow, // moon inner
L: NNTV.leafDark, // hedge
D: NNTV.mossGreen, // hedge shadow
g: NNTV.grass,
G: NNTV.grassDark,
d: NNTV.dirt,
C: NNTV.leafGreen, // cabbage leaves
f: NNTV.dirtDark, // deep soil
};
// ── ACT 2: VEGETABLE GARDEN WALLS (3-4) — brick wall + ivy ─────────────
const BG_WALLS = [
'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'.slice(0,80),
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'.slice(0,80),
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'.slice(0,80),
];
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 (5-6) — stone battlements + torch ─────────────────
const BG_FORTRESS = [
'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.',
];
const BG_FORTRESS_PAL = {
m: NNTV.midnight,
s: NNTV.stone,
S: NNTV.stoneDark,
f: NNTV.guardBlinking, // torch flame
F: NNTV.pumpkinOrange,
k: NNTV.ink,
d: NNTV.dirtDark,
};
// ── ACT 4: UNDERGROUND (7-8) — tunnel ──────────────────────────────────
const BG_UNDERGROUND = [
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'dDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDddd',
'dddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDd',
'dDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDddd',
'sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'.slice(0,80),
'sSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSs',
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmmfmmmmmmmmmmmmmmmmmmmmmmmfmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmFFmmmmmmmmmmmmmmmmmmmmmmFFmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmffmmmmmmmmmmmmmmmmmmmmmmffmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmSSs'.slice(0,80),
'sSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSs',
'sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'.slice(0,80),
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'dDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDddd',
'dddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDdddDDd',
];
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: ROYAL PALACE (9-11) — pillars + banner ──────────────────────
const BG_PALACE = [
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmsmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGmmm',
'mmGCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGmm',
'mGC..........pp.......................pp.......................pp............CGm'.slice(0,80),
'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',
'................................................................................',
'................................................................................',
];
const BG_PALACE_PAL = {
m: NNTV.midnight,
s: NNTV.cream,
G: NNTV.gold,
C: NNTV.goldDark,
p: NNTV.moonlight,
P: NNTV.silver,
};
// ── ACT 6: PRINCESS CHAMBER (12) — ominous light halo ─────────────────
const BG_CHAMBER = [
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyYYYYYYymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYCCYYYymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYCCCCYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYCCCCCYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYYYCCCCCYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYYYYYCCCCYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmmmyyyyYYYYYYYYYYYYCCCCYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmmmyyyyyYYYYYYYYYYYYYCCYYYYYYYYyyymmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmmmyyyyYYYYYYYYYYYYYYYYCCYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmmmyyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmmyyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyyymmmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmmyyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYyymmmmmmmmmmmmmmmmmmmmmmm',
'mmmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmmm',
'mmmmyyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmm',
'mmmyyyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYymmmmmmmmmmmmmmmmmmmmmm',
];
const BG_CHAMBER_PAL = {
m: NNTV.shadow,
y: NNTV.guardBlinkingOff,
Y: NNTV.gridLit,
C: NNTV.carrot,
};
const SCENES = {
garden: { art: BG_GARDEN, pal: BG_GARDEN_PAL, label: 'Act 1 · The Outskirts', desc: 'Levels 1–2 · moonlit garden hedge' },
walls: { art: BG_WALLS, pal: BG_WALLS_PAL, label: 'Act 2 · The Vegetable Garden', desc: 'Levels 3–4 · brick walls with ivy' },
fortress: { art: BG_FORTRESS, pal: BG_FORTRESS_PAL, label: 'Act 3 · The Fortress', desc: 'Levels 5–6 · battlements, flickering torches' },
underground: { art: BG_UNDERGROUND, pal: BG_UNDERGROUND_PAL, label: 'Act 4 · The Underground', desc: 'Levels 7–8 · tunnel, distant lamp' },
palace: { art: BG_PALACE, pal: BG_PALACE_PAL, label: 'Act 5 · The Royal Palace', desc: 'Levels 9–11 · gilded hall, silver pillars' },
chamber: { art: BG_CHAMBER, pal: BG_CHAMBER_PAL, label: 'Act 6 · The Princess Chamber', desc: 'Level 12 · expanding halo — unwinnable' },
};
Object.assign(window, { SCENES });
+338
View File
@@ -0,0 +1,338 @@
// Tile set, UI elements, icons, HUD, popups — all pixel art.
// 16×16 tiles for board cells, larger for UI pieces.
// ── TILES 16×16 ────────────────────────────────────────────────────────
const TILE_EMPTY = [
'aaaaaaaaaaaaaaaa',
'abbbbbbbbbbbbbba',
'abccccccccccccba',
'abcccccccccccccb',
'abcccdddccccdccb',
'abccdcccccccccbb',
'abcccccccccccccb',
'abcccccccccddcbb',
'abccdddcccccccbb',
'abccccccccccccbb',
'abccccdcccccccbb',
'abcccccccccccccb',
'abccccccccdccccb',
'abbbbbbbbbbbbbbb',
'aaaaaaaaaaaaaaaa',
'aaaaaaaaaaaaaaaa',
];
const TILE_EMPTY_PAL = { a: NNTV.gridBorder, b: NNTV.twilight, c: NNTV.gridEmpty, d: NNTV.dusk };
const TILE_WALL = [
'kkkkkkkkkkkkkkkk',
'kssssssstsssssrk',
'ksllllllstlllsrk',
'kslmmmmlstlmmsrk',
'ksllllllstlllsrk',
'ksssssssstssssrk',
'ktttttttttttttrk',
'kssssstsssssssrk',
'kslmmstlmmmlssrk',
'kslllstllllllsrk',
'kssssstsssssssrk',
'ksssssstssssssrk',
'kslmmlstmmlssrk.',
'ksllllstlllssrk.',
'ksssssstssssrrk.',
'kkkkkkkkkkkkkkkk',
];
const TILE_WALL_PAL = {
k: NNTV.ink,
s: NNTV.stone,
l: NNTV.stoneLight,
m: NNTV.stoneDark,
r: NNTV.stoneDark,
t: NNTV.ink,
};
const TILE_GOAL = [
'kkkkkkkkkkkkkkkk',
'kggggggggggggggk',
'kghhghhhghhhgggk',
'kgghghhhghhhhggk',
'kgggghhghhhhhggk',
'kgghghhhghhhhggk',
'kggghhhhhhhhhggk',
'kggghhwhwhhhhggk',
'kggghhhhhwhhhggk',
'kggghwhhhhhhhggk',
'kgghghhhwhhhhggk',
'kggghhhhhhhhhggk',
'kgghghhhhhhhhggk',
'kggghwhhhhwhhggk',
'kggggggggggggggk',
'kkkkkkkkkkkkkkkk',
];
const TILE_GOAL_PAL = { k: NNTV.ink, g: NNTV.gridGoal, h: '#00e763', w: '#7affb0' };
const TILE_LIT = [
'yyyyyyyyyyyyyyyy',
'ywwwwwwwwwwwwwwy',
'ywyyyyyyyyyyyywy',
'ywyyfffffffffywy',
'ywyfyyyyyyyyfywy',
'ywyfyffffffyfywy',
'ywyfyfyyyyyfyfwy',
'ywyfyfyffyyfyfwy',
'ywyfyfyffyyfyfwy',
'ywyfyfyyyyyfyfwy',
'ywyfyffffffyfywy',
'ywyfyyyyyyyyfywy',
'ywyyfffffffffywy',
'ywyyyyyyyyyyyywy',
'ywwwwwwwwwwwwwwy',
'yyyyyyyyyyyyyyyy',
];
const TILE_LIT_PAL = { y: NNTV.gridLit, w: '#fff88a', f: NNTV.gridLit };
const TILE_MIRROR = [
'........kk......',
'.......kmmk.....',
'......kmllmk....',
'.....kmllllmk...',
'....kmllwwllmk..',
'...kmllwllwllmk.',
'..kmllwlllwllmk.',
'.kmllwlllllwllmk',
'kmlwlllllllwllmk',
'.kmllwlllllwllmk',
'..kmllwlllwllmk.',
'...kmllwwwllmk..',
'....kmllllmk....',
'.....kmllmk.....',
'......kmmk......',
'.......kk.......',
];
const TILE_MIRROR_PAL = { k: NNTV.ink, l: NNTV.guardMirror, m: NNTV.lettuceDark, w: NNTV.cream };
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',
];
const TILE_PREVIEW_PAL = { y: NNTV.gridLit };
// ── HEART (lives) 16×16 ────────────────────────────────────────────────
const HEART_ART = [
'................',
'..kkkk....kkkk..',
'.krrrhk..krrrhk.',
'krrwrrrkkrrrrhk.',
'krwrrrrrrrrrrhk.',
'krrrrrrrrrrrrhk.',
'krrrrrrrrrrrhhk.',
'krrrrrrrrrrrhhk.',
'.krrrrrrrrrrhk..',
'.khrrrrrrrrhhk..',
'..khrrrrrrhhk...',
'...khrrrrhhk....',
'....khrrhhk.....',
'.....khhhk......',
'......khk.......',
'.......k........',
];
const HEART_ART_EMPTY = HEART_ART.map(r => r.replace(/[rwh]/g, 'e'));
const HEART_PAL = { k: NNTV.ink, r: NNTV.scarfRed, h: '#8a1a22', w: NNTV.cream, e: NNTV.dusk };
// ── LOGO / wordmark — NNTV stylized 80×24 ─────────────────────────────
const LOGO_ART = [
'................................................................................',
'...kk.............kk..................kkkkkkk.........kk................kk......',
'..kccck..........kccck..............kkcccccccckk......kccck............kcccck...',
'..kcc.ck.........kcc.ck.............kcc.......kcck....kccccck..........kcc.cck..',
'..kcc.cck........kcc.cck............kcc.........ck....kcc..cck........kcc...ck..',
'..kcc..cck.......kcc..cck...........kcc.........ck....kcc...ck........kcc...ck..',
'..kcc..cck.......kcc..cck...........kcc.........ck....kcc...ck........kcc...ck..',
'..kcc...cck......kcc...cck..........kcc.........ck....kcc...ck........kcc..cck..',
'..kcc...ccck.....kcc...ccck.........kcc.........ck....kcc..cck........kcccccck..',
'..kcc....cck.....kcc....cck.........kcc.........ck....kccccck.........kcc..cck..',
'..kcc....ccck....kcc....ccck........kcc.........ck....kcccck..........kcc..cck..',
'..kcc.....cck....kcc.....cck........kcc.........ck....kcc.ck..........kcc...ck..',
'..kcc.....ccck...kcc.....ccck.......kcc.........ck....kcc.cck.........kcc...ck..',
'..kcc......cck...kcc......cck.......kcc.........ck....kcc..ck.........kcc...ck..',
'..kcc......ccck..kcc......ccck......kcc.........ck....kcc..cck........kcc..cck..',
'..kcc.......cck..kcc.......cck......kcc.......kcck....kcc...ck........kcc.cck...',
'..kcc.......cccckcc.......cccck.....kkcccccccckkk.....kcc...cck........kcccck...',
'..kcc........ccckcc........ccck......kkkkkkkkk........kcc....ck.........kcck....',
'..kcc.........ckkcc.........ckk......................kcc....ck..........kck.....',
'..kcc..........kcc..........k........................kcc....cck..........k......',
'..kkk...........kkk.................................kkkk....kkk..................',
'................................................................................',
'................................................................................',
'................................................................................',
];
const LOGO_PAL = { k: NNTV.ink, c: NNTV.moonlight };
// ── MOON 20×20 ─────────────────────────────────────────────────────────
const MOON_ART = [
'........kkkkk.......',
'......kkggghhk......',
'.....kgggghhhhk.....',
'....kggghhhhhhhk....',
'...kggghhhhhhhhhk...',
'...kgghhhhhhhhhhk...',
'..kggghhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kgghhhhhhhhhhhhk..',
'..kggghhhhhhhhhhhk..',
'...kgghhhhhhhhhhk...',
'...kggghhhhhhhhhk...',
'....kggghhhhhhhk....',
'.....kgggghhhhk.....',
'......kkggghhk......',
'........kkkkk.......',
'....................',
];
const MOON_PAL = { k: NNTV.ink, g: NNTV.cream, h: NNTV.moonGlow };
// ── ICONS 16×16 ────────────────────────────────────────────────────────
const mkIcon = (s) => s.split('\n').filter(Boolean).map(r => r.padEnd(16, '.').slice(0,16));
const ICON_PAL = { k: NNTV.ink, w: NNTV.moonlight, a: NNTV.scarfRed };
const ICON_UNDO = mkIcon(`
................
................
.....wwww.......
....w....w......
...w......w.....
..w...wwww.w....
..w..w.....w....
..w..w..........
.wwww.w.........
.wwwww..........
..www...........
...w............
................
................
................
................`);
const ICON_REDO = mkIcon(`
................
................
.......wwww.....
......w....w....
.....w......w...
....w.wwww...w..
....w.....w..w..
..........w..w..
.........w.wwww.
..........wwwww.
...........www..
............w...
................
................
................
................`);
const ICON_EYE = mkIcon(`
................
................
...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....
................
................
................
................`);
const ICON_PAUSE = mkIcon(`
................
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
...ww.....ww....
................
................
................
................`);
const ICON_SETTINGS = mkIcon(`
................
.....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.......
................`);
const ICON_LANG = mkIcon(`
................
.....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.......
................
................`);
const ICON_ARROW = mkIcon(`
................
................
.......w........
......ww........
.....www........
....wwww........
...wwwww........
..wwwwwwwwww....
...wwwww........
....wwww........
.....www........
......ww........
.......w........
................
................
................`);
Object.assign(window, {
TILE_EMPTY, TILE_EMPTY_PAL, TILE_WALL, TILE_WALL_PAL, TILE_GOAL, TILE_GOAL_PAL,
TILE_LIT, TILE_LIT_PAL, TILE_MIRROR, TILE_MIRROR_PAL, TILE_PREVIEW, TILE_PREVIEW_PAL,
HEART_ART, HEART_ART_EMPTY, HEART_PAL,
LOGO_ART, LOGO_PAL, MOON_ART, MOON_PAL,
ICON_UNDO, ICON_REDO, ICON_EYE, ICON_PAUSE, ICON_SETTINGS, ICON_LANG, ICON_ARROW, ICON_PAL,
});