Phase 4: web markers and JourneyMap/Xaero waypoint chat sharing

Linked accounts can place 2D markers on the map (y auto-derived from the
chunk store's heightmap); anonymous visitors keep a localStorage-only
list via the same height lookup. Sharing a marker forwards a structured
payload to the mod over its WS connection, which builds the actual
chat text (see MCMapper-Mod for the JourneyMap/Xaero formatting).

Built test-first per the project's TDD workflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-08 19:21:07 +02:00
parent 6770bc23cc
commit 66fe2ffb7e
13 changed files with 836 additions and 10 deletions
+26
View File
@@ -10,6 +10,10 @@ export const servers = pgTable("servers", {
// Phase 3: web chat is open to anonymous (unlinked) visitors by default; an admin can require
// linking per-server. No admin UI to flip this yet (Phase 6) — set directly in the DB for now.
anonymousChatAllowed: boolean("anonymous_chat_allowed").notNull().default(true),
// Phase 4: which format `/mcmapper`-side chat waypoint links are built in when a marker is
// shared — 'journeymap' (default), 'xaero', or 'off' to disable sharing entirely. No admin UI
// to flip this yet (Phase 6) — set directly in the DB for now, same as anonymousChatAllowed.
waypointFormat: text("waypoint_format").notNull().default("journeymap"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
@@ -54,6 +58,28 @@ export const chatMessages = pgTable("chat_messages", {
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// A web-placed marker, owned by a linked account (anonymous visitors keep their marker list in
// browser localStorage only — see the plan's "Feature: web markers..." section — so there's
// nothing to persist here for them). Placement is 2D-only (x/z picked on the Leaflet map); `y` is
// resolved once at creation time from `chunkColumns`' heightmap and stored, not recomputed later,
// so a marker doesn't silently move if the terrain above it changes afterward.
export const markers = pgTable("markers", {
id: uuid("id").defaultRandom().primaryKey(),
accountId: uuid("account_id")
.notNull()
.references(() => accounts.id, { onDelete: "cascade" }),
serverId: uuid("server_id")
.notNull()
.references(() => servers.id, { onDelete: "cascade" }),
dimension: integer("dimension").notNull(),
x: integer("x").notNull(),
y: integer("y").notNull(),
z: integer("z").notNull(),
name: text("name").notNull(),
color: text("color").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// Column-granularity world state: the topmost non-air block per (dimension, x, z), plus its
// height. Kept deliberately separate from `chunkSections` below — cheap to write/read for 2D
// tile rendering, which never needs full voxel data.