first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

477
api/app/models/npc.py Normal file
View File

@@ -0,0 +1,477 @@
"""
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})"
)