feat(01-01): extend Prisma schema with level-up tables and Character columns
- Add LevelUpSession model (DRAFT state, soft-archive on commit via committedAt) - Add LevelUpHistory model (append-only audit log of committed level-ups) - Add ClassProgression model (per-class, per-level grants and prof changes) - Add ClassFeatureOption model (sub-choices like doctrines/schools/instincts) - Extend Character with freeArchetype Boolean (D-08) and prereqViolations Json (D-06) - Add reverse relations levelUpSessions and levelUpHistories on Character - Schema formatted via prisma format and validated via prisma validate
This commit is contained in:
@@ -109,19 +109,19 @@ enum ResearchField {
|
||||
// ==========================================
|
||||
|
||||
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
|
||||
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")
|
||||
gmCampaigns Campaign[] @relation("CampaignGM")
|
||||
campaignMembers CampaignMember[]
|
||||
characters Character[] @relation("CharacterOwner")
|
||||
characters Character[] @relation("CharacterOwner")
|
||||
uploadedDocuments Document[]
|
||||
documentAccess DocumentAccess[]
|
||||
highlights Highlight[]
|
||||
@@ -143,14 +143,14 @@ model Campaign {
|
||||
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[]
|
||||
gm User @relation("CampaignGM", fields: [gmId], references: [id])
|
||||
members CampaignMember[]
|
||||
characters Character[]
|
||||
battleMaps BattleMap[]
|
||||
combatants Combatant[]
|
||||
battleSessions BattleSession[]
|
||||
documents Document[]
|
||||
notes Note[]
|
||||
}
|
||||
|
||||
model CampaignMember {
|
||||
@@ -169,24 +169,24 @@ model CampaignMember {
|
||||
// ==========================================
|
||||
|
||||
model Character {
|
||||
id String @id @default(uuid())
|
||||
campaignId String
|
||||
ownerId String?
|
||||
name String
|
||||
type CharacterType @default(PC)
|
||||
level Int @default(1)
|
||||
avatarUrl String?
|
||||
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)
|
||||
hpCurrent Int
|
||||
hpMax Int
|
||||
hpTemp Int @default(0)
|
||||
|
||||
// Ancestry/Class/Background (References)
|
||||
ancestryId String?
|
||||
heritageId String?
|
||||
classId String?
|
||||
backgroundId String?
|
||||
ancestryId String?
|
||||
heritageId String?
|
||||
classId String?
|
||||
backgroundId String?
|
||||
|
||||
// Experience
|
||||
experiencePoints Int @default(0)
|
||||
@@ -197,26 +197,32 @@ model Character {
|
||||
// Pathbuilder Import Data (JSON blob for original import)
|
||||
pathbuilderData Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
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[]
|
||||
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[]
|
||||
alchemyState CharacterAlchemyState?
|
||||
formulas CharacterFormula[]
|
||||
preparedItems CharacterPreparedItem[]
|
||||
|
||||
// === Phase 1 additions ===
|
||||
freeArchetype Boolean @default(false) // D-08
|
||||
prereqViolations Json? // D-06
|
||||
levelUpSessions LevelUpSession[]
|
||||
levelUpHistories LevelUpHistory[]
|
||||
}
|
||||
|
||||
model CharacterAbility {
|
||||
@@ -233,10 +239,10 @@ model CharacterAbility {
|
||||
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
|
||||
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)
|
||||
@@ -282,26 +288,26 @@ model CharacterItem {
|
||||
notes String?
|
||||
|
||||
// Player-editable: Alias/Nickname for the item
|
||||
alias String?
|
||||
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
|
||||
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())
|
||||
id String @id @default(uuid())
|
||||
characterId String
|
||||
name String
|
||||
nameGerman String?
|
||||
value Int? // For valued conditions like Frightened 2
|
||||
value Int? // For valued conditions like Frightened 2
|
||||
duration String?
|
||||
source String?
|
||||
|
||||
@@ -325,14 +331,14 @@ model CharacterResource {
|
||||
// ==========================================
|
||||
|
||||
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?
|
||||
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)
|
||||
}
|
||||
@@ -354,15 +360,15 @@ model CharacterFormula {
|
||||
}
|
||||
|
||||
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())
|
||||
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])
|
||||
@@ -390,40 +396,40 @@ model BattleMap {
|
||||
}
|
||||
|
||||
model Combatant {
|
||||
id String @id @default(uuid())
|
||||
campaignId String
|
||||
name String
|
||||
type CombatantType
|
||||
level Int
|
||||
hpMax Int
|
||||
ac Int
|
||||
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
|
||||
fortitude Int
|
||||
reflex Int
|
||||
will Int
|
||||
perception Int
|
||||
|
||||
// Speed
|
||||
speed Int @default(25)
|
||||
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[]
|
||||
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
|
||||
actionCost Int // 1, 2, 3, or 0 for free/reaction
|
||||
actionType ActionType
|
||||
description String
|
||||
damage String? // e.g. "2d6+4 slashing"
|
||||
damage String? // e.g. "2d6+4 slashing"
|
||||
traits String[]
|
||||
|
||||
combatant Combatant @relation(fields: [combatantId], references: [id], onDelete: Cascade)
|
||||
@@ -438,8 +444,8 @@ model BattleSession {
|
||||
roundNumber Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
||||
map BattleMap? @relation(fields: [mapId], references: [id])
|
||||
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
||||
map BattleMap? @relation(fields: [mapId], references: [id])
|
||||
tokens BattleToken[]
|
||||
}
|
||||
|
||||
@@ -474,7 +480,7 @@ model Document {
|
||||
category String?
|
||||
tags String[]
|
||||
filePath String
|
||||
fileType String // html, pdf
|
||||
fileType String // html, pdf
|
||||
uploadedBy String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -487,8 +493,8 @@ model Document {
|
||||
model DocumentAccess {
|
||||
id String @id @default(uuid())
|
||||
documentId String
|
||||
userId String? // NULL = all campaign members
|
||||
characterId String? // Access per character
|
||||
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])
|
||||
@@ -513,14 +519,14 @@ model Highlight {
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -542,32 +548,32 @@ model NoteShare {
|
||||
// ==========================================
|
||||
|
||||
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?
|
||||
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"
|
||||
featType String? // "General", "Skill", "Class", "Ancestry", "Archetype", "Heritage"
|
||||
rarity String? // "Common", "Uncommon", "Rare", "Unique"
|
||||
|
||||
// Prerequisites
|
||||
prerequisites String? // Text description of prerequisites
|
||||
prerequisites String? // Text description of prerequisites
|
||||
|
||||
// For class/archetype feats
|
||||
className String? // "Fighter", "Wizard", etc.
|
||||
archetypeName String? // "Sentinel", "Medic", etc.
|
||||
className String? // "Fighter", "Wizard", etc.
|
||||
archetypeName String? // "Sentinel", "Medic", etc.
|
||||
|
||||
// For ancestry feats
|
||||
ancestryName String? // "Human", "Elf", etc.
|
||||
ancestryName String? // "Human", "Elf", etc.
|
||||
|
||||
// For skill feats
|
||||
skillName String? // "Acrobatics", "Athletics", etc.
|
||||
skillName String? // "Acrobatics", "Athletics", etc.
|
||||
|
||||
// Cached German translation
|
||||
nameGerman String?
|
||||
@@ -582,49 +588,49 @@ model Feat {
|
||||
}
|
||||
|
||||
model Equipment {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
traits String[]
|
||||
itemCategory String // "Weapons", "Armor", "Consumables", "Shields", etc.
|
||||
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
|
||||
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.
|
||||
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.
|
||||
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
|
||||
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)
|
||||
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[]
|
||||
characterItems CharacterItem[]
|
||||
formulas CharacterFormula[]
|
||||
preparedItems CharacterPreparedItem[]
|
||||
}
|
||||
|
||||
model Spell {
|
||||
@@ -655,19 +661,83 @@ model Trait {
|
||||
// ==========================================
|
||||
|
||||
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
|
||||
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])
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// LEVEL-UP SYSTEM (Phase 1)
|
||||
// ==========================================
|
||||
|
||||
model LevelUpSession {
|
||||
id String @id @default(uuid())
|
||||
characterId String
|
||||
targetLevel Int
|
||||
state Json @default("{}")
|
||||
committedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
// Partial unique index "one open DRAFT per character" added in raw migration SQL —
|
||||
// Prisma 7 cannot emit partial indexes from schema (per RESEARCH.md §Don't Hand-Roll).
|
||||
|
||||
@@index([characterId])
|
||||
}
|
||||
|
||||
model LevelUpHistory {
|
||||
id String @id @default(uuid())
|
||||
characterId String
|
||||
levelFrom Int
|
||||
levelTo Int
|
||||
snapshotBefore Json
|
||||
choices Json
|
||||
committedAt DateTime @default(now())
|
||||
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([characterId, committedAt(sort: Desc)])
|
||||
}
|
||||
|
||||
model ClassProgression {
|
||||
id String @id @default(uuid())
|
||||
className String
|
||||
level Int
|
||||
grants String[]
|
||||
proficiencyChanges Json
|
||||
spellSlotIncrement Json?
|
||||
cantripIncrement Int?
|
||||
repertoireIncrement Int?
|
||||
choiceType String?
|
||||
choiceOptionsRef String?
|
||||
|
||||
@@unique([className, level])
|
||||
@@index([className])
|
||||
}
|
||||
|
||||
model ClassFeatureOption {
|
||||
id String @id @default(uuid())
|
||||
optionsRef String
|
||||
optionKey String
|
||||
name String
|
||||
nameGerman String?
|
||||
description String
|
||||
grants String[]
|
||||
proficiencyChanges Json?
|
||||
|
||||
@@unique([optionsRef, optionKey])
|
||||
@@index([optionsRef])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user