Files
Code_of_Conquest/api/docs/DATA_MODELS.md
Phillip Tarrant 76f67c4a22 feat(api): implement inventory service with equipment system
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.
2025-11-26 18:38:39 -06:00

1520 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 system
- `FIXED_PRICE` - Immediate purchase at set price
**ListingStatus:**
- `ACTIVE` - Listing is live
- `SOLD` - Item has been sold
- `EXPIRED` - Listing time ran out
- `REMOVED` - 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 responses
- `to_story_dict()` - Trimmed version for AI prompts (reduces token usage)
- `from_dict(data)` - Deserialize from YAML/JSON
**YAML Format:**
```yaml
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:**
```yaml
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:**
```python
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 responses
- `to_story_dict()` - Trimmed version for AI dialogue prompts
- `from_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:**
```json
{
"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_messages` field for quick AI context (no database query)
- **Full History**: Complete unlimited conversation history stored in `chat_messages` collection
- **Deprecated Field**: `dialogue_history` maintained 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:**
```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:**
```python
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_messages` collection
- Indexed by character_id, npc_id, timestamp for fast queries
- Unlimited history (no cap on message count)
**Example:**
```json
{
"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:**
```python
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:**
```json
{
"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:**
```python
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_messages` collection
- **AI Context**: Reads from cache (no database query) for 90% of cases
- **User Queries**: Reads from collection with pagination and filters
**Database Indexes:**
1. `idx_character_npc_time` - character_id + npc_id + timestamp DESC
2. `idx_character_time` - character_id + timestamp DESC
3. `idx_session_time` - session_id + timestamp DESC
4. `idx_context` - context
5. `idx_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 damage
- `is_legendary_only() -> bool` - Check if requires legendary rarity
- `can_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 rarity
- `can_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:**
```python
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 YAML
- `BaseItemLoader` (`/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 stats
- `get_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:**
```yaml
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:**
```python
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 storage
- `from_dict(data)` - Deserialize from dictionary
- `get_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:
```python
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:**
```yaml
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 item
- `is_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 completed
- `get_next_objective() -> Optional[QuestObjective]` - Get next incomplete objective
- `update_progress(objective_id, progress_value) -> None` - Update objective progress
- `to_dict() / from_dict()` - Serialization for JSON storage
**YAML Format:**
```yaml
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 type
- `can_offer(character_level, location, location_type, completed_quests) -> bool` - Check if quest can be offered
**Quest Offering Logic:**
1. **Location-based roll**: Towns (30%), Taverns (35%), Wilderness (5%), Dungeons (10%)
2. **Filter eligible quests**: Level requirements, location match, prerequisites met
3. **Context-aware selection**: AI analyzes narrative context to select fitting quest
4. **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 effect
- `effect_type`: Type of effect
- `value`: Damage dealt (DOT) or healing done (HOT) = power × stacks
- `shield_remaining`: Current shield strength (SHIELD only)
- `stunned`: True if this is a stun effect
- `stat_modifier`: Amount stats are modified (BUFF/DEBUFF) = power × stacks
- `expired`: True if duration reached 0
- `message`: 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 `stacks` by 1 (up to `max_stacks`)
- Refreshes `duration` to 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 > 0
- `is_dead() -> bool` - Check if combatant has HP <= 0
- `is_stunned() -> bool` - Check if any active STUN effect
- `take_damage(damage) -> int` - Apply damage with shield absorption, returns actual HP damage dealt
- `heal(amount) -> int` - Restore HP (capped at max_hp), returns actual amount healed
- `restore_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 cooldown
- `tick_effects() -> List[Dict]` - Process all active effects for this turn, remove expired
- `tick_cooldowns() -> None` - Reduce all cooldowns by 1 turn
- `add_effect(effect) -> None` - Add effect, stacks if same effect exists
**Important Notes:**
- `abilities` stores ability IDs, not full Ability objects (for serialization)
- `stats` should 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 order
- `get_current_combatant() -> Combatant` - Get the combatant whose turn it is
- `get_combatant(combatant_id) -> Combatant` - Get combatant by ID
- `advance_turn() -> None` - Move to next combatant's turn, increment round if needed
- `start_turn() -> List[Dict]` - Process effects and cooldowns at turn start
- `check_end_condition() -> CombatStatus` - Check for victory/defeat, update status
- `log_action(action_type, combatant_id, message, details) -> None` - Add entry to combat log
**Combat Flow:**
1. `initialize_combat()` - Roll initiative, sort turn order
2. Loop while status == ACTIVE:
- `start_turn()` - Tick effects, check for stun
- Execute action (if not stunned)
- `check_end_condition()` - Check if combat should end
- `advance_turn()` - Move to next combatant
3. 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)