- 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>
441 lines
12 KiB
Plaintext
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")
|
|
}
|