Add InventoryService for managing character inventory, equipment, and consumable usage. Key features: - Add/remove items with inventory capacity checks - Equipment slot validation (weapon, off_hand, helmet, chest, gloves, boots, accessory_1, accessory_2) - Level and class requirement validation for equipment - Consumable usage with instant and duration-based effects - Combat-specific consumable method returning effects for combat system - Bulk operations (add_items, get_items_by_type, get_equippable_items) Design decision: Uses full Item object storage (not IDs) to support procedurally generated items with unique identifiers. Files added: - /api/app/services/inventory_service.py (560 lines) - /api/tests/test_inventory_service.py (51 tests passing) Task 2.3 of Phase 4 Combat Implementation complete.
51 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
Procedural Item Generation (Affix System)
The game uses a Diablo-style procedural item generation system where weapons and armor are created by combining base templates with random affixes.
Core Models
Affix
Represents a prefix or suffix that modifies an item's stats and name.
| Field | Type | Description |
|---|---|---|
affix_id |
str | Unique identifier |
name |
str | Display name ("Flaming", "of Strength") |
affix_type |
AffixType | PREFIX or SUFFIX |
tier |
AffixTier | MINOR, MAJOR, or LEGENDARY |
description |
str | Affix description |
stat_bonuses |
Dict[str, int] | Stat modifications |
damage_bonus |
int | Flat damage increase |
defense_bonus |
int | Flat defense increase |
resistance_bonus |
int | Flat resistance increase |
damage_type |
DamageType | For elemental affixes |
elemental_ratio |
float | Portion of damage converted to element |
crit_chance_bonus |
float | Critical hit chance modifier |
crit_multiplier_bonus |
float | Critical damage modifier |
allowed_item_types |
List[str] | Item types this affix can apply to |
required_rarity |
str | Minimum rarity required (for legendary affixes) |
Methods:
applies_elemental_damage() -> bool- Check if affix adds elemental damageis_legendary_only() -> bool- Check if requires legendary raritycan_apply_to(item_type, rarity) -> bool- Check if affix can be applied
BaseItemTemplate
Foundation template for procedural item generation.
| Field | Type | Description |
|---|---|---|
template_id |
str | Unique identifier |
name |
str | Base item name ("Dagger") |
item_type |
str | "weapon" or "armor" |
description |
str | Template description |
base_damage |
int | Starting damage value |
base_defense |
int | Starting defense value |
base_resistance |
int | Starting resistance value |
base_value |
int | Base gold value |
damage_type |
str | Physical, fire, etc. |
crit_chance |
float | Base critical chance |
crit_multiplier |
float | Base critical multiplier |
required_level |
int | Minimum level to use |
min_rarity |
str | Minimum rarity this generates as |
drop_weight |
int | Relative drop probability |
Methods:
can_generate_at_rarity(rarity) -> bool- Check if template supports raritycan_drop_for_level(level) -> bool- Check level requirement
Item Model Updates for Generated Items
The Item dataclass includes fields for tracking generated items:
| Field | Type | Description |
|---|---|---|
applied_affixes |
List[str] | IDs of affixes on this item |
base_template_id |
str | ID of base template used |
generated_name |
str | Full name with affixes (e.g., "Flaming Dagger of Strength") |
is_generated |
bool | True if procedurally generated |
Methods:
get_display_name() -> str- Returns generated_name if available, otherwise base name
Generation Enumerations
ItemRarity
Item quality tiers affecting affix count and value:
| Value | Affix Count | Value Multiplier |
|---|---|---|
COMMON |
0 | 1.0× |
UNCOMMON |
0 | 1.5× |
RARE |
1 | 2.5× |
EPIC |
2 | 5.0× |
LEGENDARY |
3 | 10.0× |
AffixType
| Value | Description |
|---|---|
PREFIX |
Appears before item name ("Flaming Dagger") |
SUFFIX |
Appears after item name ("Dagger of Strength") |
AffixTier
Affix power level, determines eligibility by item rarity:
| Value | Description | Available For |
|---|---|---|
MINOR |
Basic affixes | RARE+ |
MAJOR |
Stronger affixes | RARE+ (higher weight at EPIC+) |
LEGENDARY |
Most powerful affixes | LEGENDARY only |
Item Generation Service
Location: /app/services/item_generator.py
Usage:
from app.services.item_generator import get_item_generator
from app.models.enums import ItemRarity
generator = get_item_generator()
# Generate specific item
item = generator.generate_item(
item_type="weapon",
rarity=ItemRarity.EPIC,
character_level=5
)
# Generate random loot drop with luck influence
item = generator.generate_loot_drop(
character_level=10,
luck_stat=12
)
Related Loaders:
AffixLoader(/app/services/affix_loader.py) - Loads affix definitions from YAMLBaseItemLoader(/app/services/base_item_loader.py) - Loads base templates from YAML
Data Files:
/app/data/affixes/prefixes.yaml- Prefix definitions/app/data/affixes/suffixes.yaml- Suffix definitions/app/data/base_items/weapons.yaml- Weapon templates/app/data/base_items/armor.yaml- Armor templates
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)