M0
Some checks failed
CI / validate (push) Has been cancelled

This commit is contained in:
2026-03-22 23:33:24 -05:00
parent 27dac51b5c
commit 177be6332b
47 changed files with 7287 additions and 0 deletions

28
client/src/App.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { Hud } from "./components/Hud";
import { GameShell } from "./game/GameShell";
export default function App() {
return (
<main className="min-h-screen bg-ink text-slate-100">
<div className="mx-auto flex min-h-screen max-w-7xl flex-col px-4 py-6 sm:px-6 lg:px-8">
<header className="mb-6 flex items-end justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.3em] text-glow/80">Stellar</p>
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">
Become the gravity well.
</h1>
</div>
<div className="max-w-sm text-right text-sm text-slate-300">
M0 scaffold: UI shell, Matter.js mount point, and API-ready project structure.
</div>
</header>
<div className="grid flex-1 gap-6 lg:grid-cols-[1fr_320px]">
<GameShell />
<Hud />
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,11 @@
import { render, screen } from "@testing-library/react";
import { Hud } from "./Hud";
describe("Hud", () => {
it("renders the boot sequence panel", () => {
render(<Hud />);
expect(screen.getByText("Boot Sequence")).toBeTruthy();
expect(screen.getByText("Bootstrap Matter.js world")).toBeTruthy();
});
});

View File

@@ -0,0 +1,47 @@
const objectives = [
"Bootstrap Matter.js world",
"Wire player state to the API",
"Start M1 absorber loop"
];
export function Hud() {
return (
<aside className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-2xl shadow-cyan-950/30 backdrop-blur">
<div className="mb-6">
<p className="text-xs uppercase tracking-[0.3em] text-glow/70">Mission Board</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Boot Sequence</h2>
</div>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-2xl bg-black/20 p-3">
<dt className="text-slate-400">Mass</dt>
<dd className="mt-1 text-xl font-semibold">12</dd>
</div>
<div className="rounded-2xl bg-black/20 p-3">
<dt className="text-slate-400">Tier</dt>
<dd className="mt-1 text-xl font-semibold">Dustling</dd>
</div>
<div className="rounded-2xl bg-black/20 p-3">
<dt className="text-slate-400">Pull Radius</dt>
<dd className="mt-1 text-xl font-semibold">48m</dd>
</div>
<div className="rounded-2xl bg-black/20 p-3">
<dt className="text-slate-400">Mode</dt>
<dd className="mt-1 text-xl font-semibold">Sandbox</dd>
</div>
</dl>
<div className="mt-6">
<p className="text-xs uppercase tracking-[0.3em] text-amber-300/70">Next Steps</p>
<ul className="mt-3 space-y-3 text-sm text-slate-200">
{objectives.map((objective) => (
<li key={objective} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
{objective}
</li>
))}
</ul>
</div>
</aside>
);
}

View File

@@ -0,0 +1,27 @@
import { useEffect, useRef } from "react";
import { initPlaceholderScene } from "./placeholderScene";
export function GameShell() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
return initPlaceholderScene(canvas);
}, []);
return (
<section className="relative overflow-hidden rounded-[2rem] border border-white/10 bg-nebula shadow-2xl shadow-slate-950/40">
<canvas ref={canvasRef} className="h-[560px] w-full" />
<div className="pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between p-4 text-xs uppercase tracking-[0.25em] text-slate-300/70">
<span>Simulation viewport</span>
<span>Placeholder render</span>
</div>
</section>
);
}

View File

@@ -0,0 +1,69 @@
export function initPlaceholderScene(canvas: HTMLCanvasElement) {
const context = canvas.getContext("2d");
if (!context) {
return;
}
let animationFrame = 0;
const resize = () => {
const rect = canvas.getBoundingClientRect();
canvas.width = Math.floor(rect.width * window.devicePixelRatio);
canvas.height = Math.floor(rect.height * window.devicePixelRatio);
context.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
};
const render = (time: number) => {
const width = canvas.width / window.devicePixelRatio;
const height = canvas.height / window.devicePixelRatio;
const cx = width / 2;
const cy = height / 2;
context.clearRect(0, 0, width, height);
const gradient = context.createRadialGradient(cx, cy, 24, cx, cy, Math.max(width, height) * 0.45);
gradient.addColorStop(0, "rgba(14, 165, 233, 0.35)");
gradient.addColorStop(0.35, "rgba(15, 23, 42, 0.4)");
gradient.addColorStop(1, "rgba(2, 6, 23, 1)");
context.fillStyle = gradient;
context.fillRect(0, 0, width, height);
for (let i = 0; i < 60; i += 1) {
const orbit = 40 + i * 6;
const angle = time * 0.0002 * (1 + i * 0.01) + i;
const x = cx + Math.cos(angle) * orbit;
const y = cy + Math.sin(angle) * orbit * 0.55;
const size = 1 + (i % 3);
context.fillStyle = i % 8 === 0 ? "#f97316" : "#e2e8f0";
context.beginPath();
context.arc(x, y, size, 0, Math.PI * 2);
context.fill();
}
context.fillStyle = "#020617";
context.beginPath();
context.arc(cx, cy, 38, 0, Math.PI * 2);
context.fill();
context.strokeStyle = "rgba(125, 211, 252, 0.45)";
context.lineWidth = 8;
context.beginPath();
context.arc(cx, cy, 54 + Math.sin(time * 0.002) * 4, 0, Math.PI * 2);
context.stroke();
animationFrame = window.requestAnimationFrame(render);
};
resize();
animationFrame = window.requestAnimationFrame(render);
window.addEventListener("resize", resize);
return () => {
window.cancelAnimationFrame(animationFrame);
window.removeEventListener("resize", resize);
};
}

11
client/src/main.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles/global.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: dark;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background:
radial-gradient(circle at top, rgba(14, 165, 233, 0.2), transparent 35%),
linear-gradient(180deg, #020617 0%, #0f172a 100%);
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
* {
box-sizing: border-box;
}
canvas {
display: block;
}

View File

@@ -0,0 +1,2 @@
export const GAME_TITLE = "Stellar";

View File

@@ -0,0 +1 @@
export {};