Files
Code_of_Conquest/api/docs/DATA_MODELS.md
2025-11-25 16:05:42 -06:00

1185 lines
39 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 |
### 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 |
| `dialogue_history` | List[Dict] | Recent conversation exchanges (max 10 per NPC) |
**Dialogue History Entry Format:**
```json
{
"player_line": "What have you heard about the old mines?",
"npc_response": "Aye, strange noises coming from there lately...",
"timestamp": "2025-11-24T10:30:00Z"
}
```
The dialogue history enables bidirectional NPC conversations - players can respond to NPC dialogue and continue conversations with context. The system maintains the last 10 exchanges per NPC to provide conversation context without excessive storage.
**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
---
## Character System
### Stats
| Field | Type | Description |
|-------|------|-------------|
| `strength` | int | Physical power |
| `dexterity` | int | Agility and precision |
| `constitution` | int | Endurance and health |
| `intelligence` | int | Magical power |
| `wisdom` | int | Perception and insight |
| `charisma` | int | Social influence |
**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.
### 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 | Skill Tree 1 | Skill Tree 2 |
|-------|-------|--------------|--------------|
| **Vanguard** | Tank/melee | Defensive (shields, armor, taunts) | Offensive (weapon mastery, heavy strikes) |
| **Assassin** | Stealth/critical | Assassination (critical hits, poisons) | Shadow (stealth, evasion) |
| **Arcanist** | Elemental spells | Pyromancy (fire spells, DoT) | Cryomancy (ice spells, crowd control) |
| **Luminary** | Healing/support | Holy (healing, buffs) | Divine Wrath (smite, undead damage) |
| **Wildstrider** | Ranged/nature | Marksmanship (bow skills, critical shots) | Beast Master (pet companion, nature magic) |
| **Oathkeeper** | Hybrid tank/healer | Protection (defensive auras, healing) | Retribution (holy damage, smites) |
| **Necromancer** | Death magic/summon | Dark Arts (curses, life drain) | Summoning (undead minions) |
| **Lorekeeper** | Support/control | Performance (buffs, debuffs via music) | Trickery (illusions, charm) |
**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)