Alchemy System: - Versatile Vials tracking with refill functionality - Research Field display (Bomber, Chirurgeon, Mutagenist, Toxicologist) - Formula Book with search and level filtering - Advanced Alchemy (daily preparation) for infused items - Quick Alchemy using versatile vials - Normal Alchemy for permanent crafted items - Auto-upgrade system for formula variants (Lesser → Greater) - Effect parsing with damage badges (damage type colors, splash, healing, bonus) - German translations for all UI elements and item effects - WebSocket sync for all alchemy state changes Rest System: - HP healing based on CON modifier × Level - Condition management (Fatigued removed, Doomed/Drained reduced) - Resource reset (spell slots, focus points, daily abilities) - Alchemy reset (infused items expire, vials refilled) - Rest modal with preview of changes Database: - CharacterAlchemyState model for vials and batch tracking - CharacterFormula model for formula book - CharacterPreparedItem model with isInfused flag - Equipment effect field for variant-specific effects - Translation germanEffect field for effect translations - Scraped effect data from Archives of Nethys (205 items) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
674 lines
18 KiB
Plaintext
674 lines
18 KiB
Plaintext
// Dimension47 - TTRPG Campaign Management Platform
|
|
// Prisma Schema
|
|
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
moduleFormat = "cjs"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// ==========================================
|
|
// ENUMS
|
|
// ==========================================
|
|
|
|
enum UserRole {
|
|
ADMIN
|
|
GM
|
|
PLAYER
|
|
}
|
|
|
|
enum CharacterType {
|
|
PC
|
|
NPC
|
|
}
|
|
|
|
enum AbilityType {
|
|
STR
|
|
DEX
|
|
CON
|
|
INT
|
|
WIS
|
|
CHA
|
|
}
|
|
|
|
enum Proficiency {
|
|
UNTRAINED
|
|
TRAINED
|
|
EXPERT
|
|
MASTER
|
|
LEGENDARY
|
|
}
|
|
|
|
enum FeatSource {
|
|
CLASS
|
|
ANCESTRY
|
|
GENERAL
|
|
SKILL
|
|
BONUS
|
|
ARCHETYPE
|
|
}
|
|
|
|
enum SpellTradition {
|
|
ARCANE
|
|
DIVINE
|
|
OCCULT
|
|
PRIMAL
|
|
}
|
|
|
|
enum CombatantType {
|
|
PC
|
|
NPC
|
|
MONSTER
|
|
}
|
|
|
|
enum ActionType {
|
|
ACTION
|
|
REACTION
|
|
FREE
|
|
}
|
|
|
|
enum HighlightColor {
|
|
YELLOW
|
|
GREEN
|
|
BLUE
|
|
PINK
|
|
}
|
|
|
|
enum TranslationType {
|
|
FEAT
|
|
EQUIPMENT
|
|
SPELL
|
|
TRAIT
|
|
ANCESTRY
|
|
HERITAGE
|
|
CLASS
|
|
BACKGROUND
|
|
CONDITION
|
|
ACTION
|
|
}
|
|
|
|
enum TranslationQuality {
|
|
HIGH
|
|
MEDIUM
|
|
LOW
|
|
}
|
|
|
|
enum ResearchField {
|
|
BOMBER
|
|
CHIRURGEON
|
|
MUTAGENIST
|
|
TOXICOLOGIST
|
|
}
|
|
|
|
// ==========================================
|
|
// USER & AUTH
|
|
// ==========================================
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
username String @unique
|
|
email String @unique
|
|
passwordHash String
|
|
role UserRole @default(PLAYER)
|
|
avatarUrl String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
gmCampaigns Campaign[] @relation("CampaignGM")
|
|
campaignMembers CampaignMember[]
|
|
characters Character[] @relation("CharacterOwner")
|
|
uploadedDocuments Document[]
|
|
documentAccess DocumentAccess[]
|
|
highlights Highlight[]
|
|
notes Note[]
|
|
noteShares NoteShare[]
|
|
}
|
|
|
|
// ==========================================
|
|
// CAMPAIGNS
|
|
// ==========================================
|
|
|
|
model Campaign {
|
|
id String @id @default(uuid())
|
|
name String
|
|
description String?
|
|
gmId String
|
|
imageUrl String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
gm User @relation("CampaignGM", fields: [gmId], references: [id])
|
|
members CampaignMember[]
|
|
characters Character[]
|
|
battleMaps BattleMap[]
|
|
combatants Combatant[]
|
|
battleSessions BattleSession[]
|
|
documents Document[]
|
|
notes Note[]
|
|
}
|
|
|
|
model CampaignMember {
|
|
campaignId String
|
|
userId String
|
|
joinedAt DateTime @default(now())
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([campaignId, userId])
|
|
}
|
|
|
|
// ==========================================
|
|
// CHARACTERS (Normalisiert)
|
|
// ==========================================
|
|
|
|
model Character {
|
|
id String @id @default(uuid())
|
|
campaignId String
|
|
ownerId String?
|
|
name String
|
|
type CharacterType @default(PC)
|
|
level Int @default(1)
|
|
avatarUrl String?
|
|
|
|
// Core Stats
|
|
hpCurrent Int
|
|
hpMax Int
|
|
hpTemp Int @default(0)
|
|
|
|
// Ancestry/Class/Background (References)
|
|
ancestryId String?
|
|
heritageId String?
|
|
classId String?
|
|
backgroundId String?
|
|
|
|
// Experience
|
|
experiencePoints Int @default(0)
|
|
|
|
// Currency (Ironvale uses Credits instead of Gold/Silver/Copper)
|
|
credits Int @default(0)
|
|
|
|
// Pathbuilder Import Data (JSON blob for original import)
|
|
pathbuilderData Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
owner User? @relation("CharacterOwner", fields: [ownerId], references: [id])
|
|
abilities CharacterAbility[]
|
|
feats CharacterFeat[]
|
|
skills CharacterSkill[]
|
|
spells CharacterSpell[]
|
|
items CharacterItem[]
|
|
conditions CharacterCondition[]
|
|
resources CharacterResource[]
|
|
battleTokens BattleToken[]
|
|
documentAccess DocumentAccess[]
|
|
|
|
// Alchemy
|
|
alchemyState CharacterAlchemyState?
|
|
formulas CharacterFormula[]
|
|
preparedItems CharacterPreparedItem[]
|
|
}
|
|
|
|
model CharacterAbility {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
ability AbilityType
|
|
score Int
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([characterId, ability])
|
|
}
|
|
|
|
model CharacterFeat {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
featId String? // Reference to Feat table
|
|
name String // English name
|
|
nameGerman String? // German translation
|
|
level Int // Level obtained
|
|
source FeatSource
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
feat Feat? @relation(fields: [featId], references: [id])
|
|
}
|
|
|
|
model CharacterSkill {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
skillName String
|
|
proficiency Proficiency @default(UNTRAINED)
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([characterId, skillName])
|
|
}
|
|
|
|
model CharacterSpell {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
spellId String?
|
|
name String
|
|
nameGerman String?
|
|
tradition SpellTradition
|
|
spellLevel Int
|
|
prepared Boolean @default(false)
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
spell Spell? @relation(fields: [spellId], references: [id])
|
|
}
|
|
|
|
model CharacterItem {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
equipmentId String? // Reference to Equipment library
|
|
name String
|
|
nameGerman String?
|
|
quantity Int @default(1)
|
|
bulk Decimal @default(0)
|
|
equipped Boolean @default(false)
|
|
invested Boolean @default(false)
|
|
containerId String? // For containers
|
|
notes String?
|
|
|
|
// Player-editable: Alias/Nickname for the item
|
|
alias String?
|
|
|
|
// GM-editable: Custom overrides for item properties
|
|
customName String? // Override display name
|
|
customDamage String? // Override damage dice (e.g. "2d6")
|
|
customDamageType String? // Override damage type (e.g. "fire")
|
|
customTraits String[] // Override/add traits
|
|
customRange String? // Override range
|
|
customHands String? // Override hands requirement
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
equipment Equipment? @relation(fields: [equipmentId], references: [id])
|
|
}
|
|
|
|
model CharacterCondition {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
name String
|
|
nameGerman String?
|
|
value Int? // For valued conditions like Frightened 2
|
|
duration String?
|
|
source String?
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model CharacterResource {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
name String
|
|
current Int
|
|
max Int
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([characterId, name])
|
|
}
|
|
|
|
// ==========================================
|
|
// ALCHEMY SYSTEM
|
|
// ==========================================
|
|
|
|
model CharacterAlchemyState {
|
|
id String @id @default(uuid())
|
|
characterId String @unique
|
|
researchField ResearchField?
|
|
versatileVialsCurrent Int @default(0)
|
|
versatileVialsMax Int @default(0)
|
|
advancedAlchemyBatch Int @default(0)
|
|
advancedAlchemyMax Int @default(0)
|
|
lastRestAt DateTime?
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model CharacterFormula {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
equipmentId String
|
|
name String
|
|
nameGerman String?
|
|
learnedAt Int @default(1)
|
|
formulaSource String? // "Pathbuilder Import", "Purchased", "Found"
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
equipment Equipment @relation(fields: [equipmentId], references: [id])
|
|
|
|
@@unique([characterId, equipmentId])
|
|
@@index([characterId])
|
|
}
|
|
|
|
model CharacterPreparedItem {
|
|
id String @id @default(uuid())
|
|
characterId String
|
|
equipmentId String
|
|
name String
|
|
nameGerman String?
|
|
quantity Int @default(1)
|
|
isQuickAlchemy Boolean @default(false)
|
|
isInfused Boolean @default(true) // Infused items expire on rest, permanent items don't
|
|
createdAt DateTime @default(now())
|
|
|
|
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
|
equipment Equipment @relation(fields: [equipmentId], references: [id])
|
|
|
|
@@index([characterId])
|
|
}
|
|
|
|
// ==========================================
|
|
// BATTLE SYSTEM
|
|
// ==========================================
|
|
|
|
model BattleMap {
|
|
id String @id @default(uuid())
|
|
campaignId String
|
|
name String
|
|
imageUrl String
|
|
gridSizeX Int @default(20)
|
|
gridSizeY Int @default(20)
|
|
gridOffsetX Int @default(0)
|
|
gridOffsetY Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
battleSessions BattleSession[]
|
|
}
|
|
|
|
model Combatant {
|
|
id String @id @default(uuid())
|
|
campaignId String
|
|
name String
|
|
type CombatantType
|
|
level Int
|
|
hpMax Int
|
|
ac Int
|
|
|
|
// Saves
|
|
fortitude Int
|
|
reflex Int
|
|
will Int
|
|
perception Int
|
|
|
|
// Speed
|
|
speed Int @default(25)
|
|
|
|
avatarUrl String?
|
|
description String?
|
|
createdAt DateTime @default(now())
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
abilities CombatantAbility[]
|
|
battleTokens BattleToken[]
|
|
}
|
|
|
|
model CombatantAbility {
|
|
id String @id @default(uuid())
|
|
combatantId String
|
|
name String
|
|
actionCost Int // 1, 2, 3, or 0 for free/reaction
|
|
actionType ActionType
|
|
description String
|
|
damage String? // e.g. "2d6+4 slashing"
|
|
traits String[]
|
|
|
|
combatant Combatant @relation(fields: [combatantId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model BattleSession {
|
|
id String @id @default(uuid())
|
|
campaignId String
|
|
mapId String?
|
|
name String?
|
|
isActive Boolean @default(false)
|
|
roundNumber Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
map BattleMap? @relation(fields: [mapId], references: [id])
|
|
tokens BattleToken[]
|
|
}
|
|
|
|
model BattleToken {
|
|
id String @id @default(uuid())
|
|
battleSessionId String
|
|
combatantId String?
|
|
characterId String?
|
|
name String
|
|
positionX Float
|
|
positionY Float
|
|
hpCurrent Int
|
|
hpMax Int
|
|
initiative Int?
|
|
conditions String[]
|
|
size Int @default(1) // 1 = Medium, 2 = Large, etc.
|
|
|
|
battleSession BattleSession @relation(fields: [battleSessionId], references: [id], onDelete: Cascade)
|
|
combatant Combatant? @relation(fields: [combatantId], references: [id])
|
|
character Character? @relation(fields: [characterId], references: [id])
|
|
}
|
|
|
|
// ==========================================
|
|
// DOCUMENTS SYSTEM
|
|
// ==========================================
|
|
|
|
model Document {
|
|
id String @id @default(uuid())
|
|
campaignId String
|
|
title String
|
|
description String?
|
|
category String?
|
|
tags String[]
|
|
filePath String
|
|
fileType String // html, pdf
|
|
uploadedBy String
|
|
createdAt DateTime @default(now())
|
|
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
uploader User @relation(fields: [uploadedBy], references: [id])
|
|
access DocumentAccess[]
|
|
highlights Highlight[]
|
|
}
|
|
|
|
model DocumentAccess {
|
|
id String @id @default(uuid())
|
|
documentId String
|
|
userId String? // NULL = all campaign members
|
|
characterId String? // Access per character
|
|
|
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id])
|
|
character Character? @relation(fields: [characterId], references: [id])
|
|
|
|
@@unique([documentId, userId, characterId])
|
|
}
|
|
|
|
model Highlight {
|
|
id String @id @default(uuid())
|
|
documentId String
|
|
userId String
|
|
selectionText String
|
|
startOffset Int
|
|
endOffset Int
|
|
color HighlightColor
|
|
note String?
|
|
createdAt DateTime @default(now())
|
|
|
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id])
|
|
}
|
|
|
|
model Note {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
campaignId String
|
|
title String
|
|
content String // Markdown
|
|
isShared Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id])
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
shares NoteShare[]
|
|
}
|
|
|
|
model NoteShare {
|
|
noteId String
|
|
userId String
|
|
|
|
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
@@id([noteId, userId])
|
|
}
|
|
|
|
// ==========================================
|
|
// REFERENCE DATA (Pathfinder 2e)
|
|
// ==========================================
|
|
|
|
model Feat {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
traits String[]
|
|
summary String?
|
|
description String? // Full feat description/benefit text
|
|
actions String? // "1", "2", "3", "free", "reaction", null for passive
|
|
url String?
|
|
level Int? // Minimum level requirement
|
|
sourceBook String?
|
|
|
|
// Feat classification
|
|
featType String? // "General", "Skill", "Class", "Ancestry", "Archetype", "Heritage"
|
|
rarity String? // "Common", "Uncommon", "Rare", "Unique"
|
|
|
|
// Prerequisites
|
|
prerequisites String? // Text description of prerequisites
|
|
|
|
// For class/archetype feats
|
|
className String? // "Fighter", "Wizard", etc.
|
|
archetypeName String? // "Sentinel", "Medic", etc.
|
|
|
|
// For ancestry feats
|
|
ancestryName String? // "Human", "Elf", etc.
|
|
|
|
// For skill feats
|
|
skillName String? // "Acrobatics", "Athletics", etc.
|
|
|
|
// Cached German translation
|
|
nameGerman String?
|
|
summaryGerman String?
|
|
|
|
characterFeats CharacterFeat[]
|
|
|
|
@@index([featType])
|
|
@@index([className])
|
|
@@index([ancestryName])
|
|
@@index([level])
|
|
}
|
|
|
|
model Equipment {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
traits String[]
|
|
itemCategory String // "Weapons", "Armor", "Consumables", "Shields", etc.
|
|
itemSubcategory String?
|
|
bulk String? // "L" for light, "1", "2", etc.
|
|
url String?
|
|
summary String?
|
|
level Int?
|
|
price Int? // In CP
|
|
|
|
// Weapon-specific fields
|
|
hands String?
|
|
damage String? // "1d8 S", "1d6 P", etc.
|
|
damageType String? // "S", "P", "B" (Slashing, Piercing, Bludgeoning)
|
|
range String?
|
|
reload String?
|
|
weaponCategory String? // "Simple", "Martial", "Advanced", "Ammunition"
|
|
weaponGroup String? // "Sword", "Axe", "Bow", etc.
|
|
|
|
// Armor-specific fields
|
|
ac Int?
|
|
dexCap Int?
|
|
checkPenalty Int?
|
|
speedPenalty Int?
|
|
strength Int? // Strength requirement
|
|
armorCategory String? // "Unarmored", "Light", "Medium", "Heavy"
|
|
armorGroup String? // "Leather", "Chain", "Plate", etc.
|
|
|
|
// Shield-specific fields
|
|
shieldHp Int?
|
|
shieldHardness Int?
|
|
shieldBt Int? // Broken Threshold
|
|
|
|
// Consumable/Equipment-specific fields
|
|
activation String? // "Cast A Spell", "[one-action]", etc.
|
|
duration String?
|
|
usage String?
|
|
effect String? // Specific effect text for item variants (Lesser, Moderate, Greater, Major)
|
|
|
|
characterItems CharacterItem[]
|
|
formulas CharacterFormula[]
|
|
preparedItems CharacterPreparedItem[]
|
|
}
|
|
|
|
model Spell {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
level Int
|
|
actions String?
|
|
traditions String[]
|
|
traits String[]
|
|
range String?
|
|
targets String?
|
|
duration String?
|
|
description String?
|
|
url String?
|
|
|
|
characterSpells CharacterSpell[]
|
|
}
|
|
|
|
model Trait {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
description String?
|
|
url String?
|
|
}
|
|
|
|
// ==========================================
|
|
// TRANSLATION CACHE
|
|
// ==========================================
|
|
|
|
model Translation {
|
|
id String @id @default(uuid())
|
|
type TranslationType
|
|
englishName String
|
|
germanName String
|
|
germanSummary String?
|
|
germanDescription String?
|
|
germanEffect String? // Translated effect text for alchemical items
|
|
quality TranslationQuality @default(MEDIUM)
|
|
translatedBy String @default("claude-api")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([type, englishName])
|
|
@@index([type])
|
|
@@index([englishName])
|
|
}
|