From af5e31269c90f7778a9e9d9e3edc2a490e074bce Mon Sep 17 00:00:00 2001 From: Ernie Cook Date: Mon, 2 Mar 2026 09:45:58 -0500 Subject: [PATCH] Add Chamber page with interactive voidlings and localStorage persistence - Set up React Router for navigation between landing and Chamber pages - Create Chamber page with floating animated voidlings - Add ability to summon, name, rename, and release voidlings - Implement mood system (idle, happy, curious, sleepy) - Add localStorage persistence to remember voidlings between sessions - Add float animation and visual effects --- WORKLOG.md | 21 ++-- package-lock.json | 56 +++++++++- package.json | 3 +- src/App.tsx | 42 ++++++-- src/index.css | 13 +++ src/pages/Chamber.tsx | 232 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 353 insertions(+), 14 deletions(-) create mode 100644 src/pages/Chamber.tsx diff --git a/WORKLOG.md b/WORKLOG.md index f9763c3..51ec671 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -9,6 +9,10 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a - [x] Project initialized with Vite + React + TypeScript + TailwindCSS - [x] Basic project structure in place - [x] First "Hello World" page running +- [x] React Router added for navigation +- [x] Main Chamber view created with floating voidlings +- [x] Ability to name and rename voidlings +- [x] localStorage persistence for voidlings ### In Progress (None yet) @@ -17,14 +21,19 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a - Initialized Vite + React + TypeScript project - Added TailwindCSS configuration - Created "The Void Chamber" landing page with interactive voidlings +- Added React Router for navigation between landing and Chamber pages +- Created Chamber page with floating animated voidlings +- Added ability to summon, name, rename, and release voidlings +- Added localStorage persistence to remember voidlings between sessions +- Added mood system (idle, happy, curious, sleepy) with click interaction ### Next Steps -1. Add React Router for navigation between pages -2. Create a main "Chamber" view where voidlings live -3. Add ability to name and interact with voidlings -4. Add persistence (localStorage) to remember your voidlings -5. Add animations and visual effects +1. Add more visual effects (particles, glows) +2. Add voidling interactions (feeding, playing) +3. Add sound effects +4. Add voidling types/classes with different behaviors +5. Add animations for different moods --- -*Last updated: Session 1 complete - Project initialized* +*Last updated: Session 2 complete - Chamber view with interactive voidlings added* diff --git a/package-lock.json b/package-lock.json index 278a47b..e9dbefa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -2107,6 +2108,18 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3230,6 +3243,42 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3297,6 +3346,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index eff879d..cf2fb0f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/App.tsx b/src/App.tsx index 0f8c4c5..f4c6e27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,10 @@ import { useState } from 'react' +import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom' +import Chamber from './pages/Chamber' -function App() { +function Landing() { const [greeted, setGreeted] = useState(false) + const navigate = useNavigate() const voidlings = [ { name: 'Glip', color: 'bg-purple-400' }, @@ -11,23 +14,28 @@ function App() { return (
-

+
+
+
+
+ +

The Void Chamber

-

+

A pocket dimension for your digital companions

{!greeted ? ( ) : ( -
+
{voidlings.map((v) => (
)} -

+

+ {greeted && ( + + )} +
+ +

{greeted ? 'Your voidlings have awakened!' : 'Click to summon your first companions'}

) } +function App() { + return ( + + + } /> + } /> + + + ) +} + export default App diff --git a/src/index.css b/src/index.css index e21ab22..88c1736 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,18 @@ @import "tailwindcss"; +@keyframes float { + 0%, 100% { + transform: translateY(0) translate(-50%, -50%); + } + 50% { + transform: translateY(-20px) translate(-50%, -50%); + } +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + :root { font-family: 'Courier New', Courier, monospace; } diff --git a/src/pages/Chamber.tsx b/src/pages/Chamber.tsx new file mode 100644 index 0000000..974db2f --- /dev/null +++ b/src/pages/Chamber.tsx @@ -0,0 +1,232 @@ +import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' + +interface Voidling { + id: string + name: string + color: string + mood: 'idle' | 'happy' | 'curious' | 'sleepy' + x: number + y: number +} + +const VOIDLING_COLORS = [ + 'bg-purple-400', + 'bg-cyan-400', + 'bg-pink-400', + 'bg-emerald-400', + 'bg-amber-400', + 'bg-violet-400', +] + +const MOOD_EMOJIS = { + idle: '◉', + happy: '✧', + curious: '❓', + sleepy: '💤', +} + +function getRandomColor() { + return VOIDLING_COLORS[Math.floor(Math.random() * VOIDLING_COLORS.length)] +} + +function getRandomPosition() { + return { + x: Math.random() * 80 + 10, + y: Math.random() * 60 + 20, + } +} + +function getRandomMood(): Voidling['mood'] { + const moods: Voidling['mood'][] = ['idle', 'happy', 'curious', 'sleepy'] + return moods[Math.floor(Math.random() * moods.length)] +} + +function generateId() { + return Math.random().toString(36).substring(2, 9) +} + +export default function Chamber() { + const [voidlings, setVoidlings] = useState([]) + const [namingVoidling, setNamingVoidling] = useState(null) + const [newName, setNewName] = useState('') + + useEffect(() => { + const saved = localStorage.getItem('voidlings') + if (saved) { + try { + setVoidlings(JSON.parse(saved)) + } catch { + createStarterVoidlings() + } + } else { + createStarterVoidlings() + } + }, []) + + useEffect(() => { + localStorage.setItem('voidlings', JSON.stringify(voidlings)) + }, [voidlings]) + + function createStarterVoidlings() { + const starters: Voidling[] = [ + { id: generateId(), name: 'Glip', color: 'bg-purple-400', mood: 'idle', ...getRandomPosition() }, + { id: generateId(), name: 'Floop', color: 'bg-cyan-400', mood: 'happy', ...getRandomPosition() }, + { id: generateId(), name: 'Norf', color: 'bg-pink-400', mood: 'curious', ...getRandomPosition() }, + ] + setVoidlings(starters) + } + + function spawnVoidling() { + const pos = getRandomPosition() + const newVoidling: Voidling = { + id: generateId(), + name: '???', + color: getRandomColor(), + mood: getRandomMood(), + x: pos.x, + y: pos.y, + } + setVoidlings([...voidlings, newVoidling]) + setNamingVoidling(newVoidling.id) + setNewName('') + } + + function startNaming(id: string) { + setNamingVoidling(id) + const v = voidlings.find(v => v.id === id) + setNewName(v?.name === '???' ? '' : v?.name || '') + } + + function finishNaming(id: string) { + if (newName.trim()) { + setVoidlings(voidlings.map(v => + v.id === id ? { ...v, name: newName.trim() } : v + )) + } + setNamingVoidling(null) + setNewName('') + } + + function changeMood(id: string) { + const moods: Voidling['mood'][] = ['idle', 'happy', 'curious', 'sleepy'] + setVoidlings(voidlings.map(v => { + if (v.id === id) { + const currentIndex = moods.indexOf(v.mood) + const nextMood = moods[(currentIndex + 1) % moods.length] + return { ...v, mood: nextMood } + } + return v + })) + } + + function releaseVoidling(id: string) { + setVoidlings(voidlings.filter(v => v.id !== id)) + } + + return ( +
+
+
+
+
+
+ +
+
+ + ← Back to Portal + +

+ The Void Chamber +

+ +
+ +
+ {voidlings.length === 0 && ( +
+

The chamber feels empty... Summon a voidling to begin.

+
+ )} + + {voidlings.map((voidling) => ( +
changeMood(voidling.id)} + onDoubleClick={() => startNaming(voidling.id)} + > +
+ {MOOD_EMOJIS[voidling.mood]} +
+ +
+ {namingVoidling === voidling.id ? ( + setNewName(e.target.value)} + onBlur={() => finishNaming(voidling.id)} + onKeyDown={(e) => { + if (e.key === 'Enter') finishNaming(voidling.id) + }} + className="bg-zinc-800 border border-zinc-600 rounded-lg px-2 py-1 text-xs text-center w-24 focus:outline-none focus:border-purple-400" + onClick={(e) => e.stopPropagation()} + /> + ) : ( + + {voidling.name} + + )} +
+ + +
+ ))} +
+ +
+

Click voidling to change mood • Double-click to rename • Click × to release

+

Your voidlings: {voidlings.length}

+
+
+
+ ) +}