// D&D Campaign Manager — Prisma schema // Postgres. The connection (DATABASE_URL) is set in prisma.config.js, not here. // Run `npx prisma migrate dev` then `npx prisma generate`; `npx prisma db seed` // adds the admin account (prisma/seed.js). Worlds are created in the app from // the seeds in /backend/seeds. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } // --------------------------------------------------------------------------- // Enums // --------------------------------------------------------------------------- enum CampaignRole { DM PLAYER } enum Species { HUMAN HALF_ELF ELF GNOME DWARF HALFLING DRAGONBORN TIEFLING HALF_ORC ORC AASIMAR GOLIATH AARAKOCRA BUGBEAR CENTAUR CHANGELING DEEP_GNOME DUERGAR ELADRIN FAIRY FIRBOLG AIR_GENASI EARTH_GENASI FIRE_GENASI WATER_GENASI GITHYANKI GITHZERAI GOBLIN HARENGON HOBGOBLIN KENKU KOBOLD LIZARDFOLK MINOTAUR SEA_ELF SHADAR_KAI SHIFTER TABAXI TORTLE TRITON YUAN_TI OWLIN SATYR } enum CharacterClass { FIGHTER BARBARIAN ROGUE RANGER WIZARD SORCERER CLERIC PALADIN BARD WARLOCK DRUID MONK ARTIFICER } 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 CHOOSING_SUBCLASS ROLLING_MOTIVATION ASSIGNING_SCORES ASSIGNING_PROFICIENCIES CHOOSING_ABILITIES CHOOSING_BACKGROUND CHOOSING_APPEARANCE CHOOSING_EQUIPMENT COMPLETE } enum EncounterStatus { PLANNING ACTIVE COMPLETE } // What kind of scene an encounter is: a fight run in initiative order, a // quest scene built around NPCs and an objective, or free roam, where the // party wanders between places the DM has laid out (to shop, to meet people). enum EncounterMode { BATTLE QUEST FREE_ROAM } enum NpcDisposition { FRIENDLY NEUTRAL HOSTILE } enum RewardType { GOLD WEAPON ITEM } // The money a character carries. The coins' worth is fixed in utils/grants.js: // 1 gold = 10 silver = 100 copper. Cash ($) is modern money for crossover // worlds; it has no worth in coins and is never exchanged for them. enum Currency { GOLD SILVER COPPER CASH } enum NoteVisibility { DM_ONLY SHARED } enum DiceMode { DIGITAL PHYSICAL } // Which dice a campaign plays with. BOTH leaves it to each character (and // every roll can still be typed in from a real die). enum CampaignDiceMode { DIGITAL PHYSICAL BOTH } // How a character's maximum HP grows on a level up: the fixed average of the // hit die (rounded up), or a roll of it. enum HpMethod { AVERAGE ROLL } // How the builder arrives at the six ability scores. enum ScoreMethod { ROLL_4D6 STANDARD_ARRAY POINT_BUY } // Who sees a roll. ALL: everyone at the table. OWN: a player sees their own; // the DM always sees every roll. enum RollVisibility { ALL OWN } // What a critical hit does to the damage: roll the dice twice (the rule), or // take the dice at their maximum and roll them once more on top. enum CritRule { DOUBLE_DICE MAX_PLUS_ROLL } // --------------------------------------------------------------------------- // 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[] goldTransactions GoldTransaction[] @@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) // Off: players see only a health bar for monsters and NPCs, not the numbers. showMonsterHp Boolean @default(true) // An attack on several targets is harder to land: each target's AC counts // as this much higher for every target beyond the first. 0 turns it off. areaAttackAcPenalty Int @default(2) // How much a character can have equipped at once: weapons and items // together, and weapons alone. Only equipped gear can be used in an // encounter or gives its effects, so this is what a character can bring to // bear at one time. A pair of daggers is one weapon, made as such. maxEquipped Int @default(2) maxEquippedWeapons Int @default(1) // Off: nobody chooses a subclass. Each class follows its first one without // being asked, and subclasses are not named anywhere. subclassesEnabled Boolean @default(true) // On: a character can take levels in more than one class (game/multiclass.js). multiclassEnabled Boolean @default(false) // Which dice are rolled at this table; see CampaignDiceMode. diceMode CampaignDiceMode @default(BOTH) // On: when a monster's (or an NPC's) turn comes up in a battle, the server // picks a target, rolls the attack and the damage, and puts it to the DM to // accept, change or throw out. autoRollMonsters Boolean @default(false) autoRollNpcs Boolean @default(false) // Off: experience is not kept. No XP on the sheets, nothing to award after // an encounter, and nobody is shown as due a level. trackXp Boolean @default(true) hpOnLevelUp HpMethod @default(AVERAGE) abilityScoreMethod ScoreMethod @default(ROLL_4D6) rollVisibility RollVisibility @default(ALL) // On: the DM's rolls for monsters and NPCs are seen by the DM alone until // they are put into play. hideMonsterRolls Boolean @default(true) critRule CritRule @default(DOUBLE_DICE) ownerId String // Set while the campaign is archived: kept in full, but moved off the main // list and closed to new members until the DM restores it. archivedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members CampaignMember[] characters Character[] weapons Weapon[] spells Spell[] items Item[] monsters Monster[] npcs Npc[] encounters Encounter[] saleOffers SaleOffer[] notes Note[] locations Location[] diceRolls DiceRoll[] @@map("campaigns") } // A roll made at the table's dice roller, kept so the table (and above all // the DM) can see it. Digital rolls are made by the server; physical ones are // typed in and checked against their dice. Who sees a roll follows the // campaign's rollVisibility; a `hidden` one is the DM's alone. model DiceRoll { id String @id @default(cuid()) campaignId String userId String rollerName String // who rolled, as shown: kept so a roll outlives a renamed account label String? // what it was for: "Aria: Stealth" notation String // "1d20+3", "2d6+1d8+2" // Every die, in order: [{ sides, value, kept, sign }]. `kept` is false for // the d20 that advantage or disadvantage dropped. dice Json modifier Int @default(0) total Int natural Int? // the d20 that counted, for a roll of a single d20 mode String @default("NORMAL") // NORMAL | ADVANTAGE | DISADVANTAGE physical Boolean @default(false) // rolled at the table and typed in hidden Boolean @default(false) // the DM's alone encounterId String? createdAt DateTime @default(now()) campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) @@index([campaignId, createdAt]) @@map("dice_rolls") } 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[] encounters Encounter[] visits CharacterLocationVisit[] // Who is found here, what is for sale here, and the free-roam scenes it is a stop in. npcs Npc[] wares SaleOffer[] freeRoams EncounterLocation[] @@index([campaignId]) @@map("locations") } // A location a character has been to. Written whenever a character is put // somewhere (by the DM, or by an encounter held there), and never removed by // moving on: it is what decides which locations a player may see. model CharacterLocationVisit { id String @id @default(cuid()) characterId String locationId String firstVisitedAt DateTime @default(now()) character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) location Location @relation(fields: [locationId], references: [id], onDelete: Cascade) @@unique([characterId, locationId]) @@map("character_location_visits") } 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]) @@index([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? // The path taken within the class: a key from game/data/subclasses.js. Null // until the class reaches the level it chooses at. subclass String? level Int @default(1) // Levels the DM has approved that the character has not yet taken: each is // one level-up checklist waiting for its picks. The player resolves them // (or the DM does), one at a time. levelUpsPending Int @default(0) // Where the campaign rolls HP on a level up: how far the rolls so far are // from the average they replace, in all (a 2 on a d8 is -3). Added to max HP. hpRollAdjust Int @default(0) 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) // The ability bonuses the player chose for their species, on top of its // fixed ones, as { ability: amount }: a Half-Elf's +1/+1, a Goblin's +2/+1... // Empty for a species with nothing to choose (game/data/species.js). speciesBonusChoices Json @default("{}") currentHp Int @default(0) tempHp Int @default(0) // Death saves, counted while the character is at 0 HP (utils/hp.js). Three // failures is dead; three successes sets `stable` and clears the tallies. deathSaveSuccesses Int @default(0) deathSaveFailures Int @default(0) stable Boolean @default(false) // at 0 HP but no longer rolling // What has been spent since the last rest. Spell slots by slot level (the // first entry is 1st-level slots); how many there are comes from class and // level (game/calculations.js, spellSlotsFor). A character has one Hit Die // per level, spent on short rests. spellSlotsUsed Int[] @default([]) hitDiceUsed Int @default(0) // Class counters spent since the rest that gives them back, as // { key: used } (game/classResources.js says which a character has). resourcesUsed Json @default("{}") // How the character is doing beyond HP. Reminders for the table: none of // them changes a number on the sheet. Conditions are the character's own, // so they outlast the encounter they were given in. inspiration Boolean @default(false) exhaustion Int @default(0) // 0 to 6 conditions String[] @default([]) armorBonus Int @default(0) // a flat AC adjustment the DM sets by hand, on top of what equipped gear gives speedOverride Int? // The next three are composed by the builder from the player's picks // (game/data/characterOptions.js). After that only the DM can edit them. specialAbilities String? // shown in the quick-stats panel // The purse: each coin (and cash) is counted on its own and only changes through the // money ledger (GoldTransaction). gold Int @default(0) silver Int @default(0) copper Int @default(0) cash Int @default(0) // whole dollars background String? appearance String? 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[] goldTransactions GoldTransaction[] weapons CharacterWeapon[] spells CharacterSpell[] notes Note[] encounterEntries EncounterParticipant[] locationVisits CharacterLocationVisit[] // Levels in classes other than characterClass, for a multiclassed character // (game/multiclass.js). The first class's own level is `level` less these. classLevels CharacterClassLevel[] @@index([campaignId]) @@index([userId]) @@map("characters") } // A class a character has taken levels in besides their first (characterClass), // with its own level and subclass. A single-class character has none. model CharacterClassLevel { id String @id @default(cuid()) characterId String characterClass CharacterClass level Int @default(1) subclass String? createdAt DateTime @default(now()) character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) @@unique([characterId, characterClass]) @@map("character_class_levels") } 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? // damageDice is the level-1 damage; a held copy's dice climb the damage // ladder (game/calculations.js) by this many rungs per level above 1. damageScaling Int @default(1) // The level this weapon starts at: a held copy is never below it, and // damageDice is the damage at this level. minLevel Int @default(1) // Offered in the character builder's starting-equipment step. Off by default: // the DM opts entries in, so the pool can hold gear that must be earned. isStartingOption Boolean @default(false) // What it does to the sheet of whoever has it equipped: a list of // { stat, amount } (game/calculations.js, GEAR_STATS), and a note for // anything that is not a number. effects Json @default("[]") effectNote String? createdAt DateTime @default(now()) campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) assigned CharacterWeapon[] rewards EncounterReward[] offers SaleOffer[] @@index([campaignId]) @@map("weapons") } model CharacterWeapon { id String @id @default(cuid()) characterId String weaponId String quantity Int @default(1) equipped Boolean @default(false) level Int @default(1) // this copy's level; the character must be at least this level to use it character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) weapon Weapon @relation(fields: [weaponId], references: [id], onDelete: Cascade) @@unique([characterId, weaponId]) @@map("character_weapons") } // Campaign-level item pool, DM-managed like weapons and spells. model Item { id String @id @default(cuid()) campaignId String name String description String? damageDice String? // optional dice (damage, healing...) at its starting level; scales like a weapon's damageScaling Int @default(1) minLevel Int @default(1) isStartingOption Boolean @default(false) // As on a weapon: what it does to whoever has it equipped. Armor is an // item with an AC effect. effects Json @default("[]") effectNote String? // Set on armor and shields. Its AC is still an effect; this says what kind // it is, so the sheet knows when a character is unarmored (Unarmored // Defense, a monk's speed) or in heavy armor (a barbarian's Fast Movement). armorType ArmorType? createdAt DateTime @default(now()) campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) held InventoryItem[] rewards EncounterReward[] offers SaleOffer[] @@index([campaignId]) @@map("items") } enum ArmorType { LIGHT MEDIUM HEAVY SHIELD } // An Item from the campaign pool held by a character. model InventoryItem { id String @id @default(cuid()) characterId String itemId String quantity Int @default(1) equipped Boolean @default(false) level Int @default(1) character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) @@unique([characterId, itemId]) @@map("inventory_items") } // Ledger behind a character's purse: every change to a coin balance is one // row here. (The name predates silver and copper.) model GoldTransaction { id String @id @default(cuid()) characterId String currency Currency @default(GOLD) // which coin this entry is in amount Int // signed: positive = gained, negative = spent balanceAfter Int // that coin's balance afterwards reason String? createdById String? createdAt DateTime @default(now()) character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull) @@index([characterId, createdAt]) @@map("gold_transactions") } // --------------------------------------------------------------------------- // Spells // --------------------------------------------------------------------------- model Spell { id String @id @default(cuid()) campaignId String name String level Int // 0 = cantrip. Also the character level needed to use the spell. damageDice String? // fixed; spells don't level up school String? castingTime String? range String? duration String? description String? isStartingOption Boolean @default(false) // Brings back the dead: a character who can cast it may revive a dead ally // on their turn in a battle (encounters.routes.js). revives Boolean @default(false) // How it lands in a battle (game/combat.js). attackRoll: the caster rolls a // spell attack against AC. saveAbility: each target saves against the // caster's spell DC with this ability, taking half on a success when // halfOnSave. Neither: it simply takes effect. attackRoll Boolean @default(false) saveAbility Ability? halfOnSave Boolean @default(false) campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) known CharacterSpell[] @@index([campaignId]) @@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? // Named attacks, for a monster with more than the one above: a list of // { name, attackBonus, damageDice, damageType?, count? }. `count` is how // many times it is made each turn (a multiattack). Empty: the single // attackBonus/damageDice above is its attack. attacks Json @default("[]") campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) encounterAppearances EncounterParticipant[] @@index([campaignId]) @@map("monsters") } // A non-player character the DM runs: a quest-giver, shopkeeper, rival... // Combat stats are optional — most NPCs are there to be talked to. model Npc { id String @id @default(cuid()) campaignId String name String role String? // e.g. "Innkeeper", "Captain of the Watch" disposition NpcDisposition @default(NEUTRAL) description String? // what the party can see and knows dmNotes String? // secrets, motives — never sent to players ac Int? maxHp Int? attackBonus Int? damageDice String? // A money changer swaps gold and cash at their own rate: exchangeGold gold // for exchangeCash cash, either way. Both null: this NPC does not exchange. exchangeGold Int? exchangeCash Int? // Where this NPC is usually found. In a free-roam scene they are met (and // traded with) by whoever goes there. locationId String? createdAt DateTime @default(now()) campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) encounterAppearances EncounterParticipant[] wares SaleOffer[] @@index([campaignId]) @@map("npcs") } model Encounter { id String @id @default(cuid()) campaignId String name String mode EncounterMode @default(BATTLE) status EncounterStatus @default(PLANNING) currentRound Int @default(1) // Whose turn it is in a battle; null until the fight starts. activeParticipantId String? // Who is being attacked (or healed) right now, shown to everyone, in the // order they were picked. More than one makes it an area attack. Picked by // the DM or by the player on turn, and cleared when the turn passes. targetParticipantIds String[] @default([]) description String? // the scene / objective, shown to everyone dmNotes String? // never sent to players // Where the scene takes place. While the encounter is in progress, the // characters in it are there too. locationId String? // Set while archived: kept in full, but off the campaign's list — players // no longer see it there at all. archivedAt DateTime? // Place in the campaign's own order, lowest first. The DM rearranges it. sortOrder Int @default(0) // The experience points handed out for this encounter, in all; null until // the DM awards them. They are awarded once. xpAwarded Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) participants EncounterParticipant[] rewards EncounterReward[] wares SaleOffer[] actions EncounterAction[] // Free roam: the places the party can go to in this scene. places EncounterLocation[] @@index([campaignId]) @@map("encounters") } // A place that is a stop in a free-roam scene: one of the campaign's // locations, picked by the DM as somewhere the party can go right now. model EncounterLocation { id String @id @default(cuid()) encounterId String locationId String createdAt DateTime @default(now()) encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade) location Location @relation(fields: [locationId], references: [id], onDelete: Cascade) @@unique([encounterId, locationId]) @@map("encounter_locations") } // An attack or a spell in a battle, from the moment it is declared to the // moment its damage lands (game/combat.js). A player declares their own on // their turn; the DM declares anyone's; and with the campaign's auto-roll // settings the server declares a monster's or NPC's, already rolled. While // PENDING it gathers what it needs (a d20 against each target's AC, or each // target's saving throw, and the damage); the DM then applies it, changed as // they see fit, or discards it. `hidden` keeps it from the players until it // is applied. model EncounterAction { id String @id @default(cuid()) encounterId String round Int actorParticipantId String? actorName String kind String // ATTACK (a d20 against AC) | SAVE (targets save against a DC) | AUTO (it just lands) name String // "Longsword", "Fireball", "Bite" // What it rolls with: { attackBonus?, saveAbility?, saveDC?, halfOnSave?, // damageDice?, damageBonus, damageType?, characterId?, slotLevel? }. detail Json // One entry per target: { participantId, name, kind, ac, penalty, saveBonus, // d20, outcome (set by the DM to overrule the dice), damage (likewise) }. targets Json // The damage once rolled or entered: { total, dice?, modifier?, crit? }. damage Json? status String @default("PENDING") // PENDING | APPLIED | DISCARDED auto Boolean @default(false) // declared and rolled by the server hidden Boolean @default(false) createdById String? createdAt DateTime @default(now()) appliedAt DateTime? encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade) @@index([encounterId, status]) @@map("encounter_actions") } // Loot the DM lines up for an encounter and later hands to a character. Until // it is awarded it is the DM's secret; awarding it is what actually changes // the character (gold ledger entry, or the weapon/item added to what they hold). model EncounterReward { id String @id @default(cuid()) encounterId String type RewardType gold Int? // GOLD: how many coins currency Currency @default(GOLD) // GOLD: which coin they are weaponId String? // WEAPON itemId String? // ITEM quantity Int @default(1) level Int @default(1) // gear level the weapon/item is handed over at note String? awardedTo String? // who received it, as display text ("Aria", "Aria, Bo") awardedAt DateTime? createdAt DateTime @default(now()) encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade) weapon Weapon? @relation(fields: [weaponId], references: [id], onDelete: Cascade) item Item? @relation(fields: [itemId], references: [id], onDelete: Cascade) @@index([encounterId]) @@map("encounter_rewards") } // Something for sale: a weapon or item from the campaign pools, at a price. // It belongs to an NPC (and is on offer wherever that NPC is met), to an // encounter itself (a stall, a vending machine), or to a location (a shop // that is always there, open to whoever goes to it in a free-roam scene). // Only the DM sets these up; players see them, and buy, in a quest or // free-roam encounter in progress (encounters.routes.js). model SaleOffer { id String @id @default(cuid()) campaignId String npcId String? // exactly one of npcId / encounterId / locationId is set encounterId String? locationId String? weaponId String? // exactly one of weaponId / itemId is set itemId String? price Int // for one, in `currency` currency Currency @default(GOLD) level Int @default(1) // gear level it is handed over at stock Int? // how many are left; null = no limit createdAt DateTime @default(now()) campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) npc Npc? @relation(fields: [npcId], references: [id], onDelete: Cascade) encounter Encounter? @relation(fields: [encounterId], references: [id], onDelete: Cascade) location Location? @relation(fields: [locationId], references: [id], onDelete: Cascade) weapon Weapon? @relation(fields: [weaponId], references: [id], onDelete: Cascade) item Item? @relation(fields: [itemId], references: [id], onDelete: Cascade) @@index([campaignId]) @@index([npcId]) @@index([encounterId]) @@index([locationId]) @@map("sale_offers") } // A row per combatant in an encounter — a character, a monster or an NPC. // A monster's currentHp/maxHp live here, per instance. A character's HP has // one source of truth, the Character row: encounters read and write that, and // the columns here only hold the last values seen (used if the character is // later deleted and the row loses its link). model EncounterParticipant { id String @id @default(cuid()) encounterId String characterId String? monsterId String? npcId String? displayName String // denormalized so a monster can be renamed per-instance, e.g. "Goblin #2" // The d20 rolled for initiative (1–20); null until it is rolled. initiativeRoll Int? // The roll plus the combatant's initiative bonus, worked out when the roll // is entered. This is what the turn order sorts on. initiative Int @default(0) currentHp Int maxHp Int conditions String[] @default([]) // e.g. ["poisoned", "prone"]; a character's own are on the Character row // Conditions that wear off by themselves, as { condition: rounds left }. // Counted down each time the round goes up; at 0 the condition is removed. conditionRounds Json @default("{}") 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) npc Npc? @relation(fields: [npcId], references: [id], onDelete: SetNull) @@index([encounterId]) @@index([characterId]) @@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]) @@index([campaignId]) @@index([characterId]) @@map("notes") }