""" NPC data models for persistent non-player characters. This module defines NPC and related dataclasses that represent structured NPC definitions loaded from YAML files. NPCs have rich personality, knowledge, and interaction data that the AI uses for dialogue generation. """ from dataclasses import dataclass, field from typing import Dict, List, Any, Optional @dataclass class NPCPersonality: """ NPC personality definition for AI dialogue generation. Provides the AI with guidance on how to roleplay the NPC's character, including their general traits, speaking patterns, and distinctive behaviors. Attributes: traits: List of personality descriptors (e.g., "gruff", "kind", "suspicious") speech_style: Description of how the NPC speaks (accent, vocabulary, patterns) quirks: List of distinctive behaviors or habits """ traits: List[str] speech_style: str quirks: List[str] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: """Serialize personality to dictionary.""" return { "traits": self.traits, "speech_style": self.speech_style, "quirks": self.quirks, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCPersonality': """Deserialize personality from dictionary.""" return cls( traits=data.get("traits", []), speech_style=data.get("speech_style", ""), quirks=data.get("quirks", []), ) @dataclass class NPCAppearance: """ NPC physical description. Provides visual context for AI narration and player information. Attributes: brief: Short one-line description for lists and quick reference detailed: Optional longer description for detailed encounters """ brief: str detailed: Optional[str] = None def to_dict(self) -> Dict[str, Any]: """Serialize appearance to dictionary.""" return { "brief": self.brief, "detailed": self.detailed, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCAppearance': """Deserialize appearance from dictionary.""" if isinstance(data, str): # Handle simple string format return cls(brief=data) return cls( brief=data.get("brief", ""), detailed=data.get("detailed"), ) @dataclass class NPCKnowledgeCondition: """ Condition for NPC to reveal secret knowledge. Defines when and how an NPC will share information they normally keep hidden. Conditions are evaluated against the character's interaction state. Attributes: condition: Expression describing what triggers the reveal (e.g., "interaction_count >= 3", "relationship_level >= 75") reveals: The information that gets revealed when condition is met """ condition: str reveals: str def to_dict(self) -> Dict[str, Any]: """Serialize condition to dictionary.""" return { "condition": self.condition, "reveals": self.reveals, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCKnowledgeCondition': """Deserialize condition from dictionary.""" return cls( condition=data.get("condition", ""), reveals=data.get("reveals", ""), ) @dataclass class NPCKnowledge: """ Knowledge an NPC possesses - public and secret. Organizes what information an NPC knows and under what circumstances they will share it with players. Attributes: public: Knowledge the NPC will freely share with anyone secret: Knowledge the NPC keeps hidden (for AI reference only) will_share_if: Conditional reveals based on character interaction state """ public: List[str] = field(default_factory=list) secret: List[str] = field(default_factory=list) will_share_if: List[NPCKnowledgeCondition] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: """Serialize knowledge to dictionary.""" return { "public": self.public, "secret": self.secret, "will_share_if": [c.to_dict() for c in self.will_share_if], } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCKnowledge': """Deserialize knowledge from dictionary.""" conditions = [ NPCKnowledgeCondition.from_dict(c) for c in data.get("will_share_if", []) ] return cls( public=data.get("public", []), secret=data.get("secret", []), will_share_if=conditions, ) @dataclass class NPCRelationship: """ NPC's relationship with another NPC. Defines how this NPC feels about other NPCs in the world, providing context for dialogue and interactions. Attributes: npc_id: The other NPC's identifier attitude: How this NPC feels (e.g., "friendly", "distrustful", "romantic") reason: Optional explanation for the attitude """ npc_id: str attitude: str reason: Optional[str] = None def to_dict(self) -> Dict[str, Any]: """Serialize relationship to dictionary.""" return { "npc_id": self.npc_id, "attitude": self.attitude, "reason": self.reason, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCRelationship': """Deserialize relationship from dictionary.""" return cls( npc_id=data["npc_id"], attitude=data["attitude"], reason=data.get("reason"), ) @dataclass class NPCInventoryItem: """ Item an NPC has for sale. Defines items available for purchase from merchant NPCs. Attributes: item_id: Reference to item definition price: Cost in gold quantity: Stock count (None = unlimited) """ item_id: str price: int quantity: Optional[int] = None def to_dict(self) -> Dict[str, Any]: """Serialize inventory item to dictionary.""" return { "item_id": self.item_id, "price": self.price, "quantity": self.quantity, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCInventoryItem': """Deserialize inventory item from dictionary.""" # Handle shorthand format: { item: "ale", price: 2 } item_id = data.get("item_id") or data.get("item", "") return cls( item_id=item_id, price=data.get("price", 0), quantity=data.get("quantity"), ) @dataclass class NPCDialogueHooks: """ Pre-defined dialogue snippets for AI context. Provides example phrases the AI can use or adapt to maintain consistent NPC voice across conversations. Attributes: greeting: What NPC says when first addressed farewell: What NPC says when conversation ends busy: What NPC says when occupied or dismissive quest_complete: What NPC says when player completes their quest """ greeting: Optional[str] = None farewell: Optional[str] = None busy: Optional[str] = None quest_complete: Optional[str] = None def to_dict(self) -> Dict[str, Any]: """Serialize dialogue hooks to dictionary.""" return { "greeting": self.greeting, "farewell": self.farewell, "busy": self.busy, "quest_complete": self.quest_complete, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCDialogueHooks': """Deserialize dialogue hooks from dictionary.""" return cls( greeting=data.get("greeting"), farewell=data.get("farewell"), busy=data.get("busy"), quest_complete=data.get("quest_complete"), ) @dataclass class NPC: """ Persistent NPC definition. NPCs are fixed to locations and have rich personality, knowledge, and interaction data used by the AI for dialogue generation. Attributes: npc_id: Unique identifier (e.g., "npc_grom_001") name: Display name (e.g., "Grom Ironbeard") role: NPC's job/title (e.g., "bartender", "blacksmith") location_id: ID of location where this NPC resides personality: Personality traits and speech patterns appearance: Physical description knowledge: What the NPC knows (public and secret) relationships: How NPC feels about other NPCs inventory_for_sale: Items NPC sells (if merchant) dialogue_hooks: Pre-defined dialogue snippets quest_giver_for: Quest IDs this NPC can give reveals_locations: Location IDs this NPC can unlock through conversation tags: Metadata tags for filtering (e.g., "merchant", "quest_giver") """ npc_id: str name: str role: str location_id: str personality: NPCPersonality appearance: NPCAppearance knowledge: Optional[NPCKnowledge] = None relationships: List[NPCRelationship] = field(default_factory=list) inventory_for_sale: List[NPCInventoryItem] = field(default_factory=list) dialogue_hooks: Optional[NPCDialogueHooks] = None quest_giver_for: List[str] = field(default_factory=list) reveals_locations: List[str] = field(default_factory=list) tags: List[str] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: """ Serialize NPC to dictionary for JSON responses. Returns: Dictionary containing all NPC data """ return { "npc_id": self.npc_id, "name": self.name, "role": self.role, "location_id": self.location_id, "personality": self.personality.to_dict(), "appearance": self.appearance.to_dict(), "knowledge": self.knowledge.to_dict() if self.knowledge else None, "relationships": [r.to_dict() for r in self.relationships], "inventory_for_sale": [i.to_dict() for i in self.inventory_for_sale], "dialogue_hooks": self.dialogue_hooks.to_dict() if self.dialogue_hooks else None, "quest_giver_for": self.quest_giver_for, "reveals_locations": self.reveals_locations, "tags": self.tags, } def to_story_dict(self) -> Dict[str, Any]: """ Serialize NPC for AI dialogue context. Returns a trimmed version focused on roleplay-relevant data to reduce token usage in AI prompts. Returns: Dictionary containing story-relevant NPC data """ result = { "name": self.name, "role": self.role, "personality": { "traits": self.personality.traits, "speech_style": self.personality.speech_style, "quirks": self.personality.quirks, }, "appearance": self.appearance.brief, } # Include dialogue hooks if available if self.dialogue_hooks: result["dialogue_hooks"] = self.dialogue_hooks.to_dict() return result @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPC': """ Deserialize NPC from dictionary. Args: data: Dictionary containing NPC data (from YAML or JSON) Returns: NPC instance """ # Parse personality personality_data = data.get("personality", {}) personality = NPCPersonality.from_dict(personality_data) # Parse appearance appearance_data = data.get("appearance", {"brief": ""}) appearance = NPCAppearance.from_dict(appearance_data) # Parse knowledge (optional) knowledge = None if data.get("knowledge"): knowledge = NPCKnowledge.from_dict(data["knowledge"]) # Parse relationships relationships = [ NPCRelationship.from_dict(r) for r in data.get("relationships", []) ] # Parse inventory inventory = [ NPCInventoryItem.from_dict(i) for i in data.get("inventory_for_sale", []) ] # Parse dialogue hooks (optional) dialogue_hooks = None if data.get("dialogue_hooks"): dialogue_hooks = NPCDialogueHooks.from_dict(data["dialogue_hooks"]) return cls( npc_id=data["npc_id"], name=data["name"], role=data["role"], location_id=data["location_id"], personality=personality, appearance=appearance, knowledge=knowledge, relationships=relationships, inventory_for_sale=inventory, dialogue_hooks=dialogue_hooks, quest_giver_for=data.get("quest_giver_for", []), reveals_locations=data.get("reveals_locations", []), tags=data.get("tags", []), ) def __repr__(self) -> str: """String representation of the NPC.""" return f"NPC({self.npc_id}, {self.name}, {self.role})" @dataclass class NPCInteractionState: """ Tracks a character's interaction history with an NPC. Stored on the Character record to persist relationship data across multiple game sessions. Attributes: npc_id: The NPC this state tracks first_met: ISO timestamp of first interaction last_interaction: ISO timestamp of most recent interaction interaction_count: Total number of conversations revealed_secrets: Indices of secrets that have been revealed relationship_level: 0-100 scale (50 is neutral) custom_flags: Arbitrary flags for special conditions (e.g., {"helped_with_rats": true}) """ npc_id: str first_met: str last_interaction: str interaction_count: int = 0 revealed_secrets: List[int] = field(default_factory=list) relationship_level: int = 50 custom_flags: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Serialize interaction state to dictionary.""" return { "npc_id": self.npc_id, "first_met": self.first_met, "last_interaction": self.last_interaction, "interaction_count": self.interaction_count, "revealed_secrets": self.revealed_secrets, "relationship_level": self.relationship_level, "custom_flags": self.custom_flags, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'NPCInteractionState': """Deserialize interaction state from dictionary.""" return cls( npc_id=data["npc_id"], first_met=data["first_met"], last_interaction=data["last_interaction"], interaction_count=data.get("interaction_count", 0), revealed_secrets=data.get("revealed_secrets", []), relationship_level=data.get("relationship_level", 50), custom_flags=data.get("custom_flags", {}), ) def __repr__(self) -> str: """String representation of the interaction state.""" return ( f"NPCInteractionState({self.npc_id}, " f"interactions={self.interaction_count}, " f"relationship={self.relationship_level})" )