Files
the-sunken-reach/backend/prisma/schema.prisma
T
joshuaandClaude Opus 5.5 94067f1268 V1.0.1: Aldenfell campaign seed, content library, party overview
- Seed now resets the database and seeds the Aldenfell campaign from the
  campaign book: admin DM user, default weapons, locations, bestiary,
  pre-built encounters, and DM/shared notes
- Default weapon/spell/monster library, character profile fields,
  locations and dice mode (new migration)
- Party overview page, pagination, info tips, build preview calc
- Remove stale demo-login hint; notes preserve line breaks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:24:48 +02:00

441 lines
12 KiB
Plaintext

// D&D Campaign Manager — Prisma schema
// Postgres. Run `npx prisma migrate dev --name init` after `DATABASE_URL` is set,
// then `npx prisma db seed` using the seed script built from /seed-data/*.json
// (see CLAUDE_BUILD_PROMPT.md, "Database" section).
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
enum CampaignRole {
DM
PLAYER
}
enum Species {
HUMAN
HALF_ELF
ELF
GNOME
DWARF
HALFLING
DRAGONBORN
TIEFLING
HALF_ORC
ORC
}
enum CharacterClass {
FIGHTER
BARBARIAN
ROGUE
RANGER
WIZARD
SORCERER
CLERIC
PALADIN
BARD
WARLOCK
DRUID
MONK
}
enum Ability {
STR
DEX
CON
INT
WIS
CHA
}
enum Skill {
ACROBATICS
ANIMAL_HANDLING
ARCANA
ATHLETICS
DECEPTION
HISTORY
INSIGHT
INTIMIDATION
INVESTIGATION
MEDICINE
NATURE
PERCEPTION
PERFORMANCE
PERSUASION
RELIGION
SLEIGHT_OF_HAND
STEALTH
SURVIVAL
}
enum BuildStage {
NOT_STARTED
ROLLING_SPECIES
ROLLING_CLASS
ROLLING_MOTIVATION
ASSIGNING_SCORES
ASSIGNING_PROFICIENCIES
CHOOSING_EQUIPMENT
COMPLETE
}
enum EncounterStatus {
PLANNING
ACTIVE
COMPLETE
}
enum NoteVisibility {
DM_ONLY
SHARED
}
enum DiceMode {
DIGITAL
PHYSICAL
}
// ---------------------------------------------------------------------------
// Identity & campaigns
// ---------------------------------------------------------------------------
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
displayName String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships CampaignMember[]
charactersOwned Character[] @relation("PlayerCharacters")
charactersCreated Character[] @relation("CreatedBy")
notesAuthored Note[]
@@map("users")
}
model Campaign {
id String @id @default(cuid())
name String
description String?
inviteCode String @unique
startingGold Int @default(0)
startingItemCount Int @default(0)
startingSpellCount Int @default(0)
skipMotivationRoll Boolean @default(false)
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members CampaignMember[]
characters Character[]
weapons Weapon[]
spells Spell[]
monsters Monster[]
encounters Encounter[]
notes Note[]
locations Location[]
@@map("campaigns")
}
model Location {
id String @id @default(cuid())
campaignId String
name String
description String?
createdAt DateTime @default(now())
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
characters Character[]
@@map("locations")
}
model CampaignMember {
id String @id @default(cuid())
campaignId String
userId String
role CampaignRole @default(PLAYER)
joinedAt DateTime @default(now())
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([campaignId, userId])
@@map("campaign_members")
}
// ---------------------------------------------------------------------------
// Characters
// ---------------------------------------------------------------------------
model Character {
id String @id @default(cuid())
campaignId String
userId String? // owning player; null for a DM-controlled NPC/ally
createdById String
name String
species Species?
characterClass CharacterClass?
level Int @default(1)
xp Int @default(0)
motivation String?
// Rolled ability scores (before species bonus)
strScore Int @default(10)
dexScore Int @default(10)
conScore Int @default(10)
intScore Int @default(10)
wisScore Int @default(10)
chaScore Int @default(10)
// Half-Elf's flexible +1/+1 choice; null unless species = HALF_ELF
halfElfBonusAbility1 Ability?
halfElfBonusAbility2 Ability?
currentHp Int @default(0)
tempHp Int @default(0)
armorBonus Int @default(0) // flat AC bonus from equipped armor/shield
speedOverride Int?
specialAbilities String? // free-text list shown in the quick-stats panel
gold Int @default(0)
background String? // free-text story background, player-editable
appearance String? // free-text character look/description, player-editable
privateNotes String? // visible to the owning player only — not even the DM
diceMode DiceMode @default(DIGITAL) // chosen once, before the first build roll
currentLocationId String?
buildStage BuildStage @default(NOT_STARTED)
buildState Json? // in-progress dice rolls / pending choices while building
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
user User? @relation("PlayerCharacters", fields: [userId], references: [id])
createdBy User @relation("CreatedBy", fields: [createdById], references: [id])
currentLocation Location? @relation(fields: [currentLocationId], references: [id], onDelete: SetNull)
skillProficiencies CharacterSkillProficiency[]
saveProficiencies CharacterSaveProficiency[]
inventoryItems InventoryItem[]
weapons CharacterWeapon[]
spells CharacterSpell[]
notes Note[]
encounterEntries EncounterParticipant[]
@@map("characters")
}
model CharacterSkillProficiency {
id String @id @default(cuid())
characterId String
skill Skill
proficient Boolean @default(true)
expertise Boolean @default(false) // double proficiency bonus, e.g. Rogue
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
@@unique([characterId, skill])
@@map("character_skill_proficiencies")
}
model CharacterSaveProficiency {
id String @id @default(cuid())
characterId String
ability Ability
proficient Boolean @default(true)
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
@@unique([characterId, ability])
@@map("character_save_proficiencies")
}
// ---------------------------------------------------------------------------
// Weapons & inventory
// ---------------------------------------------------------------------------
model Weapon {
id String @id @default(cuid())
campaignId String
name String
damageDice String // e.g. "1d8"
damageType String // e.g. "slashing"
attackAbility String // "STR" | "DEX" | "FINESSE" (finesse = better of STR/DEX)
properties String[] // e.g. ["finesse", "light", "thrown"]
rangeNormal Int?
rangeLong Int?
createdAt DateTime @default(now())
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
assigned CharacterWeapon[]
@@map("weapons")
}
model CharacterWeapon {
id String @id @default(cuid())
characterId String
weaponId String
quantity Int @default(1)
equipped Boolean @default(false)
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
weapon Weapon @relation(fields: [weaponId], references: [id], onDelete: Cascade)
@@unique([characterId, weaponId])
@@map("character_weapons")
}
model InventoryItem {
id String @id @default(cuid())
characterId String
name String
description String?
quantity Int @default(1)
equipped Boolean @default(false)
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
@@map("inventory_items")
}
// ---------------------------------------------------------------------------
// Spells
// ---------------------------------------------------------------------------
model Spell {
id String @id @default(cuid())
campaignId String
name String
level Int // 0 = cantrip
school String?
castingTime String?
range String?
duration String?
description String?
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
known CharacterSpell[]
@@map("spells")
}
model CharacterSpell {
id String @id @default(cuid())
characterId String
spellId String
prepared Boolean @default(false)
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
spell Spell @relation(fields: [spellId], references: [id], onDelete: Cascade)
@@unique([characterId, spellId])
@@map("character_spells")
}
// ---------------------------------------------------------------------------
// Monsters & encounters
// ---------------------------------------------------------------------------
model Monster {
id String @id @default(cuid())
campaignId String
name String
ac Int
maxHp Int
speed Int @default(30)
strScore Int @default(10)
dexScore Int @default(10)
conScore Int @default(10)
intScore Int @default(10)
wisScore Int @default(10)
chaScore Int @default(10)
attackBonus Int @default(0)
damageDice String?
challengeRating String?
specialTrait String?
notes String?
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
encounterAppearances EncounterParticipant[]
@@map("monsters")
}
model Encounter {
id String @id @default(cuid())
campaignId String
name String
status EncounterStatus @default(PLANNING)
currentRound Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
participants EncounterParticipant[]
@@map("encounters")
}
// A row per combatant in an encounter — either a character or a monster.
// currentHp/maxHp are snapshotted here (not just read off Character/Monster)
// so damage tracking during a fight doesn't need to mutate the character's or
// monster's permanent record until the DM chooses to apply it back.
model EncounterParticipant {
id String @id @default(cuid())
encounterId String
characterId String?
monsterId String?
displayName String // denormalized so a monster can be renamed per-instance, e.g. "Goblin #2"
initiative Int @default(0)
currentHp Int
maxHp Int
conditions String[] @default([]) // e.g. ["poisoned", "prone"]
isMonster Boolean
encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade)
character Character? @relation(fields: [characterId], references: [id], onDelete: SetNull)
monster Monster? @relation(fields: [monsterId], references: [id], onDelete: SetNull)
@@map("encounter_participants")
}
// ---------------------------------------------------------------------------
// Notes
// ---------------------------------------------------------------------------
model Note {
id String @id @default(cuid())
campaignId String?
characterId String?
authorId String
body String
visibility NoteVisibility @default(SHARED)
createdAt DateTime @default(now())
campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: Cascade)
character Character? @relation(fields: [characterId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id])
@@map("notes")
}