// 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 } // --------------------------------------------------------------------------- // 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[] @@map("campaigns") } 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) 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]) 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") }