game: starting crew seeded on new game; exploration rolls for a joining crew member (same per-planet stream)
This commit is contained in:
+45
-5
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
|
||||
import type { GameDb } from '@/lib/db/client'
|
||||
import { persist } from '@/lib/db/client'
|
||||
import * as schema from '@/lib/db/schema'
|
||||
import { generateStartingCrew, rollCrewMember, startingCrewRng, type CrewMember } from './crew'
|
||||
import { bodyRng, explorationRng, generatePlanets, generateRing, generateStartRegion, mulberry32, rollExploration, type FoundItem } from './world'
|
||||
|
||||
// Save lifecycle: auto-init on first hit, world state reads, and the jump action.
|
||||
@@ -67,6 +68,13 @@ async function insertRoutes(db: GameDb, edges: [number, number][], resolve: (ind
|
||||
}
|
||||
}
|
||||
|
||||
// Seeds the starting crew for a fresh save: 2–3 members derived from the seed alone.
|
||||
async function seedStartingCrew(db: GameDb, saveId: number, seed: string): Promise<void> {
|
||||
for (const member of generateStartingCrew(startingCrewRng(seed))) {
|
||||
await db.db.insert(schema.crew).values({ saveId, name: member.name, role: member.role, traitGood: member.traitGood, traitBad: member.traitBad, origin: 'STARTING CREW' })
|
||||
}
|
||||
}
|
||||
|
||||
// Creates the save + starting region when the DB has none. No-op otherwise — safe to call
|
||||
// on every request (single player, one process: check-then-insert cannot race in practice).
|
||||
export async function ensureSave(db: GameDb): Promise<void> {
|
||||
@@ -86,6 +94,7 @@ export async function ensureSave(db: GameDb): Promise<void> {
|
||||
|
||||
const resolve = (index: number) => (index === 0 ? startId : neighborIds[index - 1])
|
||||
await insertRoutes(db, region.edges, resolve)
|
||||
await seedStartingCrew(db, save.id, seed)
|
||||
|
||||
await db.db.insert(schema.events).values({ saveId: save.id, turn: 0, type: 'new_game', message: `New game — seed ${seed}, starting system ${region.start.name}`, createdAt: nowIso })
|
||||
await persist(db)
|
||||
@@ -210,13 +219,32 @@ export async function getSystemState(db: GameDb): Promise<SystemState> {
|
||||
return readSystemState(db)
|
||||
}
|
||||
|
||||
// --- Crew roster --------------------------------------------------------------
|
||||
|
||||
export interface CrewDto {
|
||||
id: number
|
||||
name: string
|
||||
role: string
|
||||
traitGood: string
|
||||
traitBad: string
|
||||
origin: string // 'STARTING CREW' or '<planet>, <system>' for members found while exploring
|
||||
}
|
||||
|
||||
// The full roster in join order (starting crew first, then finds). Auto-initializes like the other reads.
|
||||
export async function getCrewState(db: GameDb): Promise<CrewDto[]> {
|
||||
await ensureSave(db)
|
||||
return (await db.db.select().from(schema.crew)).map((c) => ({ id: c.id, name: c.name, role: c.role, traitGood: c.traitGood, traitBad: c.traitBad, origin: c.origin }))
|
||||
}
|
||||
|
||||
export interface ExploreResult extends SystemState {
|
||||
outcome: FoundItem[] // what this scan found (may be empty — some scans find nothing)
|
||||
crewFound: CrewDto | null // a member who joined the ship on this scan (null when none)
|
||||
}
|
||||
|
||||
// Scan one body of the save: deterministic haul from (seed, planet id), marks it explored,
|
||||
// costs a turn, accumulates finds into inventory and writes an event. Any existing planet
|
||||
// may be scanned (the UI only offers the current system's rows).
|
||||
// costs a turn, accumulates finds into inventory and writes an event. The same per-planet
|
||||
// rng stream also rolls for a crew member (~18% of scans) who joins the roster. Any existing
|
||||
// planet may be scanned (the UI only offers the current system's rows).
|
||||
export async function explorePlanet(db: GameDb, planetId: number): Promise<ExploreResult> {
|
||||
await ensureSave(db)
|
||||
const [save] = await db.db.select().from(schema.saves).limit(1)
|
||||
@@ -225,7 +253,10 @@ export async function explorePlanet(db: GameDb, planetId: number): Promise<Explo
|
||||
if (!target) throw new ExploreError(404, `Unknown planet ${planetId}`)
|
||||
if (target.explored) throw new ExploreError(409, `${target.name} was already explored`)
|
||||
|
||||
const outcome = rollExploration(explorationRng(save.seed, target.id))
|
||||
const rng = explorationRng(save.seed, target.id)
|
||||
const outcome = rollExploration(rng) // resource/part haul first…
|
||||
const takenNames = new Set((await db.db.select().from(schema.crew)).map((c) => c.name))
|
||||
const foundMember = rollCrewMember(rng, takenNames) // …then the crew roll on the same stream
|
||||
const turn = save.turn + 1
|
||||
|
||||
await db.db.update(schema.planets).set({ explored: true }).where(eq(schema.planets.id, target.id))
|
||||
@@ -239,10 +270,19 @@ export async function explorePlanet(db: GameDb, planetId: number): Promise<Explo
|
||||
}
|
||||
}
|
||||
|
||||
let crewFound: CrewDto | null = null
|
||||
if (foundMember) {
|
||||
const systems = await db.db.select().from(schema.solarSystems)
|
||||
const systemName = systems.find((s) => s.id === target.systemId)?.name ?? 'unknown system'
|
||||
const [row] = await db.db.insert(schema.crew).values({ saveId: save.id, name: foundMember.name, role: foundMember.role, traitGood: foundMember.traitGood, traitBad: foundMember.traitBad, origin: `${target.name}, ${systemName}` }).returning()
|
||||
crewFound = { id: row.id, name: row.name, role: row.role, traitGood: row.traitGood, traitBad: row.traitBad, origin: row.origin }
|
||||
}
|
||||
|
||||
const foundText = outcome.length > 0 ? outcome.map((i) => `${i.qty}× ${i.name}`).join(', ') : 'nothing'
|
||||
const crewText = crewFound ? ` — ${crewFound.name} joined the crew` : ''
|
||||
await db.db.update(schema.saves).set({ turn }).where(eq(schema.saves.id, save.id))
|
||||
await db.db.insert(schema.events).values({ saveId: save.id, turn, type: 'explore', message: `Explored ${target.name} — found ${foundText}`, createdAt: new Date().toISOString() })
|
||||
await db.db.insert(schema.events).values({ saveId: save.id, turn, type: 'explore', message: `Explored ${target.name} — found ${foundText}${crewText}`, createdAt: new Date().toISOString() })
|
||||
await persist(db)
|
||||
|
||||
return { ...(await readSystemState(db)), outcome }
|
||||
return { ...(await readSystemState(db)), outcome, crewFound }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user