official.ghost.logic
Deploy D&D Campaign Manager v2
71b378e
"""
NPC (Non-Player Character) data models
"""
from datetime import datetime
from typing import Optional, List, Dict
from enum import Enum
from pydantic import BaseModel, Field
class NPCRole(str, Enum):
"""NPC roles in the story"""
QUEST_GIVER = "Quest Giver"
MERCHANT = "Merchant"
ALLY = "Ally"
RIVAL = "Rival"
VILLAIN = "Villain"
MENTOR = "Mentor"
INFORMANT = "Informant"
GUARD = "Guard"
NOBLE = "Noble"
COMMONER = "Commoner"
MONSTER = "Monster"
COMPANION = "Companion"
class NPCDisposition(str, Enum):
"""NPC attitude toward party"""
HOSTILE = "Hostile"
UNFRIENDLY = "Unfriendly"
NEUTRAL = "Neutral"
FRIENDLY = "Friendly"
HELPFUL = "Helpful"
class NPCPersonality(BaseModel):
"""NPC personality traits"""
traits: List[str] = Field(default_factory=list, description="2-3 personality traits")
mannerisms: List[str] = Field(default_factory=list, description="Physical mannerisms")
voice_description: str = Field(default="", description="How they sound")
motivations: List[str] = Field(default_factory=list, description="What drives them")
fears: List[str] = Field(default_factory=list, description="What they fear")
secrets: List[str] = Field(default_factory=list, description="Hidden secrets")
class NPCRelationship(BaseModel):
"""Relationship between NPC and character/party"""
character_id: str = Field(description="Character or 'party' for group")
relationship_type: str = Field(description="Type of relationship")
affinity: int = Field(ge=-100, le=100, default=0, description="Relationship strength")
history: str = Field(default="", description="Shared history")
notes: str = Field(default="", description="Relationship notes")
class NPC(BaseModel):
"""Non-player character"""
# Core identity
id: Optional[str] = Field(default=None, description="NPC ID")
name: str = Field(min_length=1, max_length=100, description="NPC name")
title: Optional[str] = Field(default=None, description="Title/honorific")
# Basic info
race: str = Field(description="NPC race/species")
age: Optional[str] = Field(default=None, description="Age or age category")
gender: Optional[str] = Field(default=None, description="Gender")
occupation: str = Field(description="Occupation/profession")
# Role in campaign
role: NPCRole = Field(description="Story role")
disposition: NPCDisposition = Field(default=NPCDisposition.NEUTRAL)
importance: int = Field(ge=1, le=5, default=3, description="Story importance (1-5)")
# Description
appearance: str = Field(description="Physical description")
personality: NPCPersonality = Field(default_factory=NPCPersonality)
# Game stats (optional)
challenge_rating: Optional[float] = Field(default=None, description="CR if combatant")
armor_class: Optional[int] = Field(default=None)
hit_points: Optional[int] = Field(default=None)
# Story elements
backstory: str = Field(default="", description="NPC backstory")
current_situation: str = Field(default="", description="Current circumstances")
goals: List[str] = Field(default_factory=list, description="NPC goals")
connections: List[str] = Field(default_factory=list, description="Connected NPCs/factions")
# Relationships
relationships: List[NPCRelationship] = Field(default_factory=list)
# Location & availability
location: Optional[str] = Field(default=None, description="Current location")
availability: str = Field(default="Available", description="When/where to find them")
# Dialogue
greeting: Optional[str] = Field(default=None, description="Standard greeting")
catchphrase: Optional[str] = Field(default=None, description="Memorable phrase")
dialogue_samples: List[str] = Field(default_factory=list, description="Sample dialogue")
# Items & abilities
notable_items: List[str] = Field(default_factory=list, description="Important items")
special_abilities: List[str] = Field(default_factory=list, description="Special abilities")
# Metadata
campaign_id: Optional[str] = Field(default=None, description="Associated campaign")
first_appearance: Optional[int] = Field(default=None, description="Session first appeared")
last_appearance: Optional[int] = Field(default=None, description="Session last appeared")
is_alive: bool = Field(default=True, description="Living status")
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
# GM notes
gm_notes: str = Field(default="", description="Private GM notes")
plot_hooks: List[str] = Field(default_factory=list, description="Plot hooks involving this NPC")
def add_relationship(self, character_id: str, relationship_type: str, affinity: int = 0):
"""Add or update relationship"""
for rel in self.relationships:
if rel.character_id == character_id:
rel.relationship_type = relationship_type
rel.affinity = affinity
self.updated_at = datetime.now()
return
# Create new relationship
new_rel = NPCRelationship(
character_id=character_id,
relationship_type=relationship_type,
affinity=affinity
)
self.relationships.append(new_rel)
self.updated_at = datetime.now()
def change_disposition(self, new_disposition: NPCDisposition):
"""Change NPC disposition"""
self.disposition = new_disposition
self.updated_at = datetime.now()
def update_location(self, location: str):
"""Update NPC location"""
self.location = location
self.updated_at = datetime.now()
def record_appearance(self, session_number: int):
"""Record NPC appearance in session"""
if self.first_appearance is None:
self.first_appearance = session_number
self.last_appearance = session_number
self.updated_at = datetime.now()
def get_relationship_with(self, character_id: str) -> Optional[NPCRelationship]:
"""Get relationship with specific character"""
for rel in self.relationships:
if rel.character_id == character_id:
return rel
return None
def to_roleplay_prompt(self) -> str:
"""Generate prompt for AI to roleplay this NPC"""
return f"""You are roleplaying as {self.name}, a {self.race} {self.occupation}.
APPEARANCE: {self.appearance}
PERSONALITY:
- Traits: {', '.join(self.personality.traits)}
- Mannerisms: {', '.join(self.personality.mannerisms)}
- Voice: {self.personality.voice_description}
BACKGROUND: {self.backstory}
CURRENT SITUATION: {self.current_situation}
MOTIVATIONS: {', '.join(self.personality.motivations)}
DISPOSITION: {self.disposition.value}
TYPICAL GREETING: {self.greeting or 'Generic greeting'}
Roleplay this character authentically, staying in character and reflecting their personality, motivations, and current circumstances.
"""
def to_markdown(self) -> str:
"""Generate markdown NPC sheet"""
return f"""# {self.name}
{f'*{self.title}*' if self.title else ''}
**{self.race} {self.occupation}** | **{self.role.value}**
**Disposition:** {self.disposition.value} | **Importance:** {'⭐' * self.importance}
## Appearance
{self.appearance}
## Personality
**Traits:** {', '.join(self.personality.traits)}
**Mannerisms:** {', '.join(self.personality.mannerisms)}
**Voice:** {self.personality.voice_description}
**Motivations:** {', '.join(self.personality.motivations)}
**Fears:** {', '.join(self.personality.fears)}
## Background
{self.backstory}
## Current Situation
{self.current_situation}
## Goals
{chr(10).join(f"- {goal}" for goal in self.goals)}
## Location & Availability
**Location:** {self.location or 'Unknown'}
**Availability:** {self.availability}
## Connections
{', '.join(self.connections)}
## Dialogue
**Greeting:** "{self.greeting or 'Hello there.'}"
**Catchphrase:** "{self.catchphrase or 'N/A'}"
## Combat Stats
{f"**CR:** {self.challenge_rating} | **AC:** {self.armor_class} | **HP:** {self.hit_points}" if self.challenge_rating else "*Not a combatant*"}
## Plot Hooks
{chr(10).join(f"- {hook}" for hook in self.plot_hooks)}
## GM Notes
{self.gm_notes}
"""
class Config:
json_schema_extra = {
"example": {
"name": "Elara Moonwhisper",
"race": "Elf",
"occupation": "Sage",
"role": "Quest Giver",
"disposition": "Friendly",
"appearance": "Ancient elf with silver hair and piercing blue eyes",
"personality": {
"traits": ["Wise", "Mysterious", "Patient"],
"voice_description": "Soft and melodic"
}
}
}