Add new Luck stat to the character stats system with class-specific values: - Assassin: 12 (highest - critical specialists) - Luminary: 11 (divine favor) - Wildstrider/Lorekeeper: 10 (average) - Arcanist/Oathkeeper: 9 (modest) - Vanguard: 8 (default - relies on strength) - Necromancer: 7 (lowest - dark arts cost) Changes: - Add luck field to Stats dataclass with default of 8 - Add LUCK to StatType enum - Update all 8 class YAML files with luck values - Display LUK in character panel (play page) and detail page - Update DATA_MODELS.md documentation Backward compatible: existing characters without luck default to 8.
46 KiB
Data Models
All data models use Python dataclasses serialized to JSON for storage in Appwrite.
Type System (Enums)
All enum types are defined in /app/models/enums.py for type safety throughout the application.
EffectType
| Value | Description |
|---|---|
BUFF |
Temporarily increase stats |
DEBUFF |
Temporarily decrease stats |
DOT |
Damage over time (poison, bleed, burn) |
HOT |
Heal over time (regeneration) |
STUN |
Prevent actions (skip turn) |
SHIELD |
Absorb damage before HP loss |
DamageType
| Value | Description |
|---|---|
PHYSICAL |
Standard weapon damage |
FIRE |
Fire-based magic damage |
ICE |
Ice-based magic damage |
LIGHTNING |
Lightning-based magic damage |
HOLY |
Holy/divine damage |
SHADOW |
Dark/shadow magic damage |
POISON |
Poison damage (usually DoT) |
ItemType
| Value | Description |
|---|---|
WEAPON |
Adds damage, may have special effects |
ARMOR |
Adds defense/resistance |
CONSUMABLE |
One-time use (potions, scrolls) |
QUEST_ITEM |
Story-related, non-tradeable |
StatType
| Value | Description |
|---|---|
STRENGTH |
Physical power |
DEXTERITY |
Agility and precision |
CONSTITUTION |
Endurance and health |
INTELLIGENCE |
Magical power |
WISDOM |
Perception and insight |
CHARISMA |
Social influence |
LUCK |
Fortune and fate (affects crits, loot, random outcomes) |
AbilityType
| Value | Description |
|---|---|
ATTACK |
Basic physical attack |
SPELL |
Magical spell |
SKILL |
Special class ability |
ITEM_USE |
Using a consumable item |
DEFEND |
Defensive action |
CombatStatus
| Value | Description |
|---|---|
ACTIVE |
Combat is ongoing |
VICTORY |
Player(s) won |
DEFEAT |
Player(s) lost |
FLED |
Player(s) escaped |
SessionStatus
| Value | Description |
|---|---|
ACTIVE |
Session is ongoing |
COMPLETED |
Session ended normally |
TIMEOUT |
Session ended due to inactivity |
ListingType & ListingStatus
ListingType:
AUCTION- Bidding systemFIXED_PRICE- Immediate purchase at set price
ListingStatus:
ACTIVE- Listing is liveSOLD- Item has been soldEXPIRED- Listing time ran outREMOVED- Seller cancelled listing
LocationType
Types of locations in the game world (defined in both enums.py and action_prompt.py).
| Value | Description |
|---|---|
TOWN |
Populated settlements |
TAVERN |
Taverns and inns |
WILDERNESS |
Outdoor areas, forests, fields |
DUNGEON |
Dungeons and caves |
RUINS |
Ancient ruins |
LIBRARY |
Libraries and archives |
SAFE_AREA |
Protected zones, temples |
Location System
Locations define the game world structure. They are loaded from YAML files at runtime via LocationLoader.
Location
Represents a defined location in the game world.
| Field | Type | Description |
|---|---|---|
location_id |
str | Unique identifier (e.g., "crossville_tavern") |
name |
str | Display name (e.g., "The Rusty Anchor Tavern") |
location_type |
LocationType | Type (town, tavern, wilderness, dungeon, etc.) |
region_id |
str | Parent region this location belongs to |
description |
str | Full description for AI narrative context |
lore |
Optional[str] | Historical/background information |
ambient_description |
Optional[str] | Atmospheric details for AI narration |
available_quests |
List[str] | Quest IDs discoverable at this location |
npc_ids |
List[str] | NPC IDs present at this location |
discoverable_locations |
List[str] | Location IDs that can be revealed from here |
is_starting_location |
bool | Whether valid for new character spawn |
tags |
List[str] | Metadata tags for filtering/categorization |
Methods:
to_dict()- Serialize for JSON responsesto_story_dict()- Trimmed version for AI prompts (reduces token usage)from_dict(data)- Deserialize from YAML/JSON
YAML Format:
location_id: "crossville_tavern"
name: "The Rusty Anchor Tavern"
location_type: "tavern"
region_id: "crossville"
description: "A cozy tavern known for its hearty stew and warm atmosphere."
lore: "Built fifty years ago by a retired sailor."
ambient_description: "The smell of roasting meat and spilled ale fills the air."
available_quests:
- "quest_rats_tavern"
npc_ids:
- "npc_grom_001"
- "npc_elara_001"
discoverable_locations:
- "crossville_market"
- "crossville_forest_path"
is_starting_location: false
tags:
- "social"
- "rest"
Region
Represents a geographical region containing multiple locations.
| Field | Type | Description |
|---|---|---|
region_id |
str | Unique identifier (e.g., "crossville") |
name |
str | Display name (e.g., "Crossville Province") |
description |
str | Region overview and atmosphere |
location_ids |
List[str] | All location IDs in this region |
YAML Format:
region_id: "crossville"
name: "Crossville Province"
description: "A peaceful farming region on the edge of the kingdom."
location_ids:
- "crossville_village"
- "crossville_tavern"
- "crossville_market"
LocationLoader Service
Singleton service that loads and caches location/region data from YAML files.
Location: /app/services/location_loader.py
Usage:
from app.services.location_loader import get_location_loader
loader = get_location_loader()
# Get specific location
location = loader.get_location("crossville_tavern")
# Get all locations in a region
locations = loader.get_locations_by_region("crossville")
# Get starting locations for new characters
starting_locations = loader.get_starting_locations()
# Get connected locations for travel
available = loader.get_discoverable_from("crossville_village")
Data Files:
/app/data/regions/crossville.yaml- Region definition with locations
NPC System
NPCs are persistent non-player characters with rich personality, knowledge, and interaction tracking. They are loaded from YAML files via NPCLoader.
NPC
Main NPC definition with personality and dialogue data for AI generation.
| Field | Type | Description |
|---|---|---|
npc_id |
str | Unique identifier (e.g., "npc_grom_001") |
name |
str | Display name (e.g., "Grom Ironbeard") |
role |
str | NPC's job/title (e.g., "bartender", "blacksmith") |
location_id |
str | ID of location where NPC resides |
personality |
NPCPersonality | Personality traits and speech patterns |
appearance |
NPCAppearance | Physical description |
image_url |
Optional[str] | URL path to NPC portrait image (e.g., "/static/images/npcs/crossville/grom_ironbeard.png") |
knowledge |
Optional[NPCKnowledge] | What the NPC knows (public and secret) |
relationships |
List[NPCRelationship] | How NPC feels about other NPCs |
inventory_for_sale |
List[NPCInventoryItem] | Items NPC sells (if merchant) |
dialogue_hooks |
Optional[NPCDialogueHooks] | Pre-defined dialogue snippets |
quest_giver_for |
List[str] | Quest IDs this NPC can give |
reveals_locations |
List[str] | Location IDs this NPC can unlock |
tags |
List[str] | Metadata tags (e.g., "merchant", "quest_giver") |
Methods:
to_dict()- Serialize for JSON responsesto_story_dict()- Trimmed version for AI dialogue promptsfrom_dict(data)- Deserialize from YAML/JSON
NPCPersonality
Personality traits for AI dialogue generation.
| Field | Type | Description |
|---|---|---|
traits |
List[str] | Personality descriptors (e.g., "gruff", "kind", "suspicious") |
speech_style |
str | How the NPC speaks (accent, vocabulary, patterns) |
quirks |
List[str] | Distinctive behaviors or habits |
NPCAppearance
Physical description for AI narration.
| Field | Type | Description |
|---|---|---|
brief |
str | Short one-line description for lists |
detailed |
Optional[str] | Longer description for detailed encounters |
NPCKnowledge
Knowledge an NPC possesses - public and conditionally revealed.
| Field | Type | Description |
|---|---|---|
public |
List[str] | Knowledge NPC freely shares with anyone |
secret |
List[str] | Hidden knowledge (for AI reference only) |
will_share_if |
List[NPCKnowledgeCondition] | Conditional reveals based on interaction |
NPCKnowledgeCondition
Condition for revealing secret knowledge.
| Field | Type | Description |
|---|---|---|
condition |
str | Expression (e.g., "interaction_count >= 3") |
reveals |
str | Information revealed when condition is met |
NPCDialogueHooks
Pre-defined dialogue snippets for consistent NPC voice.
| Field | Type | Description |
|---|---|---|
greeting |
Optional[str] | What NPC says when first addressed |
farewell |
Optional[str] | What NPC says when conversation ends |
busy |
Optional[str] | What NPC says when occupied |
quest_complete |
Optional[str] | What NPC says when player completes their quest |
NPCRelationship
NPC-to-NPC relationship for dialogue context.
| Field | Type | Description |
|---|---|---|
npc_id |
str | The other NPC's identifier |
attitude |
str | Feeling (e.g., "friendly", "distrustful") |
reason |
Optional[str] | Explanation for the attitude |
NPCInventoryItem
Item available for purchase from merchant NPCs.
| Field | Type | Description |
|---|---|---|
item_id |
str | Reference to item definition |
price |
int | Cost in gold |
quantity |
Optional[int] | Stock count (None = unlimited) |
NPCInteractionState
Tracks a character's interaction history with an NPC. Stored on Character record.
| Field | Type | Description |
|---|---|---|
npc_id |
str | The NPC this state tracks |
first_met |
str | ISO timestamp of first interaction |
last_interaction |
str | ISO timestamp of most recent interaction |
interaction_count |
int | Total number of conversations |
revealed_secrets |
List[int] | Indices of secrets revealed |
relationship_level |
int | 0-100 scale (50 is neutral) |
custom_flags |
Dict[str, Any] | Arbitrary flags for special conditions |
recent_messages |
List[Dict] | Last 3 messages for quick AI context |
total_messages |
int | Total conversation message count |
dialogue_history |
List[Dict] | DEPRECATED - Use ChatMessageService for full history |
Recent Messages Entry Format:
{
"player_message": "What have you heard about the old mines?",
"npc_response": "Aye, strange noises coming from there lately...",
"timestamp": "2025-11-25T10:30:00Z"
}
Conversation History Architecture:
- Recent Messages Cache: Last 3 messages stored in
recent_messagesfield for quick AI context (no database query) - Full History: Complete unlimited conversation history stored in
chat_messagescollection - Deprecated Field:
dialogue_historymaintained for backward compatibility, will be removed after full migration
The recent messages cache enables fast AI dialogue generation by providing immediate context without querying the chat_messages collection. For full conversation history, use the ChatMessageService (see Chat/Conversation History API endpoints).
Relationship Levels:
- 0-20: Hostile
- 21-40: Unfriendly
- 41-60: Neutral
- 61-80: Friendly
- 81-100: Trusted
Example NPC YAML:
npc_id: "npc_grom_001"
name: "Grom Ironbeard"
role: "bartender"
location_id: "crossville_tavern"
image_url: "/static/images/npcs/crossville/grom_ironbeard.png"
personality:
traits:
- "gruff"
- "honest"
- "protective of locals"
speech_style: "Short sentences, occasional dwarven expressions"
quirks:
- "Polishes same glass repeatedly when nervous"
- "Refuses to serve anyone who insults his stew"
appearance:
brief: "A stocky dwarf with a magnificent iron-grey beard"
detailed: "A weathered dwarf standing about four feet tall..."
knowledge:
public:
- "The tavern was built by his grandfather"
- "Knows most travelers who pass through"
secret:
- "Saw strange lights in the forest last week"
will_share_if:
- condition: "relationship_level >= 70"
reveals: "Has heard rumors of goblins gathering in the old mines"
dialogue_hooks:
greeting: "Welcome to the Rusty Anchor! What'll it be?"
farewell: "Safe travels, friend."
busy: "Can't talk now, got orders to fill."
inventory_for_sale:
- item: "ale"
price: 2
- item: "hearty_stew"
price: 5
quest_giver_for:
- "quest_rats_tavern"
reveals_locations:
- "crossville_old_mines"
tags:
- "merchant"
- "quest_giver"
- "information"
NPCLoader Service
Singleton service that loads and caches NPC data from YAML files.
Location: /app/services/npc_loader.py
Usage:
from app.services.npc_loader import get_npc_loader
loader = get_npc_loader()
# Get specific NPC
npc = loader.get_npc("npc_grom_001")
# Get all NPCs at a location
npcs = loader.get_npcs_at_location("crossville_tavern")
# Get NPCs by tag
merchants = loader.get_npcs_by_tag("merchant")
Data Files:
/app/data/npcs/crossville_npcs.yaml- NPCs for Crossville region
Chat / Conversation History System
The chat system stores complete player-NPC conversation history in a dedicated chat_messages collection for unlimited history, with a performance-optimized cache in character documents.
ChatMessage
Complete message exchange between player and NPC.
Location: /app/models/chat_message.py
| Field | Type | Description |
|---|---|---|
message_id |
str | Unique identifier (UUID) |
character_id |
str | Player's character ID |
npc_id |
str | NPC identifier |
player_message |
str | What the player said (max 2000 chars) |
npc_response |
str | NPC's reply (max 5000 chars) |
timestamp |
str | ISO 8601 timestamp |
session_id |
Optional[str] | Game session reference |
location_id |
Optional[str] | Where conversation happened |
context |
MessageContext | Type of interaction (enum) |
metadata |
Dict[str, Any] | Extensible metadata (quest_id, item_id, etc.) |
is_deleted |
bool | Soft delete flag (default: False) |
Storage:
- Stored in Appwrite
chat_messagescollection - Indexed by character_id, npc_id, timestamp for fast queries
- Unlimited history (no cap on message count)
Example:
{
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"character_id": "char_123",
"npc_id": "npc_grom_ironbeard",
"player_message": "What rumors have you heard?",
"npc_response": "*leans in* Strange folk been coming through lately...",
"timestamp": "2025-11-25T14:30:00Z",
"context": "dialogue",
"location_id": "crossville_tavern",
"session_id": "sess_789",
"metadata": {},
"is_deleted": false
}
MessageContext (Enum)
Type of interaction that generated the message.
| Value | Description |
|---|---|
dialogue |
General conversation |
quest_offered |
Quest offering dialogue |
quest_completed |
Quest completion dialogue |
shop |
Merchant transaction |
location_revealed |
New location discovered through chat |
lore |
Lore/backstory reveals |
Usage:
from app.models.chat_message import MessageContext
context = MessageContext.QUEST_OFFERED
ConversationSummary
Summary of all messages with a specific NPC for UI display.
| Field | Type | Description |
|---|---|---|
npc_id |
str | NPC identifier |
npc_name |
str | NPC display name |
last_message_timestamp |
str | When the last message was sent |
message_count |
int | Total number of messages exchanged |
recent_preview |
str | Short preview of most recent NPC response |
Example:
{
"npc_id": "npc_grom_ironbeard",
"npc_name": "Grom Ironbeard",
"last_message_timestamp": "2025-11-25T14:30:00Z",
"message_count": 15,
"recent_preview": "Aye, the rats in the cellar have been causing trouble..."
}
ChatMessageService
Service for managing player-NPC conversation history.
Location: /app/services/chat_message_service.py
Core Methods:
from app.services.chat_message_service import get_chat_message_service
from app.models.chat_message import MessageContext
service = get_chat_message_service()
# Save a dialogue exchange (also updates character's recent_messages cache)
message = service.save_dialogue_exchange(
character_id="char_123",
user_id="user_456",
npc_id="npc_grom_ironbeard",
player_message="What rumors have you heard?",
npc_response="*leans in* Strange folk...",
context=MessageContext.DIALOGUE,
metadata={},
session_id="sess_789",
location_id="crossville_tavern"
)
# Get conversation history with pagination
messages = service.get_conversation_history(
character_id="char_123",
user_id="user_456",
npc_id="npc_grom_ironbeard",
limit=50,
offset=0
)
# Search messages with filters
results = service.search_messages(
character_id="char_123",
user_id="user_456",
search_text="quest",
npc_id="npc_grom_ironbeard",
context=MessageContext.QUEST_OFFERED,
date_from="2025-11-01T00:00:00Z",
date_to="2025-11-30T23:59:59Z",
limit=50,
offset=0
)
# Get all conversations summary for UI
summaries = service.get_all_conversations_summary(
character_id="char_123",
user_id="user_456"
)
# Soft delete a message (privacy/moderation)
success = service.soft_delete_message(
message_id="msg_abc123",
character_id="char_123",
user_id="user_456"
)
Performance Architecture:
- Recent Messages Cache: Last 3 messages stored in
character.npc_interactions[npc_id].recent_messages - Full History: All messages in dedicated
chat_messagescollection - AI Context: Reads from cache (no database query) for 90% of cases
- User Queries: Reads from collection with pagination and filters
Database Indexes:
idx_character_npc_time- character_id + npc_id + timestamp DESCidx_character_time- character_id + timestamp DESCidx_session_time- session_id + timestamp DESCidx_context- contextidx_timestamp- timestamp DESC
See Also:
- Chat API endpoints in API_REFERENCE.md
- CHAT_SYSTEM.md for architecture details
Character System
Stats
| Field | Type | Default | Description |
|---|---|---|---|
strength |
int | 10 | Physical power |
dexterity |
int | 10 | Agility and precision |
constitution |
int | 10 | Endurance and health |
intelligence |
int | 10 | Magical power |
wisdom |
int | 10 | Perception and insight |
charisma |
int | 10 | Social influence |
luck |
int | 8 | Fortune and fate (affects crits, loot, random outcomes) |
Derived Properties (Computed):
hit_points= 10 + (constitution × 2)mana_points= 10 + (intelligence × 2)defense= constitution // 2 (physical damage reduction)resistance= wisdom // 2 (magical damage reduction)
Note: Defense and resistance are computed properties, not stored values. They are calculated on-the-fly from constitution and wisdom.
Luck Stat: The luck stat has a lower default (8) compared to other stats (10). Each class has a specific luck value ranging from 7 (Necromancer) to 12 (Assassin). Luck will influence critical hit chance, hit/miss calculations, base damage variance, NPC interactions, loot generation, and spell power in future implementations.
SkillNode
| Field | Type | Description |
|---|---|---|
skill_id |
str | Unique identifier |
name |
str | Display name |
description |
str | What the skill does |
tier |
int | 1-5 (1=basic, 5=master) |
prerequisites |
List[str] | Required skill_ids |
effects |
Dict | Stat bonuses, abilities unlocked |
unlocked |
bool | Current unlock status |
Effect Types:
- Passive bonuses (permanent stat increases)
- Active abilities (new spells/skills to use)
- Unlocks (access to equipment types or features)
SkillTree
| Field | Type | Description |
|---|---|---|
tree_id |
str | Unique identifier |
name |
str | Tree name |
description |
str | Tree theme |
nodes |
List[SkillNode] | All nodes in tree |
Methods:
can_unlock(skill_id, unlocked_skills)- Check if prerequisites met
Progression Rules:
- Must unlock tier 1 before accessing tier 2
- Some nodes have prerequisites within same tier
- 1 skill point earned per level
- Respec available (costs gold, scales with level)
PlayerClass
| Field | Type | Description |
|---|---|---|
class_id |
str | Unique identifier |
name |
str | Class name |
description |
str | Class theme |
base_stats |
Stats | Starting stats |
skill_trees |
List[SkillTree] | 2+ skill trees |
starting_equipment |
List[str] | Starting item IDs |
Initial 8 Player Classes
| Class | Theme | LUK | Skill Tree 1 | Skill Tree 2 |
|---|---|---|---|---|
| Vanguard | Tank/melee | 8 | Defensive (shields, armor, taunts) | Offensive (weapon mastery, heavy strikes) |
| Assassin | Stealth/critical | 12 | Assassination (critical hits, poisons) | Shadow (stealth, evasion) |
| Arcanist | Elemental spells | 9 | Pyromancy (fire spells, DoT) | Cryomancy (ice spells, crowd control) |
| Luminary | Healing/support | 11 | Holy (healing, buffs) | Divine Wrath (smite, undead damage) |
| Wildstrider | Ranged/nature | 10 | Marksmanship (bow skills, critical shots) | Beast Master (pet companion, nature magic) |
| Oathkeeper | Hybrid tank/healer | 9 | Protection (defensive auras, healing) | Retribution (holy damage, smites) |
| Necromancer | Death magic/summon | 7 | Dark Arts (curses, life drain) | Summoning (undead minions) |
| Lorekeeper | Support/control | 10 | Performance (buffs, debuffs via music) | Trickery (illusions, charm) |
Class Luck Values:
- Assassin (12): Highest luck - critical strike specialists benefit most from fortune
- Luminary (11): Divine favor grants above-average luck
- Wildstrider (10): Average luck - self-reliant nature
- Lorekeeper (10): Average luck - knowledge is their advantage
- Arcanist (9): Slight chaos magic influence
- Oathkeeper (9): Honorable path grants modest fortune
- Vanguard (8): Relies on strength and skill, not luck
- Necromancer (7): Lowest luck - dark arts exact a toll
Extensibility: Class system designed to easily add more classes in future updates.
Item
| Field | Type | Description |
|---|---|---|
item_id |
str | Unique identifier |
name |
str | Item name |
item_type |
str | weapon, armor, consumable, quest_item |
stats |
Dict[str, int] | {"damage": 10, "defense": 5} |
effects |
List[Effect] | Buffs/debuffs on use/equip |
value |
int | Gold value |
description |
str | Item lore/description |
is_tradeable |
bool | Can be sold on marketplace |
Item Types:
- Weapon: Adds damage, may have special effects
- Armor: Adds defense/resistance
- Consumable: One-time use (potions, scrolls)
- Quest Item: Story-related, non-tradeable
Ability
Abilities represent actions that can be taken in combat (attacks, spells, skills, item uses).
| Field | Type | Description |
|---|---|---|
ability_id |
str | Unique identifier |
name |
str | Display name |
description |
str | What the ability does |
ability_type |
AbilityType | ATTACK, SPELL, SKILL, ITEM_USE, DEFEND |
base_power |
int | Base damage or healing value |
damage_type |
DamageType | Type of damage dealt (if applicable) |
scaling_stat |
StatType | Which stat scales this ability's power |
scaling_factor |
float | Multiplier for scaling stat (default 0.5) |
mana_cost |
int | MP required to use this ability |
cooldown |
int | Turns before ability can be used again |
effects_applied |
List[Effect] | Effects applied to target(s) on hit |
is_aoe |
bool | Whether this affects multiple targets |
target_count |
int | Number of targets if AoE (0 = all) |
Damage/Healing Calculation:
Final Power = base_power + (scaling_stat × scaling_factor)
Minimum power is always 1
Example:
- Fireball: base_power=30, scaling_stat=INTELLIGENCE, scaling_factor=0.5
- If caster has 16 intelligence: 30 + (16 × 0.5) = 38 power
Methods:
calculate_power(caster_stats)- Calculate final power based on caster's statsget_effects_to_apply()- Get copies of effects to apply to targets
AbilityLoader
Abilities are loaded from YAML configuration files in /app/data/abilities/ for data-driven game design.
YAML Format:
ability_id: "fireball"
name: "Fireball"
description: "Hurl a ball of fire at enemies"
ability_type: "spell"
base_power: 30
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 15
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "burn"
name: "Burning"
effect_type: "dot"
duration: 3
power: 5
max_stacks: 3
Usage:
from app.models.abilities import AbilityLoader
loader = AbilityLoader()
fireball = loader.load_ability("fireball")
power = fireball.calculate_power(caster_stats)
Benefits:
- Game designers can add/modify abilities without code changes
- Easy balancing and iteration
- Version control friendly (text files)
- Hot-reloading capable
Character
| Field | Type | Description |
|---|---|---|
character_id |
str | Unique identifier |
user_id |
str | Owner user ID |
name |
str | Character name |
player_class |
PlayerClass | Character class |
level |
int | Current level |
experience |
int | XP points |
stats |
Stats | Current stats |
unlocked_skills |
List[str] | Unlocked skill_ids |
inventory |
List[Item] | All items |
equipped |
Dict[str, Item] | {"weapon": Item, "armor": Item} |
gold |
int | Currency |
active_quests |
List[str] | Quest IDs |
discovered_locations |
List[str] | Location IDs |
Methods:
to_dict()- Serialize to dictionary for JSON storagefrom_dict(data)- Deserialize from dictionaryget_effective_stats(active_effects)- THE CRITICAL METHOD - Calculate final stats
get_effective_stats() Details:
This is the single source of truth for all stat calculations in the game. It combines modifiers from all sources in this order:
def get_effective_stats(self, active_effects: Optional[List[Effect]] = None) -> Stats:
"""
Calculate final effective stats from all sources:
1. Base stats (from character)
2. Equipment bonuses (from equipped items)
3. Skill tree bonuses (from unlocked skills)
4. Active effect modifiers (buffs/debuffs from combat)
Returns fully typed Stats object with all modifiers applied.
Debuffs are clamped to minimum stat value of 1.
"""
Example Calculation:
- Base strength: 12
- Equipped weapon bonus: +5 strength
- Unlocked skill bonus: +5 strength
- Active buff effect: +3 strength
- Final effective strength: 25
Important Notes:
- Defense and resistance are calculated from final constitution/wisdom
- Debuffs cannot reduce stats below 1 (minimum clamping)
- Equipment stat_bonuses dictionary:
{"strength": 5, "constitution": 3} - Skill effects dictionary:
{"strength": 5}extracted from unlocked skills
Story Progression System
ActionPrompt
Represents a button-based action prompt available to players during story progression turns.
| Field | Type | Description |
|---|---|---|
prompt_id |
str | Unique identifier (e.g., "ask_surroundings") |
category |
str | Action category: "ask", "travel", "gather" |
display_text |
str | Button text shown to player |
description |
str | Tooltip/help text |
tier_required |
str | Minimum tier: "free", "basic", "premium", "elite" |
context_filter |
Optional[str] | Where action is available: "town", "wilderness", "any" |
dm_prompt_template |
str | Jinja2 template for AI prompt generation |
Methods:
is_available(user_tier, location_type) -> bool- Check if action available to user
YAML Format:
prompt_id: "ask_surroundings"
category: "ask"
display_text: "What do I see around me?"
description: "Get a description of your current surroundings"
tier_required: "free"
context_filter: "any"
dm_prompt_template: |
The player is currently in {{ location_name }}.
Describe what they see, hear, and sense around them.
Tier-Based Availability:
- Free tier: 4 basic actions (ask surroundings, check dangers, travel, explore)
- Premium tier: +3 actions (recall memory, ask around, visit tavern)
- Elite tier: +3 actions (search secrets, seek elder, chart course)
- Premium/Elite: Custom free-form input (250/500 char limits)
Loading:
Actions are loaded from /app/data/action_prompts.yaml via ActionPromptLoader service.
AI Response Parser
Data structures for parsing structured game actions from AI narrative responses.
ParsedAIResponse
Complete parsed AI response with narrative and game state changes.
| Field | Type | Description |
|---|---|---|
narrative |
str | The narrative text to display to player |
game_changes |
GameStateChanges | Structured game state changes |
raw_response |
str | Original unparsed response |
parse_success |
bool | Whether parsing succeeded |
parse_errors |
List[str] | Any errors encountered |
GameStateChanges
Structured game state changes extracted from AI response.
| Field | Type | Description |
|---|---|---|
items_given |
List[ItemGrant] | Items to add to player inventory |
items_taken |
List[str] | Item IDs to remove |
gold_given |
int | Gold to add to player |
gold_taken |
int | Gold to remove from player |
experience_given |
int | XP to award player |
quest_offered |
Optional[str] | Quest ID to offer |
quest_completed |
Optional[str] | Quest ID completed |
location_change |
Optional[str] | New location ID |
ItemGrant
Represents an item granted by the AI during gameplay.
| Field | Type | Description |
|---|---|---|
item_id |
Optional[str] | ID for existing items from registry |
name |
Optional[str] | Name for generic items |
item_type |
Optional[str] | Type: weapon, armor, consumable, quest_item |
description |
Optional[str] | Description for generic items |
value |
int | Gold value (default 0) |
quantity |
int | Number of items (default 1) |
Methods:
is_existing_item() -> bool- Check if references existing itemis_generic_item() -> bool- Check if AI-generated generic item
Files:
- Parser:
/app/ai/response_parser.py - Validator:
/app/services/item_validator.py - Templates:
/app/data/generic_items.yaml
Quest System
Quest
Represents a quest with objectives and rewards.
| Field | Type | Description |
|---|---|---|
quest_id |
str | Unique identifier (e.g., "quest_rats_tavern") |
name |
str | Display name (e.g., "Rat Problem") |
description |
str | Full quest description |
quest_giver |
str | NPC or source name |
difficulty |
str | "easy", "medium", "hard", "epic" |
objectives |
List[QuestObjective] | List of objectives to complete |
rewards |
QuestReward | Rewards for completion |
offering_triggers |
QuestTriggers | When/where quest can be offered |
narrative_hooks |
List[str] | Story snippets for AI to use |
status |
str | "available", "active", "completed", "failed" |
progress |
Dict[str, Any] | Objective progress tracking |
Methods:
is_complete() -> bool- Check if all objectives completedget_next_objective() -> Optional[QuestObjective]- Get next incomplete objectiveupdate_progress(objective_id, progress_value) -> None- Update objective progressto_dict() / from_dict()- Serialization for JSON storage
YAML Format:
quest_id: "quest_rats_tavern"
name: "Rat Problem"
description: "Clear giant rats from the tavern basement"
quest_giver: "Tavern Keeper"
difficulty: "easy"
objectives:
- objective_id: "kill_rats"
description: "Kill 10 giant rats"
objective_type: "kill"
required_progress: 10
rewards:
gold: 50
experience: 100
items: []
offering_triggers:
location_types: ["town"]
min_character_level: 1
max_character_level: 3
probability_weights:
town: 0.30
wilderness: 0.0
narrative_hooks:
- "The tavern keeper waves you over, mentioning strange noises from the basement."
QuestObjective
Represents a single objective within a quest.
| Field | Type | Description |
|---|---|---|
objective_id |
str | Unique ID (e.g., "kill_rats") |
description |
str | Player-facing description |
objective_type |
str | "kill", "collect", "travel", "interact", "discover" |
required_progress |
int | Target value (e.g., 10 rats) |
current_progress |
int | Current value (e.g., 5 rats killed) |
completed |
bool | Objective completion status |
Objective Types:
- kill: Defeat X enemies
- collect: Gather X items
- travel: Reach a specific location
- interact: Talk to NPCs or interact with objects
- discover: Find new locations or secrets
QuestReward
Rewards granted upon quest completion.
| Field | Type | Description |
|---|---|---|
gold |
int | Gold reward |
experience |
int | XP reward (may trigger level up) |
items |
List[str] | Item IDs to grant |
reputation |
Optional[str] | Reputation faction (future feature) |
QuestTriggers
Defines when and where a quest can be offered.
| Field | Type | Description |
|---|---|---|
location_types |
List[str] | ["town", "wilderness", "dungeon"] or ["any"] |
specific_locations |
List[str] | Specific location IDs or empty for any |
min_character_level |
int | Minimum level required |
max_character_level |
int | Maximum level (for scaling) |
required_quests_completed |
List[str] | Quest prerequisites |
probability_weights |
Dict[str, float] | Location-specific offering chances |
Methods:
get_offer_probability(location_type) -> float- Get probability for location typecan_offer(character_level, location, location_type, completed_quests) -> bool- Check if quest can be offered
Quest Offering Logic:
- Location-based roll: Towns (30%), Taverns (35%), Wilderness (5%), Dungeons (10%)
- Filter eligible quests: Level requirements, location match, prerequisites met
- Context-aware selection: AI analyzes narrative context to select fitting quest
- Max 2 active quests: Limit enforced to prevent player overwhelm
Quest Storage:
Quests are defined in YAML files in /app/data/quests/ organized by difficulty:
/app/data/quests/easy/- Levels 1-3/app/data/quests/medium/- Levels 3-7/app/data/quests/hard/- Levels 10+/app/data/quests/epic/- End-game content
Combat System
Effect
Effects are temporary status modifiers applied to combatants during combat.
| Field | Type | Description |
|---|---|---|
effect_id |
str | Unique identifier |
name |
str | Effect name |
effect_type |
EffectType | BUFF, DEBUFF, DOT, HOT, STUN, SHIELD |
duration |
int | Turns remaining before expiration |
power |
int | Damage/healing per turn or stat modifier |
stat_affected |
StatType | Which stat is modified (for BUFF/DEBUFF only) |
stacks |
int | Current number of stacks (default 1) |
max_stacks |
int | Maximum stacks allowed (default 5) |
source |
str | Who/what applied it (ability_id or character_id) |
Effect Types:
| Type | Description | Power Usage |
|---|---|---|
| BUFF | Increase stats temporarily | Stat modifier (×stacks) |
| DEBUFF | Decrease stats temporarily | Stat modifier (×stacks) |
| DOT | Damage over time (poison, bleed, burn) | Damage per turn (×stacks) |
| HOT | Heal over time (regeneration) | Healing per turn (×stacks) |
| STUN | Skip turn (cannot act) | Not used |
| SHIELD | Absorb damage before HP loss | Shield strength (×stacks) |
Methods:
tick() -> Dict[str, Any]
Process one turn of this effect. Called at the start of each combatant's turn.
Returns dictionary with:
effect_name: Name of the effecteffect_type: Type of effectvalue: Damage dealt (DOT) or healing done (HOT) = power × stacksshield_remaining: Current shield strength (SHIELD only)stunned: True if this is a stun effectstat_modifier: Amount stats are modified (BUFF/DEBUFF) = power × stacksexpired: True if duration reached 0message: Human-readable description
Duration is decremented by 1 each tick. Effect is marked expired when duration reaches 0.
apply_stack(additional_duration) -> None
Apply an additional stack of this effect (stacking mechanic).
Behavior:
- Increases
stacksby 1 (up tomax_stacks) - Refreshes
durationto maximum - If already at max_stacks, only refreshes duration
Example: Poison with 2 stacks gets re-applied → becomes 3 stacks, duration refreshes
reduce_shield(damage) -> int
Reduce shield strength by damage amount (SHIELD effects only).
Returns remaining damage after shield absorption.
Examples:
- Shield power=50, damage=30 → power becomes 20, returns 0 (all absorbed)
- Shield power=20, damage=30 → power becomes 0, duration=0, returns 10 (partial)
Effect Stacking Rules:
- Same effect applied multiple times increases stacks
- Stacks are capped at
max_stacks(default 5, configurable per effect) - Power scales linearly: 3 stacks of 5 power poison = 15 damage per turn
- Duration refreshes on re-application (does not stack cumulatively)
- Different effects (even same name) don't stack with each other
Combatant
Wrapper for a Character or Enemy in combat. Tracks combat-specific state.
| Field | Type | Description |
|---|---|---|
combatant_id |
str | Character or enemy ID |
name |
str | Display name |
is_player |
bool | True for player characters, False for NPCs |
current_hp |
int | Current health points |
max_hp |
int | Maximum health points |
current_mp |
int | Current mana points |
max_mp |
int | Maximum mana points |
stats |
Stats | Combat stats (use Character.get_effective_stats()) |
active_effects |
List[Effect] | Currently active effects on this combatant |
abilities |
List[str] | Available ability IDs (not full Ability objects) |
cooldowns |
Dict[str, int] | {ability_id: turns_remaining} for abilities on cooldown |
initiative |
int | Turn order value (rolled at combat start) |
Methods:
is_alive() -> bool- Check if combatant has HP > 0is_dead() -> bool- Check if combatant has HP <= 0is_stunned() -> bool- Check if any active STUN effecttake_damage(damage) -> int- Apply damage with shield absorption, returns actual HP damage dealtheal(amount) -> int- Restore HP (capped at max_hp), returns actual amount healedrestore_mana(amount) -> int- Restore MP (capped at max_mp)can_use_ability(ability_id, ability) -> bool- Check if ability can be used (mana, cooldown)use_ability_cost(ability, ability_id) -> None- Consume mana and set cooldowntick_effects() -> List[Dict]- Process all active effects for this turn, remove expiredtick_cooldowns() -> None- Reduce all cooldowns by 1 turnadd_effect(effect) -> None- Add effect, stacks if same effect exists
Important Notes:
abilitiesstores ability IDs, not full Ability objects (for serialization)statsshould be set to Character.get_effective_stats() for players- Shield effects are processed automatically in
take_damage() - Effects tick at start of turn via
tick_effects()
CombatEncounter
| Field | Type | Description |
|---|---|---|
encounter_id |
str | Unique identifier |
combatants |
List[Combatant] | All fighters |
turn_order |
List[str] | Combatant IDs in order |
current_turn_index |
int | Index in turn_order |
round_number |
int | Current round |
combat_log |
List[Dict] | Action history |
status |
str | active, victory, defeat |
Methods:
initialize_combat() -> None- Roll initiative for all combatants, set turn orderget_current_combatant() -> Combatant- Get the combatant whose turn it isget_combatant(combatant_id) -> Combatant- Get combatant by IDadvance_turn() -> None- Move to next combatant's turn, increment round if neededstart_turn() -> List[Dict]- Process effects and cooldowns at turn startcheck_end_condition() -> CombatStatus- Check for victory/defeat, update statuslog_action(action_type, combatant_id, message, details) -> None- Add entry to combat log
Combat Flow:
initialize_combat()- Roll initiative, sort turn order- Loop while status == ACTIVE:
start_turn()- Tick effects, check for stun- Execute action (if not stunned)
check_end_condition()- Check if combat should endadvance_turn()- Move to next combatant
- End when status becomes VICTORY, DEFEAT, or FLED
Session System
SessionConfig
| Field | Type | Description |
|---|---|---|
min_players |
int | Session ends if below this |
timeout_minutes |
int | Inactivity timeout |
auto_save_interval |
int | Turns between auto-saves |
GameSession
| Field | Type | Description |
|---|---|---|
session_id |
str | Unique identifier |
party_member_ids |
List[str] | Character IDs in party |
config |
SessionConfig | Session settings |
combat_encounter |
CombatEncounter | Current combat or null |
conversation_history |
List[Dict] | Turn-by-turn log |
game_state |
GameState | Current world state |
turn_order |
List[str] | Character turn order |
current_turn |
int | Index in turn_order |
turn_number |
int | Global turn counter |
created_at |
ISO Timestamp | Session start |
last_activity |
ISO Timestamp | Last action time |
status |
str | active, completed, timeout |
GameState
| Field | Type | Description |
|---|---|---|
current_location |
str | Location name/ID |
discovered_locations |
List[str] | Location IDs |
active_quests |
List[str] | Quest IDs |
world_events |
List[Dict] | Server-wide events |
Conversation History Entry
| Field | Type | Description |
|---|---|---|
turn |
int | Turn number |
character_id |
str | Acting character |
character_name |
str | Character name |
action |
str | Player action text |
dm_response |
str | AI-generated response |
combat_log |
List[Dict] | Combat actions (if any) |
Marketplace System
MarketplaceListing
| Field | Type | Description |
|---|---|---|
listing_id |
str | Unique identifier |
seller_id |
str | User ID |
character_id |
str | Character ID |
item_data |
Item | Full item details |
listing_type |
str | "auction" or "fixed_price" |
price |
int | For fixed_price |
starting_bid |
int | For auction |
current_bid |
int | For auction |
buyout_price |
int | Optional instant buy |
bids |
List[Bid] | Bid history |
auction_end |
ISO Timestamp | For auction |
status |
str | active, sold, expired, removed |
created_at |
ISO Timestamp | Listing creation |
Bid
| Field | Type | Description |
|---|---|---|
bidder_id |
str | User ID |
bidder_name |
str | Character name |
amount |
int | Bid amount |
timestamp |
ISO Timestamp | Bid time |
Transaction
| Field | Type | Description |
|---|---|---|
transaction_id |
str | Unique identifier |
buyer_id |
str | User ID |
seller_id |
str | User ID |
listing_id |
str | Listing ID |
item_data |
Item | Item details |
price |
int | Final price |
timestamp |
ISO Timestamp | Transaction time |
transaction_type |
str | marketplace_sale, shop_purchase, etc. |
NPC Shop System
ShopItem
| Field | Type | Description |
|---|---|---|
item_id |
str | Item identifier |
item |
Item | Item details |
stock |
int | Available quantity (-1 = unlimited) |
price |
int | Fixed gold price |
Shop Categories:
- Consumables (health potions, mana potions)
- Basic weapons (tier 1-2)
- Basic armor (tier 1-2)
- Crafting materials (future feature)
Purpose:
- Provides gold sink to prevent inflation
- Always available (not affected by marketplace access)
- Sells basic items at fixed prices
Skill Tree Design
Each skill tree has 5 tiers with 3-5 nodes per tier.
Example: Vanguard - Shield Bearer Tree
| Tier | Node | Type | Prerequisites | Effects |
|---|---|---|---|---|
| 1 | Shield Bash | Active | None | Unlock shield_bash ability, 5 damage, 1 turn stun |
| 1 | Fortify | Passive | None | +5 Defense |
| 2 | Shield Wall | Active | Shield Bash | Unlock shield_wall ability, block all damage 1 turn, 3 turn cooldown |
| 2 | Iron Skin | Passive | Fortify | +10 Defense, +5 HP |
| 3 | Guardian's Resolve | Passive | Shield Wall | Immune to stun |
| 3 | Riposte | Active | Shield Bash | Unlock riposte ability, counter attack on block |
| 4 | Bulwark | Passive | Iron Skin | +15 Defense, +10 HP, damage reduction 10% |
| 5 | Unbreakable | Ultimate | Bulwark | Unlock unbreakable ability, 5 turn buff: 50% damage reduction |
Data Serialization
JSON Storage in Appwrite
All complex dataclasses are serialized to JSON strings for storage:
Storage:
Character dataclass → JSON string → Appwrite document field
Retrieval:
Appwrite document field → JSON string → Character dataclass
Benefits
- Schema flexibility (easy to add fields)
- No database migrations needed
- Type safety in application code
- Easy to serialize/deserialize
Future Data Models (Backlog)
Planned Additions
- Guild: Player organizations
- WorldEvent: Server-wide quests
- Achievement: Badge system
- CraftingRecipe: Item creation
- PetCompanion: Beast Master pets
- LeaderboardEntry: Rankings
Additional Player Classes (Backlog)
- Monk (martial arts, chi energy)
- Druid (shapeshifting, nature magic)
- Warlock (pact magic, debuffs)
- Artificer (gadgets, constructs)