Files
Dimension-47/server/prisma/schema.prisma
Alexander Zielonka 090aae53d8 Initial commit: Dimension47 project setup
- NestJS backend with JWT auth, Prisma ORM, Swagger docs
- Vite + React 19 frontend with TypeScript
- Tailwind CSS v4 with custom dark theme design system
- Auth module: Login, Register, Protected routes
- Campaigns module: CRUD, Member management
- Full Prisma schema for PF2e campaign management
- Docker Compose for PostgreSQL

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 16:24:18 +01:00

543 lines
13 KiB
Plaintext

// Dimension47 - TTRPG Campaign Management Platform
// Prisma Schema
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
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
}
// ==========================================
// 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)
// 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[]
}
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?
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])
}
// ==========================================
// 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?
actions String?
url String?
level Int?
sourceBook String?
characterFeats CharacterFeat[]
}
model Equipment {
id String @id @default(uuid())
name String @unique
traits String[]
itemCategory String
itemSubcategory String?
bulk String?
url String?
summary String?
activation String?
hands String?
damage String?
range String?
weaponCategory String?
price Int? // In CP
level Int?
characterItems CharacterItem[]
}
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?
quality TranslationQuality @default(MEDIUM)
translatedBy String @default("claude-api")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([type, englishName])
@@index([type])
@@index([englishName])
}