Compare commits
29 Commits
e6e7cdb7b7
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 70b2b0f124 | |||
| 805d04cf4e | |||
| f9e463bfc6 | |||
| 06ef8f6f0b | |||
| 72cf92021e | |||
| df26abd207 | |||
| e7e329e6ed | |||
| 8bd494a52f | |||
| 32af625d14 | |||
| 8784fbaa88 | |||
| a8767b34e2 | |||
| d9bc46adc1 | |||
| 45cfa25911 | |||
| 7c0e257540 | |||
| 6d3fb63355 | |||
| dd92cf5991 | |||
| 94c4ca9e95 | |||
| 19b537d8b0 | |||
| 58f0c1b8f6 | |||
| 29b4853c84 | |||
| fdd48034e4 | |||
| a38906b445 | |||
| 4ced1b04df | |||
| 76f67c4a22 | |||
| 185be7fee0 | |||
| f3ac0c8647 | |||
| 03ab783eeb | |||
| 30c3b800e6 | |||
| d789b5df65 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,3 +35,4 @@ Thumbs.db
|
|||||||
logs/
|
logs/
|
||||||
app/logs/
|
app/logs/
|
||||||
*.log
|
*.log
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -169,8 +169,31 @@ def register_blueprints(app: Flask) -> None:
|
|||||||
app.register_blueprint(chat_bp)
|
app.register_blueprint(chat_bp)
|
||||||
logger.info("Chat API blueprint registered")
|
logger.info("Chat API blueprint registered")
|
||||||
|
|
||||||
|
# Import and register Combat API blueprint
|
||||||
|
from app.api.combat import combat_bp
|
||||||
|
app.register_blueprint(combat_bp)
|
||||||
|
logger.info("Combat API blueprint registered")
|
||||||
|
|
||||||
|
# Import and register Inventory API blueprint
|
||||||
|
from app.api.inventory import inventory_bp
|
||||||
|
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
|
# TODO: Register additional blueprints as they are created
|
||||||
# from app.api import combat, marketplace, shop
|
# from app.api import marketplace
|
||||||
# app.register_blueprint(combat.bp, url_prefix='/api/v1/combat')
|
|
||||||
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')
|
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')
|
||||||
# app.register_blueprint(shop.bp, url_prefix='/api/v1/shop')
|
|
||||||
|
|||||||
@@ -447,7 +447,10 @@ class NarrativeGenerator:
|
|||||||
user_tier: UserTier,
|
user_tier: UserTier,
|
||||||
npc_relationship: str | None = None,
|
npc_relationship: str | None = None,
|
||||||
previous_dialogue: list[dict[str, Any]] | None = None,
|
previous_dialogue: list[dict[str, Any]] | None = None,
|
||||||
npc_knowledge: list[str] | None = None
|
npc_knowledge: list[str] | None = None,
|
||||||
|
quest_offering_context: dict[str, Any] | None = None,
|
||||||
|
quest_ineligibility_context: dict[str, Any] | None = None,
|
||||||
|
player_asking_for_quests: bool = False
|
||||||
) -> NarrativeResponse:
|
) -> NarrativeResponse:
|
||||||
"""
|
"""
|
||||||
Generate NPC dialogue in response to player conversation.
|
Generate NPC dialogue in response to player conversation.
|
||||||
@@ -461,6 +464,9 @@ class NarrativeGenerator:
|
|||||||
npc_relationship: Optional description of relationship with NPC.
|
npc_relationship: Optional description of relationship with NPC.
|
||||||
previous_dialogue: Optional list of previous exchanges.
|
previous_dialogue: Optional list of previous exchanges.
|
||||||
npc_knowledge: Optional list of things this NPC knows about.
|
npc_knowledge: Optional list of things this NPC knows about.
|
||||||
|
quest_offering_context: Optional quest offer context from QuestEligibilityService.
|
||||||
|
quest_ineligibility_context: Optional context explaining why player can't take a quest.
|
||||||
|
player_asking_for_quests: Whether the player is explicitly asking for quests/work.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
NarrativeResponse with NPC dialogue.
|
NarrativeResponse with NPC dialogue.
|
||||||
@@ -500,6 +506,9 @@ class NarrativeGenerator:
|
|||||||
npc_relationship=npc_relationship,
|
npc_relationship=npc_relationship,
|
||||||
previous_dialogue=previous_dialogue or [],
|
previous_dialogue=previous_dialogue or [],
|
||||||
npc_knowledge=npc_knowledge or [],
|
npc_knowledge=npc_knowledge or [],
|
||||||
|
quest_offering_context=quest_offering_context,
|
||||||
|
quest_ineligibility_context=quest_ineligibility_context,
|
||||||
|
player_asking_for_quests=player_asking_for_quests,
|
||||||
max_tokens=model_config.max_tokens
|
max_tokens=model_config.max_tokens
|
||||||
)
|
)
|
||||||
except PromptTemplateError as e:
|
except PromptTemplateError as e:
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ Response parser for AI narrative responses.
|
|||||||
This module handles AI response parsing. Game state changes (items, gold, XP)
|
This module handles AI response parsing. Game state changes (items, gold, XP)
|
||||||
are now handled exclusively through predetermined dice check outcomes in
|
are now handled exclusively through predetermined dice check outcomes in
|
||||||
action templates, not through AI-generated JSON.
|
action templates, not through AI-generated JSON.
|
||||||
|
|
||||||
|
Quest offers are extracted from NPC dialogue using [QUEST_OFFER:quest_id] markers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -158,3 +161,83 @@ def _parse_game_actions(data: dict[str, Any]) -> GameStateChanges:
|
|||||||
changes.location_change = data.get("location_change")
|
changes.location_change = data.get("location_change")
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# NPC Dialogue Parsing
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParsedNPCDialogue:
|
||||||
|
"""
|
||||||
|
Parsed NPC dialogue response with quest offer extraction.
|
||||||
|
|
||||||
|
When NPCs offer quests during conversation, they include a
|
||||||
|
[QUEST_OFFER:quest_id] marker that signals the UI to show
|
||||||
|
a quest accept/decline modal.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
dialogue: The cleaned dialogue text (marker removed)
|
||||||
|
quest_offered: Quest ID if a quest was offered, None otherwise
|
||||||
|
raw_response: The original response text
|
||||||
|
"""
|
||||||
|
|
||||||
|
dialogue: str
|
||||||
|
quest_offered: Optional[str] = None
|
||||||
|
raw_response: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# Regex pattern for quest offer markers
|
||||||
|
# Matches: [QUEST_OFFER:quest_id] or [QUEST_OFFER: quest_id]
|
||||||
|
QUEST_OFFER_PATTERN = re.compile(r'\[QUEST_OFFER:\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\]')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_npc_dialogue(response_text: str) -> ParsedNPCDialogue:
|
||||||
|
"""
|
||||||
|
Parse an NPC dialogue response, extracting quest offer markers.
|
||||||
|
|
||||||
|
The AI is instructed to include [QUEST_OFFER:quest_id] on its own line
|
||||||
|
when offering a quest. This function extracts the marker and returns
|
||||||
|
the cleaned dialogue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_text: The raw AI dialogue response
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedNPCDialogue with cleaned dialogue and optional quest_offered
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> response = '''*leans in* Got a problem, friend.
|
||||||
|
... [QUEST_OFFER:quest_cellar_rats]
|
||||||
|
... Giant rats in me cellar.'''
|
||||||
|
>>> result = parse_npc_dialogue(response)
|
||||||
|
>>> result.quest_offered
|
||||||
|
'quest_cellar_rats'
|
||||||
|
>>> '[QUEST_OFFER' in result.dialogue
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
logger.debug("Parsing NPC dialogue", response_length=len(response_text))
|
||||||
|
|
||||||
|
quest_offered = None
|
||||||
|
dialogue = response_text.strip()
|
||||||
|
|
||||||
|
# Search for quest offer marker
|
||||||
|
match = QUEST_OFFER_PATTERN.search(dialogue)
|
||||||
|
if match:
|
||||||
|
quest_offered = match.group(1)
|
||||||
|
# Remove the marker from the dialogue
|
||||||
|
dialogue = QUEST_OFFER_PATTERN.sub('', dialogue)
|
||||||
|
# Clean up any extra whitespace/newlines left behind
|
||||||
|
dialogue = re.sub(r'\n\s*\n', '\n\n', dialogue)
|
||||||
|
dialogue = dialogue.strip()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Quest offer extracted from dialogue",
|
||||||
|
quest_id=quest_offered,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ParsedNPCDialogue(
|
||||||
|
dialogue=dialogue,
|
||||||
|
quest_offered=quest_offered,
|
||||||
|
raw_response=response_text,
|
||||||
|
)
|
||||||
|
|||||||
@@ -92,6 +92,107 @@ Work these into the dialogue naturally - don't dump all information at once.
|
|||||||
Make it feel earned, like the NPC is opening up to someone they trust.
|
Make it feel earned, like the NPC is opening up to someone they trust.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if quest_offering_context and quest_offering_context.should_offer %}
|
||||||
|
## QUEST TO OFFER
|
||||||
|
The NPC has a quest to offer to the player.
|
||||||
|
|
||||||
|
**Quest:** {{ quest_offering_context.quest_name }}
|
||||||
|
**Quest ID:** {{ quest_offering_context.quest_id }}
|
||||||
|
|
||||||
|
{% if quest_offering_context.offer_dialogue %}
|
||||||
|
**How the NPC Would Present It:**
|
||||||
|
{{ quest_offering_context.offer_dialogue }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if quest_offering_context.npc_quest_knowledge %}
|
||||||
|
**What the NPC Knows About This Quest:**
|
||||||
|
{% for fact in quest_offering_context.npc_quest_knowledge %}
|
||||||
|
- {{ fact }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if quest_offering_context.narrative_hooks %}
|
||||||
|
**Narrative Hooks (use 1-2 naturally):**
|
||||||
|
{% for hook in quest_offering_context.narrative_hooks %}
|
||||||
|
- The NPC {{ hook }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if player_asking_for_quests %}
|
||||||
|
**CRITICAL: The player is explicitly asking for quests/work. You MUST offer this quest NOW.**
|
||||||
|
In your response:
|
||||||
|
1. Describe the quest situation naturally in your dialogue
|
||||||
|
2. End your response with the quest offer marker on its own line: [QUEST_OFFER:{{ quest_offering_context.quest_id }}]
|
||||||
|
|
||||||
|
The marker MUST appear - it triggers the UI to show accept/decline buttons.
|
||||||
|
{% else %}
|
||||||
|
**Quest Offering Guidelines:**
|
||||||
|
- Weave the quest naturally into conversation
|
||||||
|
- If the player shows interest, include the marker: [QUEST_OFFER:{{ quest_offering_context.quest_id }}]
|
||||||
|
- The marker signals the UI to show quest accept/decline options
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if quest_ineligibility_context and player_asking_for_quests %}
|
||||||
|
## QUEST UNAVAILABLE - EXPLAIN WHY
|
||||||
|
The player is asking about quests, but they don't meet the requirements. Explain this in character.
|
||||||
|
|
||||||
|
{% if quest_ineligibility_context.reason_type == "level_too_low" %}
|
||||||
|
**Reason:** The player (level {{ quest_ineligibility_context.current_level }}) isn't experienced enough. They need to be level {{ quest_ineligibility_context.required_level }}.
|
||||||
|
**How to convey this:** The NPC should politely but firmly indicate the task is too dangerous for someone of their current skill level. Suggest they gain more experience first. Be encouraging but realistic - don't offer false hope.
|
||||||
|
**Example tone:** "I appreciate your enthusiasm, but this task requires someone with more experience. The bandits we're dealing with are seasoned fighters. Come back when you've proven yourself in a few more battles."
|
||||||
|
{% elif quest_ineligibility_context.reason_type == "level_too_high" %}
|
||||||
|
**Reason:** The player is too experienced for available tasks.
|
||||||
|
**How to convey this:** The NPC should indicate they have nothing worthy of such an accomplished adventurer right now.
|
||||||
|
{% elif quest_ineligibility_context.reason_type == "prerequisite_missing" %}
|
||||||
|
**Reason:** The player needs to complete other tasks first.
|
||||||
|
**How to convey this:** Hint that there's something else they should do first, or that circumstances aren't right yet.
|
||||||
|
{% elif quest_ineligibility_context.reason_type == "relationship_too_low" %}
|
||||||
|
**Reason:** The NPC doesn't trust the player enough yet.
|
||||||
|
**How to convey this:** Be guarded. Hint that you might have work, but you need to know you can trust them first.
|
||||||
|
{% elif quest_ineligibility_context.reason_type == "quest_already_active" %}
|
||||||
|
**Reason:** The player is already working on this quest.
|
||||||
|
**How to convey this:** Remind them they already accepted this task and should focus on completing it.
|
||||||
|
{% elif quest_ineligibility_context.reason_type == "quest_already_completed" %}
|
||||||
|
**Reason:** The player already completed this quest.
|
||||||
|
**How to convey this:** Thank them again for their previous help, mention you have nothing else right now.
|
||||||
|
{% elif quest_ineligibility_context.reason_type == "too_many_quests" %}
|
||||||
|
**Reason:** The player has too many active quests.
|
||||||
|
**How to convey this:** Suggest they finish some of their current commitments before taking on more.
|
||||||
|
{% else %}
|
||||||
|
**Reason:** {{ quest_ineligibility_context.message }}
|
||||||
|
**How to convey this:** Politely decline, staying in character.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
**IMPORTANT:** Do NOT offer the quest. Explain the situation naturally in dialogue.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if lore_context and lore_context.has_content %}
|
||||||
|
## Relevant World Knowledge
|
||||||
|
The NPC may reference this lore if contextually appropriate:
|
||||||
|
|
||||||
|
{% if lore_context.quest %}
|
||||||
|
**Quest Background:**
|
||||||
|
{% for entry in lore_context.quest[:2] %}
|
||||||
|
- {{ entry.content | truncate_text(150) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if lore_context.regional %}
|
||||||
|
**Local Knowledge:**
|
||||||
|
{% for entry in lore_context.regional[:3] %}
|
||||||
|
- {{ entry.content | truncate_text(100) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if lore_context.world %}
|
||||||
|
**Historical Knowledge (if NPC would know):**
|
||||||
|
{% for entry in lore_context.world[:2] %}
|
||||||
|
- {{ entry.content | truncate_text(100) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if npc.relationships %}
|
{% if npc.relationships %}
|
||||||
## NPC Relationships (for context)
|
## NPC Relationships (for context)
|
||||||
{% for rel in npc.relationships %}
|
{% for rel in npc.relationships %}
|
||||||
|
|||||||
129
api/app/api/abilities.py
Normal file
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())
|
||||||
1093
api/app/api/combat.py
Normal file
1093
api/app/api/combat.py
Normal file
File diff suppressed because it is too large
Load Diff
639
api/app/api/inventory.py
Normal file
639
api/app/api/inventory.py
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
"""
|
||||||
|
Inventory API Blueprint
|
||||||
|
|
||||||
|
Endpoints for managing character inventory and equipment.
|
||||||
|
All endpoints require authentication and enforce ownership validation.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- GET /api/v1/characters/<id>/inventory - Get character inventory and equipped items
|
||||||
|
- POST /api/v1/characters/<id>/inventory/equip - Equip an item
|
||||||
|
- POST /api/v1/characters/<id>/inventory/unequip - Unequip an item
|
||||||
|
- POST /api/v1/characters/<id>/inventory/use - Use a consumable item
|
||||||
|
- DELETE /api/v1/characters/<id>/inventory/<item_id> - Drop an item
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from app.services.inventory_service import (
|
||||||
|
get_inventory_service,
|
||||||
|
ItemNotFoundError,
|
||||||
|
CannotEquipError,
|
||||||
|
InvalidSlotError,
|
||||||
|
CannotUseItemError,
|
||||||
|
InventoryFullError,
|
||||||
|
VALID_SLOTS,
|
||||||
|
MAX_INVENTORY_SIZE,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize logger
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
# Create blueprint
|
||||||
|
inventory_bp = Blueprint('inventory', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# API Endpoints
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@inventory_bp.route('/api/v1/characters/<character_id>/inventory', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
|
def get_inventory(character_id: str):
|
||||||
|
"""
|
||||||
|
Get character inventory and equipped items.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character_id: Character ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
200: Inventory and equipment data
|
||||||
|
401: Not authenticated
|
||||||
|
404: Character not found or not owned by user
|
||||||
|
500: Internal server error
|
||||||
|
|
||||||
|
Example Response:
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"inventory": [
|
||||||
|
{
|
||||||
|
"item_id": "gen_abc123",
|
||||||
|
"name": "Flaming Dagger",
|
||||||
|
"item_type": "weapon",
|
||||||
|
"rarity": "rare",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"equipped": {
|
||||||
|
"weapon": {...},
|
||||||
|
"helmet": null,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"inventory_count": 5,
|
||||||
|
"max_inventory": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user = get_current_user()
|
||||||
|
logger.info("Getting inventory",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id)
|
||||||
|
|
||||||
|
# Get character (validates ownership)
|
||||||
|
char_service = get_character_service()
|
||||||
|
character = char_service.get_character(character_id, user.id)
|
||||||
|
|
||||||
|
# Get inventory service
|
||||||
|
inventory_service = get_inventory_service()
|
||||||
|
|
||||||
|
# Get inventory items
|
||||||
|
inventory_items = inventory_service.get_inventory(character)
|
||||||
|
|
||||||
|
# Get equipped items
|
||||||
|
equipped_items = inventory_service.get_equipped_items(character)
|
||||||
|
|
||||||
|
# Build equipped dict with all slots (None for empty slots)
|
||||||
|
equipped_response = {}
|
||||||
|
for slot in VALID_SLOTS:
|
||||||
|
item = equipped_items.get(slot)
|
||||||
|
equipped_response[slot] = item.to_dict() if item else None
|
||||||
|
|
||||||
|
logger.info("Inventory retrieved successfully",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_count=len(inventory_items))
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
result={
|
||||||
|
"inventory": [item.to_dict() for item in inventory_items],
|
||||||
|
"equipped": equipped_response,
|
||||||
|
"inventory_count": len(inventory_items),
|
||||||
|
"max_inventory": MAX_INVENTORY_SIZE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except CharacterNotFound as e:
|
||||||
|
logger.warning("Character not found",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get inventory",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="INVENTORY_GET_ERROR",
|
||||||
|
message="Failed to retrieve inventory",
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/equip', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def equip_item(character_id: str):
|
||||||
|
"""
|
||||||
|
Equip an item from inventory to a specified slot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character_id: Character ID
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
{
|
||||||
|
"item_id": "gen_abc123",
|
||||||
|
"slot": "weapon"
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
200: Item equipped successfully
|
||||||
|
400: Cannot equip item (wrong type, level requirement, etc.)
|
||||||
|
401: Not authenticated
|
||||||
|
404: Character or item not found
|
||||||
|
422: Validation error (invalid slot)
|
||||||
|
500: Internal server error
|
||||||
|
|
||||||
|
Example Response:
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"message": "Equipped Flaming Dagger to weapon slot",
|
||||||
|
"equipped": {...},
|
||||||
|
"unequipped_item": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
item_id = data.get('item_id', '').strip()
|
||||||
|
slot = data.get('slot', '').strip().lower()
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
validation_errors = {}
|
||||||
|
|
||||||
|
if not item_id:
|
||||||
|
validation_errors['item_id'] = "item_id is required"
|
||||||
|
|
||||||
|
if not slot:
|
||||||
|
validation_errors['slot'] = "slot is required"
|
||||||
|
|
||||||
|
if validation_errors:
|
||||||
|
return validation_error_response(
|
||||||
|
message="Validation failed",
|
||||||
|
details=validation_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Equipping item",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id,
|
||||||
|
slot=slot)
|
||||||
|
|
||||||
|
# Get character (validates ownership)
|
||||||
|
char_service = get_character_service()
|
||||||
|
character = char_service.get_character(character_id, user.id)
|
||||||
|
|
||||||
|
# Get inventory service
|
||||||
|
inventory_service = get_inventory_service()
|
||||||
|
|
||||||
|
# Equip item
|
||||||
|
previous_item = inventory_service.equip_item(
|
||||||
|
character, item_id, slot, user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get item name for message
|
||||||
|
equipped_item = character.equipped.get(slot)
|
||||||
|
item_name = equipped_item.get_display_name() if equipped_item else item_id
|
||||||
|
|
||||||
|
# Build equipped response
|
||||||
|
equipped_response = {}
|
||||||
|
for s in VALID_SLOTS:
|
||||||
|
item = character.equipped.get(s)
|
||||||
|
equipped_response[s] = item.to_dict() if item else None
|
||||||
|
|
||||||
|
logger.info("Item equipped successfully",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id,
|
||||||
|
slot=slot,
|
||||||
|
previous_item=previous_item.item_id if previous_item else None)
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
result={
|
||||||
|
"message": f"Equipped {item_name} to {slot} slot",
|
||||||
|
"equipped": equipped_response,
|
||||||
|
"unequipped_item": previous_item.to_dict() if previous_item else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except CharacterNotFound as e:
|
||||||
|
logger.warning("Character not found for equip",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except ItemNotFoundError as e:
|
||||||
|
logger.warning("Item not found for equip",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id if 'item_id' in locals() else 'unknown',
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except InvalidSlotError as e:
|
||||||
|
logger.warning("Invalid slot for equip",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
slot=slot if 'slot' in locals() else 'unknown',
|
||||||
|
error=str(e))
|
||||||
|
return validation_error_response(
|
||||||
|
message=str(e),
|
||||||
|
details={"slot": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
|
except CannotEquipError as e:
|
||||||
|
logger.warning("Cannot equip item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id if 'item_id' in locals() else 'unknown',
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="CANNOT_EQUIP",
|
||||||
|
message=str(e),
|
||||||
|
status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to equip item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="EQUIP_ERROR",
|
||||||
|
message="Failed to equip item",
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/unequip', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def unequip_item(character_id: str):
|
||||||
|
"""
|
||||||
|
Unequip an item from a specified slot (returns to inventory).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character_id: Character ID
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
{
|
||||||
|
"slot": "weapon"
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
200: Item unequipped successfully (or slot was empty)
|
||||||
|
400: Inventory full, cannot unequip
|
||||||
|
401: Not authenticated
|
||||||
|
404: Character not found
|
||||||
|
422: Validation error (invalid slot)
|
||||||
|
500: Internal server error
|
||||||
|
|
||||||
|
Example Response:
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"message": "Unequipped Flaming Dagger from weapon slot",
|
||||||
|
"unequipped_item": {...},
|
||||||
|
"equipped": {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
slot = data.get('slot', '').strip().lower()
|
||||||
|
|
||||||
|
if not slot:
|
||||||
|
return validation_error_response(
|
||||||
|
message="Validation failed",
|
||||||
|
details={"slot": "slot is required"}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Unequipping item",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
slot=slot)
|
||||||
|
|
||||||
|
# Get character (validates ownership)
|
||||||
|
char_service = get_character_service()
|
||||||
|
character = char_service.get_character(character_id, user.id)
|
||||||
|
|
||||||
|
# Get inventory service
|
||||||
|
inventory_service = get_inventory_service()
|
||||||
|
|
||||||
|
# Unequip item
|
||||||
|
unequipped_item = inventory_service.unequip_item(character, slot, user.id)
|
||||||
|
|
||||||
|
# Build equipped response
|
||||||
|
equipped_response = {}
|
||||||
|
for s in VALID_SLOTS:
|
||||||
|
item = character.equipped.get(s)
|
||||||
|
equipped_response[s] = item.to_dict() if item else None
|
||||||
|
|
||||||
|
# Build message
|
||||||
|
if unequipped_item:
|
||||||
|
message = f"Unequipped {unequipped_item.get_display_name()} from {slot} slot"
|
||||||
|
else:
|
||||||
|
message = f"Slot '{slot}' was already empty"
|
||||||
|
|
||||||
|
logger.info("Item unequipped",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
slot=slot,
|
||||||
|
unequipped_item=unequipped_item.item_id if unequipped_item else None)
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
result={
|
||||||
|
"message": message,
|
||||||
|
"unequipped_item": unequipped_item.to_dict() if unequipped_item else None,
|
||||||
|
"equipped": equipped_response,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except CharacterNotFound as e:
|
||||||
|
logger.warning("Character not found for unequip",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except InvalidSlotError as e:
|
||||||
|
logger.warning("Invalid slot for unequip",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
slot=slot if 'slot' in locals() else 'unknown',
|
||||||
|
error=str(e))
|
||||||
|
return validation_error_response(
|
||||||
|
message=str(e),
|
||||||
|
details={"slot": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
|
except InventoryFullError as e:
|
||||||
|
logger.warning("Inventory full, cannot unequip",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="INVENTORY_FULL",
|
||||||
|
message=str(e),
|
||||||
|
status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to unequip item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="UNEQUIP_ERROR",
|
||||||
|
message="Failed to unequip item",
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/use', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def use_item(character_id: str):
|
||||||
|
"""
|
||||||
|
Use a consumable item from inventory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character_id: Character ID
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
{
|
||||||
|
"item_id": "health_potion_small"
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
200: Item used successfully
|
||||||
|
400: Cannot use item (not consumable)
|
||||||
|
401: Not authenticated
|
||||||
|
404: Character or item not found
|
||||||
|
500: Internal server error
|
||||||
|
|
||||||
|
Example Response:
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"item_used": "Small Health Potion",
|
||||||
|
"effects_applied": [
|
||||||
|
{
|
||||||
|
"effect_name": "Healing",
|
||||||
|
"effect_type": "hot",
|
||||||
|
"value": 25,
|
||||||
|
"message": "Restored 25 HP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hp_restored": 25,
|
||||||
|
"mp_restored": 0,
|
||||||
|
"message": "Used Small Health Potion: Restored 25 HP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
item_id = data.get('item_id', '').strip()
|
||||||
|
|
||||||
|
if not item_id:
|
||||||
|
return validation_error_response(
|
||||||
|
message="Validation failed",
|
||||||
|
details={"item_id": "item_id is required"}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Using item",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id)
|
||||||
|
|
||||||
|
# Get character (validates ownership)
|
||||||
|
char_service = get_character_service()
|
||||||
|
character = char_service.get_character(character_id, user.id)
|
||||||
|
|
||||||
|
# Get inventory service
|
||||||
|
inventory_service = get_inventory_service()
|
||||||
|
|
||||||
|
# Use consumable
|
||||||
|
result = inventory_service.use_consumable(character, item_id, user.id)
|
||||||
|
|
||||||
|
logger.info("Item used successfully",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id,
|
||||||
|
hp_restored=result.hp_restored,
|
||||||
|
mp_restored=result.mp_restored)
|
||||||
|
|
||||||
|
return success_response(result=result.to_dict())
|
||||||
|
|
||||||
|
except CharacterNotFound as e:
|
||||||
|
logger.warning("Character not found for use item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except ItemNotFoundError as e:
|
||||||
|
logger.warning("Item not found for use",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id if 'item_id' in locals() else 'unknown',
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except CannotUseItemError as e:
|
||||||
|
logger.warning("Cannot use item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id if 'item_id' in locals() else 'unknown',
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="CANNOT_USE_ITEM",
|
||||||
|
message=str(e),
|
||||||
|
status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to use item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="USE_ITEM_ERROR",
|
||||||
|
message="Failed to use item",
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/<item_id>', methods=['DELETE'])
|
||||||
|
@require_auth
|
||||||
|
def drop_item(character_id: str, item_id: str):
|
||||||
|
"""
|
||||||
|
Drop (remove) an item from inventory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character_id: Character ID
|
||||||
|
item_id: Item ID to drop
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
200: Item dropped successfully
|
||||||
|
401: Not authenticated
|
||||||
|
404: Character or item not found
|
||||||
|
500: Internal server error
|
||||||
|
|
||||||
|
Example Response:
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"message": "Dropped Rusty Sword",
|
||||||
|
"dropped_item": {...},
|
||||||
|
"inventory_count": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user = get_current_user()
|
||||||
|
|
||||||
|
logger.info("Dropping item",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id)
|
||||||
|
|
||||||
|
# Get character (validates ownership)
|
||||||
|
char_service = get_character_service()
|
||||||
|
character = char_service.get_character(character_id, user.id)
|
||||||
|
|
||||||
|
# Get inventory service
|
||||||
|
inventory_service = get_inventory_service()
|
||||||
|
|
||||||
|
# Drop item
|
||||||
|
dropped_item = inventory_service.drop_item(character, item_id, user.id)
|
||||||
|
|
||||||
|
logger.info("Item dropped successfully",
|
||||||
|
user_id=user.id,
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id,
|
||||||
|
item_name=dropped_item.get_display_name())
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
result={
|
||||||
|
"message": f"Dropped {dropped_item.get_display_name()}",
|
||||||
|
"dropped_item": dropped_item.to_dict(),
|
||||||
|
"inventory_count": len(character.inventory),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except CharacterNotFound as e:
|
||||||
|
logger.warning("Character not found for drop",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except ItemNotFoundError as e:
|
||||||
|
logger.warning("Item not found for drop",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id,
|
||||||
|
error=str(e))
|
||||||
|
return not_found_response(message=str(e))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to drop item",
|
||||||
|
user_id=user.id if 'user' in locals() else 'unknown',
|
||||||
|
character_id=character_id,
|
||||||
|
item_id=item_id,
|
||||||
|
error=str(e))
|
||||||
|
return error_response(
|
||||||
|
code="DROP_ITEM_ERROR",
|
||||||
|
message="Failed to drop item",
|
||||||
|
status=500
|
||||||
|
)
|
||||||
@@ -16,6 +16,7 @@ from app.services.session_service import get_session_service, SessionNotFound
|
|||||||
from app.services.character_service import get_character_service, CharacterNotFound
|
from app.services.character_service import get_character_service, CharacterNotFound
|
||||||
from app.services.npc_loader import get_npc_loader
|
from app.services.npc_loader import get_npc_loader
|
||||||
from app.services.location_loader import get_location_loader
|
from app.services.location_loader import get_location_loader
|
||||||
|
from app.services.quest_eligibility_service import get_quest_eligibility_service
|
||||||
from app.tasks.ai_tasks import enqueue_ai_task, TaskType
|
from app.tasks.ai_tasks import enqueue_ai_task, TaskType
|
||||||
from app.utils.response import (
|
from app.utils.response import (
|
||||||
success_response,
|
success_response,
|
||||||
@@ -192,6 +193,57 @@ def talk_to_npc(npc_id: str):
|
|||||||
interaction
|
interaction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for quest eligibility
|
||||||
|
quest_offering_context = None
|
||||||
|
quest_ineligibility_context = None # For explaining why player can't take a quest
|
||||||
|
player_asking_for_quests = _is_player_asking_for_quests(topic)
|
||||||
|
try:
|
||||||
|
quest_eligibility_service = get_quest_eligibility_service()
|
||||||
|
location_type = _get_location_type(session.game_state.current_location)
|
||||||
|
|
||||||
|
# If player is explicitly asking about quests, bypass probability roll
|
||||||
|
force_probability = 1.0 if player_asking_for_quests else None
|
||||||
|
|
||||||
|
eligibility_result = quest_eligibility_service.check_eligibility(
|
||||||
|
npc_id=npc_id,
|
||||||
|
character=character,
|
||||||
|
location_type=location_type,
|
||||||
|
location_id=session.game_state.current_location,
|
||||||
|
force_probability=force_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
if eligibility_result.should_offer_quest and eligibility_result.selected_quest_context:
|
||||||
|
quest_offering_context = eligibility_result.selected_quest_context.to_dict()
|
||||||
|
# Add should_offer flag for template conditional check
|
||||||
|
quest_offering_context['should_offer'] = True
|
||||||
|
logger.debug(
|
||||||
|
"Quest eligible for offering",
|
||||||
|
npc_id=npc_id,
|
||||||
|
quest_id=quest_offering_context.get("quest_id"),
|
||||||
|
character_id=character.character_id
|
||||||
|
)
|
||||||
|
elif player_asking_for_quests and eligibility_result.blocking_reasons:
|
||||||
|
# Player asked for quests but isn't eligible - tell them why
|
||||||
|
quest_ineligibility_context = _build_ineligibility_context(
|
||||||
|
eligibility_result.blocking_reasons,
|
||||||
|
character.level,
|
||||||
|
npc_id
|
||||||
|
)
|
||||||
|
if quest_ineligibility_context:
|
||||||
|
logger.debug(
|
||||||
|
"Quest ineligible - providing reason to AI",
|
||||||
|
npc_id=npc_id,
|
||||||
|
reason=quest_ineligibility_context.get("reason_type"),
|
||||||
|
character_level=character.level
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Don't fail the conversation if quest eligibility check fails
|
||||||
|
logger.warning(
|
||||||
|
"Quest eligibility check failed",
|
||||||
|
npc_id=npc_id,
|
||||||
|
error=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
# Build NPC knowledge for AI context
|
# Build NPC knowledge for AI context
|
||||||
npc_knowledge = []
|
npc_knowledge = []
|
||||||
if npc.knowledge:
|
if npc.knowledge:
|
||||||
@@ -220,6 +272,9 @@ def talk_to_npc(npc_id: str):
|
|||||||
"interaction_count": interaction["interaction_count"],
|
"interaction_count": interaction["interaction_count"],
|
||||||
"relationship_level": interaction.get("relationship_level", 50),
|
"relationship_level": interaction.get("relationship_level", 50),
|
||||||
"previous_dialogue": previous_dialogue, # Pass conversation history
|
"previous_dialogue": previous_dialogue, # Pass conversation history
|
||||||
|
"quest_offering_context": quest_offering_context, # Quest offer if eligible
|
||||||
|
"quest_ineligibility_context": quest_ineligibility_context, # Why player can't take quest
|
||||||
|
"player_asking_for_quests": player_asking_for_quests, # Player explicitly asking for work
|
||||||
}
|
}
|
||||||
|
|
||||||
# Enqueue AI task
|
# Enqueue AI task
|
||||||
@@ -428,3 +483,163 @@ def set_npc_flag(npc_id: str):
|
|||||||
npc_id=npc_id,
|
npc_id=npc_id,
|
||||||
error=str(e))
|
error=str(e))
|
||||||
return error_response("Failed to set flag", 500)
|
return error_response("Failed to set flag", 500)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_location_type(location_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Extract location type from location_id for quest probability calculation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location_id: The location identifier string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Location type string (tavern, shop, wilderness, dungeon, or town)
|
||||||
|
"""
|
||||||
|
location_lower = location_id.lower()
|
||||||
|
|
||||||
|
if "tavern" in location_lower or "inn" in location_lower:
|
||||||
|
return "tavern"
|
||||||
|
elif "shop" in location_lower or "market" in location_lower or "store" in location_lower:
|
||||||
|
return "shop"
|
||||||
|
elif "wilderness" in location_lower or "forest" in location_lower or "road" in location_lower:
|
||||||
|
return "wilderness"
|
||||||
|
elif "dungeon" in location_lower or "cave" in location_lower or "mine" in location_lower:
|
||||||
|
return "dungeon"
|
||||||
|
|
||||||
|
return "town" # Default for town centers, squares, etc.
|
||||||
|
|
||||||
|
|
||||||
|
def _build_ineligibility_context(
|
||||||
|
blocking_reasons: dict[str, str],
|
||||||
|
character_level: int,
|
||||||
|
npc_id: str
|
||||||
|
) -> dict | None:
|
||||||
|
"""
|
||||||
|
Build context explaining why a player can't take a quest.
|
||||||
|
|
||||||
|
Parses the blocking reasons and creates a structured context
|
||||||
|
that the AI can use to explain to the player why they can't
|
||||||
|
help with the quest yet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
blocking_reasons: Dict of quest_id -> reason string
|
||||||
|
character_level: Player's current level
|
||||||
|
npc_id: The NPC they're talking to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with reason_type, message, and details, or None if no relevant reason
|
||||||
|
"""
|
||||||
|
if not blocking_reasons:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Look through blocking reasons for level-related issues
|
||||||
|
for quest_id, reason in blocking_reasons.items():
|
||||||
|
if "level too low" in reason.lower():
|
||||||
|
# Extract required level from reason string like "Character level too low (need 3)"
|
||||||
|
import re
|
||||||
|
match = re.search(r'need (\d+)', reason)
|
||||||
|
required_level = int(match.group(1)) if match else character_level + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"reason_type": "level_too_low",
|
||||||
|
"current_level": character_level,
|
||||||
|
"required_level": required_level,
|
||||||
|
"message": f"The player is level {character_level} but needs to be level {required_level}",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "level too high" in reason.lower():
|
||||||
|
return {
|
||||||
|
"reason_type": "level_too_high",
|
||||||
|
"current_level": character_level,
|
||||||
|
"message": "The player is too experienced for this task",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "prerequisite" in reason.lower():
|
||||||
|
return {
|
||||||
|
"reason_type": "prerequisite_missing",
|
||||||
|
"message": "The player hasn't completed a required earlier task",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "already active" in reason.lower():
|
||||||
|
return {
|
||||||
|
"reason_type": "quest_already_active",
|
||||||
|
"message": "The player is already working on this quest",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "already completed" in reason.lower():
|
||||||
|
return {
|
||||||
|
"reason_type": "quest_already_completed",
|
||||||
|
"message": "The player has already completed this quest",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "relationship" in reason.lower():
|
||||||
|
return {
|
||||||
|
"reason_type": "relationship_too_low",
|
||||||
|
"message": "The NPC doesn't trust the player enough yet",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "max" in reason.lower() and "quest" in reason.lower():
|
||||||
|
return {
|
||||||
|
"reason_type": "too_many_quests",
|
||||||
|
"message": "The player already has too many active quests",
|
||||||
|
"quest_id": quest_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_player_asking_for_quests(topic: str) -> bool:
|
||||||
|
"""
|
||||||
|
Detect if the player is explicitly asking about quests or work.
|
||||||
|
|
||||||
|
This is used to bypass the probability roll when the player
|
||||||
|
clearly intends to find quests.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: The player's message/conversation topic
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if player is asking about quests, False otherwise
|
||||||
|
"""
|
||||||
|
topic_lower = topic.lower()
|
||||||
|
|
||||||
|
# Quest-related keywords
|
||||||
|
quest_keywords = [
|
||||||
|
"quest",
|
||||||
|
"quests",
|
||||||
|
"any work",
|
||||||
|
"work for me",
|
||||||
|
"job",
|
||||||
|
"jobs",
|
||||||
|
"task",
|
||||||
|
"tasks",
|
||||||
|
"help you",
|
||||||
|
"help with",
|
||||||
|
"need help",
|
||||||
|
"anything i can do",
|
||||||
|
"can i help",
|
||||||
|
"how can i help",
|
||||||
|
"i'd love to help",
|
||||||
|
"i would love to help",
|
||||||
|
"want to help",
|
||||||
|
"like to help",
|
||||||
|
"offer my services",
|
||||||
|
"hire me",
|
||||||
|
"bounty",
|
||||||
|
"bounties",
|
||||||
|
"adventure",
|
||||||
|
"mission",
|
||||||
|
"missions",
|
||||||
|
]
|
||||||
|
|
||||||
|
for keyword in quest_keywords:
|
||||||
|
if keyword in topic_lower:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
724
api/app/api/quests.py
Normal file
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",
|
||||||
|
)
|
||||||
@@ -132,23 +132,44 @@ def list_sessions():
|
|||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
user_id = user.id
|
user_id = user.id
|
||||||
session_service = get_session_service()
|
session_service = get_session_service()
|
||||||
|
character_service = get_character_service()
|
||||||
|
|
||||||
# Get user's active sessions
|
# Get user's active sessions
|
||||||
sessions = session_service.get_user_sessions(user_id, active_only=True)
|
sessions = session_service.get_user_sessions(user_id, active_only=True)
|
||||||
|
|
||||||
|
# Build character name lookup for efficiency
|
||||||
|
character_ids = [s.solo_character_id for s in sessions if s.solo_character_id]
|
||||||
|
character_names = {}
|
||||||
|
for char_id in character_ids:
|
||||||
|
try:
|
||||||
|
char = character_service.get_character(char_id, user_id)
|
||||||
|
if char:
|
||||||
|
character_names[char_id] = char.name
|
||||||
|
except Exception:
|
||||||
|
pass # Character may have been deleted
|
||||||
|
|
||||||
# Build response with basic session info
|
# Build response with basic session info
|
||||||
sessions_list = []
|
sessions_list = []
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
|
# Get combat round if in combat
|
||||||
|
combat_round = None
|
||||||
|
if session.is_in_combat() and session.combat_encounter:
|
||||||
|
combat_round = session.combat_encounter.round_number
|
||||||
|
|
||||||
sessions_list.append({
|
sessions_list.append({
|
||||||
'session_id': session.session_id,
|
'session_id': session.session_id,
|
||||||
'character_id': session.solo_character_id,
|
'character_id': session.solo_character_id,
|
||||||
|
'character_name': character_names.get(session.solo_character_id),
|
||||||
'turn_number': session.turn_number,
|
'turn_number': session.turn_number,
|
||||||
'status': session.status.value,
|
'status': session.status.value,
|
||||||
'created_at': session.created_at,
|
'created_at': session.created_at,
|
||||||
'last_activity': session.last_activity,
|
'last_activity': session.last_activity,
|
||||||
|
'in_combat': session.is_in_combat(),
|
||||||
'game_state': {
|
'game_state': {
|
||||||
'current_location': session.game_state.current_location,
|
'current_location': session.game_state.current_location,
|
||||||
'location_type': session.game_state.location_type.value
|
'location_type': session.game_state.location_type.value,
|
||||||
|
'in_combat': session.is_in_combat(),
|
||||||
|
'combat_round': combat_round
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -485,10 +506,12 @@ def get_session_state(session_id: str):
|
|||||||
"character_id": session.get_character_id(),
|
"character_id": session.get_character_id(),
|
||||||
"turn_number": session.turn_number,
|
"turn_number": session.turn_number,
|
||||||
"status": session.status.value,
|
"status": session.status.value,
|
||||||
|
"in_combat": session.is_in_combat(),
|
||||||
"game_state": {
|
"game_state": {
|
||||||
"current_location": session.game_state.current_location,
|
"current_location": session.game_state.current_location,
|
||||||
"location_type": session.game_state.location_type.value,
|
"location_type": session.game_state.location_type.value,
|
||||||
"active_quests": session.game_state.active_quests
|
"active_quests": session.game_state.active_quests,
|
||||||
|
"in_combat": session.is_in_combat()
|
||||||
},
|
},
|
||||||
"available_actions": available_actions
|
"available_actions": available_actions
|
||||||
})
|
})
|
||||||
|
|||||||
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"
|
ability_type: "spell"
|
||||||
base_power: 25
|
base_power: 25
|
||||||
damage_type: "holy"
|
damage_type: "holy"
|
||||||
scaling_stat: "intelligence"
|
scaling_stat: "wisdom"
|
||||||
scaling_factor: 0.5
|
scaling_factor: 0.5
|
||||||
mana_cost: 10
|
mana_cost: 10
|
||||||
cooldown: 0
|
cooldown: 0
|
||||||
|
|||||||
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: []
|
||||||
177
api/app/data/affixes/prefixes.yaml
Normal file
177
api/app/data/affixes/prefixes.yaml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Item Prefix Affixes
|
||||||
|
# Prefixes appear before the item name: "Flaming Dagger"
|
||||||
|
#
|
||||||
|
# Affix Structure:
|
||||||
|
# affix_id: Unique identifier
|
||||||
|
# name: Display name (what appears in the item name)
|
||||||
|
# affix_type: "prefix"
|
||||||
|
# tier: "minor" (RARE), "major" (EPIC), "legendary" (LEGENDARY only)
|
||||||
|
# description: Flavor text describing the effect
|
||||||
|
# stat_bonuses: Dict of stat_name -> bonus value
|
||||||
|
# defense_bonus: Direct defense bonus
|
||||||
|
# resistance_bonus: Direct resistance bonus
|
||||||
|
# damage_bonus: Flat damage bonus (weapons)
|
||||||
|
# damage_type: Elemental damage type
|
||||||
|
# elemental_ratio: Portion converted to elemental (0.0-1.0)
|
||||||
|
# crit_chance_bonus: Added to crit chance
|
||||||
|
# crit_multiplier_bonus: Added to crit multiplier
|
||||||
|
# allowed_item_types: [] = all types, or ["weapon", "armor"]
|
||||||
|
# required_rarity: null = any, or "legendary"
|
||||||
|
|
||||||
|
prefixes:
|
||||||
|
# ==================== ELEMENTAL PREFIXES (FIRE) ====================
|
||||||
|
flaming:
|
||||||
|
affix_id: "flaming"
|
||||||
|
name: "Flaming"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Imbued with fire magic, dealing bonus fire damage"
|
||||||
|
damage_type: "fire"
|
||||||
|
elemental_ratio: 0.25
|
||||||
|
damage_bonus: 3
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
blazing:
|
||||||
|
affix_id: "blazing"
|
||||||
|
name: "Blazing"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Wreathed in intense flames"
|
||||||
|
damage_type: "fire"
|
||||||
|
elemental_ratio: 0.35
|
||||||
|
damage_bonus: 6
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
# ==================== ELEMENTAL PREFIXES (ICE) ====================
|
||||||
|
frozen:
|
||||||
|
affix_id: "frozen"
|
||||||
|
name: "Frozen"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Enchanted with frost magic"
|
||||||
|
damage_type: "ice"
|
||||||
|
elemental_ratio: 0.25
|
||||||
|
damage_bonus: 3
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
glacial:
|
||||||
|
affix_id: "glacial"
|
||||||
|
name: "Glacial"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Encased in eternal ice"
|
||||||
|
damage_type: "ice"
|
||||||
|
elemental_ratio: 0.35
|
||||||
|
damage_bonus: 6
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
# ==================== ELEMENTAL PREFIXES (LIGHTNING) ====================
|
||||||
|
shocking:
|
||||||
|
affix_id: "shocking"
|
||||||
|
name: "Shocking"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Crackles with electrical energy"
|
||||||
|
damage_type: "lightning"
|
||||||
|
elemental_ratio: 0.25
|
||||||
|
damage_bonus: 3
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
thundering:
|
||||||
|
affix_id: "thundering"
|
||||||
|
name: "Thundering"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Charged with the power of storms"
|
||||||
|
damage_type: "lightning"
|
||||||
|
elemental_ratio: 0.35
|
||||||
|
damage_bonus: 6
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
# ==================== MATERIAL PREFIXES ====================
|
||||||
|
iron:
|
||||||
|
affix_id: "iron"
|
||||||
|
name: "Iron"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Reinforced with sturdy iron"
|
||||||
|
stat_bonuses:
|
||||||
|
constitution: 1
|
||||||
|
defense_bonus: 2
|
||||||
|
|
||||||
|
steel:
|
||||||
|
affix_id: "steel"
|
||||||
|
name: "Steel"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Forged from fine steel"
|
||||||
|
stat_bonuses:
|
||||||
|
constitution: 2
|
||||||
|
strength: 1
|
||||||
|
defense_bonus: 4
|
||||||
|
|
||||||
|
# ==================== QUALITY PREFIXES ====================
|
||||||
|
sharp:
|
||||||
|
affix_id: "sharp"
|
||||||
|
name: "Sharp"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Honed to a fine edge"
|
||||||
|
damage_bonus: 3
|
||||||
|
crit_chance_bonus: 0.02
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
keen:
|
||||||
|
affix_id: "keen"
|
||||||
|
name: "Keen"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Razor-sharp edge that finds weak points"
|
||||||
|
damage_bonus: 5
|
||||||
|
crit_chance_bonus: 0.04
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
|
||||||
|
# ==================== DEFENSIVE PREFIXES ====================
|
||||||
|
sturdy:
|
||||||
|
affix_id: "sturdy"
|
||||||
|
name: "Sturdy"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Built to withstand punishment"
|
||||||
|
defense_bonus: 3
|
||||||
|
allowed_item_types: ["armor"]
|
||||||
|
|
||||||
|
reinforced:
|
||||||
|
affix_id: "reinforced"
|
||||||
|
name: "Reinforced"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Heavily reinforced for maximum protection"
|
||||||
|
defense_bonus: 5
|
||||||
|
resistance_bonus: 2
|
||||||
|
allowed_item_types: ["armor"]
|
||||||
|
|
||||||
|
# ==================== LEGENDARY PREFIXES ====================
|
||||||
|
infernal:
|
||||||
|
affix_id: "infernal"
|
||||||
|
name: "Infernal"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "legendary"
|
||||||
|
description: "Burns with hellfire"
|
||||||
|
damage_type: "fire"
|
||||||
|
elemental_ratio: 0.45
|
||||||
|
damage_bonus: 12
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
required_rarity: "legendary"
|
||||||
|
|
||||||
|
vorpal:
|
||||||
|
affix_id: "vorpal"
|
||||||
|
name: "Vorpal"
|
||||||
|
affix_type: "prefix"
|
||||||
|
tier: "legendary"
|
||||||
|
description: "Cuts through anything with supernatural precision"
|
||||||
|
damage_bonus: 10
|
||||||
|
crit_chance_bonus: 0.08
|
||||||
|
crit_multiplier_bonus: 0.5
|
||||||
|
allowed_item_types: ["weapon"]
|
||||||
|
required_rarity: "legendary"
|
||||||
155
api/app/data/affixes/suffixes.yaml
Normal file
155
api/app/data/affixes/suffixes.yaml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Item Suffix Affixes
|
||||||
|
# Suffixes appear after the item name: "Dagger of Strength"
|
||||||
|
#
|
||||||
|
# Suffix naming convention:
|
||||||
|
# - Minor tier: "of [Stat]" (e.g., "of Strength")
|
||||||
|
# - Major tier: "of the [Animal/Element]" (e.g., "of the Bear")
|
||||||
|
# - Legendary tier: "of the [Mythical]" (e.g., "of the Titan")
|
||||||
|
|
||||||
|
suffixes:
|
||||||
|
# ==================== STAT SUFFIXES (MINOR) ====================
|
||||||
|
of_strength:
|
||||||
|
affix_id: "of_strength"
|
||||||
|
name: "of Strength"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants physical power"
|
||||||
|
stat_bonuses:
|
||||||
|
strength: 2
|
||||||
|
|
||||||
|
of_dexterity:
|
||||||
|
affix_id: "of_dexterity"
|
||||||
|
name: "of Dexterity"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants agility and precision"
|
||||||
|
stat_bonuses:
|
||||||
|
dexterity: 2
|
||||||
|
|
||||||
|
of_constitution:
|
||||||
|
affix_id: "of_constitution"
|
||||||
|
name: "of Fortitude"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants endurance"
|
||||||
|
stat_bonuses:
|
||||||
|
constitution: 2
|
||||||
|
|
||||||
|
of_intelligence:
|
||||||
|
affix_id: "of_intelligence"
|
||||||
|
name: "of Intelligence"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants magical aptitude"
|
||||||
|
stat_bonuses:
|
||||||
|
intelligence: 2
|
||||||
|
|
||||||
|
of_wisdom:
|
||||||
|
affix_id: "of_wisdom"
|
||||||
|
name: "of Wisdom"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants insight and perception"
|
||||||
|
stat_bonuses:
|
||||||
|
wisdom: 2
|
||||||
|
|
||||||
|
of_charisma:
|
||||||
|
affix_id: "of_charisma"
|
||||||
|
name: "of Charm"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants social influence"
|
||||||
|
stat_bonuses:
|
||||||
|
charisma: 2
|
||||||
|
|
||||||
|
of_luck:
|
||||||
|
affix_id: "of_luck"
|
||||||
|
name: "of Fortune"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Grants favor from fate"
|
||||||
|
stat_bonuses:
|
||||||
|
luck: 2
|
||||||
|
|
||||||
|
# ==================== ENHANCED STAT SUFFIXES (MAJOR) ====================
|
||||||
|
of_the_bear:
|
||||||
|
affix_id: "of_the_bear"
|
||||||
|
name: "of the Bear"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Grants the might and endurance of a bear"
|
||||||
|
stat_bonuses:
|
||||||
|
strength: 4
|
||||||
|
constitution: 2
|
||||||
|
|
||||||
|
of_the_fox:
|
||||||
|
affix_id: "of_the_fox"
|
||||||
|
name: "of the Fox"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Grants the cunning and agility of a fox"
|
||||||
|
stat_bonuses:
|
||||||
|
dexterity: 4
|
||||||
|
luck: 2
|
||||||
|
|
||||||
|
of_the_owl:
|
||||||
|
affix_id: "of_the_owl"
|
||||||
|
name: "of the Owl"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Grants the wisdom and insight of an owl"
|
||||||
|
stat_bonuses:
|
||||||
|
intelligence: 3
|
||||||
|
wisdom: 3
|
||||||
|
|
||||||
|
# ==================== DEFENSIVE SUFFIXES ====================
|
||||||
|
of_protection:
|
||||||
|
affix_id: "of_protection"
|
||||||
|
name: "of Protection"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "minor"
|
||||||
|
description: "Offers physical protection"
|
||||||
|
defense_bonus: 3
|
||||||
|
|
||||||
|
of_warding:
|
||||||
|
affix_id: "of_warding"
|
||||||
|
name: "of Warding"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "major"
|
||||||
|
description: "Wards against physical and magical harm"
|
||||||
|
defense_bonus: 5
|
||||||
|
resistance_bonus: 3
|
||||||
|
|
||||||
|
# ==================== LEGENDARY SUFFIXES ====================
|
||||||
|
of_the_titan:
|
||||||
|
affix_id: "of_the_titan"
|
||||||
|
name: "of the Titan"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "legendary"
|
||||||
|
description: "Grants titanic strength and endurance"
|
||||||
|
stat_bonuses:
|
||||||
|
strength: 8
|
||||||
|
constitution: 4
|
||||||
|
required_rarity: "legendary"
|
||||||
|
|
||||||
|
of_the_wind:
|
||||||
|
affix_id: "of_the_wind"
|
||||||
|
name: "of the Wind"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "legendary"
|
||||||
|
description: "Swift as the wind itself"
|
||||||
|
stat_bonuses:
|
||||||
|
dexterity: 8
|
||||||
|
luck: 4
|
||||||
|
crit_chance_bonus: 0.05
|
||||||
|
required_rarity: "legendary"
|
||||||
|
|
||||||
|
of_invincibility:
|
||||||
|
affix_id: "of_invincibility"
|
||||||
|
name: "of Invincibility"
|
||||||
|
affix_type: "suffix"
|
||||||
|
tier: "legendary"
|
||||||
|
description: "Grants supreme protection"
|
||||||
|
defense_bonus: 10
|
||||||
|
resistance_bonus: 8
|
||||||
|
required_rarity: "legendary"
|
||||||
152
api/app/data/base_items/armor.yaml
Normal file
152
api/app/data/base_items/armor.yaml
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# Base Armor Templates for Procedural Generation
|
||||||
|
#
|
||||||
|
# These templates define the foundation that affixes attach to.
|
||||||
|
# Example: "Leather Vest" + "Sturdy" prefix = "Sturdy Leather Vest"
|
||||||
|
#
|
||||||
|
# Armor categories:
|
||||||
|
# - Cloth: Low defense, high resistance (mages)
|
||||||
|
# - Leather: Balanced defense/resistance (rogues)
|
||||||
|
# - Chain: Medium defense, low resistance (versatile)
|
||||||
|
# - Plate: High defense, low resistance (warriors)
|
||||||
|
|
||||||
|
armor:
|
||||||
|
# ==================== CLOTH (MAGE ARMOR) ====================
|
||||||
|
cloth_robe:
|
||||||
|
template_id: "cloth_robe"
|
||||||
|
name: "Cloth Robe"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Simple cloth robes favored by spellcasters"
|
||||||
|
base_defense: 2
|
||||||
|
base_resistance: 5
|
||||||
|
base_value: 15
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.3
|
||||||
|
|
||||||
|
silk_robe:
|
||||||
|
template_id: "silk_robe"
|
||||||
|
name: "Silk Robe"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Fine silk robes that channel magical energy"
|
||||||
|
base_defense: 3
|
||||||
|
base_resistance: 8
|
||||||
|
base_value: 40
|
||||||
|
required_level: 3
|
||||||
|
drop_weight: 0.9
|
||||||
|
|
||||||
|
arcane_vestments:
|
||||||
|
template_id: "arcane_vestments"
|
||||||
|
name: "Arcane Vestments"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Robes woven with magical threads"
|
||||||
|
base_defense: 5
|
||||||
|
base_resistance: 12
|
||||||
|
base_value: 80
|
||||||
|
required_level: 5
|
||||||
|
drop_weight: 0.6
|
||||||
|
min_rarity: "uncommon"
|
||||||
|
|
||||||
|
# ==================== LEATHER (ROGUE ARMOR) ====================
|
||||||
|
leather_vest:
|
||||||
|
template_id: "leather_vest"
|
||||||
|
name: "Leather Vest"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Basic leather protection for agile fighters"
|
||||||
|
base_defense: 5
|
||||||
|
base_resistance: 2
|
||||||
|
base_value: 20
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.3
|
||||||
|
|
||||||
|
studded_leather:
|
||||||
|
template_id: "studded_leather"
|
||||||
|
name: "Studded Leather"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Leather armor reinforced with metal studs"
|
||||||
|
base_defense: 8
|
||||||
|
base_resistance: 3
|
||||||
|
base_value: 45
|
||||||
|
required_level: 3
|
||||||
|
drop_weight: 1.0
|
||||||
|
|
||||||
|
hardened_leather:
|
||||||
|
template_id: "hardened_leather"
|
||||||
|
name: "Hardened Leather"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Boiled and hardened leather for superior protection"
|
||||||
|
base_defense: 12
|
||||||
|
base_resistance: 5
|
||||||
|
base_value: 75
|
||||||
|
required_level: 5
|
||||||
|
drop_weight: 0.7
|
||||||
|
min_rarity: "uncommon"
|
||||||
|
|
||||||
|
# ==================== CHAIN (VERSATILE) ====================
|
||||||
|
chain_shirt:
|
||||||
|
template_id: "chain_shirt"
|
||||||
|
name: "Chain Shirt"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "A shirt of interlocking metal rings"
|
||||||
|
base_defense: 7
|
||||||
|
base_resistance: 2
|
||||||
|
base_value: 35
|
||||||
|
required_level: 2
|
||||||
|
drop_weight: 1.0
|
||||||
|
|
||||||
|
chainmail:
|
||||||
|
template_id: "chainmail"
|
||||||
|
name: "Chainmail"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Full chainmail armor covering torso and arms"
|
||||||
|
base_defense: 10
|
||||||
|
base_resistance: 3
|
||||||
|
base_value: 50
|
||||||
|
required_level: 3
|
||||||
|
drop_weight: 1.0
|
||||||
|
|
||||||
|
heavy_chainmail:
|
||||||
|
template_id: "heavy_chainmail"
|
||||||
|
name: "Heavy Chainmail"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Thick chainmail with reinforced rings"
|
||||||
|
base_defense: 14
|
||||||
|
base_resistance: 4
|
||||||
|
base_value: 85
|
||||||
|
required_level: 5
|
||||||
|
drop_weight: 0.7
|
||||||
|
min_rarity: "uncommon"
|
||||||
|
|
||||||
|
# ==================== PLATE (WARRIOR ARMOR) ====================
|
||||||
|
scale_mail:
|
||||||
|
template_id: "scale_mail"
|
||||||
|
name: "Scale Mail"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Overlapping metal scales on leather backing"
|
||||||
|
base_defense: 12
|
||||||
|
base_resistance: 2
|
||||||
|
base_value: 60
|
||||||
|
required_level: 4
|
||||||
|
drop_weight: 0.8
|
||||||
|
|
||||||
|
half_plate:
|
||||||
|
template_id: "half_plate"
|
||||||
|
name: "Half Plate"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Plate armor protecting vital areas"
|
||||||
|
base_defense: 16
|
||||||
|
base_resistance: 2
|
||||||
|
base_value: 120
|
||||||
|
required_level: 6
|
||||||
|
drop_weight: 0.5
|
||||||
|
min_rarity: "rare"
|
||||||
|
|
||||||
|
plate_armor:
|
||||||
|
template_id: "plate_armor"
|
||||||
|
name: "Plate Armor"
|
||||||
|
item_type: "armor"
|
||||||
|
description: "Full metal plate protection"
|
||||||
|
base_defense: 22
|
||||||
|
base_resistance: 3
|
||||||
|
base_value: 200
|
||||||
|
required_level: 7
|
||||||
|
drop_weight: 0.4
|
||||||
|
min_rarity: "rare"
|
||||||
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
|
||||||
227
api/app/data/base_items/weapons.yaml
Normal file
227
api/app/data/base_items/weapons.yaml
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# Base Weapon Templates for Procedural Generation
|
||||||
|
#
|
||||||
|
# These templates define the foundation that affixes attach to.
|
||||||
|
# Example: "Dagger" + "Flaming" prefix = "Flaming Dagger"
|
||||||
|
#
|
||||||
|
# Template Structure:
|
||||||
|
# template_id: Unique identifier
|
||||||
|
# name: Base item name
|
||||||
|
# item_type: "weapon"
|
||||||
|
# description: Flavor text
|
||||||
|
# base_damage: Weapon damage
|
||||||
|
# base_value: Gold value
|
||||||
|
# damage_type: "physical" (default)
|
||||||
|
# crit_chance: Critical hit chance (0.0-1.0)
|
||||||
|
# crit_multiplier: Crit damage multiplier
|
||||||
|
# required_level: Min level to use/drop
|
||||||
|
# drop_weight: Higher = more common (1.0 = standard)
|
||||||
|
# min_rarity: Minimum rarity for this template
|
||||||
|
|
||||||
|
weapons:
|
||||||
|
# ==================== ONE-HANDED SWORDS ====================
|
||||||
|
dagger:
|
||||||
|
template_id: "dagger"
|
||||||
|
name: "Dagger"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A small, quick blade for close combat"
|
||||||
|
base_damage: 6
|
||||||
|
base_value: 15
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.08
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.5
|
||||||
|
|
||||||
|
short_sword:
|
||||||
|
template_id: "short_sword"
|
||||||
|
name: "Short Sword"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A versatile one-handed blade"
|
||||||
|
base_damage: 10
|
||||||
|
base_value: 30
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.06
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.3
|
||||||
|
|
||||||
|
longsword:
|
||||||
|
template_id: "longsword"
|
||||||
|
name: "Longsword"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A standard warrior's blade"
|
||||||
|
base_damage: 14
|
||||||
|
base_value: 50
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.05
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 3
|
||||||
|
drop_weight: 1.0
|
||||||
|
|
||||||
|
# ==================== TWO-HANDED WEAPONS ====================
|
||||||
|
greatsword:
|
||||||
|
template_id: "greatsword"
|
||||||
|
name: "Greatsword"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A massive two-handed blade"
|
||||||
|
base_damage: 22
|
||||||
|
base_value: 100
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.04
|
||||||
|
crit_multiplier: 2.5
|
||||||
|
required_level: 5
|
||||||
|
drop_weight: 0.7
|
||||||
|
min_rarity: "uncommon"
|
||||||
|
|
||||||
|
# ==================== AXES ====================
|
||||||
|
hatchet:
|
||||||
|
template_id: "hatchet"
|
||||||
|
name: "Hatchet"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A small throwing axe"
|
||||||
|
base_damage: 8
|
||||||
|
base_value: 20
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.06
|
||||||
|
crit_multiplier: 2.2
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.2
|
||||||
|
|
||||||
|
battle_axe:
|
||||||
|
template_id: "battle_axe"
|
||||||
|
name: "Battle Axe"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A heavy axe designed for combat"
|
||||||
|
base_damage: 16
|
||||||
|
base_value: 60
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.05
|
||||||
|
crit_multiplier: 2.3
|
||||||
|
required_level: 4
|
||||||
|
drop_weight: 0.9
|
||||||
|
|
||||||
|
# ==================== BLUNT WEAPONS ====================
|
||||||
|
club:
|
||||||
|
template_id: "club"
|
||||||
|
name: "Club"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A simple wooden club"
|
||||||
|
base_damage: 7
|
||||||
|
base_value: 10
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.04
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.5
|
||||||
|
|
||||||
|
mace:
|
||||||
|
template_id: "mace"
|
||||||
|
name: "Mace"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A flanged mace for crushing armor"
|
||||||
|
base_damage: 12
|
||||||
|
base_value: 40
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.05
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 2
|
||||||
|
drop_weight: 1.0
|
||||||
|
|
||||||
|
# ==================== STAVES ====================
|
||||||
|
quarterstaff:
|
||||||
|
template_id: "quarterstaff"
|
||||||
|
name: "Quarterstaff"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A simple wooden staff"
|
||||||
|
base_damage: 6
|
||||||
|
base_value: 10
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.05
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.2
|
||||||
|
|
||||||
|
wizard_staff:
|
||||||
|
template_id: "wizard_staff"
|
||||||
|
name: "Wizard Staff"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A staff attuned to magical energy"
|
||||||
|
base_damage: 4
|
||||||
|
base_spell_power: 12
|
||||||
|
base_value: 45
|
||||||
|
damage_type: "arcane"
|
||||||
|
crit_chance: 0.05
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 3
|
||||||
|
drop_weight: 0.8
|
||||||
|
|
||||||
|
arcane_staff:
|
||||||
|
template_id: "arcane_staff"
|
||||||
|
name: "Arcane Staff"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A powerful staff pulsing with arcane power"
|
||||||
|
base_damage: 6
|
||||||
|
base_spell_power: 18
|
||||||
|
base_value: 90
|
||||||
|
damage_type: "arcane"
|
||||||
|
crit_chance: 0.06
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 5
|
||||||
|
drop_weight: 0.6
|
||||||
|
min_rarity: "uncommon"
|
||||||
|
|
||||||
|
# ==================== WANDS ====================
|
||||||
|
wand:
|
||||||
|
template_id: "wand"
|
||||||
|
name: "Wand"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A simple magical focus"
|
||||||
|
base_damage: 2
|
||||||
|
base_spell_power: 8
|
||||||
|
base_value: 30
|
||||||
|
damage_type: "arcane"
|
||||||
|
crit_chance: 0.06
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.0
|
||||||
|
|
||||||
|
crystal_wand:
|
||||||
|
template_id: "crystal_wand"
|
||||||
|
name: "Crystal Wand"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A wand topped with a magical crystal"
|
||||||
|
base_damage: 3
|
||||||
|
base_spell_power: 14
|
||||||
|
base_value: 60
|
||||||
|
damage_type: "arcane"
|
||||||
|
crit_chance: 0.07
|
||||||
|
crit_multiplier: 2.2
|
||||||
|
required_level: 4
|
||||||
|
drop_weight: 0.8
|
||||||
|
|
||||||
|
# ==================== RANGED ====================
|
||||||
|
shortbow:
|
||||||
|
template_id: "shortbow"
|
||||||
|
name: "Shortbow"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A compact bow for quick shots"
|
||||||
|
base_damage: 8
|
||||||
|
base_value: 25
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.07
|
||||||
|
crit_multiplier: 2.0
|
||||||
|
required_level: 1
|
||||||
|
drop_weight: 1.1
|
||||||
|
|
||||||
|
longbow:
|
||||||
|
template_id: "longbow"
|
||||||
|
name: "Longbow"
|
||||||
|
item_type: "weapon"
|
||||||
|
description: "A powerful bow with excellent range"
|
||||||
|
base_damage: 14
|
||||||
|
base_value: 55
|
||||||
|
damage_type: "physical"
|
||||||
|
crit_chance: 0.08
|
||||||
|
crit_multiplier: 2.2
|
||||||
|
required_level: 4
|
||||||
|
drop_weight: 0.9
|
||||||
@@ -8,7 +8,7 @@ description: >
|
|||||||
excel in devastating spell damage, capable of incinerating groups of foes or freezing
|
excel in devastating spell damage, capable of incinerating groups of foes or freezing
|
||||||
enemies in place. Choose your element: embrace the flames or command the frost.
|
enemies in place. Choose your element: embrace the flames or command the frost.
|
||||||
|
|
||||||
# Base stats (total: 65)
|
# Base stats (total: 65 + luck)
|
||||||
base_stats:
|
base_stats:
|
||||||
strength: 8 # Low physical power
|
strength: 8 # Low physical power
|
||||||
dexterity: 10 # Average agility
|
dexterity: 10 # Average agility
|
||||||
@@ -16,11 +16,12 @@ base_stats:
|
|||||||
intelligence: 15 # Exceptional magical power
|
intelligence: 15 # Exceptional magical power
|
||||||
wisdom: 12 # Above average perception
|
wisdom: 12 # Above average perception
|
||||||
charisma: 11 # Above average social
|
charisma: 11 # Above average social
|
||||||
|
luck: 9 # Slight chaos magic boost
|
||||||
|
|
||||||
starting_equipment:
|
starting_equipment:
|
||||||
- worn_staff
|
- worn_staff
|
||||||
- cloth_armor
|
- cloth_armor
|
||||||
- rusty_knife
|
- health_potion_small
|
||||||
|
|
||||||
starting_abilities:
|
starting_abilities:
|
||||||
- basic_attack
|
- basic_attack
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ description: >
|
|||||||
capable of becoming an elusive phantom or a master of critical strikes. Choose your path: embrace
|
capable of becoming an elusive phantom or a master of critical strikes. Choose your path: embrace
|
||||||
the shadows or perfect the killing blow.
|
the shadows or perfect the killing blow.
|
||||||
|
|
||||||
# Base stats (total: 65)
|
# Base stats (total: 65 + luck)
|
||||||
base_stats:
|
base_stats:
|
||||||
strength: 11 # Above average physical power
|
strength: 11 # Above average physical power
|
||||||
dexterity: 15 # Exceptional agility
|
dexterity: 15 # Exceptional agility
|
||||||
@@ -16,11 +16,12 @@ base_stats:
|
|||||||
intelligence: 9 # Below average magic
|
intelligence: 9 # Below average magic
|
||||||
wisdom: 10 # Average perception
|
wisdom: 10 # Average perception
|
||||||
charisma: 10 # Average social
|
charisma: 10 # Average social
|
||||||
|
luck: 12 # High luck for crits and precision
|
||||||
|
|
||||||
starting_equipment:
|
starting_equipment:
|
||||||
- rusty_dagger
|
- rusty_dagger
|
||||||
- cloth_armor
|
- cloth_armor
|
||||||
- rusty_knife
|
- health_potion_small
|
||||||
|
|
||||||
starting_abilities:
|
starting_abilities:
|
||||||
- basic_attack
|
- basic_attack
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user