official.ghost.logic
Deploy D&D Campaign Manager v2
71b378e
"""
Campaign and session management data models
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from enum import Enum
from pydantic import BaseModel, Field
class CampaignTheme(str, Enum):
"""Campaign themes"""
HIGH_FANTASY = "High Fantasy"
DARK_FANTASY = "Dark Fantasy"
URBAN_FANTASY = "Urban Fantasy"
POLITICAL_INTRIGUE = "Political Intrigue"
HORROR = "Horror"
EXPLORATION = "Exploration"
DUNGEON_CRAWL = "Dungeon Crawl"
CUSTOM = "Custom"
class EventType(str, Enum):
"""Types of campaign events"""
COMBAT = "Combat"
SOCIAL = "Social"
EXPLORATION = "Exploration"
DISCOVERY = "Discovery"
PLOT_DEVELOPMENT = "Plot Development"
CHARACTER_MOMENT = "Character Moment"
NPC_INTERACTION = "NPC Interaction"
QUEST_UPDATE = "Quest Update"
class CampaignEvent(BaseModel):
"""Individual campaign event/memory"""
id: Optional[str] = Field(default=None, description="Event ID")
campaign_id: str = Field(description="Campaign this event belongs to")
session_number: int = Field(ge=1, description="Session number")
event_type: EventType = Field(description="Type of event")
title: str = Field(min_length=1, max_length=200, description="Event title")
description: str = Field(description="Full event description")
# Participants
characters_involved: List[str] = Field(default_factory=list, description="Character IDs")
npcs_involved: List[str] = Field(default_factory=list, description="NPC IDs")
locations: List[str] = Field(default_factory=list, description="Location names")
# Context
consequences: List[str] = Field(default_factory=list, description="Event consequences")
items_gained: List[str] = Field(default_factory=list, description="Items acquired")
items_lost: List[str] = Field(default_factory=list, description="Items lost")
experience_awarded: int = Field(ge=0, default=0, description="XP awarded")
# Metadata
timestamp: datetime = Field(default_factory=datetime.now)
importance: int = Field(ge=1, le=5, default=3, description="Event importance (1-5)")
tags: List[str] = Field(default_factory=list, description="Event tags")
# GM notes
gm_notes: str = Field(default="", description="Private GM notes")
player_visible: bool = Field(default=True, description="Visible to players")
def to_markdown(self) -> str:
"""Convert to markdown"""
return f"""## {self.title}
**Type:** {self.event_type.value} | **Importance:** {'⭐' * self.importance}
**Session:** {self.session_number} | **Date:** {self.timestamp.strftime('%Y-%m-%d')}
{self.description}
**Participants:** {', '.join(self.characters_involved)}
**NPCs:** {', '.join(self.npcs_involved)}
**Locations:** {', '.join(self.locations)}
**Consequences:**
{chr(10).join(f"- {c}" for c in self.consequences)}
"""
class CampaignMemory(BaseModel):
"""Searchable campaign memory/context"""
campaign_id: str = Field(description="Campaign ID")
# Full context for AI
full_context: str = Field(default="", description="Complete campaign context")
# Key information
major_npcs: Dict[str, str] = Field(default_factory=dict, description="NPC name -> description")
key_locations: Dict[str, str] = Field(default_factory=dict, description="Location -> description")
active_quests: List[str] = Field(default_factory=list, description="Current quests")
completed_quests: List[str] = Field(default_factory=list, description="Finished quests")
# Story beats
important_events: List[str] = Field(default_factory=list, description="Key story moments")
unresolved_threads: List[str] = Field(default_factory=list, description="Plot threads")
secrets_discovered: List[str] = Field(default_factory=list, description="Revealed secrets")
secrets_hidden: List[str] = Field(default_factory=list, description="Hidden secrets")
# Relationships
faction_standings: Dict[str, int] = Field(default_factory=dict, description="Faction name -> standing (-100 to 100)")
npc_relationships: Dict[str, str] = Field(default_factory=dict, description="NPC -> relationship status")
# Metadata
last_updated: datetime = Field(default_factory=datetime.now)
total_events: int = Field(ge=0, default=0)
def add_event_to_memory(self, event: CampaignEvent):
"""Update memory with new event"""
self.total_events += 1
# Add NPCs
for npc in event.npcs_involved:
if npc not in self.major_npcs:
self.major_npcs[npc] = "Recently introduced"
# Add locations
for location in event.locations:
if location not in self.key_locations:
self.key_locations[location] = "Visited"
# Add to important events if high importance
if event.importance >= 4:
self.important_events.append(event.title)
self.last_updated = datetime.now()
class Campaign(BaseModel):
"""Complete D&D campaign"""
# Core identity
id: Optional[str] = Field(default=None, description="Campaign ID")
name: str = Field(min_length=1, max_length=100, description="Campaign name")
theme: CampaignTheme = Field(description="Campaign theme")
# Setting
setting: str = Field(description="Campaign setting description")
world_name: str = Field(default="", description="World/realm name")
starting_location: str = Field(default="", description="Starting location")
# Campaign info
summary: str = Field(description="Campaign summary/hook")
current_arc: str = Field(default="", description="Current story arc")
level_range: str = Field(default="1-5", description="Expected level range")
# Story elements
main_conflict: str = Field(description="Central conflict")
key_factions: List[str] = Field(default_factory=list, description="Important factions")
major_villains: List[str] = Field(default_factory=list, description="Main antagonists")
central_mysteries: List[str] = Field(default_factory=list, description="Ongoing mysteries")
# Characters
character_ids: List[str] = Field(default_factory=list, description="Player character IDs")
party_size: int = Field(ge=1, le=10, default=4, description="Expected party size")
# Session tracking
current_session: int = Field(ge=1, default=1, description="Current session number")
total_sessions: int = Field(ge=0, default=0, description="Total sessions played")
# Memory
memory: CampaignMemory = Field(default=None, description="Campaign memory")
# Metadata
game_master: str = Field(default="", description="GM name")
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
last_session_date: Optional[datetime] = Field(default=None)
# Settings
is_active: bool = Field(default=True, description="Campaign active?")
homebrew_rules: List[str] = Field(default_factory=list, description="Custom rules")
notes: str = Field(default="", description="GM notes")
def __init__(self, **data):
super().__init__(**data)
if self.memory is None:
self.memory = CampaignMemory(campaign_id=self.id or "")
def add_character(self, character_id: str):
"""Add character to campaign"""
if character_id not in self.character_ids:
self.character_ids.append(character_id)
self.updated_at = datetime.now()
def remove_character(self, character_id: str):
"""Remove character from campaign"""
if character_id in self.character_ids:
self.character_ids.remove(character_id)
self.updated_at = datetime.now()
def start_new_session(self):
"""Start a new session"""
self.current_session += 1
self.total_sessions += 1
self.last_session_date = datetime.now()
self.updated_at = datetime.now()
def add_event(self, event: CampaignEvent):
"""Add event to campaign memory"""
self.memory.add_event_to_memory(event)
self.updated_at = datetime.now()
def to_context_string(self) -> str:
"""Generate context string for AI"""
return f"""Campaign: {self.name}
Theme: {self.theme.value}
Setting: {self.setting}
Main Conflict: {self.main_conflict}
Current Arc: {self.current_arc}
Session: {self.current_session}
Key Factions: {', '.join(self.key_factions)}
Major Villains: {', '.join(self.major_villains)}
Active Quests: {', '.join(self.memory.active_quests)}
Important Events: {', '.join(self.memory.important_events[-5:])} # Last 5 events
Party Size: {self.party_size}
Level Range: {self.level_range}
"""
def to_markdown(self) -> str:
"""Generate markdown campaign summary"""
return f"""# {self.name}
**{self.theme.value} Campaign**
## Setting
{self.setting}
**World:** {self.world_name}
**Starting Location:** {self.starting_location}
## Campaign Summary
{self.summary}
## Main Conflict
{self.main_conflict}
## Current Story Arc
{self.current_arc}
## Key Elements
**Factions:** {', '.join(self.key_factions)}
**Villains:** {', '.join(self.major_villains)}
**Mysteries:** {', '.join(self.central_mysteries)}
## Party Information
**Party Size:** {self.party_size}
**Level Range:** {self.level_range}
**Current Session:** {self.current_session}
**Total Sessions:** {self.total_sessions}
## Campaign Status
**Active:** {'Yes' if self.is_active else 'No'}
**Last Session:** {self.last_session_date.strftime('%Y-%m-%d') if self.last_session_date else 'Not started'}
## GM Notes
{self.notes}
"""
class Config:
json_schema_extra = {
"example": {
"name": "The Shattered Crown",
"theme": "High Fantasy",
"setting": "A kingdom torn by civil war",
"summary": "Adventurers must unite the realm",
"main_conflict": "Succession crisis threatens the kingdom",
"party_size": 4,
"level_range": "1-10"
}
}