# 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

### 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)
