Files
ZacharyZhang-NYandClaude Opus 5.5 2d7af224dd
CI / Typecheck, lint, tests, e2e (push) Failing after 3m51s
Website: 16-bit pixel-art rebuild with a live Three.js office, downloads from Gitea v1.5.1
- Title screen over a real-time voxel office rendered at low resolution and quantized to a rotating 32-colour palette with Bayer dithering; day part and season re-light the scene and every surface.
- 16-bit UI kit: name-plate dialogs, menu cursor, inventory, XP bar, badges, NPC speech, GAME OVER with CONTINUE, hand-placed 12x12 icons, Fusion Pixel font (Latin subset preloaded).
- Sections as a game: quest log (onboarding), character select (AI CEO / AI Co-Founder), inventory (features), party roster (providers), live download XP, save point (all v1.5.1 builds), FAQ as NPC dialog, credits.
- Downloads and source point to the Gitea release; the download function only redirects to git.zacharyzhang.com.
- Tailwind and the old components removed; unit tests for platform/downloads, rewritten e2e suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:37:23 -04:00

158 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
BasicShadowMap, Camera, HalfFloatType, Mesh, NearestFilter, OrthographicCamera, PlaneGeometry, Scene, Vector2,
WebGLRenderer, WebGLRenderTarget,
} from "three";
import type { DayPart } from "../../lib/palette";
import { createAgents } from "./agents";
import { applyLighting, createLights } from "./lighting";
import { buildOffice } from "./office";
import { createQuantizeMaterial, setQuantizePalette } from "./quantize";
import { createSky, setSkyPalette } from "./sky";
import { UNIT_BOX, createInks, paintInks } from "./voxel";
export interface World {
setPalette(colours: string[], part: DayPart): void;
resize(width: number, height: number): void;
setPointer(x: number, y: number): void;
setScroll(progress: number): void;
setFocusRight(right: boolean): void;
setRunning(running: boolean): void;
render(): void;
dispose(): void;
}
const ART_PIXELS = 320 * 180;
const MAX_DPR = 2;
const TARGET = { x: 1.6, y: 0.2, z: 0.4 };
const RIGHT_SHARE = 0.55;
/** Real-time office diorama drawn at ~320×180, then palette-quantized onto the canvas. */
export function createWorld(canvas: HTMLCanvasElement): World {
const renderer = new WebGLRenderer({ canvas, antialias: false, alpha: false, powerPreference: "low-power" });
renderer.setPixelRatio(1);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = BasicShadowMap;
const target = new WebGLRenderTarget(320, 180, { type: HalfFloatType, minFilter: NearestFilter, magFilter: NearestFilter });
const quantize = createQuantizeMaterial(target.texture);
const post = new Scene();
const postQuad = new Mesh(new PlaneGeometry(2, 2), quantize);
postQuad.frustumCulled = false;
post.add(postQuad);
const postCamera = new Camera();
const scene = new Scene();
const inks = createInks();
const office = buildOffice(inks);
const agents = createAgents(inks);
const lights = createLights();
const sky = createSky();
scene.add(sky, office.root, agents.root, lights.sun, lights.sun.target, lights.sky);
const camera = new OrthographicCamera(-1, 1, 1, -1, 1, 120);
const pointer = new Vector2();
const eased = new Vector2();
let scroll = 0;
let focusRight = false;
let aspect = 16 / 9;
let time = 0;
let last = 0;
let frame = 0;
function place(): void {
eased.lerp(pointer, 0.06);
const yaw = Math.PI / 4 + Math.sin(time * 0.08) * 0.16 + eased.x * 0.12;
const pitch = 0.56 + eased.y * 0.05;
camera.position.set(
TARGET.x + Math.sin(yaw) * Math.cos(pitch) * 50,
TARGET.y + Math.sin(pitch) * 50,
TARGET.z + Math.cos(yaw) * Math.cos(pitch) * 50,
);
camera.lookAt(TARGET.x, TARGET.y, TARGET.z);
const share = focusRight ? RIGHT_SHARE : 1;
const half = Math.max(10.6, 13 / (aspect * share)) / (1 + scroll * 0.18);
const width = half * aspect * 2;
const centre = focusRight ? 1 - share / 2 : 0.5;
camera.top = half;
camera.bottom = -half;
camera.left = -width * centre;
camera.right = width * (1 - centre);
camera.updateProjectionMatrix();
(sky.material.uniforms.offset!.value as Vector2).set(-eased.x * 14 - Math.sin(time * 0.08) * 16, 0);
sky.material.uniforms.time!.value = time;
}
function render(): void {
place();
office.update(time);
renderer.setRenderTarget(target);
renderer.render(scene, camera);
renderer.setRenderTarget(null);
renderer.render(post, postCamera);
}
function tick(now: number): void {
const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;
last = now;
time += dt;
agents.update(dt, time);
render();
frame = requestAnimationFrame(tick);
}
return {
setPalette(colours, part) {
paintInks(inks, colours);
setSkyPalette(sky.material, colours, part);
setQuantizePalette(quantize, colours);
applyLighting(lights, office, colours, part);
},
resize(width, height) {
const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
const deviceWidth = width * dpr;
const deviceHeight = height * dpr;
const scale = Math.max(1, Math.round(Math.sqrt((deviceWidth * deviceHeight) / ART_PIXELS)));
const artWidth = Math.ceil(deviceWidth / scale);
const artHeight = Math.ceil(deviceHeight / scale);
aspect = artWidth / artHeight;
renderer.setSize(artWidth, artHeight, false);
target.setSize(artWidth, artHeight);
(sky.material.uniforms.resolution!.value as Vector2).set(artWidth, artHeight);
canvas.style.width = `${(artWidth * scale) / dpr}px`;
canvas.style.height = `${(artHeight * scale) / dpr}px`;
},
setPointer(x, y) {
pointer.set(x, y);
},
setScroll(progress) {
scroll = progress;
},
setFocusRight(right) {
focusRight = right;
},
setRunning(running) {
cancelAnimationFrame(frame);
last = 0;
if (running) frame = requestAnimationFrame(tick);
},
render() {
eased.copy(pointer);
render();
},
dispose() {
cancelAnimationFrame(frame);
office.dispose();
inks.forEach((material) => material.dispose());
UNIT_BOX.dispose();
sky.geometry.dispose();
sky.material.dispose();
postQuad.geometry.dispose();
quantize.dispose();
lights.sun.dispose();
lights.sky.dispose();
target.dispose();
renderer.dispose();
},
};
}