Initial scaffold and plan

This commit is contained in:
Feror 2026-04-25 18:22:52 +02:00
commit 7a61c9d27d
9 changed files with 643 additions and 0 deletions

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

106
AGENTS.md Normal file
View file

@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.

106
CLAUDE.md Normal file
View file

@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# multiportfolio
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.12. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

26
bun.lock Normal file
View file

@ -0,0 +1,26 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "multiportfolio",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
}
}

View file

@ -0,0 +1,313 @@
# Multiplayer Portfolio Plan
## Project Summary
This project is a portfolio presented as a cozy low-poly 3D village with light game mechanics. Visitors enter the world as small characters, explore project areas, discover hidden details, and eventually understand the work, personality, and experience behind the portfolio.
The defining feature is multiplayer presence: visitors should be able to see other people walking around the portfolio in real time. Many portfolios use 3D environments, but this one should feel like a shared place rather than a private interactive page.
The first playable version does not need full multiplayer immediately. The core world, controls, camera, navigation, and project presentation should be stable first. Multiplayer should still influence technical choices from the beginning so it can be added without rewriting the whole application.
## Experience Goals
- Feel like a small game world inspired by Pokemon and Animal Crossing.
- Present portfolio and resume information through exploration instead of static pages.
- Make each project feel like a distinct place in the city, with its own layout, mood, props, and interactions.
- Let visitors choose an avatar and enter a display name before entering the world.
- Spawn visitors at a central train station.
- Use a train arrival as the in-world transition into the portfolio.
- Keep the tone cozy, readable, and approachable rather than realistic or visually noisy.
- Add hidden details, secrets, or optional discoveries that reward exploration.
- Eventually support proximity voice chat, but only after the basic world and multiplayer presence work well.
## World Concept
The portfolio is a low-poly village or small city. The map is navigated from a fixed top-down camera angle. The camera always stays south of the player and looks north, creating a readable Pokemon-like orientation while still using 3D depth.
The train station sits near the center of the map and acts as the arrival point. A railroad runs east to west across the city, slightly elevated above the ground and supported by pillars. Players can walk underneath the tracks. When a new visitor joins, a train arrives at the station and visually drops them off.
Each major project gets its own area. Instead of every project being represented by the same kind of panel, each space should be designed around the project's identity. For example, a web app could be a small office or control room, a game could be an arcade or park, and a technical tool could be a workshop.
## Main User Flow
1. The visitor opens the portfolio.
2. The visitor sees an entry screen where they choose an avatar and enter a display name.
3. The visitor confirms and waits briefly while the world loads.
4. A train arrives at the central station.
5. The visitor's character appears or steps out near the platform.
6. The visitor can move around with keyboard controls.
7. The visitor explores project areas and interacts with project-specific objects.
8. Other connected visitors are visible as characters with names.
9. Hidden details reward visitors who explore beyond the obvious route.
## Controls And Camera
Movement should support:
- `WASD`
- `ZQSD` for AZERTY keyboards
- Arrow keys
The camera should:
- Follow the local player.
- Keep a fixed orientation.
- Stay south of the player and look north.
- Use a top-down angle inspired by classic handheld games.
- Avoid free orbit controls in the main experience.
The player should not need to understand 3D navigation controls. Movement should feel immediate and simple.
## Technical Direction
### Runtime And Server
Use Bun as the runtime and server layer.
- Use `Bun.serve()` for HTTP routes.
- Use Bun's built-in WebSocket support for realtime multiplayer state.
- Avoid Express and separate WebSocket libraries.
- Keep the first backend simple: connection management, player join/leave, player movement broadcasts, and eventually room/session state.
### Frontend Stack
React Three Fiber is a good candidate for the frontend because it provides a React-friendly way to build Three.js scenes. It should be adequate for this project if the scene is kept modular and performance is considered early.
Use React Three Fiber for:
- Scene composition.
- Player character components.
- Project area components.
- Camera follow behavior.
- Interaction triggers.
- Loading and rendering GLTF or generated assets.
Use plain Three.js APIs where needed for lower-level control, especially for collision helpers, animation mixers, raycasting, or performance-sensitive systems.
### Multiplayer Model
The first multiplayer version should be intentionally simple.
The server should own:
- Connected clients.
- Player ids.
- Display names.
- Avatar choices.
- Latest known positions.
- Join and leave events.
The client should own:
- Local input.
- Local prediction or immediate movement feel.
- Rendering remote players.
- Interpolation of remote player movement.
The server does not need full authoritative physics at the beginning. It can validate basic position updates later if cheating or griefing becomes relevant.
### Suggested Message Types
Initial WebSocket messages can be shaped around events like:
- `join`: client sends display name and avatar choice.
- `welcome`: server returns player id and current world snapshot.
- `player_joined`: server announces a new visible visitor.
- `player_left`: server announces a disconnected visitor.
- `player_move`: client sends current position and facing direction.
- `player_update`: server broadcasts another player's position.
- `train_arrival`: server or client triggers the arrival sequence for a new visitor.
These message names are only a starting point. The important rule is to keep messages typed, explicit, and easy to evolve.
## Core Systems
### Player
- Local player movement.
- Remote player rendering.
- Display name labels.
- Avatar selection.
- Idle and walk animations.
- Spawn position at the train station.
- Facing direction based on movement.
### World
- Low-poly terrain and paths.
- Central train station.
- Elevated east-west railway.
- Pillars supporting the railway.
- Walkable area under the railway.
- Project districts.
- Boundaries and collision.
- Decorative props that reinforce the village mood.
### Project Areas
Each project area should include:
- A distinct visual setup.
- A clear project name.
- A short in-world interaction.
- A way to open deeper project details.
- Links to live demos, source code, case studies, or media when relevant.
- Optional hidden details for curious visitors.
Project presentation should avoid feeling like a generic modal gallery. The city layout should communicate the portfolio structure.
### Interactions
Possible interaction types:
- Read a sign.
- Open a project panel.
- Enter a project zone.
- Trigger a small animation.
- Discover a hidden object.
- Follow a path to a themed project area.
Interactions should be simple in the first version. A single key such as `E` near an object is enough.
### Train Arrival
The train arrival is both a narrative detail and a loading transition.
First version:
- Show the train arriving when the local visitor enters the world.
- Spawn the player at or near the platform.
- Let the train leave after a short moment.
Later multiplayer version:
- Other visitors can see the train arrive when someone joins.
- The new visitor appears as the train stops.
- The sequence should not block existing players from moving.
## Build Checklist
### Phase 1: Project Foundation
- [ ] Decide and install the frontend dependencies.
- [ ] Add a Bun server entry point using `Bun.serve()`.
- [ ] Serve the frontend through Bun.
- [ ] Add a basic React app.
- [ ] Add React Three Fiber and render a minimal Three.js scene.
- [ ] Define the main folder structure for client, server, shared types, and assets.
- [ ] Add TypeScript types for core game objects.
### Phase 2: Single-Player Prototype
- [ ] Create a simple ground plane.
- [ ] Add a temporary player character.
- [ ] Implement keyboard movement with `WASD`, `ZQSD`, and arrow keys.
- [ ] Add fixed-orientation camera follow.
- [ ] Add basic collision boundaries.
- [ ] Add player facing direction.
- [ ] Add a temporary spawn point at the future train station.
- [ ] Confirm the game is readable from the chosen camera angle.
### Phase 3: Village Blockout
- [ ] Block out the central train station.
- [ ] Add an east-west railway.
- [ ] Elevate the railway above the ground.
- [ ] Add support pillars.
- [ ] Ensure the player can walk under the railway.
- [ ] Add paths from the station to project areas.
- [ ] Add placeholder project zones.
- [ ] Add map boundaries and visible landmarks.
### Phase 4: Portfolio Interaction Layer
- [ ] Add interactable objects.
- [ ] Add an interaction prompt near usable objects.
- [ ] Add project information panels.
- [ ] Add external links for project demos or source code.
- [ ] Add project-specific props or themed layouts.
- [ ] Add at least one hidden object or optional discovery.
- [ ] Add a lightweight resume or about-me area.
### Phase 5: Entry And Avatar Selection
- [ ] Build the first screen before entering the world.
- [ ] Let the visitor enter a display name.
- [ ] Let the visitor choose from a small set of placeholder avatars.
- [ ] Store the chosen name and avatar in client state.
- [ ] Start the world after confirmation.
- [ ] Spawn the player at the train station.
- [ ] Trigger the local train arrival sequence.
### Phase 6: Multiplayer Presence
- [ ] Add WebSocket upgrade handling in `Bun.serve()`.
- [ ] Track connected players on the server.
- [ ] Send a `welcome` message with the assigned player id.
- [ ] Broadcast new players to existing clients.
- [ ] Remove disconnected players from the world.
- [ ] Send local player position updates to the server.
- [ ] Broadcast remote player updates to other clients.
- [ ] Render remote visitors with names and avatars.
- [ ] Smooth remote movement with interpolation.
- [ ] Make train arrivals visible when a new visitor joins.
### Phase 7: Polish And Performance
- [ ] Replace temporary character with a designed low-poly avatar.
- [ ] Add simple walk and idle animations.
- [ ] Improve lighting, shadows, and color palette.
- [ ] Add low-poly trees, benches, signs, lamps, fences, and station details.
- [ ] Optimize scene geometry and materials.
- [ ] Add loading states.
- [ ] Add mobile or touch fallback if desired.
- [ ] Add accessibility considerations for project panels and links.
- [ ] Test multiple simultaneous browser sessions.
### Phase 8: Later Features
- [ ] Add proximity text chat.
- [ ] Add proximity voice chat.
- [ ] Add persistent visitor preferences.
- [ ] Add seasonal or time-based world changes.
- [ ] Add more secrets and optional interactions.
- [ ] Add analytics for visited project areas.
- [ ] Add server-side validation for movement.
- [ ] Add multiple spawn animations or arrival variants.
## Early Implementation Priorities
The first milestone should prove that the experience feels good before building too many assets.
Recommended first milestone:
- Bun server serves the frontend.
- React Three Fiber renders a small test map.
- A placeholder player moves with keyboard controls.
- The camera follows from the correct fixed angle.
- A rough train station and elevated track exist.
- One placeholder project area can be reached and interacted with.
After that, the multiplayer layer can be added while the world is still simple. This keeps the defining feature visible early without forcing every visual detail to be complete first.
## Open Design Questions
- How large should the city be for the first public version?
- How many projects should be represented at launch?
- Should project panels pause movement, or should the world continue behind them?
- Should visitors collide with each other, or pass through each other?
- Should hidden discoveries be purely decorative, or should they reveal extra portfolio content?
- Should the train arrival be a short cutscene, a background event, or something the player can move during?
- Should the avatar style be human, animal-like, or abstract?
## Definition Of Done For The First Public Version
- A visitor can choose a name and avatar.
- A visitor arrives at the central train station.
- A visitor can move around the village with keyboard controls.
- The camera feels intentional and readable.
- At least three project areas exist and feel visually distinct.
- Each project area communicates what the project is and links to deeper material.
- Multiple visitors can see each other in real time.
- Joining and leaving is handled cleanly.
- The experience feels like a coherent place, not only a 3D menu.

1
index.ts Normal file
View file

@ -0,0 +1 @@
console.log("Hello via Bun!");

12
package.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "multiportfolio",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

30
tsconfig.json Normal file
View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}