481 lines
15 KiB
Python
481 lines
15 KiB
Python
"""
|
|
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
|
|
image_url: Optional[str] = None
|
|
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,
|
|
"image_url": self.image_url,
|
|
"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"],
|
|
image_url=data.get("image_url"),
|
|
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})"
|
|
)
|