""" 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" } }