Compare commits
12 Commits
45cfa25911
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 70b2b0f124 | |||
| 805d04cf4e | |||
| f9e463bfc6 | |||
| 06ef8f6f0b | |||
| 72cf92021e | |||
| df26abd207 | |||
| e7e329e6ed | |||
| 8bd494a52f | |||
| 32af625d14 | |||
| 8784fbaa88 | |||
| a8767b34e2 | |||
| d9bc46adc1 |
@@ -179,7 +179,21 @@ def register_blueprints(app: Flask) -> None:
|
||||
app.register_blueprint(inventory_bp)
|
||||
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
|
||||
# from app.api import marketplace, shop
|
||||
# from app.api import marketplace
|
||||
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')
|
||||
# app.register_blueprint(shop.bp, url_prefix='/api/v1/shop')
|
||||
|
||||
@@ -447,7 +447,10 @@ class NarrativeGenerator:
|
||||
user_tier: UserTier,
|
||||
npc_relationship: str | 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:
|
||||
"""
|
||||
Generate NPC dialogue in response to player conversation.
|
||||
@@ -461,6 +464,9 @@ class NarrativeGenerator:
|
||||
npc_relationship: Optional description of relationship with NPC.
|
||||
previous_dialogue: Optional list of previous exchanges.
|
||||
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:
|
||||
NarrativeResponse with NPC dialogue.
|
||||
@@ -500,6 +506,9 @@ class NarrativeGenerator:
|
||||
npc_relationship=npc_relationship,
|
||||
previous_dialogue=previous_dialogue 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
|
||||
)
|
||||
except PromptTemplateError as e:
|
||||
|
||||
@@ -4,8 +4,11 @@ Response parser for AI narrative responses.
|
||||
This module handles AI response parsing. Game state changes (items, gold, XP)
|
||||
are now handled exclusively through predetermined dice check outcomes in
|
||||
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 typing import Any, Optional
|
||||
|
||||
@@ -158,3 +161,83 @@ def _parse_game_actions(data: dict[str, Any]) -> GameStateChanges:
|
||||
changes.location_change = data.get("location_change")
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
{% 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 %}
|
||||
## NPC Relationships (for context)
|
||||
{% for rel in npc.relationships %}
|
||||
|
||||
129
api/app/api/abilities.py
Normal file
129
api/app/api/abilities.py
Normal 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())
|
||||
@@ -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.npc_loader import get_npc_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.utils.response import (
|
||||
success_response,
|
||||
@@ -192,6 +193,57 @@ def talk_to_npc(npc_id: str):
|
||||
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
|
||||
npc_knowledge = []
|
||||
if npc.knowledge:
|
||||
@@ -220,6 +272,9 @@ def talk_to_npc(npc_id: str):
|
||||
"interaction_count": interaction["interaction_count"],
|
||||
"relationship_level": interaction.get("relationship_level", 50),
|
||||
"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
|
||||
@@ -428,3 +483,163 @@ def set_npc_flag(npc_id: str):
|
||||
npc_id=npc_id,
|
||||
error=str(e))
|
||||
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
724
api/app/api/quests.py
Normal 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
474
api/app/api/shop.py
Normal 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
|
||||
)
|
||||
34
api/app/data/abilities/absolute_zero.yaml
Normal file
34
api/app/data/abilities/absolute_zero.yaml
Normal 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"
|
||||
16
api/app/data/abilities/aimed_shot.yaml
Normal file
16
api/app/data/abilities/aimed_shot.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/arcane_brilliance.yaml
Normal file
25
api/app/data/abilities/arcane_brilliance.yaml
Normal 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"
|
||||
25
api/app/data/abilities/arcane_weakness.yaml
Normal file
25
api/app/data/abilities/arcane_weakness.yaml
Normal 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"
|
||||
25
api/app/data/abilities/army_of_the_dead.yaml
Normal file
25
api/app/data/abilities/army_of_the_dead.yaml
Normal 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"
|
||||
25
api/app/data/abilities/bestial_wrath.yaml
Normal file
25
api/app/data/abilities/bestial_wrath.yaml
Normal 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"
|
||||
16
api/app/data/abilities/blessed_sacrifice.yaml
Normal file
16
api/app/data/abilities/blessed_sacrifice.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/blizzard.yaml
Normal file
25
api/app/data/abilities/blizzard.yaml
Normal 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"
|
||||
16
api/app/data/abilities/cleanse.yaml
Normal file
16
api/app/data/abilities/cleanse.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/cleave.yaml
Normal file
16
api/app/data/abilities/cleave.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/confuse.yaml
Normal file
25
api/app/data/abilities/confuse.yaml
Normal 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"
|
||||
25
api/app/data/abilities/consecrated_ground.yaml
Normal file
25
api/app/data/abilities/consecrated_ground.yaml
Normal 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"
|
||||
25
api/app/data/abilities/consecration.yaml
Normal file
25
api/app/data/abilities/consecration.yaml
Normal 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"
|
||||
16
api/app/data/abilities/coordinated_attack.yaml
Normal file
16
api/app/data/abilities/coordinated_attack.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/corpse_explosion.yaml
Normal file
16
api/app/data/abilities/corpse_explosion.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/coup_de_grace.yaml
Normal file
16
api/app/data/abilities/coup_de_grace.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/curse_of_agony.yaml
Normal file
25
api/app/data/abilities/curse_of_agony.yaml
Normal 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"
|
||||
25
api/app/data/abilities/death_mark.yaml
Normal file
25
api/app/data/abilities/death_mark.yaml
Normal 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"
|
||||
25
api/app/data/abilities/death_pact.yaml
Normal file
25
api/app/data/abilities/death_pact.yaml
Normal 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"
|
||||
25
api/app/data/abilities/divine_aegis.yaml
Normal file
25
api/app/data/abilities/divine_aegis.yaml
Normal 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"
|
||||
34
api/app/data/abilities/divine_blessing.yaml
Normal file
34
api/app/data/abilities/divine_blessing.yaml
Normal 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"
|
||||
16
api/app/data/abilities/divine_intervention.yaml
Normal file
16
api/app/data/abilities/divine_intervention.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/divine_storm.yaml
Normal file
25
api/app/data/abilities/divine_storm.yaml
Normal 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"
|
||||
25
api/app/data/abilities/drain_life.yaml
Normal file
25
api/app/data/abilities/drain_life.yaml
Normal 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"
|
||||
34
api/app/data/abilities/epidemic.yaml
Normal file
34
api/app/data/abilities/epidemic.yaml
Normal 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"
|
||||
16
api/app/data/abilities/execute.yaml
Normal file
16
api/app/data/abilities/execute.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/explosive_shot.yaml
Normal file
25
api/app/data/abilities/explosive_shot.yaml
Normal 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"
|
||||
25
api/app/data/abilities/firestorm.yaml
Normal file
25
api/app/data/abilities/firestorm.yaml
Normal 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"
|
||||
16
api/app/data/abilities/flame_burst.yaml
Normal file
16
api/app/data/abilities/flame_burst.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/frozen_orb.yaml
Normal file
25
api/app/data/abilities/frozen_orb.yaml
Normal 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"
|
||||
25
api/app/data/abilities/glacial_spike.yaml
Normal file
25
api/app/data/abilities/glacial_spike.yaml
Normal 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"
|
||||
25
api/app/data/abilities/guardian_angel.yaml
Normal file
25
api/app/data/abilities/guardian_angel.yaml
Normal 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"
|
||||
25
api/app/data/abilities/hammer_of_justice.yaml
Normal file
25
api/app/data/abilities/hammer_of_justice.yaml
Normal 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"
|
||||
25
api/app/data/abilities/haste.yaml
Normal file
25
api/app/data/abilities/haste.yaml
Normal 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"
|
||||
@@ -7,7 +7,7 @@ description: "Channel divine energy to restore an ally's health"
|
||||
ability_type: "spell"
|
||||
base_power: 25
|
||||
damage_type: "holy"
|
||||
scaling_stat: "intelligence"
|
||||
scaling_stat: "wisdom"
|
||||
scaling_factor: 0.5
|
||||
mana_cost: 10
|
||||
cooldown: 0
|
||||
|
||||
25
api/app/data/abilities/holy_fire.yaml
Normal file
25
api/app/data/abilities/holy_fire.yaml
Normal 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"
|
||||
25
api/app/data/abilities/holy_shield.yaml
Normal file
25
api/app/data/abilities/holy_shield.yaml
Normal 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"
|
||||
25
api/app/data/abilities/ice_shard.yaml
Normal file
25
api/app/data/abilities/ice_shard.yaml
Normal 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"
|
||||
25
api/app/data/abilities/inferno.yaml
Normal file
25
api/app/data/abilities/inferno.yaml
Normal 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"
|
||||
34
api/app/data/abilities/last_stand.yaml
Normal file
34
api/app/data/abilities/last_stand.yaml
Normal 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"
|
||||
25
api/app/data/abilities/lay_on_hands.yaml
Normal file
25
api/app/data/abilities/lay_on_hands.yaml
Normal 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"
|
||||
25
api/app/data/abilities/mass_confusion.yaml
Normal file
25
api/app/data/abilities/mass_confusion.yaml
Normal 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"
|
||||
25
api/app/data/abilities/mass_domination.yaml
Normal file
25
api/app/data/abilities/mass_domination.yaml
Normal 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"
|
||||
25
api/app/data/abilities/mass_enhancement.yaml
Normal file
25
api/app/data/abilities/mass_enhancement.yaml
Normal 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"
|
||||
25
api/app/data/abilities/mass_heal.yaml
Normal file
25
api/app/data/abilities/mass_heal.yaml
Normal 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"
|
||||
25
api/app/data/abilities/mesmerize.yaml
Normal file
25
api/app/data/abilities/mesmerize.yaml
Normal 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"
|
||||
25
api/app/data/abilities/miracle.yaml
Normal file
25
api/app/data/abilities/miracle.yaml
Normal 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"
|
||||
25
api/app/data/abilities/mirror_image.yaml
Normal file
25
api/app/data/abilities/mirror_image.yaml
Normal 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"
|
||||
16
api/app/data/abilities/multishot.yaml
Normal file
16
api/app/data/abilities/multishot.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/phantasmal_killer.yaml
Normal file
25
api/app/data/abilities/phantasmal_killer.yaml
Normal 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"
|
||||
25
api/app/data/abilities/piercing_shot.yaml
Normal file
25
api/app/data/abilities/piercing_shot.yaml
Normal 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"
|
||||
25
api/app/data/abilities/plague.yaml
Normal file
25
api/app/data/abilities/plague.yaml
Normal 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"
|
||||
16
api/app/data/abilities/power_strike.yaml
Normal file
16
api/app/data/abilities/power_strike.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/precise_strike.yaml
Normal file
16
api/app/data/abilities/precise_strike.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/primal_fury.yaml
Normal file
16
api/app/data/abilities/primal_fury.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/rain_of_arrows.yaml
Normal file
25
api/app/data/abilities/rain_of_arrows.yaml
Normal 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"
|
||||
25
api/app/data/abilities/raise_ghoul.yaml
Normal file
25
api/app/data/abilities/raise_ghoul.yaml
Normal 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"
|
||||
34
api/app/data/abilities/reality_shift.yaml
Normal file
34
api/app/data/abilities/reality_shift.yaml
Normal 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"
|
||||
25
api/app/data/abilities/rending_blow.yaml
Normal file
25
api/app/data/abilities/rending_blow.yaml
Normal 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"
|
||||
16
api/app/data/abilities/resurrection.yaml
Normal file
16
api/app/data/abilities/resurrection.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/riposte.yaml
Normal file
16
api/app/data/abilities/riposte.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/shadow_assault.yaml
Normal file
25
api/app/data/abilities/shadow_assault.yaml
Normal 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"
|
||||
16
api/app/data/abilities/shadowstep.yaml
Normal file
16
api/app/data/abilities/shadowstep.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/shield_of_faith.yaml
Normal file
25
api/app/data/abilities/shield_of_faith.yaml
Normal 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"
|
||||
25
api/app/data/abilities/shield_wall.yaml
Normal file
25
api/app/data/abilities/shield_wall.yaml
Normal 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"
|
||||
16
api/app/data/abilities/smite.yaml
Normal file
16
api/app/data/abilities/smite.yaml
Normal 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: []
|
||||
25
api/app/data/abilities/smoke_bomb.yaml
Normal file
25
api/app/data/abilities/smoke_bomb.yaml
Normal 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"
|
||||
34
api/app/data/abilities/soul_rot.yaml
Normal file
34
api/app/data/abilities/soul_rot.yaml
Normal 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"
|
||||
25
api/app/data/abilities/stampede.yaml
Normal file
25
api/app/data/abilities/stampede.yaml
Normal 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"
|
||||
25
api/app/data/abilities/summon_abomination.yaml
Normal file
25
api/app/data/abilities/summon_abomination.yaml
Normal 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"
|
||||
25
api/app/data/abilities/summon_companion.yaml
Normal file
25
api/app/data/abilities/summon_companion.yaml
Normal 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"
|
||||
25
api/app/data/abilities/summon_skeleton.yaml
Normal file
25
api/app/data/abilities/summon_skeleton.yaml
Normal 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"
|
||||
25
api/app/data/abilities/sun_burst.yaml
Normal file
25
api/app/data/abilities/sun_burst.yaml
Normal 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"
|
||||
25
api/app/data/abilities/taunt.yaml
Normal file
25
api/app/data/abilities/taunt.yaml
Normal 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"
|
||||
25
api/app/data/abilities/thousand_cuts.yaml
Normal file
25
api/app/data/abilities/thousand_cuts.yaml
Normal 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"
|
||||
25
api/app/data/abilities/time_warp.yaml
Normal file
25
api/app/data/abilities/time_warp.yaml
Normal 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"
|
||||
25
api/app/data/abilities/titans_wrath.yaml
Normal file
25
api/app/data/abilities/titans_wrath.yaml
Normal 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"
|
||||
25
api/app/data/abilities/unbreakable.yaml
Normal file
25
api/app/data/abilities/unbreakable.yaml
Normal 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"
|
||||
25
api/app/data/abilities/vanish.yaml
Normal file
25
api/app/data/abilities/vanish.yaml
Normal 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"
|
||||
16
api/app/data/abilities/vital_strike.yaml
Normal file
16
api/app/data/abilities/vital_strike.yaml
Normal 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: []
|
||||
16
api/app/data/abilities/word_of_healing.yaml
Normal file
16
api/app/data/abilities/word_of_healing.yaml
Normal 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: []
|
||||
246
api/app/data/base_items/shields.yaml
Normal file
246
api/app/data/base_items/shields.yaml
Normal 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
|
||||
@@ -21,7 +21,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- worn_staff
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -21,7 +21,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- rusty_dagger
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -21,7 +21,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- tome
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -21,7 +21,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- rusty_mace
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -21,7 +21,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- bone_wand
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -22,7 +22,7 @@ starting_equipment:
|
||||
- rusty_sword
|
||||
- rusty_shield
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -22,7 +22,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- rusty_sword
|
||||
- cloth_armor
|
||||
- rusty_knife # Everyone gets pocket knife
|
||||
- health_potion_small
|
||||
|
||||
# Starting abilities
|
||||
starting_abilities:
|
||||
|
||||
@@ -21,7 +21,7 @@ base_stats:
|
||||
starting_equipment:
|
||||
- rusty_bow
|
||||
- cloth_armor
|
||||
- rusty_knife
|
||||
- health_potion_small
|
||||
|
||||
starting_abilities:
|
||||
- basic_attack
|
||||
|
||||
@@ -81,9 +81,8 @@ dialogue_hooks:
|
||||
busy: "*keeps hammering* Talk while I work. Time is iron."
|
||||
quest_complete: "*nods approvingly* Fine work. You've got the heart of a warrior."
|
||||
|
||||
quest_giver_for:
|
||||
- quest_ore_delivery
|
||||
- quest_equipment_repair
|
||||
# Note: Quest offerings are now defined in quest YAML files (quest-centric design)
|
||||
# See /api/app/data/quests/ for quest definitions that reference this NPC
|
||||
|
||||
reveals_locations: []
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ dialogue_hooks:
|
||||
busy: "Got thirsty folk to serve. Make it quick."
|
||||
quest_complete: "*actually smiles* Well done, lad. Drink's on the house."
|
||||
|
||||
quest_giver_for:
|
||||
- quest_cellar_rats
|
||||
# Note: Quest offerings are now defined in quest YAML files (quest-centric design)
|
||||
# See /api/app/data/quests/ for quest definitions that reference this NPC
|
||||
|
||||
reveals_locations:
|
||||
- crossville_dungeon
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user