Skip to content
PabloFLPsPublic

About

Roll20 Killer! xD

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Loreforge

An AI-first tabletop RPG platform: build a campaign's world, NPCs, quests and story graph, run live multiplayer sessions on tactical maps with a built-in campaign-aware AI copilot that has memory of everything that has happened.

The tactical map is server-authoritative: movement, line of sight and fog of war are decided by a pure grid engine (src/lib/tactics) that the socket server and the browser both run, so what you see is what the server will allow.

Built with Next.js 16 (App Router), React 19, Prisma + SQLite, Socket.IO, Zustand, Tailwind CSS v4 and @xyflow/react.

Getting started

npm install        # runs prisma generate
cp .env.example .env
npm run db:push    # create the SQLite schema
npm run db:seed    # idempotent demo campaign ("The Fall of Eldoria")
npm run dev        # http://localhost:3000

Demo account: there is no auth. The app runs in demo mode and uses a fixed GM user (gm@loreforge.demo). A demo campaign, map, story graph, characters, NPC relationships, campaign timeline and sessions (including a live one) are seeded so every feature is reachable immediately.

AI provider

The app works fully offline with the mock provider (default):

AI_PROVIDER=mock

To get real suggestions, set:

AI_PROVIDER=openrouter
OPENROUTER_API_KEY=sk-or-...
AI_MODEL=openrouter/auto

The AI never mutates state itself — it proposes suggestions (NPCs, quests, story nodes) and you accept them into the campaign.

Scripts

Command What it does
npm run dev Dev server (custom server with Socket.IO)
npm run build Production build
npm run start Production server (Socket.IO included)
npm run lint ESLint
npm run typegen next typegen — writes Next's route types to .next/types
npm run typecheck next typegen + tsc --noEmit
npm test Vitest — the whole suite, once
npm run test:watch Vitest in watch mode
npm run db:push Push schema to SQLite
npm run db:seed Seed/refresh the demo data

Note: npm run dev does not hot-reload the custom server (server.ts / src/server/socket.ts). Restart the dev process after editing those files.

Note: typecheck runs next typegen first on purpose. Next 16 generates the route/layout types (LayoutProps, PageProps, …) into .next/types, so on a clean checkout a bare tsc --noEmit fails with Cannot find name 'LayoutProps'.

Note: either package manager works. npm installs missing peer dependencies for you and yarn 1 does not, so every required peer is declared explicitly in package.json — and verify's first gate (scripts/check-peers.mjs) fails, with the name of the package to add, if one ever is not.

Tests

npm test runs everything under src/**/*.test.{ts,tsx} with Vitest — 346 tests across 29 files. No database, dev server or network is needed: Prisma is mocked and route handlers are called as plain functions.

Layer Where What it covers
Engine + pure logic src/lib/**/__tests__ grid rules, pathfinding, line of sight, fog, AoE, camera, initiative, server-side movement, dice, zod schemas, visibility filters, mock AI
UI kit src/test/components rendering + accessible names (@testing-library/react)
Feature components src/components/maps/__tests__, src/components/sessions/__tests__ editor toolbar/panels, combat panel, party panel, movement preview, own-player resolution
API route handlers src/app/api/**/__tests__ happy paths, 422 validation, 404 ownership, x-gm-key authorization
Socket protocol src/server/__tests__/socket.test.ts event surface, join state filtering, GM/player permissions

A few component tests assert design-system invariants rather than behaviour — exactly one primary-variant button per context, and no emoji in the object picker — so breaking a rule in DESIGN_SYSTEM.md fails the suite.

Shared test infrastructure lives in src/test/:

  • setup.ts — registers @testing-library/jest-dom matchers and unmounts components after each test (wired via setupFiles in vitest.config.mts).

  • prisma-mock.ts — createPrismaMock() returns a fully typed vi.fn() delegate tree. Mock the db module at the top of a test file:

    vi.mock("@/lib/db", async () => {
      const { createPrismaMock } = await import("@/test/prisma-mock");
      return { prisma: createPrismaMock() };
    });
  • fixtures.ts — row builders typed against the generated Prisma models (sessionRow(), npcRow(), …).

  • http.ts — jsonRequest() / getRequest() / expectResponse() for calling route handlers with a real Request and a ctx whose params is a Promise (this repo's route contract).

  • socket-harness.ts — in-memory Socket.IO doubles (createFakeIo()) that drive registerSocketHandlers without starting a server and record every broadcast.

The default environment is jsdom; route-handler and socket tests opt into Node with a // @vitest-environment node docblock.

CI (.github/workflows/ci.yml) runs on every push and pull request: Node 22, npm ci --ignore-scripts, an explicit retrying prisma generate, next typegen, then typecheck, lint and tests.

Feature tour

  • Dashboard — campaigns at a glance, create a new campaign.
  • Campaign overview — campaign memory (recent timeline events), latest session, prep shortcut, start a live session, open the AI copilot, quick stats.
  • Command palette (⌘K) — search NPCs, locations, quests, characters, story nodes, maps, sessions and campaign events from anywhere in a campaign; arrow keys + Enter to jump.
  • World — locations and factions with public/GM-only visibility; discover locations during play.
  • NPCs — manage NPCs, track the party's relationship with each one (friendly/neutral/hostile), log "met"/"interaction" events, ask the copilot for secrets or dialogue.
  • Characters — player character sheets with stats, HP and conditions (poisoned, frightened, …).
  • Quests — quest hooks with status; status changes are written to the campaign timeline.
  • Story — a React Flow story graph: drag nodes, connect plot points, edit details, delete with the keyboard or right-click menu.
  • Maps — grid maps with generated backgrounds; paint terrain (floor, wall, difficult, water, hazard, void) with a 1–3 tile brush; place tokens and objects and give each token a footprint, speed and vision radius; pick the movement ruleset (diagonals as 1, alternating 5-10-5, or no diagonals) and toggle dynamic vision per map; preview exactly what one token can see; measure in tiles and feet; wheel zoom, zoom indicator, grid-opacity control, delete confirmation.
  • Tactical map — the same renderer in the editor and at the table. Terrain decides what costs what and what blocks sight; walls, pillars, trees and rocks placed as objects block movement and sight without repainting anything. Selecting a token you control shows every tile it can reach within its remaining movement, and hovering shows the route it would walk with its cost — dashed and red when it exceeds the budget. Large tokens path by their whole footprint.
  • Dynamic vision and fog — fog is tri-state: unexplored is black, explored-but-unseen keeps a dimmed hint of the terrain, and only what a party token can currently see is drawn in full. Walking into the dark reveals it: the server recomputes party vision after every move and persists the newly explored tiles, so the GM does not paint every room by hand. The reveal/hide brush and a GM see-all toggle are still there.
  • Templates, pings and measurement — the GM drops AoE templates (circle, cone, line, square, with size, facing and color) to answer "what would this hit"; anyone at the table can ping a tile to point at it, and anyone can measure a distance in tiles and feet. Templates and pings are overlays, never saved to the campaign.
  • Session prep — the copilot reads the campaign timeline, open quests and the last recap and produces a prep brief plus suggested story beats you can accept as story nodes.
  • Sessions — live multiplayer via WebSockets: lobby → start → play, the tactical map above, event capture, session recaps (AI-generated, editable), GM-only chat and tools. Players move their own token and the server validates it; refusals ("It is not your turn.") are shown verbatim rather than failing silently. Sessions are marked LOBBY → ACTIVE → PAUSED → ENDED; gameplay requires an ACTIVE session. The session screen shows a live connection indicator and an icon rail for the side panels (party, chat, players, GM tools).
  • Initiative and combat — add combatants from campaign characters or as one-offs, roll initiative for the whole order at once (d20 + the character's initiative modifier), advance turns, and see each combatant's remaining movement. Inside combat, movement is budgeted and only the active combatant may move; outside it, repositioning is free. Players can read the tracker; only the GM can act on it.
  • AI copilot — slide-over panel with contextual quick prompts (campaign, NPC or session context), "Reviewing your campaign…" loading state, and suggestion cards you can accept, edit or dismiss. Suggestions never change the campaign until accepted.
  • Design system — a single design language with dark + light themes (tokens, typography, spacing, radius, shadows) documented in DESIGN_SYSTEM.md; lucide icons only, no emoji in UI.

Views and background art

Every map is drawn either top-down (the classic battle mat) or isometric 2.5D (a diamond field with raised walls and standing figures). It is a per-map setting, toggleable live from the session toolbar or the map editor, and it changes nothing about the rules — the engine is the same square grid either way.

A map's background can be an image you upload (PNG, JPEG, WebP, GIF or SVG up to 8MB, stored under public/uploads/maps) or a scene composed from a description you type. The composed art is deterministic and generated locally, so it works offline and with no image model configured.

Showcase maps

npm run db:seed adds three maps to the demo campaign, each built to exercise a different part of the tactical engine:

  • Blackwood Crypt (34x24) — a room-and-corridor dungeon for line of sight: a flooded nave crossed by two stepping stones, brazier hazards, a chasm, and a 2x2 Bone Horror that has to fit its whole footprint through a door.
  • The Sunken Causeway (40x26) — costly ground and pathfinding: wading the channel is double cost, the raised causeway is broken at its centre, and the fast crossing is a detour through the shallows.
  • Emberfall Gatehouse (28x20) — the D&D alternating 5-10-5 diagonal ruleset, a 3x3 boss on cracked, burning floor, and a party that has only explored as far as the hall's mouth.

They are idempotent: re-running the seed refreshes terrain and token stats without duplicating anything. src/lib/__tests__/showcase-maps.test.ts runs the real engine over them, so a token placed inside a wall or an unreachable enemy fails the suite rather than the demo.

Testing live sessions

Start the app, open a campaign, click Start session, then open the returned GM link. Open the join link in another browser (or incognito) and join as a player. scripts/socket-test.mjs runs an automated end-to-end check of the socket contract (permissions, GM-only chat, dice, visibility, combat, fog, events) against a running server — it needs a live server and database, so it is not part of npm test; the unit-level protocol checks in src/server/__tests__/socket.test.ts are.

Docs

  • ARCHITECTURE.md — how the app is put together.
  • DECISIONS.md — notable technical decisions and trade-offs.
  • DESIGN_SYSTEM.md — visual design system: tokens, typography, spacing, radius, shadows, component rules and screen structure.

About

Roll20 Killer! xD

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages