2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -34,4 +34,4 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker compose -f docker/docker-compose.yml build
|
||||
run: docker compose build
|
||||
|
||||
@@ -23,3 +23,4 @@ COPY --from=builder /app/client/dist ./client/dist
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server/dist/server.js"]
|
||||
|
||||
@@ -31,6 +31,7 @@ npm run docker-up
|
||||
|
||||
This starts the single production-style container on `http://localhost:8080`.
|
||||
Static assets and API routes are served by the same Express process.
|
||||
You can also run `docker build .` directly from the repo root.
|
||||
|
||||
## Scripts
|
||||
|
||||
@@ -46,7 +47,7 @@ npm run docker-build
|
||||
|
||||
- `client/` React frontend and game runtime
|
||||
- `server/` Express API, static asset host, and future persistence layer
|
||||
- `docker/` container definitions
|
||||
- `Dockerfile` and `compose.yml` define the container runtime
|
||||
- `docs/` architecture and contributor docs
|
||||
- `tests/` reserved for shared test assets and future end-to-end coverage
|
||||
|
||||
@@ -58,4 +59,4 @@ npm run docker-build
|
||||
- `npm run lint`
|
||||
- `npm run test`
|
||||
- `npm run build`
|
||||
- `docker compose -f docker/docker-compose.yml build`
|
||||
- `docker compose build`
|
||||
|
||||
12
UNRAID.md
12
UNRAID.md
@@ -25,7 +25,7 @@ cd stellar
|
||||
Build the image from the repo root:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t stellar:local .
|
||||
docker build -t stellar:local .
|
||||
```
|
||||
|
||||
Start the container:
|
||||
@@ -51,11 +51,11 @@ http://<your-unraid-ip>:8080
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yml build
|
||||
docker compose -f docker/docker-compose.yml up -d
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This uses the repo’s single-service compose file and exposes the app on port `8080`.
|
||||
This uses the repo's single-service compose file and exposes the app on port `8080`.
|
||||
|
||||
## Option 3: Pull A Prebuilt Image
|
||||
|
||||
@@ -99,7 +99,7 @@ If building from source:
|
||||
```bash
|
||||
cd /mnt/user/appdata/stellar
|
||||
git pull
|
||||
docker build -f docker/Dockerfile -t stellar:local .
|
||||
docker build -t stellar:local .
|
||||
docker rm -f stellar
|
||||
docker run -d \
|
||||
--name stellar \
|
||||
@@ -115,7 +115,7 @@ If using compose:
|
||||
```bash
|
||||
cd /mnt/user/appdata/stellar
|
||||
git pull
|
||||
docker compose -f docker/docker-compose.yml up -d --build
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -7,6 +7,6 @@ describe("Hud", () => {
|
||||
render(<Hud gameState={defaultGameState} />);
|
||||
|
||||
expect(screen.getByText("Sector Absorption")).toBeTruthy();
|
||||
expect(screen.getByText("Reach mass 40 to stabilize your pull")).toBeTruthy();
|
||||
expect(screen.getByText("Clear lower-tier debris to unlock heavier targets")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type GameState } from "../game/types";
|
||||
|
||||
const objectives = [
|
||||
"Reach mass 40 to stabilize your pull",
|
||||
"Keep sweeping the field to chain dust pickups",
|
||||
"Clear lower-tier debris to unlock heavier targets",
|
||||
"Meteorites and larger bodies only absorb after their mass gate",
|
||||
"Persist profile data in M3"
|
||||
];
|
||||
|
||||
@@ -37,11 +37,19 @@ export function Hud({ gameState }: HudProps) {
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Consumed</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">{gameState.consumedDust}</dd>
|
||||
<dd className="mt-1 text-xl font-semibold">{gameState.consumedObjects}</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Dust Left</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">{gameState.dustRemaining}</dd>
|
||||
<dt className="text-slate-400">Objects Left</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">{gameState.objectsRemaining}</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Max Prey</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">{gameState.preyTier}</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Next Unlock</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">{gameState.nextUnlock ?? "Complete"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Bodies,
|
||||
Body,
|
||||
Composite,
|
||||
Engine,
|
||||
Events,
|
||||
Vector,
|
||||
@@ -9,18 +8,17 @@ import {
|
||||
type Body as MatterBody,
|
||||
type IEventCollision
|
||||
} from "matter-js";
|
||||
import { getTierForMass } from "./progression";
|
||||
import { getCurrentPreyTier, getNextUnlock, getTierForMass, getUnlockedKinds } from "./progression";
|
||||
import { CELESTIAL_OBJECTS, type CelestialKind, type CelestialObjectDefinition } from "./spaceObjects";
|
||||
import { defaultGameState, type GameState } from "./types";
|
||||
|
||||
const STARTING_MASS = defaultGameState.mass;
|
||||
const STARTING_RADIUS = defaultGameState.radius;
|
||||
const DUST_COUNT = 36;
|
||||
const DUST_REWARD = 2;
|
||||
const EMIT_INTERVAL_MS = 900;
|
||||
|
||||
interface DustBody extends MatterBody {
|
||||
interface CelestialBody extends MatterBody {
|
||||
plugin: {
|
||||
isDust?: boolean;
|
||||
celestialKind?: CelestialKind;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +41,7 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
}
|
||||
});
|
||||
const boundaries: MatterBody[] = [];
|
||||
const dustBodies = new Set<DustBody>();
|
||||
const celestialBodies = new Set<CelestialBody>();
|
||||
const consumedBodies = new WeakSet<MatterBody>();
|
||||
const pointer = { x: 0, y: 0 };
|
||||
const worldSize = { width: 0, height: 0 };
|
||||
@@ -54,7 +52,7 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
let animationFrame = 0;
|
||||
let lastTimestamp = 0;
|
||||
let fps = 60;
|
||||
let consumedDust = 0;
|
||||
let consumedObjects = 0;
|
||||
let lastEmit = 0;
|
||||
|
||||
World.add(engine.world, blackHole);
|
||||
@@ -63,14 +61,24 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
|
||||
const getPullRadius = () => radius * 3.1;
|
||||
|
||||
const getRadiusForMass = (nextMass: number) => STARTING_RADIUS + Math.sqrt(nextMass - STARTING_MASS) * 2.6;
|
||||
|
||||
const getObjectDef = (kind: CelestialKind) =>
|
||||
CELESTIAL_OBJECTS.find((objectDef) => objectDef.kind === kind) as CelestialObjectDefinition;
|
||||
|
||||
const getFieldCount = (kind: CelestialKind) =>
|
||||
Array.from(celestialBodies).filter((body) => body.plugin.celestialKind === kind).length;
|
||||
|
||||
const emitState = () => {
|
||||
onStateChange({
|
||||
mass,
|
||||
radius,
|
||||
pullRadius: getPullRadius(),
|
||||
tier: getTierForMass(mass),
|
||||
consumedDust,
|
||||
dustRemaining: dustBodies.size,
|
||||
consumedObjects,
|
||||
objectsRemaining: celestialBodies.size,
|
||||
preyTier: getCurrentPreyTier(mass),
|
||||
nextUnlock: getNextUnlock(mass),
|
||||
fps
|
||||
});
|
||||
};
|
||||
@@ -111,7 +119,7 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
World.add(engine.world, boundaries);
|
||||
};
|
||||
|
||||
const createDust = () => {
|
||||
const createCelestialObject = (objectDef: CelestialObjectDefinition) => {
|
||||
const padding = 48;
|
||||
let attempts = 0;
|
||||
let position = { x: worldSize.width / 2, y: worldSize.height / 2 };
|
||||
@@ -129,33 +137,35 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
attempts += 1;
|
||||
}
|
||||
|
||||
const dust = Bodies.circle(position.x, position.y, 4 + Math.random() * 4, {
|
||||
restitution: 0.95,
|
||||
frictionAir: 0.01,
|
||||
const body = Bodies.circle(position.x, position.y, objectDef.radius + (Math.random() - 0.5) * objectDef.radius * 0.2, {
|
||||
restitution: 0.92,
|
||||
frictionAir: objectDef.frictionAir,
|
||||
friction: 0,
|
||||
density: 0.0008,
|
||||
density: objectDef.density,
|
||||
render: {
|
||||
visible: false
|
||||
}
|
||||
}) as DustBody;
|
||||
}) as CelestialBody;
|
||||
|
||||
dust.plugin = {
|
||||
...dust.plugin,
|
||||
isDust: true
|
||||
body.plugin = {
|
||||
...body.plugin,
|
||||
celestialKind: objectDef.kind
|
||||
};
|
||||
|
||||
Body.setVelocity(dust, {
|
||||
x: (Math.random() - 0.5) * 1.8,
|
||||
y: (Math.random() - 0.5) * 1.8
|
||||
Body.setVelocity(body, {
|
||||
x: (Math.random() - 0.5) * (1.9 - objectDef.radius * 0.02),
|
||||
y: (Math.random() - 0.5) * (1.9 - objectDef.radius * 0.02)
|
||||
});
|
||||
|
||||
dustBodies.add(dust);
|
||||
World.add(engine.world, dust);
|
||||
celestialBodies.add(body);
|
||||
World.add(engine.world, body);
|
||||
};
|
||||
|
||||
const topOffDust = () => {
|
||||
while (dustBodies.size < DUST_COUNT) {
|
||||
createDust();
|
||||
const topOffField = () => {
|
||||
for (const objectDef of CELESTIAL_OBJECTS) {
|
||||
while (getFieldCount(objectDef.kind) < objectDef.fieldTarget) {
|
||||
createCelestialObject(objectDef);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,31 +175,43 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
Body.scale(blackHole, scale, scale);
|
||||
};
|
||||
|
||||
const absorbDust = (dust: DustBody) => {
|
||||
if (consumedBodies.has(dust)) {
|
||||
const canConsume = (kind: CelestialKind) => getUnlockedKinds(mass).includes(kind);
|
||||
|
||||
const rejectBody = (body: CelestialBody) => {
|
||||
const direction = Vector.normalise(Vector.sub(body.position, blackHole.position));
|
||||
Body.setVelocity(body, Vector.mult(direction, 5.5));
|
||||
};
|
||||
|
||||
const absorbBody = (body: CelestialBody) => {
|
||||
if (consumedBodies.has(body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
consumedBodies.add(dust);
|
||||
dustBodies.delete(dust);
|
||||
World.remove(engine.world, dust);
|
||||
consumedBodies.add(body);
|
||||
celestialBodies.delete(body);
|
||||
World.remove(engine.world, body);
|
||||
|
||||
consumedDust += 1;
|
||||
mass += DUST_REWARD;
|
||||
consumedObjects += 1;
|
||||
mass += getObjectDef(body.plugin.celestialKind as CelestialKind).rewardMass;
|
||||
|
||||
const targetRadius = STARTING_RADIUS + Math.sqrt(mass - STARTING_MASS) * 2.6;
|
||||
syncHoleScale(targetRadius);
|
||||
syncHoleScale(getRadiusForMass(mass));
|
||||
emitState();
|
||||
};
|
||||
|
||||
const handleCollision = (event: IEventCollision<Engine>) => {
|
||||
for (const pair of event.pairs) {
|
||||
const [bodyA, bodyB] = [pair.bodyA as DustBody, pair.bodyB as DustBody];
|
||||
const dust = bodyA.plugin?.isDust ? bodyA : bodyB.plugin?.isDust ? bodyB : null;
|
||||
const other = dust === bodyA ? bodyB : bodyA;
|
||||
const [bodyA, bodyB] = [pair.bodyA as CelestialBody, pair.bodyB as CelestialBody];
|
||||
const body = bodyA.plugin?.celestialKind ? bodyA : bodyB.plugin?.celestialKind ? bodyB : null;
|
||||
const other = body === bodyA ? bodyB : bodyA;
|
||||
|
||||
if (dust && other === blackHole) {
|
||||
absorbDust(dust);
|
||||
if (body && other === blackHole) {
|
||||
const kind = body.plugin.celestialKind as CelestialKind;
|
||||
|
||||
if (canConsume(kind)) {
|
||||
absorbBody(body);
|
||||
} else {
|
||||
rejectBody(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -264,11 +286,31 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (const dust of dustBodies) {
|
||||
context.fillStyle = "rgba(226, 232, 240, 0.95)";
|
||||
for (const body of celestialBodies) {
|
||||
const objectDef = getObjectDef(body.plugin.celestialKind as CelestialKind);
|
||||
const isUnlocked = canConsume(objectDef.kind);
|
||||
const bodyRadius = body.circleRadius ?? objectDef.radius;
|
||||
|
||||
context.fillStyle = objectDef.color;
|
||||
context.beginPath();
|
||||
context.arc(dust.position.x, dust.position.y, dust.circleRadius ?? 5, 0, Math.PI * 2);
|
||||
context.arc(body.position.x, body.position.y, bodyRadius, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
|
||||
if (objectDef.kind !== "dust") {
|
||||
context.strokeStyle = objectDef.accent;
|
||||
context.lineWidth = objectDef.kind === "star" ? 3 : 2;
|
||||
context.beginPath();
|
||||
context.arc(body.position.x, body.position.y, bodyRadius + (objectDef.kind === "planet" ? 5 : 3), 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
if (!isUnlocked) {
|
||||
context.strokeStyle = "rgba(248, 113, 113, 0.8)";
|
||||
context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.arc(body.position.x, body.position.y, bodyRadius + 7, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
const pullRadius = getPullRadius();
|
||||
@@ -314,21 +356,26 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
Body.setVelocity(blackHole, { x: 0, y: 0 });
|
||||
}
|
||||
|
||||
for (const dust of dustBodies) {
|
||||
const toHole = Vector.sub(blackHole.position, dust.position);
|
||||
for (const body of celestialBodies) {
|
||||
const kind = body.plugin.celestialKind as CelestialKind;
|
||||
const objectDef = getObjectDef(kind);
|
||||
const toHole = Vector.sub(blackHole.position, body.position);
|
||||
const distance = Math.max(1, Vector.magnitude(toHole));
|
||||
|
||||
if (distance < getPullRadius()) {
|
||||
const direction = Vector.normalise(toHole);
|
||||
const strength = clamp((getPullRadius() - distance) / getPullRadius(), 0, 1) * 0.00016;
|
||||
Body.applyForce(dust, dust.position, Vector.mult(direction, strength * dust.mass));
|
||||
const strength =
|
||||
clamp((getPullRadius() - distance) / getPullRadius(), 0, 1) *
|
||||
(canConsume(kind) ? 0.00016 : 0.00008) *
|
||||
(1 / Math.max(1, objectDef.radius * 0.08));
|
||||
Body.applyForce(body, body.position, Vector.mult(direction, strength * body.mass));
|
||||
}
|
||||
}
|
||||
|
||||
Engine.update(engine, delta);
|
||||
|
||||
if (timestamp - lastEmit > EMIT_INTERVAL_MS) {
|
||||
topOffDust();
|
||||
topOffField();
|
||||
emitState();
|
||||
lastEmit = timestamp;
|
||||
}
|
||||
@@ -338,7 +385,7 @@ export function initMatterScene(canvas: HTMLCanvasElement, onStateChange: (state
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
topOffDust();
|
||||
topOffField();
|
||||
emitState();
|
||||
|
||||
Events.on(engine, "collisionStart", handleCollision);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTierForMass } from "./progression";
|
||||
import { getCurrentPreyTier, getNextUnlock, getTierForMass } from "./progression";
|
||||
|
||||
describe("getTierForMass", () => {
|
||||
it("maps early mass to Dustling", () => {
|
||||
@@ -8,8 +8,25 @@ describe("getTierForMass", () => {
|
||||
|
||||
it("promotes mass thresholds into later tiers", () => {
|
||||
expect(getTierForMass(20)).toBe("Rift Seed");
|
||||
expect(getTierForMass(40)).toBe("Event Core");
|
||||
expect(getTierForMass(70)).toBe("World Eater");
|
||||
expect(getTierForMass(110)).toBe("Proto Star");
|
||||
expect(getTierForMass(24)).toBe("Event Core");
|
||||
expect(getTierForMass(50)).toBe("World Eater");
|
||||
expect(getTierForMass(95)).toBe("Planet Breaker");
|
||||
expect(getTierForMass(160)).toBe("Star Forger");
|
||||
});
|
||||
|
||||
it("tracks prey unlocks by mass", () => {
|
||||
expect(getCurrentPreyTier(12)).toBe("Dust");
|
||||
expect(getCurrentPreyTier(24)).toBe("Meteorite");
|
||||
expect(getCurrentPreyTier(50)).toBe("Asteroid");
|
||||
expect(getCurrentPreyTier(95)).toBe("Planet");
|
||||
expect(getCurrentPreyTier(160)).toBe("Star");
|
||||
});
|
||||
|
||||
it("returns the next unlock target", () => {
|
||||
expect(getNextUnlock(12)).toBe("Meteorite");
|
||||
expect(getNextUnlock(24)).toBe("Asteroid");
|
||||
expect(getNextUnlock(50)).toBe("Planet");
|
||||
expect(getNextUnlock(95)).toBe("Star");
|
||||
expect(getNextUnlock(200)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { CELESTIAL_OBJECTS, type CelestialKind } from "./spaceObjects";
|
||||
|
||||
export function getTierForMass(mass: number) {
|
||||
if (mass >= 110) {
|
||||
return "Proto Star";
|
||||
if (mass >= 160) {
|
||||
return "Star Forger";
|
||||
}
|
||||
|
||||
if (mass >= 70) {
|
||||
if (mass >= 95) {
|
||||
return "Planet Breaker";
|
||||
}
|
||||
|
||||
if (mass >= 50) {
|
||||
return "World Eater";
|
||||
}
|
||||
|
||||
if (mass >= 40) {
|
||||
if (mass >= 24) {
|
||||
return "Event Core";
|
||||
}
|
||||
|
||||
@@ -17,3 +23,17 @@ export function getTierForMass(mass: number) {
|
||||
|
||||
return "Dustling";
|
||||
}
|
||||
|
||||
export function getUnlockedKinds(mass: number): CelestialKind[] {
|
||||
return CELESTIAL_OBJECTS.filter((objectDef) => mass >= objectDef.unlockMass).map(
|
||||
(objectDef) => objectDef.kind
|
||||
);
|
||||
}
|
||||
|
||||
export function getCurrentPreyTier(mass: number) {
|
||||
return CELESTIAL_OBJECTS.filter((objectDef) => mass >= objectDef.unlockMass).at(-1)?.label ?? "Dust";
|
||||
}
|
||||
|
||||
export function getNextUnlock(mass: number) {
|
||||
return CELESTIAL_OBJECTS.find((objectDef) => mass < objectDef.unlockMass)?.label ?? null;
|
||||
}
|
||||
|
||||
77
client/src/game/spaceObjects.ts
Normal file
77
client/src/game/spaceObjects.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export type CelestialKind = "dust" | "meteorite" | "asteroid" | "planet" | "star";
|
||||
|
||||
export interface CelestialObjectDefinition {
|
||||
kind: CelestialKind;
|
||||
label: string;
|
||||
radius: number;
|
||||
rewardMass: number;
|
||||
unlockMass: number;
|
||||
density: number;
|
||||
frictionAir: number;
|
||||
color: string;
|
||||
accent: string;
|
||||
fieldTarget: number;
|
||||
}
|
||||
|
||||
export const CELESTIAL_OBJECTS: CelestialObjectDefinition[] = [
|
||||
{
|
||||
kind: "dust",
|
||||
label: "Dust",
|
||||
radius: 5,
|
||||
rewardMass: 2,
|
||||
unlockMass: 0,
|
||||
density: 0.0008,
|
||||
frictionAir: 0.01,
|
||||
color: "#e2e8f0",
|
||||
accent: "#ffffff",
|
||||
fieldTarget: 24
|
||||
},
|
||||
{
|
||||
kind: "meteorite",
|
||||
label: "Meteorite",
|
||||
radius: 10,
|
||||
rewardMass: 6,
|
||||
unlockMass: 24,
|
||||
density: 0.0015,
|
||||
frictionAir: 0.012,
|
||||
color: "#f97316",
|
||||
accent: "#fdba74",
|
||||
fieldTarget: 10
|
||||
},
|
||||
{
|
||||
kind: "asteroid",
|
||||
label: "Asteroid",
|
||||
radius: 16,
|
||||
rewardMass: 14,
|
||||
unlockMass: 50,
|
||||
density: 0.0026,
|
||||
frictionAir: 0.013,
|
||||
color: "#94a3b8",
|
||||
accent: "#cbd5e1",
|
||||
fieldTarget: 6
|
||||
},
|
||||
{
|
||||
kind: "planet",
|
||||
label: "Planet",
|
||||
radius: 24,
|
||||
rewardMass: 30,
|
||||
unlockMass: 95,
|
||||
density: 0.0035,
|
||||
frictionAir: 0.014,
|
||||
color: "#38bdf8",
|
||||
accent: "#a5f3fc",
|
||||
fieldTarget: 3
|
||||
},
|
||||
{
|
||||
kind: "star",
|
||||
label: "Star",
|
||||
radius: 34,
|
||||
rewardMass: 60,
|
||||
unlockMass: 160,
|
||||
density: 0.0042,
|
||||
frictionAir: 0.015,
|
||||
color: "#facc15",
|
||||
accent: "#fde68a",
|
||||
fieldTarget: 1
|
||||
}
|
||||
];
|
||||
@@ -5,8 +5,10 @@ export interface GameState {
|
||||
radius: number;
|
||||
pullRadius: number;
|
||||
tier: string;
|
||||
consumedDust: number;
|
||||
dustRemaining: number;
|
||||
consumedObjects: number;
|
||||
objectsRemaining: number;
|
||||
preyTier: string;
|
||||
nextUnlock: string | null;
|
||||
fps: number;
|
||||
}
|
||||
|
||||
@@ -15,7 +17,9 @@ export const defaultGameState: GameState = {
|
||||
radius: 24,
|
||||
pullRadius: 74.4,
|
||||
tier: getTierForMass(12),
|
||||
consumedDust: 0,
|
||||
dustRemaining: 0,
|
||||
consumedObjects: 0,
|
||||
objectsRemaining: 0,
|
||||
preyTier: "Dust",
|
||||
nextUnlock: "Meteorite",
|
||||
fps: 60
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
PORT: "3000"
|
||||
LOG_LEVEL: info
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- `client/` hosts the React application and game shell.
|
||||
- `server/` hosts the Express API, production static asset serving, and future persistence services.
|
||||
- `docker/` contains a single-container production build and local compose entrypoint.
|
||||
- `Dockerfile` and `compose.yml` define the single-container production build and local compose entrypoint.
|
||||
|
||||
## Immediate Direction
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"lint": "npm run lint:client && npm run lint:server",
|
||||
"lint:client": "npm --workspace client run lint",
|
||||
"lint:server": "npm --workspace server run lint",
|
||||
"docker-build": "docker compose -f docker/docker-compose.yml build",
|
||||
"docker-up": "docker compose -f docker/docker-compose.yml up --build",
|
||||
"docker-down": "docker compose -f docker/docker-compose.yml down"
|
||||
"docker-build": "docker compose build",
|
||||
"docker-up": "docker compose up --build",
|
||||
"docker-down": "docker compose down"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1"
|
||||
|
||||
Reference in New Issue
Block a user