mirror of
https://github.com/tiennm99/nntv.git
synced 2026-10-16 06:14:38 +00:00
feat: add design canvas assets from claude design
This commit is contained in:
1 parent
286edc3c1d
commit
6a1b3ec572
8 files changed
+1828
No files matched your search
@@ -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>
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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 />);
|
||||
@@ -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 });
|
||||
@@ -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 });
|
||||
@@ -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 });
|
||||
@@ -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,
|
||||
});
|
||||
Reference in new issue
Block a user