Compare commits

..

14 Commits

Author SHA1 Message Date
70b2b0f124 fixing leveling xp reporting 2025-11-30 18:20:40 -06:00
805d04cf4e Merge branch 'bugfix/combat-abilities-fix' into dev 2025-11-29 19:05:54 -06:00
f9e463bfc6 Root Cause
When using combat abilities (like "smite"), the web frontend was calling GET /api/v1/abilities/{ability_id} to fetch ability details for display, but this endpoint didn't exist, causing a 404 error.
  Additionally, after fixing that, the ability would execute but:

  1. Modal onclick issue: The onclick="closeModal()" on ability buttons was removing the button from the DOM before HTMX could fire the request
  2. Field name mismatch: The API returns mana_cost but the frontend expected mp_cost
  3. Duplicate text in combat log: The web view was adding "You" as actor and damage separately, but the API message already contained both
  4. Page not auto-refreshing: Various attempts to use HX-Trigger headers failed due to HTMX overwriting them

  Fixes Made

  1. Created /api/app/api/abilities.py - New abilities API endpoint with GET /api/v1/abilities and GET /api/v1/abilities/<ability_id>
  2. Modified /api/app/__init__.py - Registered the new abilities blueprint
  3. Modified /public_web/templates/game/partials/ability_modal.html - Changed onclick="closeModal()" to hx-on::before-request="closeModal()" so HTMX captures the request before modal closes
  4. Modified /public_web/app/views/combat_views.py:
    - Fixed mp_cost → mana_cost field name lookup
    - Removed duplicate actor/damage from combat log entries (API message is self-contained)
    - Added inline script to trigger page refresh after combat actions
  5. Modified /public_web/templates/game/combat.html - Updated JavaScript for combat action handling (though final fix was server-side script injection)
2025-11-29 19:05:39 -06:00
06ef8f6f0b Summary of Fixes
Issue 1: Slot Name Mismatch
  - Equipment modal used armor, accessory but API uses chest, accessory_1
  - Updated to all 8 API slots: weapon, off_hand, helmet, chest, gloves, boots, accessory_1, accessory_2

  Issue 2: HTMX Request Not Firing (the real blocker)
  - onclick=closeModal() was removing the button from DOM before HTMX could send the request
  - Changed to hx-on::after-request=closeModal() so modal closes after the request completes
2025-11-29 18:25:30 -06:00
72cf92021e fixes to make quest tracking work better, also quest rejection in via the converation with the NPC 2025-11-29 17:51:53 -06:00
df26abd207 feat: Implement Phase 5 Quest System (100% complete)
Add YAML-driven quest system with context-aware offering:

Core Implementation:
- Quest data models (Quest, QuestObjective, QuestReward, QuestTriggers)
- QuestService for YAML loading and caching
- QuestEligibilityService with level, location, and probability filtering
- LoreService stub (MockLoreService) ready for Phase 6 Weaviate integration

Quest Content:
- 5 example quests across difficulty tiers (2 easy, 2 medium, 1 hard)
- Quest-centric design: quests define their NPC givers
- Location-based probability weights for natural quest offering

AI Integration:
- Quest offering section in npc_dialogue.j2 template
- Response parser extracts [QUEST_OFFER:quest_id] markers
- AI naturally weaves quest offers into NPC conversations

API Endpoints:
- POST /api/v1/quests/accept - Accept quest offer
- POST /api/v1/quests/decline - Decline quest offer
- POST /api/v1/quests/progress - Update objective progress
- POST /api/v1/quests/complete - Complete quest, claim rewards
- POST /api/v1/quests/abandon - Abandon active quest
- GET /api/v1/characters/{id}/quests - List character quests
- GET /api/v1/quests/{quest_id} - Get quest details

Frontend:
- Quest tracker sidebar with HTMX integration
- Quest offer modal for accept/decline flow
- Quest detail modal for viewing progress
- Combat service integration for kill objective tracking

Testing:
- Unit tests for Quest models and serialization
- Integration tests for full quest lifecycle
- Comprehensive test coverage for eligibility service

Documentation:
- Reorganized docs into /docs/phases/ structure
- Added Phase 5-12 planning documents
- Updated ROADMAP.md with new structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 15:42:55 -06:00
e7e329e6ed Merge branch 'feat/Phase4-NPC-Shop' into dev 2025-11-29 01:17:01 -06:00
8bd494a52f NPC shop implimented 2025-11-29 01:16:46 -06:00
32af625d14 Merge branch 'feat/Phase4-Skilltrees' into dev 2025-11-28 22:03:16 -06:00
8784fbaa88 Phase 4b Abilities and skill trees is finished 2025-11-28 22:02:57 -06:00
a8767b34e2 adding abilities, created skill tree template and unlock mechanics 2025-11-28 21:41:46 -06:00
d9bc46adc1 general cleanup 2025-11-28 10:43:58 -06:00
45cfa25911 adding more enemies 2025-11-28 10:37:57 -06:00
7c0e257540 Merge pull request 'feat/phase4-combat-foundation' (#8) from feat/phase4-combat-foundation into dev
Reviewed-on: #8
2025-11-28 04:21:19 +00:00
203 changed files with 20172 additions and 2776 deletions

View File

@@ -179,7 +179,21 @@ def register_blueprints(app: Flask) -> None:
app.register_blueprint(inventory_bp) app.register_blueprint(inventory_bp)
logger.info("Inventory API blueprint registered") logger.info("Inventory API blueprint registered")
# Import and register Shop API blueprint
from app.api.shop import shop_bp
app.register_blueprint(shop_bp)
logger.info("Shop API blueprint registered")
# Import and register Quests API blueprint
from app.api.quests import quests_bp
app.register_blueprint(quests_bp)
logger.info("Quests API blueprint registered")
# Import and register Abilities API blueprint
from app.api.abilities import abilities_bp
app.register_blueprint(abilities_bp)
logger.info("Abilities API blueprint registered")
# TODO: Register additional blueprints as they are created # TODO: Register additional blueprints as they are created
# from app.api import marketplace, shop # from app.api import marketplace
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace') # app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')
# app.register_blueprint(shop.bp, url_prefix='/api/v1/shop')

View File

@@ -447,7 +447,10 @@ class NarrativeGenerator:
user_tier: UserTier, user_tier: UserTier,
npc_relationship: str | None = None, npc_relationship: str | None = None,
previous_dialogue: list[dict[str, Any]] | None = None, previous_dialogue: list[dict[str, Any]] | None = None,
npc_knowledge: list[str] | None = None npc_knowledge: list[str] | None = None,
quest_offering_context: dict[str, Any] | None = None,
quest_ineligibility_context: dict[str, Any] | None = None,
player_asking_for_quests: bool = False
) -> NarrativeResponse: ) -> NarrativeResponse:
""" """
Generate NPC dialogue in response to player conversation. Generate NPC dialogue in response to player conversation.
@@ -461,6 +464,9 @@ class NarrativeGenerator:
npc_relationship: Optional description of relationship with NPC. npc_relationship: Optional description of relationship with NPC.
previous_dialogue: Optional list of previous exchanges. previous_dialogue: Optional list of previous exchanges.
npc_knowledge: Optional list of things this NPC knows about. npc_knowledge: Optional list of things this NPC knows about.
quest_offering_context: Optional quest offer context from QuestEligibilityService.
quest_ineligibility_context: Optional context explaining why player can't take a quest.
player_asking_for_quests: Whether the player is explicitly asking for quests/work.
Returns: Returns:
NarrativeResponse with NPC dialogue. NarrativeResponse with NPC dialogue.
@@ -500,6 +506,9 @@ class NarrativeGenerator:
npc_relationship=npc_relationship, npc_relationship=npc_relationship,
previous_dialogue=previous_dialogue or [], previous_dialogue=previous_dialogue or [],
npc_knowledge=npc_knowledge or [], npc_knowledge=npc_knowledge or [],
quest_offering_context=quest_offering_context,
quest_ineligibility_context=quest_ineligibility_context,
player_asking_for_quests=player_asking_for_quests,
max_tokens=model_config.max_tokens max_tokens=model_config.max_tokens
) )
except PromptTemplateError as e: except PromptTemplateError as e:

View File

@@ -4,8 +4,11 @@ Response parser for AI narrative responses.
This module handles AI response parsing. Game state changes (items, gold, XP) This module handles AI response parsing. Game state changes (items, gold, XP)
are now handled exclusively through predetermined dice check outcomes in are now handled exclusively through predetermined dice check outcomes in
action templates, not through AI-generated JSON. action templates, not through AI-generated JSON.
Quest offers are extracted from NPC dialogue using [QUEST_OFFER:quest_id] markers.
""" """
import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Optional from typing import Any, Optional
@@ -158,3 +161,83 @@ def _parse_game_actions(data: dict[str, Any]) -> GameStateChanges:
changes.location_change = data.get("location_change") changes.location_change = data.get("location_change")
return changes return changes
# ============================================================================
# NPC Dialogue Parsing
# ============================================================================
@dataclass
class ParsedNPCDialogue:
"""
Parsed NPC dialogue response with quest offer extraction.
When NPCs offer quests during conversation, they include a
[QUEST_OFFER:quest_id] marker that signals the UI to show
a quest accept/decline modal.
Attributes:
dialogue: The cleaned dialogue text (marker removed)
quest_offered: Quest ID if a quest was offered, None otherwise
raw_response: The original response text
"""
dialogue: str
quest_offered: Optional[str] = None
raw_response: str = ""
# Regex pattern for quest offer markers
# Matches: [QUEST_OFFER:quest_id] or [QUEST_OFFER: quest_id]
QUEST_OFFER_PATTERN = re.compile(r'\[QUEST_OFFER:\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\]')
def parse_npc_dialogue(response_text: str) -> ParsedNPCDialogue:
"""
Parse an NPC dialogue response, extracting quest offer markers.
The AI is instructed to include [QUEST_OFFER:quest_id] on its own line
when offering a quest. This function extracts the marker and returns
the cleaned dialogue.
Args:
response_text: The raw AI dialogue response
Returns:
ParsedNPCDialogue with cleaned dialogue and optional quest_offered
Example:
>>> response = '''*leans in* Got a problem, friend.
... [QUEST_OFFER:quest_cellar_rats]
... Giant rats in me cellar.'''
>>> result = parse_npc_dialogue(response)
>>> result.quest_offered
'quest_cellar_rats'
>>> '[QUEST_OFFER' in result.dialogue
False
"""
logger.debug("Parsing NPC dialogue", response_length=len(response_text))
quest_offered = None
dialogue = response_text.strip()
# Search for quest offer marker
match = QUEST_OFFER_PATTERN.search(dialogue)
if match:
quest_offered = match.group(1)
# Remove the marker from the dialogue
dialogue = QUEST_OFFER_PATTERN.sub('', dialogue)
# Clean up any extra whitespace/newlines left behind
dialogue = re.sub(r'\n\s*\n', '\n\n', dialogue)
dialogue = dialogue.strip()
logger.info(
"Quest offer extracted from dialogue",
quest_id=quest_offered,
)
return ParsedNPCDialogue(
dialogue=dialogue,
quest_offered=quest_offered,
raw_response=response_text,
)

View File

@@ -92,6 +92,107 @@ Work these into the dialogue naturally - don't dump all information at once.
Make it feel earned, like the NPC is opening up to someone they trust. Make it feel earned, like the NPC is opening up to someone they trust.
{% endif %} {% endif %}
{% if quest_offering_context and quest_offering_context.should_offer %}
## QUEST TO OFFER
The NPC has a quest to offer to the player.
**Quest:** {{ quest_offering_context.quest_name }}
**Quest ID:** {{ quest_offering_context.quest_id }}
{% if quest_offering_context.offer_dialogue %}
**How the NPC Would Present It:**
{{ quest_offering_context.offer_dialogue }}
{% endif %}
{% if quest_offering_context.npc_quest_knowledge %}
**What the NPC Knows About This Quest:**
{% for fact in quest_offering_context.npc_quest_knowledge %}
- {{ fact }}
{% endfor %}
{% endif %}
{% if quest_offering_context.narrative_hooks %}
**Narrative Hooks (use 1-2 naturally):**
{% for hook in quest_offering_context.narrative_hooks %}
- The NPC {{ hook }}
{% endfor %}
{% endif %}
{% if player_asking_for_quests %}
**CRITICAL: The player is explicitly asking for quests/work. You MUST offer this quest NOW.**
In your response:
1. Describe the quest situation naturally in your dialogue
2. End your response with the quest offer marker on its own line: [QUEST_OFFER:{{ quest_offering_context.quest_id }}]
The marker MUST appear - it triggers the UI to show accept/decline buttons.
{% else %}
**Quest Offering Guidelines:**
- Weave the quest naturally into conversation
- If the player shows interest, include the marker: [QUEST_OFFER:{{ quest_offering_context.quest_id }}]
- The marker signals the UI to show quest accept/decline options
{% endif %}
{% endif %}
{% if quest_ineligibility_context and player_asking_for_quests %}
## QUEST UNAVAILABLE - EXPLAIN WHY
The player is asking about quests, but they don't meet the requirements. Explain this in character.
{% if quest_ineligibility_context.reason_type == "level_too_low" %}
**Reason:** The player (level {{ quest_ineligibility_context.current_level }}) isn't experienced enough. They need to be level {{ quest_ineligibility_context.required_level }}.
**How to convey this:** The NPC should politely but firmly indicate the task is too dangerous for someone of their current skill level. Suggest they gain more experience first. Be encouraging but realistic - don't offer false hope.
**Example tone:** "I appreciate your enthusiasm, but this task requires someone with more experience. The bandits we're dealing with are seasoned fighters. Come back when you've proven yourself in a few more battles."
{% elif quest_ineligibility_context.reason_type == "level_too_high" %}
**Reason:** The player is too experienced for available tasks.
**How to convey this:** The NPC should indicate they have nothing worthy of such an accomplished adventurer right now.
{% elif quest_ineligibility_context.reason_type == "prerequisite_missing" %}
**Reason:** The player needs to complete other tasks first.
**How to convey this:** Hint that there's something else they should do first, or that circumstances aren't right yet.
{% elif quest_ineligibility_context.reason_type == "relationship_too_low" %}
**Reason:** The NPC doesn't trust the player enough yet.
**How to convey this:** Be guarded. Hint that you might have work, but you need to know you can trust them first.
{% elif quest_ineligibility_context.reason_type == "quest_already_active" %}
**Reason:** The player is already working on this quest.
**How to convey this:** Remind them they already accepted this task and should focus on completing it.
{% elif quest_ineligibility_context.reason_type == "quest_already_completed" %}
**Reason:** The player already completed this quest.
**How to convey this:** Thank them again for their previous help, mention you have nothing else right now.
{% elif quest_ineligibility_context.reason_type == "too_many_quests" %}
**Reason:** The player has too many active quests.
**How to convey this:** Suggest they finish some of their current commitments before taking on more.
{% else %}
**Reason:** {{ quest_ineligibility_context.message }}
**How to convey this:** Politely decline, staying in character.
{% endif %}
**IMPORTANT:** Do NOT offer the quest. Explain the situation naturally in dialogue.
{% endif %}
{% if lore_context and lore_context.has_content %}
## Relevant World Knowledge
The NPC may reference this lore if contextually appropriate:
{% if lore_context.quest %}
**Quest Background:**
{% for entry in lore_context.quest[:2] %}
- {{ entry.content | truncate_text(150) }}
{% endfor %}
{% endif %}
{% if lore_context.regional %}
**Local Knowledge:**
{% for entry in lore_context.regional[:3] %}
- {{ entry.content | truncate_text(100) }}
{% endfor %}
{% endif %}
{% if lore_context.world %}
**Historical Knowledge (if NPC would know):**
{% for entry in lore_context.world[:2] %}
- {{ entry.content | truncate_text(100) }}
{% endfor %}
{% endif %}
{% endif %}
{% if npc.relationships %} {% if npc.relationships %}
## NPC Relationships (for context) ## NPC Relationships (for context)
{% for rel in npc.relationships %} {% for rel in npc.relationships %}

129
api/app/api/abilities.py Normal file
View File

@@ -0,0 +1,129 @@
"""
Abilities API Blueprint
This module provides API endpoints for fetching ability information:
- List all available abilities
- Get details for a specific ability
"""
from flask import Blueprint
from app.models.abilities import AbilityLoader
from app.utils.response import (
success_response,
not_found_response,
)
from app.utils.logging import get_logger
# Initialize logger
logger = get_logger(__file__)
# Create blueprint
abilities_bp = Blueprint('abilities', __name__, url_prefix='/api/v1/abilities')
# Initialize ability loader (singleton pattern)
_ability_loader = None
def get_ability_loader() -> AbilityLoader:
"""
Get the singleton AbilityLoader instance.
Returns:
AbilityLoader: The ability loader instance
"""
global _ability_loader
if _ability_loader is None:
_ability_loader = AbilityLoader()
return _ability_loader
# =============================================================================
# Ability Endpoints
# =============================================================================
@abilities_bp.route('', methods=['GET'])
def list_abilities():
"""
List all available abilities.
Returns all abilities defined in the system with their full details.
Returns:
{
"abilities": [
{
"ability_id": "smite",
"name": "Smite",
"description": "Call down holy light...",
"ability_type": "spell",
"base_power": 20,
"damage_type": "holy",
"mana_cost": 10,
"cooldown": 0,
...
},
...
],
"count": 5
}
"""
logger.info("Listing all abilities")
loader = get_ability_loader()
abilities = loader.load_all_abilities()
# Convert to list of dicts for JSON serialization
abilities_list = [ability.to_dict() for ability in abilities.values()]
logger.info("Abilities listed", count=len(abilities_list))
return success_response({
"abilities": abilities_list,
"count": len(abilities_list)
})
@abilities_bp.route('/<ability_id>', methods=['GET'])
def get_ability(ability_id: str):
"""
Get details for a specific ability.
Args:
ability_id: The unique identifier for the ability (e.g., "smite")
Returns:
{
"ability_id": "smite",
"name": "Smite",
"description": "Call down holy light to smite your enemies",
"ability_type": "spell",
"base_power": 20,
"damage_type": "holy",
"scaling_stat": "wisdom",
"scaling_factor": 0.5,
"mana_cost": 10,
"cooldown": 0,
"effects_applied": [],
"is_aoe": false,
"target_count": 1
}
Errors:
404: Ability not found
"""
logger.info("Getting ability", ability_id=ability_id)
loader = get_ability_loader()
ability = loader.load_ability(ability_id)
if ability is None:
logger.warning("Ability not found", ability_id=ability_id)
return not_found_response(
message=f"Ability '{ability_id}' not found"
)
logger.info("Ability retrieved", ability_id=ability_id, name=ability.name)
return success_response(ability.to_dict())

View File

@@ -16,6 +16,7 @@ from app.services.session_service import get_session_service, SessionNotFound
from app.services.character_service import get_character_service, CharacterNotFound from app.services.character_service import get_character_service, CharacterNotFound
from app.services.npc_loader import get_npc_loader from app.services.npc_loader import get_npc_loader
from app.services.location_loader import get_location_loader from app.services.location_loader import get_location_loader
from app.services.quest_eligibility_service import get_quest_eligibility_service
from app.tasks.ai_tasks import enqueue_ai_task, TaskType from app.tasks.ai_tasks import enqueue_ai_task, TaskType
from app.utils.response import ( from app.utils.response import (
success_response, success_response,
@@ -192,6 +193,57 @@ def talk_to_npc(npc_id: str):
interaction interaction
) )
# Check for quest eligibility
quest_offering_context = None
quest_ineligibility_context = None # For explaining why player can't take a quest
player_asking_for_quests = _is_player_asking_for_quests(topic)
try:
quest_eligibility_service = get_quest_eligibility_service()
location_type = _get_location_type(session.game_state.current_location)
# If player is explicitly asking about quests, bypass probability roll
force_probability = 1.0 if player_asking_for_quests else None
eligibility_result = quest_eligibility_service.check_eligibility(
npc_id=npc_id,
character=character,
location_type=location_type,
location_id=session.game_state.current_location,
force_probability=force_probability
)
if eligibility_result.should_offer_quest and eligibility_result.selected_quest_context:
quest_offering_context = eligibility_result.selected_quest_context.to_dict()
# Add should_offer flag for template conditional check
quest_offering_context['should_offer'] = True
logger.debug(
"Quest eligible for offering",
npc_id=npc_id,
quest_id=quest_offering_context.get("quest_id"),
character_id=character.character_id
)
elif player_asking_for_quests and eligibility_result.blocking_reasons:
# Player asked for quests but isn't eligible - tell them why
quest_ineligibility_context = _build_ineligibility_context(
eligibility_result.blocking_reasons,
character.level,
npc_id
)
if quest_ineligibility_context:
logger.debug(
"Quest ineligible - providing reason to AI",
npc_id=npc_id,
reason=quest_ineligibility_context.get("reason_type"),
character_level=character.level
)
except Exception as e:
# Don't fail the conversation if quest eligibility check fails
logger.warning(
"Quest eligibility check failed",
npc_id=npc_id,
error=str(e)
)
# Build NPC knowledge for AI context # Build NPC knowledge for AI context
npc_knowledge = [] npc_knowledge = []
if npc.knowledge: if npc.knowledge:
@@ -220,6 +272,9 @@ def talk_to_npc(npc_id: str):
"interaction_count": interaction["interaction_count"], "interaction_count": interaction["interaction_count"],
"relationship_level": interaction.get("relationship_level", 50), "relationship_level": interaction.get("relationship_level", 50),
"previous_dialogue": previous_dialogue, # Pass conversation history "previous_dialogue": previous_dialogue, # Pass conversation history
"quest_offering_context": quest_offering_context, # Quest offer if eligible
"quest_ineligibility_context": quest_ineligibility_context, # Why player can't take quest
"player_asking_for_quests": player_asking_for_quests, # Player explicitly asking for work
} }
# Enqueue AI task # Enqueue AI task
@@ -428,3 +483,163 @@ def set_npc_flag(npc_id: str):
npc_id=npc_id, npc_id=npc_id,
error=str(e)) error=str(e))
return error_response("Failed to set flag", 500) return error_response("Failed to set flag", 500)
def _get_location_type(location_id: str) -> str:
"""
Extract location type from location_id for quest probability calculation.
Args:
location_id: The location identifier string
Returns:
Location type string (tavern, shop, wilderness, dungeon, or town)
"""
location_lower = location_id.lower()
if "tavern" in location_lower or "inn" in location_lower:
return "tavern"
elif "shop" in location_lower or "market" in location_lower or "store" in location_lower:
return "shop"
elif "wilderness" in location_lower or "forest" in location_lower or "road" in location_lower:
return "wilderness"
elif "dungeon" in location_lower or "cave" in location_lower or "mine" in location_lower:
return "dungeon"
return "town" # Default for town centers, squares, etc.
def _build_ineligibility_context(
blocking_reasons: dict[str, str],
character_level: int,
npc_id: str
) -> dict | None:
"""
Build context explaining why a player can't take a quest.
Parses the blocking reasons and creates a structured context
that the AI can use to explain to the player why they can't
help with the quest yet.
Args:
blocking_reasons: Dict of quest_id -> reason string
character_level: Player's current level
npc_id: The NPC they're talking to
Returns:
Dict with reason_type, message, and details, or None if no relevant reason
"""
if not blocking_reasons:
return None
# Look through blocking reasons for level-related issues
for quest_id, reason in blocking_reasons.items():
if "level too low" in reason.lower():
# Extract required level from reason string like "Character level too low (need 3)"
import re
match = re.search(r'need (\d+)', reason)
required_level = int(match.group(1)) if match else character_level + 1
return {
"reason_type": "level_too_low",
"current_level": character_level,
"required_level": required_level,
"message": f"The player is level {character_level} but needs to be level {required_level}",
"quest_id": quest_id,
}
if "level too high" in reason.lower():
return {
"reason_type": "level_too_high",
"current_level": character_level,
"message": "The player is too experienced for this task",
"quest_id": quest_id,
}
if "prerequisite" in reason.lower():
return {
"reason_type": "prerequisite_missing",
"message": "The player hasn't completed a required earlier task",
"quest_id": quest_id,
}
if "already active" in reason.lower():
return {
"reason_type": "quest_already_active",
"message": "The player is already working on this quest",
"quest_id": quest_id,
}
if "already completed" in reason.lower():
return {
"reason_type": "quest_already_completed",
"message": "The player has already completed this quest",
"quest_id": quest_id,
}
if "relationship" in reason.lower():
return {
"reason_type": "relationship_too_low",
"message": "The NPC doesn't trust the player enough yet",
"quest_id": quest_id,
}
if "max" in reason.lower() and "quest" in reason.lower():
return {
"reason_type": "too_many_quests",
"message": "The player already has too many active quests",
"quest_id": quest_id,
}
return None
def _is_player_asking_for_quests(topic: str) -> bool:
"""
Detect if the player is explicitly asking about quests or work.
This is used to bypass the probability roll when the player
clearly intends to find quests.
Args:
topic: The player's message/conversation topic
Returns:
True if player is asking about quests, False otherwise
"""
topic_lower = topic.lower()
# Quest-related keywords
quest_keywords = [
"quest",
"quests",
"any work",
"work for me",
"job",
"jobs",
"task",
"tasks",
"help you",
"help with",
"need help",
"anything i can do",
"can i help",
"how can i help",
"i'd love to help",
"i would love to help",
"want to help",
"like to help",
"offer my services",
"hire me",
"bounty",
"bounties",
"adventure",
"mission",
"missions",
]
for keyword in quest_keywords:
if keyword in topic_lower:
return True
return False

724
api/app/api/quests.py Normal file
View File

@@ -0,0 +1,724 @@
"""
Quest API endpoints for quest management.
This module provides REST endpoints for:
- Accepting offered quests
- Declining offered quests
- Getting quest details
- Listing character quests
- Completing quests (internal use)
"""
from datetime import datetime, timezone
from flask import Blueprint, request, jsonify
import structlog
from app.utils.response import api_response, error_response
from app.utils.auth import require_auth
from app.services.quest_service import get_quest_service
from app.services.quest_eligibility_service import get_quest_eligibility_service
from app.services.character_service import get_character_service
from app.models.quest import QuestStatus, CharacterQuestState
logger = structlog.get_logger(__name__)
quests_bp = Blueprint('quests', __name__, url_prefix='/api/v1/quests')
@quests_bp.route('/<quest_id>', methods=['GET'])
@require_auth
def get_quest(user_id: str, quest_id: str):
"""
Get details for a specific quest.
Args:
quest_id: Quest identifier
Returns:
Quest details or 404 if not found
"""
quest_service = get_quest_service()
quest = quest_service.load_quest(quest_id)
if not quest:
return error_response(
message=f"Quest not found: {quest_id}",
status=404,
code="QUEST_NOT_FOUND",
)
return api_response(
data=quest.to_offer_dict(),
message="Quest details retrieved",
)
@quests_bp.route('/accept', methods=['POST'])
@require_auth
def accept_quest(user_id: str):
"""
Accept an offered quest.
Expected JSON body:
{
"character_id": "char_123",
"quest_id": "quest_cellar_rats",
"npc_id": "npc_grom_ironbeard" # Optional: NPC who offered the quest
}
Returns:
Updated character quest state
"""
data = request.get_json()
if not data:
return error_response(
message="Request body is required",
status=400,
code="MISSING_BODY",
)
character_id = data.get('character_id')
quest_id = data.get('quest_id')
npc_id = data.get('npc_id')
if not character_id or not quest_id:
return error_response(
message="character_id and quest_id are required",
status=400,
code="MISSING_FIELDS",
)
# Get character
character_service = get_character_service()
character = character_service.get_character(character_id)
if not character:
return error_response(
message=f"Character not found: {character_id}",
status=404,
code="CHARACTER_NOT_FOUND",
)
# Verify character belongs to user
if character.user_id != user_id:
return error_response(
message="Character does not belong to this user",
status=403,
code="ACCESS_DENIED",
)
# Get quest
quest_service = get_quest_service()
quest = quest_service.load_quest(quest_id)
if not quest:
return error_response(
message=f"Quest not found: {quest_id}",
status=404,
code="QUEST_NOT_FOUND",
)
# Check if already at max quests
if len(character.active_quests) >= 2:
return error_response(
message="Maximum active quests reached (2)",
status=400,
code="MAX_QUESTS_REACHED",
)
# Check if quest is already active
if quest_id in character.active_quests:
return error_response(
message="Quest is already active",
status=400,
code="QUEST_ALREADY_ACTIVE",
)
# Check if quest is already completed
completed_quests = getattr(character, 'completed_quests', [])
if quest_id in completed_quests:
return error_response(
message="Quest has already been completed",
status=400,
code="QUEST_ALREADY_COMPLETED",
)
# Add quest to active quests
character.active_quests.append(quest_id)
# Create quest state tracking
quest_state = CharacterQuestState(
quest_id=quest_id,
status=QuestStatus.ACTIVE,
accepted_at=datetime.now(timezone.utc).isoformat(),
objectives_progress={
obj.objective_id: 0 for obj in quest.objectives
},
)
# Store quest state in character (would normally go to database)
if not hasattr(character, 'quest_states'):
character.quest_states = {}
character.quest_states[quest_id] = quest_state.to_dict()
# Update NPC relationship if NPC provided
if npc_id and npc_id in character.npc_interactions:
npc_interaction = character.npc_interactions[npc_id]
current_relationship = npc_interaction.get('relationship_level', 50)
npc_interaction['relationship_level'] = min(100, current_relationship + 5)
# Set accepted flag
if 'custom_flags' not in npc_interaction:
npc_interaction['custom_flags'] = {}
npc_interaction['custom_flags'][f'accepted_{quest_id}'] = True
# Save character
character_service.update_character(character)
logger.info(
"Quest accepted",
character_id=character_id,
quest_id=quest_id,
npc_id=npc_id,
)
return api_response(
data={
"quest_id": quest_id,
"quest_name": quest.name,
"active_quests": character.active_quests,
"quest_state": quest_state.to_dict(),
},
message=f"Quest accepted: {quest.name}",
)
@quests_bp.route('/decline', methods=['POST'])
@require_auth
def decline_quest(user_id: str):
"""
Decline an offered quest.
Sets a flag on the NPC interaction to prevent immediate re-offering.
Expected JSON body:
{
"character_id": "char_123",
"quest_id": "quest_cellar_rats",
"npc_id": "npc_grom_ironbeard"
}
Returns:
Confirmation of decline
"""
data = request.get_json()
if not data:
return error_response(
message="Request body is required",
status=400,
code="MISSING_BODY",
)
character_id = data.get('character_id')
quest_id = data.get('quest_id')
npc_id = data.get('npc_id')
if not character_id or not quest_id:
return error_response(
message="character_id and quest_id are required",
status=400,
code="MISSING_FIELDS",
)
# Get character
character_service = get_character_service()
character = character_service.get_character(character_id)
if not character:
return error_response(
message=f"Character not found: {character_id}",
status=404,
code="CHARACTER_NOT_FOUND",
)
# Verify character belongs to user
if character.user_id != user_id:
return error_response(
message="Character does not belong to this user",
status=403,
code="ACCESS_DENIED",
)
# Set declined flag on NPC interaction
if npc_id:
if npc_id not in character.npc_interactions:
character.npc_interactions[npc_id] = {
'npc_id': npc_id,
'relationship_level': 50,
'custom_flags': {},
}
npc_interaction = character.npc_interactions[npc_id]
if 'custom_flags' not in npc_interaction:
npc_interaction['custom_flags'] = {}
# Set declined flag - this will be checked by quest eligibility
npc_interaction['custom_flags'][f'declined_{quest_id}'] = True
# Save character
character_service.update_character(character)
logger.info(
"Quest declined",
character_id=character_id,
quest_id=quest_id,
npc_id=npc_id,
)
return api_response(
data={
"quest_id": quest_id,
"declined": True,
},
message="Quest declined",
)
@quests_bp.route('/characters/<character_id>/quests', methods=['GET'])
@require_auth
def get_character_quests(user_id: str, character_id: str):
"""
Get a character's active and completed quests.
Args:
character_id: Character identifier
Returns:
Lists of active and completed quests with details
"""
# Get character
character_service = get_character_service()
character = character_service.get_character(character_id)
if not character:
return error_response(
message=f"Character not found: {character_id}",
status=404,
code="CHARACTER_NOT_FOUND",
)
# Verify character belongs to user
if character.user_id != user_id:
return error_response(
message="Character does not belong to this user",
status=403,
code="ACCESS_DENIED",
)
# Get quest details for active quests
quest_service = get_quest_service()
active_quests = []
for quest_id in character.active_quests:
quest = quest_service.load_quest(quest_id)
if quest:
quest_data = quest.to_offer_dict()
# Add progress if available
quest_states = getattr(character, 'quest_states', {})
if quest_id in quest_states:
quest_data['progress'] = quest_states[quest_id]
active_quests.append(quest_data)
# Get completed quest IDs
completed_quests = getattr(character, 'completed_quests', [])
return api_response(
data={
"active_quests": active_quests,
"completed_quest_ids": completed_quests,
"active_count": len(active_quests),
"completed_count": len(completed_quests),
},
message="Character quests retrieved",
)
@quests_bp.route('/complete', methods=['POST'])
@require_auth
def complete_quest(user_id: str):
"""
Complete a quest and grant rewards.
This endpoint is typically called by the game system when all
objectives are completed, not directly by the player.
Expected JSON body:
{
"character_id": "char_123",
"quest_id": "quest_cellar_rats",
"npc_id": "npc_grom_ironbeard" # Optional: for reward dialogue
}
Returns:
Rewards granted and updated character state
"""
data = request.get_json()
if not data:
return error_response(
message="Request body is required",
status=400,
code="MISSING_BODY",
)
character_id = data.get('character_id')
quest_id = data.get('quest_id')
npc_id = data.get('npc_id')
if not character_id or not quest_id:
return error_response(
message="character_id and quest_id are required",
status=400,
code="MISSING_FIELDS",
)
# Get character
character_service = get_character_service()
character = character_service.get_character(character_id)
if not character:
return error_response(
message=f"Character not found: {character_id}",
status=404,
code="CHARACTER_NOT_FOUND",
)
# Verify character belongs to user
if character.user_id != user_id:
return error_response(
message="Character does not belong to this user",
status=403,
code="ACCESS_DENIED",
)
# Check quest is active
if quest_id not in character.active_quests:
return error_response(
message="Quest is not active for this character",
status=400,
code="QUEST_NOT_ACTIVE",
)
# Get quest for rewards
quest_service = get_quest_service()
quest = quest_service.load_quest(quest_id)
if not quest:
return error_response(
message=f"Quest not found: {quest_id}",
status=404,
code="QUEST_NOT_FOUND",
)
# Remove from active quests
character.active_quests.remove(quest_id)
# Add to completed quests
if not hasattr(character, 'completed_quests'):
character.completed_quests = []
character.completed_quests.append(quest_id)
# Grant rewards
rewards = quest.rewards
character.gold += rewards.gold
leveled_up = character.add_experience(rewards.experience)
# Apply relationship bonuses
for bonus_npc_id, bonus_amount in rewards.relationship_bonuses.items():
if bonus_npc_id in character.npc_interactions:
npc_interaction = character.npc_interactions[bonus_npc_id]
current_relationship = npc_interaction.get('relationship_level', 50)
npc_interaction['relationship_level'] = min(100, current_relationship + bonus_amount)
# Reveal locations
for location_id in rewards.reveals_locations:
if location_id not in character.discovered_locations:
character.discovered_locations.append(location_id)
# Update quest state
quest_states = getattr(character, 'quest_states', {})
if quest_id in quest_states:
quest_states[quest_id]['status'] = QuestStatus.COMPLETED.value
quest_states[quest_id]['completed_at'] = datetime.now(timezone.utc).isoformat()
# Save character
character_service.update_character(character)
# Get completion dialogue if NPC provided
completion_dialogue = ""
if npc_id:
completion_dialogue = quest.get_completion_dialogue(npc_id)
logger.info(
"Quest completed",
character_id=character_id,
quest_id=quest_id,
gold_granted=rewards.gold,
xp_granted=rewards.experience,
leveled_up=leveled_up,
)
return api_response(
data={
"quest_id": quest_id,
"quest_name": quest.name,
"rewards": {
"gold": rewards.gold,
"experience": rewards.experience,
"items": rewards.items,
"relationship_bonuses": rewards.relationship_bonuses,
"reveals_locations": rewards.reveals_locations,
},
"leveled_up": leveled_up,
"new_level": character.level if leveled_up else None,
"completion_dialogue": completion_dialogue,
},
message=f"Quest completed: {quest.name}",
)
@quests_bp.route('/progress', methods=['POST'])
@require_auth
def update_quest_progress(user_id: str):
"""
Update progress on a quest objective.
This endpoint is called when a player completes actions that contribute
to quest objectives (kills enemies, collects items, etc.).
Expected JSON body:
{
"character_id": "char_123",
"quest_id": "quest_cellar_rats",
"objective_id": "kill_rats",
"amount": 1 # Optional, defaults to 1
}
Returns:
Updated progress state with completion flags
"""
data = request.get_json()
if not data:
return error_response(
message="Request body is required",
status=400,
code="MISSING_BODY",
)
character_id = data.get('character_id')
quest_id = data.get('quest_id')
objective_id = data.get('objective_id')
amount = data.get('amount', 1)
if not character_id or not quest_id or not objective_id:
return error_response(
message="character_id, quest_id, and objective_id are required",
status=400,
code="MISSING_FIELDS",
)
# Get character
character_service = get_character_service()
character = character_service.get_character(character_id)
if not character:
return error_response(
message=f"Character not found: {character_id}",
status=404,
code="CHARACTER_NOT_FOUND",
)
# Verify character belongs to user
if character.user_id != user_id:
return error_response(
message="Character does not belong to this user",
status=403,
code="ACCESS_DENIED",
)
# Check quest is active
if quest_id not in character.active_quests:
return error_response(
message="Quest is not active for this character",
status=400,
code="QUEST_NOT_ACTIVE",
)
# Get quest definition to validate objective and get required amount
quest_service = get_quest_service()
quest = quest_service.load_quest(quest_id)
if not quest:
return error_response(
message=f"Quest not found: {quest_id}",
status=404,
code="QUEST_NOT_FOUND",
)
# Find the objective
objective = None
for obj in quest.objectives:
if obj.objective_id == objective_id:
objective = obj
break
if not objective:
return error_response(
message=f"Objective not found: {objective_id}",
status=404,
code="OBJECTIVE_NOT_FOUND",
)
# Get or create quest state
quest_states = getattr(character, 'quest_states', {})
if quest_id not in quest_states:
# Initialize quest state if missing
quest_states[quest_id] = {
'quest_id': quest_id,
'status': QuestStatus.ACTIVE.value,
'accepted_at': datetime.now(timezone.utc).isoformat(),
'objectives_progress': {obj.objective_id: 0 for obj in quest.objectives},
'completed_at': None,
}
character.quest_states = quest_states
quest_state = quest_states[quest_id]
objectives_progress = quest_state.get('objectives_progress', {})
# Update progress
current_progress = objectives_progress.get(objective_id, 0)
new_progress = min(current_progress + amount, objective.required_progress)
objectives_progress[objective_id] = new_progress
quest_state['objectives_progress'] = objectives_progress
# Check if this objective is complete
objective_complete = new_progress >= objective.required_progress
# Check if entire quest is complete (all objectives met)
quest_complete = True
for obj in quest.objectives:
obj_progress = objectives_progress.get(obj.objective_id, 0)
if obj_progress < obj.required_progress:
quest_complete = False
break
# Save character
character_service.update_character(character)
logger.info(
"Quest progress updated",
character_id=character_id,
quest_id=quest_id,
objective_id=objective_id,
new_progress=new_progress,
required=objective.required_progress,
objective_complete=objective_complete,
quest_complete=quest_complete,
)
return api_response(
data={
"quest_id": quest_id,
"objective_id": objective_id,
"new_progress": new_progress,
"required": objective.required_progress,
"objective_complete": objective_complete,
"quest_complete": quest_complete,
"all_progress": objectives_progress,
},
message=f"Progress updated: {new_progress}/{objective.required_progress}",
)
@quests_bp.route('/abandon', methods=['POST'])
@require_auth
def abandon_quest(user_id: str):
"""
Abandon an active quest.
Expected JSON body:
{
"character_id": "char_123",
"quest_id": "quest_cellar_rats"
}
Returns:
Confirmation of abandonment
"""
data = request.get_json()
if not data:
return error_response(
message="Request body is required",
status=400,
code="MISSING_BODY",
)
character_id = data.get('character_id')
quest_id = data.get('quest_id')
if not character_id or not quest_id:
return error_response(
message="character_id and quest_id are required",
status=400,
code="MISSING_FIELDS",
)
# Get character
character_service = get_character_service()
character = character_service.get_character(character_id)
if not character:
return error_response(
message=f"Character not found: {character_id}",
status=404,
code="CHARACTER_NOT_FOUND",
)
# Verify character belongs to user
if character.user_id != user_id:
return error_response(
message="Character does not belong to this user",
status=403,
code="ACCESS_DENIED",
)
# Check quest is active
if quest_id not in character.active_quests:
return error_response(
message="Quest is not active for this character",
status=400,
code="QUEST_NOT_ACTIVE",
)
# Remove from active quests
character.active_quests.remove(quest_id)
# Update quest state
quest_states = getattr(character, 'quest_states', {})
if quest_id in quest_states:
quest_states[quest_id]['status'] = QuestStatus.FAILED.value
# Save character
character_service.update_character(character)
logger.info(
"Quest abandoned",
character_id=character_id,
quest_id=quest_id,
)
return api_response(
data={
"quest_id": quest_id,
"abandoned": True,
"active_quests": character.active_quests,
},
message="Quest abandoned",
)

474
api/app/api/shop.py Normal file
View File

@@ -0,0 +1,474 @@
"""
Shop API Blueprint
Endpoints for browsing NPC shop inventory and making purchases/sales.
All endpoints require authentication.
Endpoints:
- GET /api/v1/shop/<shop_id>/inventory - Get shop inventory with character context
- POST /api/v1/shop/<shop_id>/purchase - Purchase an item
- POST /api/v1/shop/<shop_id>/sell - Sell an item back to the shop
"""
from flask import Blueprint, request
from app.services.shop_service import (
get_shop_service,
ShopNotFoundError,
ItemNotInShopError,
InsufficientGoldError,
ItemNotOwnedError,
)
from app.services.character_service import (
get_character_service,
CharacterNotFound,
)
from app.utils.response import (
success_response,
error_response,
not_found_response,
validation_error_response,
)
from app.utils.auth import require_auth, get_current_user
from app.utils.logging import get_logger
logger = get_logger(__file__)
shop_bp = Blueprint('shop', __name__)
# =============================================================================
# API Endpoints
# =============================================================================
@shop_bp.route('/api/v1/shop/<shop_id>/inventory', methods=['GET'])
@require_auth
def get_shop_inventory(shop_id: str):
"""
Get shop inventory with character context.
Query Parameters:
character_id: Character ID to use for gold/affordability checks
Args:
shop_id: Shop identifier
Returns:
200: Shop inventory with enriched item data
401: Not authenticated
404: Shop or character not found
422: Validation error (missing character_id)
500: Internal server error
Example Response:
{
"result": {
"shop": {
"shop_id": "general_store",
"shop_name": "General Store",
"shopkeeper_name": "Merchant Guildmaster",
"sell_rate": 0.5
},
"character": {
"character_id": "char_abc",
"gold": 500
},
"inventory": [
{
"item": {...},
"shop_price": 25,
"stock": -1,
"can_afford": true,
"item_id": "health_potion_small"
}
],
"categories": ["consumable", "weapon", "armor"]
}
}
"""
try:
user = get_current_user()
# Get character_id from query params
character_id = request.args.get('character_id', '').strip()
if not character_id:
return validation_error_response(
message="Validation failed",
details={"character_id": "character_id query parameter is required"}
)
logger.info(
"Getting shop inventory",
user_id=user.id,
shop_id=shop_id,
character_id=character_id
)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Get enriched shop inventory
shop_service = get_shop_service()
result = shop_service.get_shop_inventory_for_character(shop_id, character)
logger.info(
"Shop inventory retrieved",
user_id=user.id,
shop_id=shop_id,
item_count=len(result["inventory"])
)
return success_response(result=result)
except CharacterNotFound as e:
logger.warning(
"Character not found for shop",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id if 'character_id' in locals() else 'unknown',
error=str(e)
)
return not_found_response(message=str(e))
except ShopNotFoundError as e:
logger.warning(
"Shop not found",
shop_id=shop_id,
error=str(e)
)
return not_found_response(message=str(e))
except Exception as e:
logger.error(
"Failed to get shop inventory",
user_id=user.id if 'user' in locals() else 'unknown',
shop_id=shop_id,
error=str(e)
)
return error_response(
code="SHOP_INVENTORY_ERROR",
message="Failed to retrieve shop inventory",
status=500
)
@shop_bp.route('/api/v1/shop/<shop_id>/purchase', methods=['POST'])
@require_auth
def purchase_item(shop_id: str):
"""
Purchase an item from the shop.
Args:
shop_id: Shop identifier
Request Body:
{
"character_id": "char_abc",
"item_id": "health_potion_small",
"quantity": 1,
"session_id": "optional_session_id"
}
Returns:
200: Purchase successful
400: Insufficient gold or invalid quantity
401: Not authenticated
404: Shop, character, or item not found
422: Validation error
500: Internal server error
Example Response:
{
"result": {
"purchase": {
"item_id": "health_potion_small",
"quantity": 2,
"total_cost": 50
},
"character": {
"character_id": "char_abc",
"gold": 450
},
"items_added": ["health_potion_small_abc123", "health_potion_small_def456"]
}
}
"""
try:
user = get_current_user()
# Get request data
data = request.get_json()
if not data:
return validation_error_response(
message="Request body is required",
details={"error": "Request body is required"}
)
character_id = data.get('character_id', '').strip()
item_id = data.get('item_id', '').strip()
quantity = data.get('quantity', 1)
session_id = data.get('session_id', '').strip() or None
# Validate required fields
validation_errors = {}
if not character_id:
validation_errors['character_id'] = "character_id is required"
if not item_id:
validation_errors['item_id'] = "item_id is required"
if not isinstance(quantity, int) or quantity < 1:
validation_errors['quantity'] = "quantity must be a positive integer"
if validation_errors:
return validation_error_response(
message="Validation failed",
details=validation_errors
)
logger.info(
"Processing purchase",
user_id=user.id,
shop_id=shop_id,
character_id=character_id,
item_id=item_id,
quantity=quantity
)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Process purchase
shop_service = get_shop_service()
result = shop_service.purchase_item(
character=character,
shop_id=shop_id,
item_id=item_id,
quantity=quantity,
session_id=session_id
)
# Save updated character
char_service.update_character(character)
logger.info(
"Purchase completed",
user_id=user.id,
shop_id=shop_id,
character_id=character_id,
item_id=item_id,
quantity=quantity,
total_cost=result["purchase"]["total_cost"]
)
return success_response(result=result)
except CharacterNotFound as e:
logger.warning(
"Character not found for purchase",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id if 'character_id' in locals() else 'unknown',
error=str(e)
)
return not_found_response(message=str(e))
except ShopNotFoundError as e:
logger.warning(
"Shop not found for purchase",
shop_id=shop_id,
error=str(e)
)
return not_found_response(message=str(e))
except ItemNotInShopError as e:
logger.warning(
"Item not in shop",
shop_id=shop_id,
item_id=item_id if 'item_id' in locals() else 'unknown',
error=str(e)
)
return not_found_response(message=str(e))
except InsufficientGoldError as e:
logger.warning(
"Insufficient gold for purchase",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id if 'character_id' in locals() else 'unknown',
error=str(e)
)
return error_response(
code="INSUFFICIENT_GOLD",
message=str(e),
status=400
)
except Exception as e:
logger.error(
"Failed to process purchase",
user_id=user.id if 'user' in locals() else 'unknown',
shop_id=shop_id,
error=str(e)
)
return error_response(
code="PURCHASE_ERROR",
message="Failed to process purchase",
status=500
)
@shop_bp.route('/api/v1/shop/<shop_id>/sell', methods=['POST'])
@require_auth
def sell_item(shop_id: str):
"""
Sell an item back to the shop.
Args:
shop_id: Shop identifier
Request Body:
{
"character_id": "char_abc",
"item_instance_id": "health_potion_small_abc123",
"quantity": 1,
"session_id": "optional_session_id"
}
Returns:
200: Sale successful
401: Not authenticated
404: Shop, character, or item not found
422: Validation error
500: Internal server error
Example Response:
{
"result": {
"sale": {
"item_id": "health_potion_small_abc123",
"item_name": "Small Health Potion",
"quantity": 1,
"total_earned": 12
},
"character": {
"character_id": "char_abc",
"gold": 512
}
}
}
"""
try:
user = get_current_user()
# Get request data
data = request.get_json()
if not data:
return validation_error_response(
message="Request body is required",
details={"error": "Request body is required"}
)
character_id = data.get('character_id', '').strip()
item_instance_id = data.get('item_instance_id', '').strip()
quantity = data.get('quantity', 1)
session_id = data.get('session_id', '').strip() or None
# Validate required fields
validation_errors = {}
if not character_id:
validation_errors['character_id'] = "character_id is required"
if not item_instance_id:
validation_errors['item_instance_id'] = "item_instance_id is required"
if not isinstance(quantity, int) or quantity < 1:
validation_errors['quantity'] = "quantity must be a positive integer"
if validation_errors:
return validation_error_response(
message="Validation failed",
details=validation_errors
)
logger.info(
"Processing sale",
user_id=user.id,
shop_id=shop_id,
character_id=character_id,
item_instance_id=item_instance_id,
quantity=quantity
)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Process sale
shop_service = get_shop_service()
result = shop_service.sell_item(
character=character,
shop_id=shop_id,
item_instance_id=item_instance_id,
quantity=quantity,
session_id=session_id
)
# Save updated character
char_service.update_character(character)
logger.info(
"Sale completed",
user_id=user.id,
shop_id=shop_id,
character_id=character_id,
item_instance_id=item_instance_id,
total_earned=result["sale"]["total_earned"]
)
return success_response(result=result)
except CharacterNotFound as e:
logger.warning(
"Character not found for sale",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id if 'character_id' in locals() else 'unknown',
error=str(e)
)
return not_found_response(message=str(e))
except ShopNotFoundError as e:
logger.warning(
"Shop not found for sale",
shop_id=shop_id,
error=str(e)
)
return not_found_response(message=str(e))
except ItemNotOwnedError as e:
logger.warning(
"Item not owned for sale",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id if 'character_id' in locals() else 'unknown',
item_instance_id=item_instance_id if 'item_instance_id' in locals() else 'unknown',
error=str(e)
)
return not_found_response(message=str(e))
except Exception as e:
logger.error(
"Failed to process sale",
user_id=user.id if 'user' in locals() else 'unknown',
shop_id=shop_id,
error=str(e)
)
return error_response(
code="SALE_ERROR",
message="Failed to process sale",
status=500
)

View File

@@ -0,0 +1,34 @@
# Absolute Zero - Arcanist Cryomancy ultimate
# Ultimate freeze all enemies
ability_id: "absolute_zero"
name: "Absolute Zero"
description: "Lower the temperature to absolute zero, freezing all enemies solid and dealing massive ice damage"
ability_type: "spell"
base_power: 90
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 70
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "absolute_freeze"
name: "Absolute Zero"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "absolute_zero"
- effect_id: "shattered"
name: "Shattered"
effect_type: "dot"
duration: 2
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "absolute_zero"

View File

@@ -0,0 +1,16 @@
# Aimed Shot - Wildstrider Marksmanship ability
# High accuracy ranged attack
ability_id: "aimed_shot"
name: "Aimed Shot"
description: "Take careful aim and fire a precise shot at your target"
ability_type: "attack"
base_power: 18
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.6
mana_cost: 8
cooldown: 1
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Arcane Brilliance - Lorekeeper Arcane Weaving ability
# Intelligence buff
ability_id: "arcane_brilliance"
name: "Arcane Brilliance"
description: "Grant an ally increased intelligence and magical power"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.4
mana_cost: 10
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "arcane_brilliance_buff"
name: "Arcane Brilliance"
effect_type: "buff"
duration: 5
power: 10
stat_affected: "intelligence"
stacks: 1
max_stacks: 1
source: "arcane_brilliance"

View File

@@ -0,0 +1,25 @@
# Arcane Weakness - Lorekeeper Arcane Weaving ability
# Stat debuff on enemy
ability_id: "arcane_weakness"
name: "Arcane Weakness"
description: "Expose the weaknesses in your enemy's defenses, reducing their resistances"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 25
cooldown: 3
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "weakened_defenses"
name: "Weakened"
effect_type: "debuff"
duration: 4
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "arcane_weakness"

View File

@@ -0,0 +1,25 @@
# Army of the Dead - Necromancer Raise Dead ultimate
# Summon undead army
ability_id: "army_of_the_dead"
name: "Army of the Dead"
description: "Raise an entire army of undead to overwhelm your enemies"
ability_type: "spell"
base_power: 80
damage_type: "shadow"
scaling_stat: "charisma"
scaling_factor: 0.7
mana_cost: 70
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "undead_army"
name: "Army of the Dead"
effect_type: "buff"
duration: 5
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "army_of_the_dead"

View File

@@ -0,0 +1,25 @@
# Bestial Wrath - Wildstrider Beast Companion ability
# Pet damage buff
ability_id: "bestial_wrath"
name: "Bestial Wrath"
description: "Enrage your companion, increasing their damage for 3 turns"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.4
mana_cost: 25
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "enraged_companion"
name: "Enraged Companion"
effect_type: "buff"
duration: 3
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "bestial_wrath"

View File

@@ -0,0 +1,16 @@
# Blessed Sacrifice - Oathkeeper Redemption ability
# Transfer ally wounds to self
ability_id: "blessed_sacrifice"
name: "Blessed Sacrifice"
description: "Take an ally's wounds upon yourself, healing them while damaging yourself"
ability_type: "spell"
base_power: 50
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 25
cooldown: 4
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Blizzard - Arcanist Cryomancy ability
# AoE ice damage with slow
ability_id: "blizzard"
name: "Blizzard"
description: "Summon a devastating blizzard that damages and slows all enemies"
ability_type: "spell"
base_power: 40
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 32
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "frostbitten"
name: "Frostbitten"
effect_type: "debuff"
duration: 3
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "blizzard"

View File

@@ -0,0 +1,16 @@
# Cleanse - Oathkeeper Redemption ability
# Remove all debuffs
ability_id: "cleanse"
name: "Cleanse"
description: "Purify an ally, removing all negative effects"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.3
mana_cost: 18
cooldown: 3
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Cleave - Vanguard Weapon Master ability
# AoE attack hitting all enemies
ability_id: "cleave"
name: "Cleave"
description: "Swing your weapon in a wide arc, hitting all enemies"
ability_type: "attack"
base_power: 20
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.5
mana_cost: 15
cooldown: 2
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Confuse - Lorekeeper Illusionist ability
# Random target attacks
ability_id: "confuse"
name: "Confuse"
description: "Confuse your enemy, causing them to attack random targets"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 12
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "confused"
name: "Confused"
effect_type: "debuff"
duration: 2
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "confuse"

View File

@@ -0,0 +1,25 @@
# Consecrated Ground - Oathkeeper Aegis of Light ability
# Ground buff with damage reduction zone
ability_id: "consecrated_ground"
name: "Consecrated Ground"
description: "Consecrate the ground, creating a zone that reduces damage taken by all allies standing within"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.4
mana_cost: 30
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "consecrated_protection"
name: "Consecrated"
effect_type: "buff"
duration: 3
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "consecrated_ground"

View File

@@ -0,0 +1,25 @@
# Consecration - Luminary Radiant Judgment ability
# Ground AoE holy damage
ability_id: "consecration"
name: "Consecration"
description: "Consecrate the ground beneath your feet, dealing holy damage to all nearby enemies"
ability_type: "spell"
base_power: 40
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 28
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "consecrated_ground"
name: "Consecrated"
effect_type: "dot"
duration: 3
power: 10
stat_affected: null
stacks: 1
max_stacks: 1
source: "consecration"

View File

@@ -0,0 +1,16 @@
# Coordinated Attack - Wildstrider Beast Companion ability
# Attack with pet
ability_id: "coordinated_attack"
name: "Coordinated Attack"
description: "Attack in perfect coordination with your companion for bonus damage"
ability_type: "skill"
base_power: 30
damage_type: "physical"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Corpse Explosion - Necromancer Raise Dead ability
# Detonate corpse/minion AoE
ability_id: "corpse_explosion"
name: "Corpse Explosion"
description: "Detonate a corpse or minion, dealing AoE shadow damage to all nearby enemies"
ability_type: "spell"
base_power: 45
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 28
cooldown: 3
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Coup de Grace - Assassin Blade Specialist ability
# Execute low HP targets
ability_id: "coup_de_grace"
name: "Coup de Grace"
description: "Deliver the killing blow. Instantly kills targets below 25% HP, otherwise deals massive damage"
ability_type: "attack"
base_power: 70
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.6
mana_cost: 40
cooldown: 4
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Curse of Agony - Necromancer Dark Affliction ability
# Heavy shadow DoT
ability_id: "curse_of_agony"
name: "Curse of Agony"
description: "Curse your target with unbearable agony, dealing increasing shadow damage over 5 turns"
ability_type: "spell"
base_power: 10
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 28
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "agony"
name: "Curse of Agony"
effect_type: "dot"
duration: 5
power: 12
stat_affected: null
stacks: 1
max_stacks: 1
source: "curse_of_agony"

View File

@@ -0,0 +1,25 @@
# Death Mark - Assassin Shadow Dancer ability
# Mark target for bonus damage
ability_id: "death_mark"
name: "Death Mark"
description: "Mark your target for death. Your next attack deals 200% damage"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "dexterity"
scaling_factor: 0.0
mana_cost: 30
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "marked_for_death"
name: "Marked for Death"
effect_type: "debuff"
duration: 2
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "death_mark"

View File

@@ -0,0 +1,25 @@
# Death Pact - Necromancer Raise Dead ability
# Sacrifice minion for HP/mana
ability_id: "death_pact"
name: "Death Pact"
description: "Sacrifice one of your minions to restore your health and mana"
ability_type: "spell"
base_power: 50
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 0
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "death_pact_heal"
name: "Death Pact"
effect_type: "hot"
duration: 1
power: 40
stat_affected: null
stacks: 1
max_stacks: 1
source: "death_pact"

View File

@@ -0,0 +1,25 @@
# Divine Aegis - Oathkeeper Aegis of Light ability
# Massive party shield
ability_id: "divine_aegis"
name: "Divine Aegis"
description: "Invoke divine protection to create a powerful shield around all allies"
ability_type: "spell"
base_power: 60
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.6
mana_cost: 45
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "divine_aegis_shield"
name: "Divine Aegis"
effect_type: "shield"
duration: 3
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_aegis"

View File

@@ -0,0 +1,34 @@
# Divine Blessing - Oathkeeper Redemption ability
# Stat buff + HoT
ability_id: "divine_blessing"
name: "Divine Blessing"
description: "Bless an ally with divine power, increasing their stats and healing over time"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 35
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "blessed"
name: "Divine Blessing"
effect_type: "buff"
duration: 4
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_blessing"
- effect_id: "blessed_healing"
name: "Blessed Healing"
effect_type: "hot"
duration: 4
power: 10
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_blessing"

View File

@@ -0,0 +1,16 @@
# Divine Intervention - Luminary Divine Protection ability
# Full heal + cleanse
ability_id: "divine_intervention"
name: "Divine Intervention"
description: "Call upon divine power to fully heal and cleanse an ally of all negative effects"
ability_type: "spell"
base_power: 80
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.6
mana_cost: 45
cooldown: 5
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Divine Storm - Luminary Radiant Judgment ultimate
# Ultimate AoE holy + stun all
ability_id: "divine_storm"
name: "Divine Storm"
description: "Unleash the full fury of the divine, dealing massive holy damage to all enemies and stunning them"
ability_type: "spell"
base_power: 95
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.7
mana_cost: 60
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "divine_judgment"
name: "Divine Judgment"
effect_type: "stun"
duration: 1
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_storm"

View File

@@ -0,0 +1,25 @@
# Drain Life - Necromancer Dark Affliction ability
# Shadow damage + self-heal
ability_id: "drain_life"
name: "Drain Life"
description: "Drain the life force from your enemy, dealing shadow damage and healing yourself"
ability_type: "spell"
base_power: 18
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 12
cooldown: 1
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "life_drain"
name: "Life Drained"
effect_type: "hot"
duration: 1
power: 9
stat_affected: null
stacks: 1
max_stacks: 1
source: "drain_life"

View File

@@ -0,0 +1,34 @@
# Epidemic - Necromancer Dark Affliction ultimate
# Ultimate multi-DoT all enemies
ability_id: "epidemic"
name: "Epidemic"
description: "Unleash a devastating epidemic that afflicts all enemies with multiple diseases"
ability_type: "spell"
base_power: 60
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 60
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "epidemic_plague"
name: "Epidemic"
effect_type: "dot"
duration: 5
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "epidemic"
- effect_id: "weakened"
name: "Weakened"
effect_type: "debuff"
duration: 5
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "epidemic"

View File

@@ -0,0 +1,16 @@
# Execute - Vanguard Weapon Master ability
# Bonus damage to low HP targets
ability_id: "execute"
name: "Execute"
description: "Finish off weakened enemies. Deals bonus damage to targets below 30% HP"
ability_type: "attack"
base_power: 60
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.6
mana_cost: 40
cooldown: 3
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Explosive Shot - Wildstrider Marksmanship ability
# Impact AoE damage
ability_id: "explosive_shot"
name: "Explosive Shot"
description: "Fire an explosive arrow that detonates on impact, dealing AoE damage"
ability_type: "attack"
base_power: 55
damage_type: "fire"
scaling_stat: "dexterity"
scaling_factor: 0.55
mana_cost: 38
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "burning_shrapnel"
name: "Burning Shrapnel"
effect_type: "dot"
duration: 2
power: 8
stat_affected: null
stacks: 1
max_stacks: 1
source: "explosive_shot"

View File

@@ -0,0 +1,25 @@
# Firestorm - Arcanist Pyromancy ability
# Massive AoE fire damage
ability_id: "firestorm"
name: "Firestorm"
description: "Call down a storm of fire from the heavens, devastating all enemies"
ability_type: "spell"
base_power: 55
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.6
mana_cost: 45
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "scorched"
name: "Scorched"
effect_type: "dot"
duration: 2
power: 12
stat_affected: null
stacks: 1
max_stacks: 3
source: "firestorm"

View File

@@ -0,0 +1,16 @@
# Flame Burst - Arcanist Pyromancy ability
# AoE fire burst centered on caster
ability_id: "flame_burst"
name: "Flame Burst"
description: "Release a burst of flames around you, scorching all nearby enemies"
ability_type: "spell"
base_power: 25
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Frozen Orb - Arcanist Cryomancy ability
# AoE freeze with damage
ability_id: "frozen_orb"
name: "Frozen Orb"
description: "Launch a swirling orb of frost that freezes enemies in its path"
ability_type: "spell"
base_power: 28
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 20
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "frozen"
name: "Frozen"
effect_type: "stun"
duration: 1
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "frozen_orb"

View File

@@ -0,0 +1,25 @@
# Glacial Spike - Arcanist Cryomancy ability
# Heavy single target with freeze
ability_id: "glacial_spike"
name: "Glacial Spike"
description: "Impale your target with a massive spike of ice, dealing heavy damage and freezing them"
ability_type: "spell"
base_power: 60
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.6
mana_cost: 40
cooldown: 3
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "deep_freeze"
name: "Deep Freeze"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "glacial_spike"

View File

@@ -0,0 +1,25 @@
# Guardian Angel - Luminary Divine Protection ability
# Death prevention buff
ability_id: "guardian_angel"
name: "Guardian Angel"
description: "Bless an ally with divine protection that prevents death once"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.4
mana_cost: 35
cooldown: 6
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "guardian_angel_buff"
name: "Guardian Angel"
effect_type: "buff"
duration: 5
power: 1
stat_affected: null
stacks: 1
max_stacks: 1
source: "guardian_angel"

View File

@@ -0,0 +1,25 @@
# Hammer of Justice - Luminary Radiant Judgment ability
# Holy damage + stun
ability_id: "hammer_of_justice"
name: "Hammer of Justice"
description: "Smash your enemy with a divine hammer, dealing holy damage and stunning them"
ability_type: "spell"
base_power: 55
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.6
mana_cost: 38
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "justice_stun"
name: "Judged"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "hammer_of_justice"

View File

@@ -0,0 +1,25 @@
# Haste - Lorekeeper Arcane Weaving ability
# Grant extra action
ability_id: "haste"
name: "Haste"
description: "Speed up time around an ally, granting them an extra action"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.4
mana_cost: 20
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "hasted"
name: "Hasted"
effect_type: "buff"
duration: 2
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "haste"

View File

@@ -7,7 +7,7 @@ description: "Channel divine energy to restore an ally's health"
ability_type: "spell" ability_type: "spell"
base_power: 25 base_power: 25
damage_type: "holy" damage_type: "holy"
scaling_stat: "intelligence" scaling_stat: "wisdom"
scaling_factor: 0.5 scaling_factor: 0.5
mana_cost: 10 mana_cost: 10
cooldown: 0 cooldown: 0

View File

@@ -0,0 +1,25 @@
# Holy Fire - Luminary Radiant Judgment ability
# Holy DoT with reduced healing
ability_id: "holy_fire"
name: "Holy Fire"
description: "Engulf your enemy in holy flames that burn over time and reduce their healing"
ability_type: "spell"
base_power: 25
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "holy_burning"
name: "Holy Fire"
effect_type: "dot"
duration: 3
power: 8
stat_affected: null
stacks: 1
max_stacks: 1
source: "holy_fire"

View File

@@ -0,0 +1,25 @@
# Holy Shield - Luminary Divine Protection ability
# Grant damage absorb shield
ability_id: "holy_shield"
name: "Holy Shield"
description: "Grant an ally a protective barrier of holy light that absorbs damage"
ability_type: "spell"
base_power: 30
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 15
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "holy_shield_barrier"
name: "Holy Shield"
effect_type: "shield"
duration: 3
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "holy_shield"

View File

@@ -0,0 +1,25 @@
# Ice Shard - Arcanist Cryomancy ability
# Single target ice damage with slow
ability_id: "ice_shard"
name: "Ice Shard"
description: "Hurl a shard of ice at your enemy, dealing frost damage and slowing them"
ability_type: "spell"
base_power: 20
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 10
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "chilled"
name: "Chilled"
effect_type: "debuff"
duration: 2
power: 20
stat_affected: null
stacks: 1
max_stacks: 3
source: "ice_shard"

View File

@@ -0,0 +1,25 @@
# Inferno - Arcanist Pyromancy ability
# AoE fire DoT
ability_id: "inferno"
name: "Inferno"
description: "Summon a raging inferno that burns all enemies for 3 turns"
ability_type: "spell"
base_power: 35
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 30
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "inferno_burn"
name: "Inferno Flames"
effect_type: "dot"
duration: 3
power: 10
stat_affected: null
stacks: 1
max_stacks: 3
source: "inferno"

View File

@@ -0,0 +1,34 @@
# Last Stand - Oathkeeper Aegis of Light ultimate
# Invulnerable + taunt all
ability_id: "last_stand"
name: "Last Stand"
description: "Make your final stand, becoming invulnerable and forcing all enemies to attack you"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.5
mana_cost: 55
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "invulnerable"
name: "Invulnerable"
effect_type: "buff"
duration: 3
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "last_stand"
- effect_id: "ultimate_taunt"
name: "Challenged"
effect_type: "debuff"
duration: 3
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "last_stand"

View File

@@ -0,0 +1,25 @@
# Lay on Hands - Oathkeeper Redemption ability
# Touch heal
ability_id: "lay_on_hands"
name: "Lay on Hands"
description: "Place your hands upon an ally to heal their wounds"
ability_type: "spell"
base_power: 25
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 12
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "gentle_healing"
name: "Soothed"
effect_type: "hot"
duration: 2
power: 5
stat_affected: null
stacks: 1
max_stacks: 1
source: "lay_on_hands"

View File

@@ -0,0 +1,25 @@
# Mass Confusion - Lorekeeper Illusionist ability
# AoE confusion
ability_id: "mass_confusion"
name: "Mass Confusion"
description: "Unleash a wave of illusions that confuses all enemies"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.55
mana_cost: 35
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "mass_confused"
name: "Bewildered"
effect_type: "debuff"
duration: 3
power: 40
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_confusion"

View File

@@ -0,0 +1,25 @@
# Mass Domination - Lorekeeper Illusionist ultimate
# Mind control all enemies
ability_id: "mass_domination"
name: "Mass Domination"
description: "Dominate the minds of all enemies, forcing them to attack each other"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.7
mana_cost: 75
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "dominated"
name: "Dominated"
effect_type: "debuff"
duration: 3
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_domination"

View File

@@ -0,0 +1,25 @@
# Mass Enhancement - Lorekeeper Arcane Weaving ability
# AoE stat buff
ability_id: "mass_enhancement"
name: "Mass Enhancement"
description: "Enhance all allies with arcane power, increasing all their stats"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 32
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "enhanced"
name: "Enhanced"
effect_type: "buff"
duration: 4
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_enhancement"

View File

@@ -0,0 +1,25 @@
# Mass Heal - Luminary Divine Protection ability
# AoE healing
ability_id: "mass_heal"
name: "Mass Heal"
description: "Channel divine energy to heal all allies"
ability_type: "spell"
base_power: 35
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 30
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "mass_regen"
name: "Divine Healing"
effect_type: "hot"
duration: 2
power: 8
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_heal"

View File

@@ -0,0 +1,25 @@
# Mesmerize - Lorekeeper Illusionist ability
# Stun for 2 turns
ability_id: "mesmerize"
name: "Mesmerize"
description: "Mesmerize your target with illusions, stunning them for 2 turns"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 22
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "mesmerized"
name: "Mesmerized"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "mesmerize"

View File

@@ -0,0 +1,25 @@
# Miracle - Oathkeeper Redemption ultimate
# Full party heal + cleanse all
ability_id: "miracle"
name: "Miracle"
description: "Perform a divine miracle that fully heals all allies and cleanses all negative effects"
ability_type: "spell"
base_power: 100
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.7
mana_cost: 70
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "miraculous_healing"
name: "Miraculous"
effect_type: "hot"
duration: 3
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "miracle"

View File

@@ -0,0 +1,25 @@
# Mirror Image - Lorekeeper Illusionist ability
# Summon decoys
ability_id: "mirror_image"
name: "Mirror Image"
description: "Create illusory copies of yourself that absorb enemy attacks"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 28
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "mirror_images"
name: "Mirror Images"
effect_type: "shield"
duration: 4
power: 40
stat_affected: null
stacks: 3
max_stacks: 3
source: "mirror_image"

View File

@@ -0,0 +1,16 @@
# Multishot - Wildstrider Marksmanship ability
# Hit multiple targets
ability_id: "multishot"
name: "Multishot"
description: "Fire multiple arrows in quick succession, hitting up to 3 targets"
ability_type: "attack"
base_power: 22
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: true
target_count: 3
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Phantasmal Killer - Lorekeeper Illusionist ability
# Psychic damage + fear
ability_id: "phantasmal_killer"
name: "Phantasmal Killer"
description: "Conjure a nightmarish illusion that terrifies and damages your target"
ability_type: "spell"
base_power: 55
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.6
mana_cost: 42
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "terrified"
name: "Terrified"
effect_type: "debuff"
duration: 3
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "phantasmal_killer"

View File

@@ -0,0 +1,25 @@
# Piercing Shot - Wildstrider Marksmanship ability
# Line AoE that pierces through enemies
ability_id: "piercing_shot"
name: "Piercing Shot"
description: "Fire a powerful arrow that pierces through all enemies in a line"
ability_type: "attack"
base_power: 40
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.55
mana_cost: 28
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "armor_pierced"
name: "Armor Pierced"
effect_type: "debuff"
duration: 2
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "piercing_shot"

View File

@@ -0,0 +1,25 @@
# Plague - Necromancer Dark Affliction ability
# Spreading poison DoT
ability_id: "plague"
name: "Plague"
description: "Infect your target with a virulent plague that spreads to nearby enemies"
ability_type: "spell"
base_power: 15
damage_type: "poison"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 20
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "plagued"
name: "Plagued"
effect_type: "dot"
duration: 4
power: 8
stat_affected: null
stacks: 1
max_stacks: 3
source: "plague"

View File

@@ -0,0 +1,16 @@
# Power Strike - Vanguard Weapon Master ability
# Heavy attack dealing 150% weapon damage
ability_id: "power_strike"
name: "Power Strike"
description: "A heavy attack that deals 150% weapon damage"
ability_type: "attack"
base_power: 15
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.6
mana_cost: 8
cooldown: 1
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Precise Strike - Assassin Blade Specialist ability
# High crit chance attack
ability_id: "precise_strike"
name: "Precise Strike"
description: "A calculated strike aimed at vital points with increased critical chance"
ability_type: "attack"
base_power: 15
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.5
mana_cost: 8
cooldown: 1
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Primal Fury - Wildstrider Beast Companion ability
# Pet AoE attack
ability_id: "primal_fury"
name: "Primal Fury"
description: "Command your companion to unleash a devastating attack on all enemies"
ability_type: "skill"
base_power: 50
damage_type: "physical"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 35
cooldown: 4
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Rain of Arrows - Wildstrider Marksmanship ultimate
# Ultimate AoE attack
ability_id: "rain_of_arrows"
name: "Rain of Arrows"
description: "Call down a devastating rain of arrows upon all enemies"
ability_type: "attack"
base_power: 85
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.7
mana_cost: 55
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "pinned"
name: "Pinned"
effect_type: "debuff"
duration: 1
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "rain_of_arrows"

View File

@@ -0,0 +1,25 @@
# Raise Ghoul - Necromancer Raise Dead ability
# Summon stronger ghoul
ability_id: "raise_ghoul"
name: "Raise Ghoul"
description: "Raise a powerful ghoul from the dead to serve you"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.55
mana_cost: 22
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "ghoul_minion"
name: "Ghoul"
effect_type: "buff"
duration: 99
power: 35
stat_affected: null
stacks: 1
max_stacks: 1
source: "raise_ghoul"

View File

@@ -0,0 +1,34 @@
# Reality Shift - Lorekeeper Arcane Weaving ultimate
# Massive buff allies + debuff enemies
ability_id: "reality_shift"
name: "Reality Shift"
description: "Alter reality itself, greatly empowering allies while weakening all enemies"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 70
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "reality_empowered"
name: "Reality Empowered"
effect_type: "buff"
duration: 5
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "reality_shift"
- effect_id: "reality_weakened"
name: "Reality Distorted"
effect_type: "debuff"
duration: 5
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "reality_shift"

View File

@@ -0,0 +1,25 @@
# Rending Blow - Vanguard Weapon Master ability
# Attack with bleed DoT
ability_id: "rending_blow"
name: "Rending Blow"
description: "Strike with such force that your enemy bleeds for 3 turns"
ability_type: "attack"
base_power: 35
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.5
mana_cost: 25
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "bleed"
name: "Bleeding"
effect_type: "dot"
duration: 3
power: 8
stat_affected: null
stacks: 1
max_stacks: 3
source: "rending_blow"

View File

@@ -0,0 +1,16 @@
# Resurrection - Luminary Divine Protection ultimate
# Revive fallen ally
ability_id: "resurrection"
name: "Resurrection"
description: "Call upon the divine to bring a fallen ally back to life with 50% HP and mana"
ability_type: "spell"
base_power: 50
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 60
cooldown: 8
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Riposte - Vanguard Shield Bearer ability
# Counter attack after blocking
ability_id: "riposte"
name: "Riposte"
description: "After blocking an attack, counter with a swift strike"
ability_type: "skill"
base_power: 30
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.5
mana_cost: 20
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Shadow Assault - Assassin Shadow Dancer ultimate
# AoE guaranteed crits
ability_id: "shadow_assault"
name: "Shadow Assault"
description: "Become one with the shadows and strike all enemies with guaranteed critical hits"
ability_type: "skill"
base_power: 80
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.7
mana_cost: 55
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "shadow_crit"
name: "Shadow Strike"
effect_type: "buff"
duration: 1
power: 100
stat_affected: "crit_chance"
stacks: 1
max_stacks: 1
source: "shadow_assault"

View File

@@ -0,0 +1,16 @@
# Shadowstep - Assassin Shadow Dancer ability
# Teleport and backstab
ability_id: "shadowstep"
name: "Shadowstep"
description: "Vanish into the shadows and reappear behind your target, striking from behind"
ability_type: "skill"
base_power: 18
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.6
mana_cost: 10
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Shield of Faith - Oathkeeper Aegis of Light ability
# Shield for self and allies
ability_id: "shield_of_faith"
name: "Shield of Faith"
description: "Create a shield of divine faith that protects you and nearby allies"
ability_type: "spell"
base_power: 35
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 20
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "faith_shield"
name: "Shield of Faith"
effect_type: "shield"
duration: 3
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "shield_of_faith"

View File

@@ -0,0 +1,25 @@
# Shield Wall - Vanguard Shield Bearer ability
# Defensive buff reducing damage
ability_id: "shield_wall"
name: "Shield Wall"
description: "Raise your shield to block incoming attacks, reducing damage by 50% for 3 turns"
ability_type: "defend"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.3
mana_cost: 12
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "shield_wall_buff"
name: "Shield Wall"
effect_type: "buff"
duration: 3
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "shield_wall"

View File

@@ -0,0 +1,16 @@
# Smite - Luminary Radiant Judgment ability
# Holy damage attack
ability_id: "smite"
name: "Smite"
description: "Call down holy light to smite your enemies"
ability_type: "spell"
base_power: 20
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 10
cooldown: 0
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Smoke Bomb - Assassin Shadow Dancer ability
# Evasion buff
ability_id: "smoke_bomb"
name: "Smoke Bomb"
description: "Throw a smoke bomb, making yourself untargetable for 1 turn"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "dexterity"
scaling_factor: 0.3
mana_cost: 15
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "smoke_screen"
name: "Smoke Screen"
effect_type: "buff"
duration: 1
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "smoke_bomb"

View File

@@ -0,0 +1,34 @@
# Soul Rot - Necromancer Dark Affliction ability
# DoT + reduced healing on target
ability_id: "soul_rot"
name: "Soul Rot"
description: "Rot your target's soul, dealing shadow damage over time and reducing their healing received"
ability_type: "spell"
base_power: 45
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.6
mana_cost: 38
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "rotting_soul"
name: "Soul Rot"
effect_type: "dot"
duration: 4
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "soul_rot"
- effect_id: "healing_reduction"
name: "Corrupted"
effect_type: "debuff"
duration: 4
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "soul_rot"

View File

@@ -0,0 +1,25 @@
# Stampede - Wildstrider Beast Companion ultimate
# Summon beast horde AoE
ability_id: "stampede"
name: "Stampede"
description: "Call upon the spirits of the wild to summon a stampede of beasts that tramples all enemies"
ability_type: "skill"
base_power: 90
damage_type: "physical"
scaling_stat: "wisdom"
scaling_factor: 0.7
mana_cost: 60
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "trampled"
name: "Trampled"
effect_type: "debuff"
duration: 2
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "stampede"

View File

@@ -0,0 +1,25 @@
# Summon Abomination - Necromancer Raise Dead ability
# Summon powerful abomination
ability_id: "summon_abomination"
name: "Summon Abomination"
description: "Stitch together corpses to create a powerful abomination"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.6
mana_cost: 45
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "abomination_minion"
name: "Abomination"
effect_type: "buff"
duration: 99
power: 60
stat_affected: null
stacks: 1
max_stacks: 1
source: "summon_abomination"

View File

@@ -0,0 +1,25 @@
# Summon Companion - Wildstrider Beast Companion ability
# Summon animal pet
ability_id: "summon_companion"
name: "Summon Companion"
description: "Call your loyal animal companion to fight by your side"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 15
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "companion_active"
name: "Animal Companion"
effect_type: "buff"
duration: 99
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "summon_companion"

View File

@@ -0,0 +1,25 @@
# Summon Skeleton - Necromancer Raise Dead ability
# Summon skeleton warrior
ability_id: "summon_skeleton"
name: "Summon Skeleton"
description: "Raise a skeleton warrior from the dead to fight for you"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 15
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "skeleton_minion"
name: "Skeleton Warrior"
effect_type: "buff"
duration: 99
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "summon_skeleton"

View File

@@ -0,0 +1,25 @@
# Sun Burst - Arcanist Pyromancy ultimate
# Ultimate fire nuke
ability_id: "sun_burst"
name: "Sun Burst"
description: "Channel the power of the sun to unleash a devastating explosion of fire on all enemies"
ability_type: "spell"
base_power: 100
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 65
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "incinerated"
name: "Incinerated"
effect_type: "dot"
duration: 3
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "sun_burst"

View File

@@ -0,0 +1,25 @@
# Taunt - Oathkeeper Aegis of Light ability
# Force enemies to attack you
ability_id: "taunt"
name: "Taunt"
description: "Force all enemies to focus their attacks on you"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.3
mana_cost: 8
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "taunted"
name: "Taunted"
effect_type: "debuff"
duration: 2
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "taunt"

View File

@@ -0,0 +1,25 @@
# Thousand Cuts - Assassin Blade Specialist ultimate
# Multi-hit flurry
ability_id: "thousand_cuts"
name: "Thousand Cuts"
description: "Unleash a flurry of strikes, each with 50% crit chance"
ability_type: "attack"
base_power: 100
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.7
mana_cost: 60
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "bleeding_wounds"
name: "Bleeding Wounds"
effect_type: "dot"
duration: 3
power: 15
stat_affected: null
stacks: 1
max_stacks: 5
source: "thousand_cuts"

View File

@@ -0,0 +1,25 @@
# Time Warp - Lorekeeper Arcane Weaving ability
# AoE extra actions
ability_id: "time_warp"
name: "Time Warp"
description: "Bend time itself, granting all allies increased speed"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 45
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "time_warped"
name: "Time Warped"
effect_type: "buff"
duration: 3
power: 75
stat_affected: null
stacks: 1
max_stacks: 1
source: "time_warp"

View File

@@ -0,0 +1,25 @@
# Titan's Wrath - Vanguard Weapon Master ultimate
# Devastating AoE attack with stun
ability_id: "titans_wrath"
name: "Titan's Wrath"
description: "Unleash a devastating attack that deals 300% weapon damage and stuns all enemies"
ability_type: "attack"
base_power: 100
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.7
mana_cost: 60
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "titans_stun"
name: "Staggered"
effect_type: "stun"
duration: 1
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "titans_wrath"

View File

@@ -0,0 +1,25 @@
# Unbreakable - Vanguard Shield Bearer ultimate
# Massive damage reduction
ability_id: "unbreakable"
name: "Unbreakable"
description: "Channel your inner strength to become nearly invulnerable, reducing all damage by 75% for 5 turns"
ability_type: "defend"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.3
mana_cost: 50
cooldown: 6
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "unbreakable_buff"
name: "Unbreakable"
effect_type: "buff"
duration: 5
power: 75
stat_affected: null
stacks: 1
max_stacks: 1
source: "unbreakable"

View File

@@ -0,0 +1,25 @@
# Vanish - Assassin Shadow Dancer ability
# Stealth for 2 turns
ability_id: "vanish"
name: "Vanish"
description: "Disappear into the shadows, becoming invisible for 2 turns and dropping threat"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "dexterity"
scaling_factor: 0.3
mana_cost: 25
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "stealth"
name: "Stealthed"
effect_type: "buff"
duration: 2
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "vanish"

View File

@@ -0,0 +1,16 @@
# Vital Strike - Assassin Blade Specialist ability
# Massive crit damage
ability_id: "vital_strike"
name: "Vital Strike"
description: "Strike a vital organ for massive critical damage"
ability_type: "attack"
base_power: 30
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.55
mana_cost: 18
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Word of Healing - Oathkeeper Redemption ability
# AoE heal
ability_id: "word_of_healing"
name: "Word of Healing"
description: "Speak a word of power that heals all nearby allies"
ability_type: "spell"
base_power: 40
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 30
cooldown: 3
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,246 @@
# Base Shield Templates for Procedural Generation
#
# These templates define the foundation that affixes attach to.
# Example: "Wooden Shield" + "Reinforced" prefix = "Reinforced Wooden Shield"
#
# Shield categories:
# - Light Shields: Low defense, high mobility (bucklers)
# - Medium Shields: Balanced defense/weight (round shields, kite shields)
# - Heavy Shields: High defense, reduced mobility (tower shields)
# - Magical Shields: Low physical defense, high resistance (arcane aegis)
#
# Template Structure:
# template_id: Unique identifier
# name: Base item name
# item_type: "armor" (shields use armor type)
# slot: "off_hand" (shields occupy off-hand slot)
# description: Flavor text
# base_defense: Physical damage reduction
# base_resistance: Magical damage reduction
# base_value: Gold value
# required_level: Min level to use/drop
# drop_weight: Higher = more common (1.0 = standard)
# min_rarity: Minimum rarity for this template
# block_chance: Chance to fully block an attack (optional)
shields:
# ==================== LIGHT SHIELDS (HIGH MOBILITY) ====================
buckler:
template_id: "buckler"
name: "Buckler"
item_type: "armor"
slot: "off_hand"
description: "A small, round shield strapped to the forearm. Offers minimal protection but doesn't hinder movement."
base_defense: 3
base_resistance: 0
base_value: 20
required_level: 1
drop_weight: 1.5
block_chance: 0.05
parrying_buckler:
template_id: "parrying_buckler"
name: "Parrying Buckler"
item_type: "armor"
slot: "off_hand"
description: "A lightweight buckler with a reinforced edge, designed for deflecting blows."
base_defense: 4
base_resistance: 0
base_value: 45
required_level: 3
drop_weight: 1.0
block_chance: 0.08
# ==================== MEDIUM SHIELDS (BALANCED) ====================
wooden_shield:
template_id: "wooden_shield"
name: "Wooden Shield"
item_type: "armor"
slot: "off_hand"
description: "A basic shield carved from sturdy oak. Reliable protection for new adventurers."
base_defense: 5
base_resistance: 1
base_value: 35
required_level: 1
drop_weight: 1.3
round_shield:
template_id: "round_shield"
name: "Round Shield"
item_type: "armor"
slot: "off_hand"
description: "A circular shield with an iron boss. Popular among infantry and mercenaries."
base_defense: 7
base_resistance: 1
base_value: 50
required_level: 2
drop_weight: 1.2
iron_shield:
template_id: "iron_shield"
name: "Iron Shield"
item_type: "armor"
slot: "off_hand"
description: "A solid iron shield. Heavy but dependable."
base_defense: 9
base_resistance: 1
base_value: 70
required_level: 3
drop_weight: 1.0
kite_shield:
template_id: "kite_shield"
name: "Kite Shield"
item_type: "armor"
slot: "off_hand"
description: "A tall, tapered shield that protects the entire body. Favored by knights."
base_defense: 10
base_resistance: 2
base_value: 80
required_level: 3
drop_weight: 1.0
heater_shield:
template_id: "heater_shield"
name: "Heater Shield"
item_type: "armor"
slot: "off_hand"
description: "A classic triangular shield with excellent balance of protection and mobility."
base_defense: 12
base_resistance: 2
base_value: 100
required_level: 4
drop_weight: 0.9
steel_shield:
template_id: "steel_shield"
name: "Steel Shield"
item_type: "armor"
slot: "off_hand"
description: "A finely crafted steel shield. Durable and well-balanced."
base_defense: 14
base_resistance: 2
base_value: 125
required_level: 5
drop_weight: 0.8
min_rarity: "uncommon"
# ==================== HEAVY SHIELDS (MAXIMUM DEFENSE) ====================
tower_shield:
template_id: "tower_shield"
name: "Tower Shield"
item_type: "armor"
slot: "off_hand"
description: "A massive shield that covers the entire body. Extremely heavy but offers unparalleled protection."
base_defense: 18
base_resistance: 3
base_value: 175
required_level: 6
drop_weight: 0.6
min_rarity: "uncommon"
block_chance: 0.12
fortified_tower_shield:
template_id: "fortified_tower_shield"
name: "Fortified Tower Shield"
item_type: "armor"
slot: "off_hand"
description: "A reinforced tower shield with iron plating. A mobile wall on the battlefield."
base_defense: 22
base_resistance: 4
base_value: 250
required_level: 7
drop_weight: 0.4
min_rarity: "rare"
block_chance: 0.15
# ==================== MAGICAL SHIELDS (HIGH RESISTANCE) ====================
enchanted_buckler:
template_id: "enchanted_buckler"
name: "Enchanted Buckler"
item_type: "armor"
slot: "off_hand"
description: "A small shield inscribed with protective runes. Weak against physical attacks but excellent against magic."
base_defense: 2
base_resistance: 6
base_value: 80
required_level: 3
drop_weight: 0.8
warded_shield:
template_id: "warded_shield"
name: "Warded Shield"
item_type: "armor"
slot: "off_hand"
description: "A shield covered in magical wards that deflect spells."
base_defense: 4
base_resistance: 8
base_value: 110
required_level: 4
drop_weight: 0.7
magical_aegis:
template_id: "magical_aegis"
name: "Magical Aegis"
item_type: "armor"
slot: "off_hand"
description: "An arcane shield that shimmers with protective energy. Prized by battle mages."
base_defense: 8
base_resistance: 10
base_value: 150
required_level: 5
drop_weight: 0.6
min_rarity: "uncommon"
spellguard:
template_id: "spellguard"
name: "Spellguard"
item_type: "armor"
slot: "off_hand"
description: "A crystalline shield forged from condensed magical energy. Near-impervious to spells."
base_defense: 6
base_resistance: 14
base_value: 200
required_level: 6
drop_weight: 0.5
min_rarity: "rare"
# ==================== SPECIALIZED SHIELDS ====================
spiked_shield:
template_id: "spiked_shield"
name: "Spiked Shield"
item_type: "armor"
slot: "off_hand"
description: "A shield with iron spikes. Can be used offensively in close combat."
base_defense: 8
base_resistance: 1
base_value: 90
required_level: 4
drop_weight: 0.8
base_damage: 5
lantern_shield:
template_id: "lantern_shield"
name: "Lantern Shield"
item_type: "armor"
slot: "off_hand"
description: "A peculiar shield with an attached lantern. Useful for dungeon exploration."
base_defense: 6
base_resistance: 1
base_value: 75
required_level: 3
drop_weight: 0.7
provides_light: true
pavise:
template_id: "pavise"
name: "Pavise"
item_type: "armor"
slot: "off_hand"
description: "A large rectangular shield that can be planted in the ground for cover. Favored by crossbowmen."
base_defense: 15
base_resistance: 2
base_value: 130
required_level: 5
drop_weight: 0.6
deployable: true

View File

@@ -21,7 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- worn_staff - worn_staff
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,7 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_dagger - rusty_dagger
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,7 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- tome - tome
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,7 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_mace - rusty_mace
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,7 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- bone_wand - bone_wand
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -22,7 +22,7 @@ starting_equipment:
- rusty_sword - rusty_sword
- rusty_shield - rusty_shield
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -22,7 +22,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_sword - rusty_sword
- cloth_armor - cloth_armor
- rusty_knife # Everyone gets pocket knife - health_potion_small
# Starting abilities # Starting abilities
starting_abilities: starting_abilities:

View File

@@ -21,7 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_bow - rusty_bow
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -0,0 +1,58 @@
# Harpy - Easy flying humanoid
# A vicious bird-woman that attacks from above
enemy_id: harpy
name: Harpy
description: >
A creature with the body of a vulture and the head and torso
of a woman, though twisted by cruelty into something inhuman.
Matted feathers cover her body, and her hands end in razor-sharp
talons. She shrieks with maddening fury as she dives at her prey.
base_stats:
strength: 8
dexterity: 14
constitution: 8
intelligence: 6
wisdom: 10
charisma: 10
luck: 8
abilities:
- basic_attack
- talon_slash
- dive_attack
loot_table:
- item_id: harpy_feather
drop_chance: 0.70
quantity_min: 2
quantity_max: 5
- item_id: harpy_talon
drop_chance: 0.35
quantity_min: 1
quantity_max: 2
- item_id: gold_coin
drop_chance: 0.40
quantity_min: 2
quantity_max: 10
experience_reward: 22
gold_reward_min: 3
gold_reward_max: 12
difficulty: easy
tags:
- monstrosity
- harpy
- flying
- female
location_tags:
- mountain
- cliff
- ruins
base_damage: 6
crit_chance: 0.10
flee_chance: 0.55

View File

@@ -0,0 +1,119 @@
# Harpy Matriarch - Hard elite leader
# The ancient ruler of a harpy flock
enemy_id: harpy_matriarch
name: Harpy Matriarch
description: >
An ancient harpy of terrible beauty and cruelty, her plumage
a striking mix of midnight black and blood red. She towers
over her lesser kin, her voice carrying both enchanting allure
and devastating power. The Matriarch rules her flock absolutely,
and her nest is decorated with the bones and treasures of
countless victims.
base_stats:
strength: 12
dexterity: 16
constitution: 14
intelligence: 12
wisdom: 14
charisma: 20
luck: 12
abilities:
- basic_attack
- talon_slash
- dive_attack
- stunning_screech
- luring_song
- sonic_blast
- call_flock
- wing_buffet
loot_table:
# Static drops - guaranteed materials
- loot_type: static
item_id: harpy_feather
drop_chance: 1.0
quantity_min: 5
quantity_max: 10
- loot_type: static
item_id: harpy_talon
drop_chance: 1.0
quantity_min: 2
quantity_max: 4
- loot_type: static
item_id: matriarch_plume
drop_chance: 0.80
quantity_min: 1
quantity_max: 2
- loot_type: static
item_id: screamer_vocal_cords
drop_chance: 0.60
quantity_min: 1
quantity_max: 1
# Nest treasures
- loot_type: static
item_id: gold_coin
drop_chance: 1.0
quantity_min: 30
quantity_max: 80
- loot_type: static
item_id: gemstone
drop_chance: 0.50
quantity_min: 1
quantity_max: 2
- loot_type: static
item_id: silver_ring
drop_chance: 0.40
quantity_min: 1
quantity_max: 1
# Consumables
- loot_type: static
item_id: health_potion_medium
drop_chance: 0.35
quantity_min: 1
quantity_max: 1
- loot_type: static
item_id: elixir_of_grace
drop_chance: 0.15
quantity_min: 1
quantity_max: 1
# Procedural equipment
- loot_type: procedural
item_type: accessory
drop_chance: 0.25
rarity_bonus: 0.15
quantity_min: 1
quantity_max: 1
- loot_type: procedural
item_type: armor
drop_chance: 0.20
rarity_bonus: 0.10
quantity_min: 1
quantity_max: 1
experience_reward: 90
gold_reward_min: 40
gold_reward_max: 100
difficulty: hard
tags:
- monstrosity
- harpy
- flying
- leader
- elite
- sonic
location_tags:
- mountain
- cliff
- ruins
base_damage: 12
crit_chance: 0.15
flee_chance: 0.20

Some files were not shown because too many files have changed in this diff Show More