Files
Dungeons-Ground/packages/shared/src/index.ts
pavel444-byte 9dd856ff8c
Some checks failed
CI / validate (push) Successful in 1h3m54s
CI / validate (pull_request) Failing after 14m14s
fix(gameplay): use AI-generated world companions
Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 20:38:12 +05:00

214 lines
8.0 KiB
TypeScript

import { z } from 'zod'
export * from './moderation'
// PostgreSQL/PostgREST serializes timestamptz values with an explicit offset
// (for example `+00:00`). Browser-created timestamps normally use `Z`.
// Both are valid ISO-8601 timestamps and must be accepted at the API boundary.
const IsoDatetimeSchema = z.string().datetime({ offset: true })
export const abilityKeys = ['str', 'dex', 'con', 'int', 'wis', 'cha'] as const
export const AbilityKeySchema = z.enum(abilityKeys)
export type AbilityKey = z.infer<typeof AbilityKeySchema>
export const AbilityScoresSchema = z.object({
str: z.number().int().min(1).max(30),
dex: z.number().int().min(1).max(30),
con: z.number().int().min(1).max(30),
int: z.number().int().min(1).max(30),
wis: z.number().int().min(1).max(30),
cha: z.number().int().min(1).max(30),
})
export type AbilityScores = z.infer<typeof AbilityScoresSchema>
export const WorldEntitySchema = z.object({
id: z.string().min(1),
kind: z.enum(['location', 'npc', 'faction', 'quest']),
name: z.string().min(1).max(120),
summary: z.string().min(1).max(1200),
tags: z.array(z.string().max(40)).max(12).default([]),
secrets: z.array(z.string().max(500)).max(6).default([]),
})
export type WorldEntity = z.infer<typeof WorldEntitySchema>
export const CharacterPersonaSchema = z.object({
voice: z.string().trim().min(1).max(240),
motivation: z.string().trim().min(1).max(300),
flaw: z.string().trim().min(1).max(300),
bond: z.string().trim().min(1).max(300),
})
export type CharacterPersona = z.infer<typeof CharacterPersonaSchema>
export const CharacterDraftSchema = z.object({
name: z.string().trim().min(1).max(80),
concept: z.string().trim().min(3).max(600),
abilities: AbilityScoresSchema,
hp: z.number().int().min(1).max(100),
maxHp: z.number().int().min(1).max(100),
defense: z.number().int().min(1).max(40),
proficiency: z.number().int().min(1).max(10),
inventory: z.array(z.string().trim().min(1).max(120)).max(12),
persona: CharacterPersonaSchema,
}).strict().refine(value => value.hp <= value.maxHp, {
message: 'hp must not exceed maxHp',
path: ['hp'],
})
export type CharacterDraft = z.infer<typeof CharacterDraftSchema>
export const WorldStarterSchema = z.object({
title: z.string().min(3).max(100),
genre: z.string().min(2).max(80),
tone: z.string().min(2).max(160),
premise: z.string().min(20).max(1800),
contentBoundaries: z.array(z.string().max(120)).min(1).max(8),
startingLocation: WorldEntitySchema.extend({ kind: z.literal('location') }),
npcs: z.array(WorldEntitySchema.extend({ kind: z.literal('npc') })).length(3),
factions: z.array(WorldEntitySchema.extend({ kind: z.literal('faction') })).length(2),
companions: z.array(CharacterDraftSchema).length(3),
hook: z.string().min(20).max(1200),
hiddenThreat: z.string().min(10).max(1000),
openingScene: z.string().min(40).max(2400),
})
export type WorldStarter = z.infer<typeof WorldStarterSchema>
export const CharacterSchema = z.object({
id: z.string().min(1),
name: z.string().min(1).max(80),
concept: z.string().min(3).max(600),
controller: z.enum(['human', 'ai', 'delegated']),
userId: z.string().nullable().default(null),
abilities: AbilityScoresSchema,
hp: z.number().int().min(0),
maxHp: z.number().int().min(1),
defense: z.number().int().min(1).max(40),
proficiency: z.number().int().min(1).max(10),
inventory: z.array(z.string().max(120)).max(50).default([]),
statuses: z.array(z.string().max(80)).max(12).default([]),
persona: z.record(z.string(), z.unknown()).optional(),
})
export type Character = z.infer<typeof CharacterSchema>
export const PlayerIntentSchema = z.object({
id: z.string().min(1),
roundId: z.string().min(1),
memberId: z.string().min(1),
characterId: z.string().min(1),
action: z.string().trim().min(1).max(2000),
ready: z.boolean().default(false),
createdAt: IsoDatetimeSchema,
updatedAt: IsoDatetimeSchema,
})
export type PlayerIntent = z.infer<typeof PlayerIntentSchema>
export const RoundContextSchema = z.object({
scene: z.string().max(6000),
storySummary: z.string().max(6000).nullable(),
recentRounds: z.array(z.object({
number: z.number().int().positive(),
narration: z.string().max(6000),
nextPrompt: z.string().max(500).nullable().optional(),
})).max(2),
humanIntents: z.array(PlayerIntentSchema).max(8),
aiCharacters: z.array(CharacterSchema).max(8),
activeCharacters: z.array(CharacterSchema).max(16),
memories: z.array(z.object({
summary: z.string().min(1).max(1200),
importance: z.number().int().min(1).max(5).optional(),
tags: z.array(z.string().max(40)).max(12).optional(),
entityIds: z.array(z.string()).max(20).optional(),
})).max(8),
})
export type RoundContext = z.infer<typeof RoundContextSchema>
export const CheckRequestSchema = z.object({
actorId: z.string().min(1),
kind: z.enum(['ability', 'attack', 'initiative', 'damage', 'healing']),
ability: AbilityKeySchema.optional(),
difficulty: z.number().int().min(1).max(40).optional(),
targetId: z.string().optional(),
mode: z.enum(['normal', 'advantage', 'disadvantage']).default('normal'),
dice: z.string().regex(/^\d+d\d+(?:[+-]\d+)?$/).optional(),
reason: z.string().min(1).max(500),
})
export type CheckRequest = z.infer<typeof CheckRequestSchema>
export const DiceRollSchema = z.object({
id: z.string().uuid(),
checkKind: CheckRequestSchema.shape.kind,
formula: z.string(),
rolls: z.array(z.number().int()),
kept: z.array(z.number().int()),
modifier: z.number().int(),
total: z.number().int(),
difficulty: z.number().int().nullable(),
success: z.boolean().nullable(),
actorId: z.string(),
targetId: z.string().nullable(),
createdAt: IsoDatetimeSchema,
})
export type DiceRoll = z.infer<typeof DiceRollSchema>
export const ProposedEventSchema = z.object({
type: z.enum(['narrative', 'relationship', 'quest', 'inventory', 'status', 'damage', 'healing']),
actorId: z.string().nullable().default(null),
targetId: z.string().nullable().default(null),
value: z.number().int().nullable().default(null),
item: z.string().max(120).nullable().default(null),
description: z.string().min(1).max(1000),
})
export type ProposedEvent = z.infer<typeof ProposedEventSchema>
export const RoundPlanSchema = z.object({
checks: z.array(CheckRequestSchema).max(20),
aiActions: z.array(z.object({
characterId: z.string(),
action: z.string().min(1).max(800),
})).max(8),
proposedEvents: z.array(ProposedEventSchema).max(30),
relevantMemoryQueries: z.array(z.string().max(120)).max(8),
})
export type RoundPlan = z.infer<typeof RoundPlanSchema>
export const RoundResolutionSchema = z.object({
narration: z.string().min(20).max(6000),
events: z.array(ProposedEventSchema).max(30),
memory: z.object({
summary: z.string().min(10).max(1200),
importance: z.number().int().min(1).max(5),
tags: z.array(z.string().max(40)).max(12),
entityIds: z.array(z.string()).max(20),
}).nullable(),
nextPrompt: z.string().min(3).max(500),
})
export type RoundResolution = z.infer<typeof RoundResolutionSchema>
export const StorySummarySchema = z.object({
summary: z.string().min(20).max(6000),
})
export type StorySummary = z.infer<typeof StorySummarySchema>
export const CoauthorMessageSchema = z.object({
role: z.enum(['user', 'assistant']),
content: z.string().trim().min(1).max(5000),
})
export const CreateWorldRequestSchema = z.object({
messages: z.array(CoauthorMessageSchema).min(1).max(12),
})
export const SubmitIntentRequestSchema = z.object({
campaignId: z.string().min(1),
characterId: z.string().min(1),
action: z.string().trim().min(1).max(2000),
ready: z.boolean().default(true),
})
export type ID = string
export function makeId(prefix: string): string {
const randomUuid = globalThis.crypto?.randomUUID?.()
// IDs are identifiers, not authentication secrets. The fallback keeps the
// shared browser/server contract usable in Node 18 VM contexts without Web Crypto.
const id = randomUuid ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`
return `${prefix}_${id}`
}