Backend: - Characters-Modul (CRUD, HP-Tracking, Conditions) - Pathbuilder 2e JSON Import Service - Claude API Integration für automatische Übersetzungen - Translations-Modul mit Datenbank-Caching - Prisma Schema erweitert (Character, Abilities, Skills, Feats, Items, Resources) Frontend: - Kampagnen-Detailseite mit Mitglieder- und Charakterverwaltung - Charakter erstellen Modal - Pathbuilder Import Modal (Datei-Upload + JSON-Paste) - Logo-Integration (Dimension 47 + Zeasy) - Cinzel Font für Branding Weitere Änderungen: - Auth 401 Redirect Fix für Login-Seite - PROGRESS.md mit Projektfortschritt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
544 lines
13 KiB
Plaintext
544 lines
13 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
|
|
}
|
|
|
|
// ==========================================
|
|
// 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])
|
|
}
|