Compare commits

..

43 Commits

Author SHA1 Message Date
32af625d14 Merge branch 'feat/Phase4-Skilltrees' into dev 2025-11-28 22:03:16 -06:00
8784fbaa88 Phase 4b Abilities and skill trees is finished 2025-11-28 22:02:57 -06:00
a8767b34e2 adding abilities, created skill tree template and unlock mechanics 2025-11-28 21:41:46 -06:00
d9bc46adc1 general cleanup 2025-11-28 10:43:58 -06:00
45cfa25911 adding more enemies 2025-11-28 10:37:57 -06:00
7c0e257540 Merge pull request 'feat/phase4-combat-foundation' (#8) from feat/phase4-combat-foundation into dev
Reviewed-on: #8
2025-11-28 04:21:19 +00:00
6d3fb63355 Combat foundation complete 2025-11-27 22:18:58 -06:00
dd92cf5991 combat testing and polishing in the dev console, many bug fixes 2025-11-27 20:37:53 -06:00
94c4ca9e95 updating docs 2025-11-27 11:51:21 -06:00
19b537d8b0 updating docs 2025-11-27 11:50:06 -06:00
58f0c1b8f6 trimming phase 4 planning doc 2025-11-27 00:10:26 -06:00
29b4853c84 updating docs 2025-11-27 00:05:33 -06:00
fdd48034e4 feat(api): implement combat loot integration with hybrid static/procedural system
Add CombatLootService that orchestrates loot generation from combat,
supporting both static item drops (consumables, materials) and procedural
equipment generation (weapons, armor with affixes).

Key changes:
- Extend LootEntry model with LootType enum (STATIC/PROCEDURAL)
- Create StaticItemLoader service for consumables/materials from YAML
- Create CombatLootService with full rarity formula incorporating:
  - Party average level
  - Enemy difficulty tier (EASY: +0%, MEDIUM: +5%, HARD: +15%, BOSS: +30%)
  - Character luck stat
  - Per-entry rarity bonus
- Integrate with CombatService._calculate_rewards() for automatic loot gen
- Add boss guaranteed drops via generate_boss_loot()

New enemy variants (goblin family proof-of-concept):
- goblin_scout (Easy) - static drops only
- goblin_warrior (Medium) - static + procedural weapon drops
- goblin_chieftain (Hard) - static + procedural weapon/armor drops

Static items added:
- consumables.yaml: health/mana potions, elixirs, food
- materials.yaml: trophy items, crafting materials

Tests: 59 new tests across 3 test files (all passing)
2025-11-27 00:01:17 -06:00
a38906b445 feat(api): integrate equipment stats into combat damage system
Equipment-Combat Integration:
- Update Stats damage formula from STR//2 to int(STR*0.75) for better scaling
- Add spell_power system for magical weapons (staves, wands)
- Add spell_power_bonus field to Stats model with spell_power property
- Add spell_power field to Item model with is_magical_weapon() method
- Update Character.get_effective_stats() to populate spell_power_bonus

Combatant Model Updates:
- Add weapon property fields (crit_chance, crit_multiplier, damage_type)
- Add elemental weapon support (elemental_damage_type, physical_ratio, elemental_ratio)
- Update serialization to handle new weapon properties

DamageCalculator Refactoring:
- Remove weapon_damage parameter from calculate_physical_damage()
- Use attacker_stats.damage directly (includes weapon bonus)
- Use attacker_stats.spell_power for magical damage calculations

Combat Service Updates:
- Extract weapon properties in _create_combatant_from_character()
- Use stats.damage_bonus for enemy combatants from templates
- Remove hardcoded _get_weapon_damage() method
- Handle elemental weapons with split damage in _execute_attack()

Item Generation Updates:
- Add base_spell_power to BaseItemTemplate dataclass
- Add ARCANE damage type to DamageType enum
- Add magical weapon templates (wizard_staff, arcane_staff, wand, crystal_wand)

Test Updates:
- Update test_stats.py for new damage formula (0.75 scaling)
- Update test_character.py for equipment bonus calculations
- Update test_damage_calculator.py for new API signatures
- Update test_combat_service.py mock fixture for equipped attribute

Tests: 174 passing
2025-11-26 19:54:58 -06:00
4ced1b04df feat(api): implement inventory API endpoints
Add REST API endpoints for character inventory management:
- GET /api/v1/characters/<id>/inventory - Get inventory and equipped items
- POST /api/v1/characters/<id>/inventory/equip - Equip item to slot
- POST /api/v1/characters/<id>/inventory/unequip - Unequip from slot
- POST /api/v1/characters/<id>/inventory/use - Use consumable item
- DELETE /api/v1/characters/<id>/inventory/<item_id> - Drop item

All endpoints include:
- Authentication via @require_auth decorator
- Ownership validation through CharacterService
- Comprehensive error handling with proper HTTP status codes
- Full logging for debugging

Includes 25 integration tests covering authentication requirements,
URL patterns, and response formats.

Task 2.4 of Phase 4 Combat Implementation complete.
2025-11-26 18:54:33 -06:00
76f67c4a22 feat(api): implement inventory service with equipment system
Add InventoryService for managing character inventory, equipment, and
consumable usage. Key features:

- Add/remove items with inventory capacity checks
- Equipment slot validation (weapon, off_hand, helmet, chest, gloves,
  boots, accessory_1, accessory_2)
- Level and class requirement validation for equipment
- Consumable usage with instant and duration-based effects
- Combat-specific consumable method returning effects for combat system
- Bulk operations (add_items, get_items_by_type, get_equippable_items)

Design decision: Uses full Item object storage (not IDs) to support
procedurally generated items with unique identifiers.

Files added:
- /api/app/services/inventory_service.py (560 lines)
- /api/tests/test_inventory_service.py (51 tests passing)

Task 2.3 of Phase 4 Combat Implementation complete.
2025-11-26 18:38:39 -06:00
185be7fee0 feat(api): implement Diablo-style item affix system
Add procedural item generation with affix naming system:
- Items with RARE/EPIC/LEGENDARY rarity get dynamic names
- Prefixes (e.g., "Flaming") add elemental damage, material bonuses
- Suffixes (e.g., "of Strength") add stat bonuses
- Affix count scales with rarity: RARE=1, EPIC=2, LEGENDARY=3

New files:
- models/affixes.py: Affix and BaseItemTemplate dataclasses
- services/affix_loader.py: YAML-based affix pool loading
- services/base_item_loader.py: Base item template loading
- services/item_generator.py: Main procedural generation service
- data/affixes/prefixes.yaml: 14 prefix definitions
- data/affixes/suffixes.yaml: 15 suffix definitions
- data/base_items/weapons.yaml: 12 weapon templates
- data/base_items/armor.yaml: 12 armor templates
- tests/test_item_generator.py: 34 comprehensive tests

Modified:
- enums.py: Added AffixType and AffixTier enums
- items.py: Added affix tracking fields (applied_affixes, generated_name)

Example output: "Frozen Dagger of the Bear" (EPIC with ice damage + STR/CON)
2025-11-26 17:57:34 -06:00
f3ac0c8647 feat(api): add ItemRarity enum to item system
- Add ItemRarity enum with 5 tiers (common, uncommon, rare, epic, legendary)
- Add rarity field to Item dataclass with COMMON default
- Update Item serialization (to_dict/from_dict) for rarity
- Export ItemRarity from models package
- Add 24 comprehensive unit tests for Item and ItemRarity

Part of Phase 4 Week 2: Inventory & Equipment System (Task 2.1)
2025-11-26 16:14:29 -06:00
03ab783eeb Combat Backend & Data Models
- Implement Combat Service
- Implement Damage Calculator
- Implement Effect Processor
- Implement Combat Actions
- Created Combat API Endpoints
2025-11-26 15:43:20 -06:00
30c3b800e6 feat(api): add luck (LUK) stat to character system
Add new Luck stat to the character stats system with class-specific values:
- Assassin: 12 (highest - critical specialists)
- Luminary: 11 (divine favor)
- Wildstrider/Lorekeeper: 10 (average)
- Arcanist/Oathkeeper: 9 (modest)
- Vanguard: 8 (default - relies on strength)
- Necromancer: 7 (lowest - dark arts cost)

Changes:
- Add luck field to Stats dataclass with default of 8
- Add LUCK to StatType enum
- Update all 8 class YAML files with luck values
- Display LUK in character panel (play page) and detail page
- Update DATA_MODELS.md documentation

Backward compatible: existing characters without luck default to 8.
2025-11-26 12:27:18 -06:00
d789b5df65 planning and docs update 2025-11-26 11:35:18 -06:00
e6e7cdb7b7 Merge pull request 'fix(api): delete orphaned sessions when character is deleted' (#7) from bug/orphaned-sessions-on-char-delete into dev
Reviewed-on: #7
2025-11-26 16:47:06 +00:00
98bb6ab589 fix(api): delete orphaned sessions when character is deleted
- Add delete_sessions_by_character() method to SessionService that
  cleans up all game sessions associated with a character
- Update delete_character() to hard delete instead of soft delete
- Call session cleanup before deleting character to prevent orphaned data
- Delete associated chat messages when cleaning up sessions
- Update API documentation to reflect new behavior

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:46:35 -06:00
1b21465dc4 Merge pull request 'feat(web): add navigation menu bar for logged-in users' (#6) from feat/menu-bar into dev
Reviewed-on: #6
2025-11-26 16:22:25 +00:00
77d913fe50 feat(web): add navigation menu bar for logged-in users
- Add horizontal nav menu with 7 items: Profile, Characters, Sessions,
  Mechanics, Leaderboard, Settings, Help
- Implement responsive hamburger menu for mobile (≤768px)
- Create pages blueprint with stub routes for new pages
- Add "Coming Soon" styled stub templates with icons
- Include active state highlighting for current page

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:21:46 -06:00
4d26c43d1d Merge pull request 'feat/session-management' (#5) from feat/session-management into dev
Reviewed-on: #5
2025-11-26 16:08:42 +00:00
51f6041ee4 fix(api): remove reference to non-existent TIER_LIMITS attribute
The RateLimiterService.__init__ was logging self.TIER_LIMITS which doesn't
exist after refactoring to config-based tier limits. Changed to log the
existing DM_QUESTION_LIMITS attribute instead.
2025-11-26 10:07:35 -06:00
19808dd44c docs: update rate limit values to match config-based system
- Update USAGE_TRACKING.md with new tier limits (50, 200, 1000, unlimited)
- Update AI_INTEGRATION.md with new tier limits
- Add note that limits are loaded from config (ai_calls_per_day)
- Document GET /api/v1/usage endpoint
- Update examples to show is_unlimited field
- Fix test examples with correct limit values

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:02:30 -06:00
61a42d3a77 feat(api,web): tier-based session limits and daily turn usage display
Backend Changes:
- Add tier-based max_sessions config (free: 1, basic: 2, premium: 3, elite: 5)
- Add DELETE /api/v1/sessions/{id} endpoint for hard session deletion
- Cascade delete chat messages when session is deleted
- Add GET /api/v1/usage endpoint for daily turn limit info
- Replace hardcoded TIER_LIMITS with config-based ai_calls_per_day
- Handle unlimited (-1) tier in rate limiter service

Frontend Changes:
- Add inline session delete buttons with HTMX on character list
- Add usage_display.html component showing remaining daily turns
- Display usage indicator on character list and game play pages
- Page refresh after session deletion to update UI state

Documentation:
- Update API_REFERENCE.md with new endpoints and tier limits
- Update API_TESTING.md with session endpoint examples
- Update SESSION_MANAGEMENT.md with tier-based limits

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:00:45 -06:00
0a7156504f fixed usernames name at the top of the page when logged in 2025-11-26 09:04:26 -06:00
8312cfe13f Merge pull request 'fix(web): restore 3-column NPC chat modal layout for desktop' (#4) from bug/non-mobile-ui-fix-for-npcs into dev
Reviewed-on: #4
2025-11-26 04:17:13 +00:00
16171dc34a fix(web): restore 3-column NPC chat modal layout for desktop
- Fixed grid layout not applying to modal content (grid was on wrong parent element)
- Applied grid to .npc-chat-container instead of .npc-modal-body--three-col
- Removed htmx-indicator class from history panel (was causing content to disappear)
- Made history loading indicator visible by default
- Updated responsive breakpoints to target correct selectors
- Added warning to HTMX_PATTERNS.md about htmx-indicator hidden behavior

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 22:16:33 -06:00
52b199ff10 Merge pull request 'feat(api): add Redis session cache to reduce Appwrite API calls by ~90%' (#3) from feat/optimize-api-auth-calls into dev
Reviewed-on: #3
2025-11-26 04:01:53 +00:00
8675f9bf75 feat(api): add Redis session cache to reduce Appwrite API calls by ~90%
- Add SessionCacheService with 5-minute TTL Redis cache
- Cache validated sessions to avoid redundant Appwrite calls
- Add /api/v1/auth/me endpoint for retrieving current user
- Invalidate cache on logout and password reset
- Add session_cache config to auth section (Redis db 2)
- Fix Docker Redis hostname (localhost -> redis)
- Handle timezone-aware datetime comparisons

Security: tokens hashed before use as cache keys, explicit
invalidation on logout/password change, graceful degradation
when Redis unavailable.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 22:01:14 -06:00
a0635499a7 Merge pull request 'feat/chat-history-upgrade' (#2) from feat/chat-history-upgrade into dev
Reviewed-on: #2
2025-11-26 03:32:06 +00:00
2419dbeb34 feat(web): implement responsive modal pattern for mobile-friendly NPC chat
- Add hybrid modal/page navigation based on screen size (1024px breakpoint)
- Desktop (>1024px): Uses modal overlays for quick interactions
- Mobile (≤1024px): Navigates to dedicated full pages for better UX
- Extract shared NPC chat content into reusable partial template
- Add responsive navigation JavaScript (responsive-modals.js)
- Create dedicated NPC chat page route with back button navigation
- Add mobile-optimized CSS with sticky header and chat input
- Fix HTMX indicator errors by using htmx-indicator class pattern
- Document responsive modal pattern for future features

Addresses mobile UX issues: cramped space, nested scrolling, keyboard conflicts,
and lack of native back button support in modals.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 21:30:51 -06:00
196346165f chat history with the NPC modal 2025-11-25 21:16:01 -06:00
20cb279793 fix: resolve NPC chat database persistence and modal targeting
Fixed two critical bugs in NPC chat functionality:

  1. Database Persistence - Metadata serialization bug
     - Empty dict {} was falsy, preventing JSON conversion
     - Changed to unconditional serialization in ChatMessageService
     - Messages now successfully save to chat_messages collection

  2. Modal Targeting - HTMX targeting lost during polling
     - poll_job() wasn't preserving hx-target/hx-swap parameters
     - Pass targeting params through query string in polling cycle
     - Responses now correctly load in modal instead of main panel

  Files modified:
  - api/app/services/chat_message_service.py
  - public_web/templates/game/partials/job_polling.html
  - public_web/app/views/game_views.py
2025-11-25 20:44:24 -06:00
4353d112f4 feat(api): implement unlimited chat history system with hybrid storage
Replaces 10-message cap dialogue_history with scalable chat_messages collection.

New Features:
- Unlimited conversation history in dedicated chat_messages collection
- Hybrid storage: recent 3 messages cached in character docs for AI context
- 4 new REST API endpoints: conversations summary, full history, search, soft delete
- Full-text search with filters (NPC, context, date range)
- Quest and faction tracking ready via context enum and metadata field
- Soft delete support for privacy/moderation

Technical Changes:
- Created ChatMessage model with MessageContext enum
- Created ChatMessageService with 5 core methods
- Added chat_messages Appwrite collection with 5 composite indexes
- Updated NPC dialogue task to save to chat_messages
- Updated CharacterService.get_npc_dialogue_history() with backward compatibility
- Created /api/v1/characters/{char_id}/chats API endpoints
- Registered chat blueprint in Flask app

Documentation:
- Updated API_REFERENCE.md with 4 new endpoints
- Updated DATA_MODELS.md with ChatMessage model and NPCInteractionState changes
- Created comprehensive CHAT_SYSTEM.md architecture documentation

Performance:
- 50x faster AI context retrieval (reads from cache, no DB query)
- 67% reduction in character document size
- Query performance O(log n) with indexed searches

Backward Compatibility:
- dialogue_history field maintained during transition
- Graceful fallback for old character documents
- No forced migration required

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 16:32:21 -06:00
9c6eb770e5 Merge pull request 'feat/playscreen' (#1) from feat/playscreen into dev
Reviewed-on: #1
2025-11-25 22:06:06 +00:00
aaa69316c2 docs update 2025-11-25 16:05:42 -06:00
bda363de76 added npc images to API and frontend 2025-11-25 15:52:22 -06:00
e198d9ac8a api docs update 2025-11-24 23:20:27 -06:00
289 changed files with 38027 additions and 2233 deletions

1
.gitignore vendored
View File

@@ -35,3 +35,4 @@ Thumbs.db
logs/ logs/
app/logs/ app/logs/
*.log *.log
CLAUDE.md

View File

@@ -164,8 +164,22 @@ def register_blueprints(app: Flask) -> None:
app.register_blueprint(npcs_bp) app.register_blueprint(npcs_bp)
logger.info("NPCs API blueprint registered") logger.info("NPCs API blueprint registered")
# Import and register Chat API blueprint
from app.api.chat import chat_bp
app.register_blueprint(chat_bp)
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")
# 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, shop
# 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') # app.register_blueprint(shop.bp, url_prefix='/api/v1/shop')

View File

@@ -15,6 +15,7 @@ from flask import Blueprint, request, make_response, render_template, redirect,
from appwrite.exception import AppwriteException from appwrite.exception import AppwriteException
from app.services.appwrite_service import AppwriteService from app.services.appwrite_service import AppwriteService
from app.services.session_cache_service import SessionCacheService
from app.utils.response import ( from app.utils.response import (
success_response, success_response,
created_response, created_response,
@@ -305,7 +306,11 @@ def api_logout():
if not token: if not token:
return unauthorized_response(message="No active session") return unauthorized_response(message="No active session")
# Logout user # Invalidate session cache before Appwrite logout
cache = SessionCacheService()
cache.invalidate_token(token)
# Logout user from Appwrite
appwrite = AppwriteService() appwrite = AppwriteService()
appwrite.logout_user(session_id=token) appwrite.logout_user(session_id=token)
@@ -340,6 +345,36 @@ def api_logout():
return error_response(message="An unexpected error occurred", code="INTERNAL_ERROR") return error_response(message="An unexpected error occurred", code="INTERNAL_ERROR")
@auth_bp.route('/api/v1/auth/me', methods=['GET'])
@require_auth
def api_get_current_user():
"""
Get the currently authenticated user's data.
This endpoint is lightweight and uses cached session data when available,
making it suitable for frequent use (e.g., checking user tier, verifying
session is still valid).
Returns:
200: User data
401: Not authenticated
"""
user = get_current_user()
if not user:
return unauthorized_response(message="Not authenticated")
return success_response(
result={
"id": user.id,
"email": user.email,
"name": user.name,
"email_verified": user.email_verified,
"tier": user.tier
}
)
@auth_bp.route('/api/v1/auth/verify-email', methods=['GET']) @auth_bp.route('/api/v1/auth/verify-email', methods=['GET'])
def api_verify_email(): def api_verify_email():
""" """
@@ -480,6 +515,10 @@ def api_reset_password():
appwrite = AppwriteService() appwrite = AppwriteService()
appwrite.confirm_password_reset(user_id=user_id, secret=secret, password=password) appwrite.confirm_password_reset(user_id=user_id, secret=secret, password=password)
# Invalidate all cached sessions for this user (security: password changed)
cache = SessionCacheService()
cache.invalidate_user(user_id)
logger.info("Password reset successfully", user_id=user_id) logger.info("Password reset successfully", user_id=user_id)
return success_response( return success_response(

320
api/app/api/chat.py Normal file
View File

@@ -0,0 +1,320 @@
"""
Chat API Endpoints.
Provides REST API for accessing player-NPC conversation history,
searching messages, and managing chat data.
"""
from flask import Blueprint, request
from datetime import datetime, timezone
from app.utils.auth import require_auth, get_current_user
from app.services.chat_message_service import (
get_chat_message_service,
ChatMessageNotFound,
ChatMessagePermissionDenied
)
from app.services.npc_loader import get_npc_loader
from app.models.chat_message import MessageContext
from app.utils.response import (
success_response,
error_response,
not_found_response,
validation_error_response
)
from app.utils.logging import get_logger
logger = get_logger(__file__)
chat_bp = Blueprint('chat', __name__)
@chat_bp.route('/api/v1/characters/<character_id>/chats', methods=['GET'])
@require_auth
def get_conversations_summary(character_id: str):
"""
Get summary of all NPC conversations for a character.
Returns list of NPCs the character has talked to, with message counts,
last message timestamp, and preview of most recent exchange.
Path params:
character_id: Character ID
Query params:
None
Returns:
JSON response with list of conversation summaries
"""
try:
user = get_current_user()
# Get conversation summaries
chat_service = get_chat_message_service()
summaries = chat_service.get_all_conversations_summary(character_id, user.id)
# Convert to dict for JSON response
conversations = [summary.to_dict() for summary in summaries]
logger.info("Retrieved conversation summaries",
user_id=user.id,
character_id=character_id,
count=len(conversations))
return success_response({"conversations": conversations})
except ChatMessagePermissionDenied as e:
logger.warning("Permission denied getting conversations",
user_id=user.id if 'user' in locals() else None,
character_id=character_id)
return error_response(str(e), 403)
except Exception as e:
logger.error("Failed to get conversations summary",
character_id=character_id,
error=str(e))
return error_response(f"Failed to retrieve conversations: {str(e)}", 500)
@chat_bp.route('/api/v1/characters/<character_id>/chats/<npc_id>', methods=['GET'])
@require_auth
def get_conversation_history(character_id: str, npc_id: str):
"""
Get full conversation history between character and specific NPC.
Returns paginated list of messages ordered by timestamp DESC (most recent first).
Path params:
character_id: Character ID
npc_id: NPC ID
Query params:
limit: Maximum messages to return (default 50, max 100)
offset: Number of messages to skip (default 0)
Returns:
JSON response with messages and pagination info
"""
try:
user = get_current_user()
# Get query parameters
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
# Validate parameters
if limit < 1:
return validation_error_response("limit must be at least 1")
if offset < 0:
return validation_error_response("offset must be at least 0")
# Get conversation history
chat_service = get_chat_message_service()
messages = chat_service.get_conversation_history(
character_id=character_id,
user_id=user.id,
npc_id=npc_id,
limit=limit,
offset=offset
)
# Get NPC name if available
npc_loader = get_npc_loader()
npc = npc_loader.load_npc(npc_id)
npc_name = npc.name if npc else npc_id.replace('_', ' ').title()
# Convert messages to dict
message_dicts = [msg.to_dict() for msg in messages]
result = {
"npc_id": npc_id,
"npc_name": npc_name,
"total_messages": len(messages), # Count in this batch
"messages": message_dicts,
"pagination": {
"limit": limit,
"offset": offset,
"has_more": len(messages) == limit # If we got a full batch, there might be more
}
}
logger.info("Retrieved conversation history",
user_id=user.id,
character_id=character_id,
npc_id=npc_id,
count=len(messages))
return success_response(result)
except ChatMessagePermissionDenied as e:
logger.warning("Permission denied getting conversation",
user_id=user.id if 'user' in locals() else None,
character_id=character_id,
npc_id=npc_id)
return error_response(str(e), 403)
except Exception as e:
logger.error("Failed to get conversation history",
character_id=character_id,
npc_id=npc_id,
error=str(e))
return error_response(f"Failed to retrieve conversation: {str(e)}", 500)
@chat_bp.route('/api/v1/characters/<character_id>/chats/search', methods=['GET'])
@require_auth
def search_messages(character_id: str):
"""
Search messages by text with optional filters.
Query params:
q (required): Search text to find in player_message and npc_response
npc_id (optional): Filter by specific NPC
context (optional): Filter by message context (dialogue, quest_offered, shop, etc.)
date_from (optional): Start date ISO format (e.g., 2025-11-25T00:00:00Z)
date_to (optional): End date ISO format
limit (optional): Maximum messages to return (default 50, max 100)
offset (optional): Number of messages to skip (default 0)
Returns:
JSON response with matching messages
"""
try:
user = get_current_user()
# Get query parameters
search_text = request.args.get('q')
npc_id = request.args.get('npc_id')
context_str = request.args.get('context')
date_from = request.args.get('date_from')
date_to = request.args.get('date_to')
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
# Validate required parameters
if not search_text:
return validation_error_response("q (search text) is required")
# Validate optional parameters
if limit < 1:
return validation_error_response("limit must be at least 1")
if offset < 0:
return validation_error_response("offset must be at least 0")
# Parse context enum if provided
context = None
if context_str:
try:
context = MessageContext(context_str)
except ValueError:
valid_contexts = [c.value for c in MessageContext]
return validation_error_response(
f"Invalid context. Must be one of: {', '.join(valid_contexts)}"
)
# Search messages
chat_service = get_chat_message_service()
messages = chat_service.search_messages(
character_id=character_id,
user_id=user.id,
search_text=search_text,
npc_id=npc_id,
context=context,
date_from=date_from,
date_to=date_to,
limit=limit,
offset=offset
)
# Convert messages to dict
message_dicts = [msg.to_dict() for msg in messages]
result = {
"search_text": search_text,
"filters": {
"npc_id": npc_id,
"context": context_str,
"date_from": date_from,
"date_to": date_to
},
"total_results": len(messages),
"messages": message_dicts,
"pagination": {
"limit": limit,
"offset": offset,
"has_more": len(messages) == limit
}
}
logger.info("Search completed",
user_id=user.id,
character_id=character_id,
search_text=search_text,
results=len(messages))
return success_response(result)
except ChatMessagePermissionDenied as e:
logger.warning("Permission denied searching messages",
user_id=user.id if 'user' in locals() else None,
character_id=character_id)
return error_response(str(e), 403)
except Exception as e:
logger.error("Failed to search messages",
character_id=character_id,
error=str(e))
return error_response(f"Search failed: {str(e)}", 500)
@chat_bp.route('/api/v1/characters/<character_id>/chats/<message_id>', methods=['DELETE'])
@require_auth
def delete_message(character_id: str, message_id: str):
"""
Soft delete a message (sets is_deleted=True).
Used for privacy/moderation. Message remains in database but filtered from queries.
Path params:
character_id: Character ID (for ownership validation)
message_id: Message ID to delete
Returns:
JSON response with success confirmation
"""
try:
user = get_current_user()
# Soft delete message
chat_service = get_chat_message_service()
success = chat_service.soft_delete_message(
message_id=message_id,
character_id=character_id,
user_id=user.id
)
logger.info("Message deleted",
user_id=user.id,
character_id=character_id,
message_id=message_id)
return success_response({
"message_id": message_id,
"deleted": success
})
except ChatMessageNotFound as e:
logger.warning("Message not found for deletion",
message_id=message_id,
character_id=character_id)
return not_found_response(str(e))
except ChatMessagePermissionDenied as e:
logger.warning("Permission denied deleting message",
user_id=user.id if 'user' in locals() else None,
character_id=character_id,
message_id=message_id)
return error_response(str(e), 403)
except Exception as e:
logger.error("Failed to delete message",
message_id=message_id,
character_id=character_id,
error=str(e))
return error_response(f"Delete failed: {str(e)}", 500)

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

View File

@@ -281,6 +281,7 @@ def get_npcs_at_location(location_id: str):
"role": npc.role, "role": npc.role,
"appearance": npc.appearance.brief, "appearance": npc.appearance.brief,
"tags": npc.tags, "tags": npc.tags,
"image_url": npc.image_url,
}) })
return success_response({ return success_response({

View File

@@ -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
} }
}) })
@@ -235,7 +256,7 @@ def create_session():
return error_response( return error_response(
status=409, status=409,
code="SESSION_LIMIT_EXCEEDED", code="SESSION_LIMIT_EXCEEDED",
message="Maximum active sessions limit reached (5). Please end an existing session first." message=str(e)
) )
except Exception as e: except Exception as e:
@@ -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
}) })
@@ -602,3 +625,111 @@ def get_history(session_id: str):
code="HISTORY_ERROR", code="HISTORY_ERROR",
message="Failed to get conversation history" message="Failed to get conversation history"
) )
@sessions_bp.route('/api/v1/sessions/<session_id>', methods=['DELETE'])
@require_auth
def delete_session(session_id: str):
"""
Permanently delete a game session.
This removes the session from the database entirely. The session cannot be
recovered after deletion. Use this to free up session slots for users who
have reached their tier limit.
Returns:
200: Session deleted successfully
401: Not authenticated
404: Session not found or not owned by user
500: Internal server error
"""
logger.info("Deleting session", session_id=session_id)
try:
# Get current user
user = get_current_user()
user_id = user.id
# Delete session (validates ownership internally)
session_service = get_session_service()
session_service.delete_session(session_id, user_id)
logger.info("Session deleted successfully",
session_id=session_id,
user_id=user_id)
return success_response({
"message": "Session deleted successfully",
"session_id": session_id
})
except SessionNotFound as e:
logger.warning("Session not found for deletion",
session_id=session_id,
error=str(e))
return not_found_response("Session not found")
except Exception as e:
logger.error("Failed to delete session",
session_id=session_id,
error=str(e),
exc_info=True)
return error_response(
status=500,
code="SESSION_DELETE_ERROR",
message="Failed to delete session"
)
@sessions_bp.route('/api/v1/usage', methods=['GET'])
@require_auth
def get_usage():
"""
Get user's daily usage information.
Returns the current daily turn usage, limit, remaining turns,
and reset time. Limits are based on user's subscription tier.
Returns:
200: Usage information
{
"user_id": "user_123",
"user_tier": "free",
"current_usage": 15,
"daily_limit": 50,
"remaining": 35,
"reset_time": "2025-11-27T00:00:00+00:00",
"is_limited": false,
"is_unlimited": false
}
401: Not authenticated
500: Internal server error
"""
logger.info("Getting usage info")
try:
# Get current user and tier
user = get_current_user()
user_id = user.id
user_tier = get_user_tier_from_user(user)
# Get usage info from rate limiter
rate_limiter = RateLimiterService()
usage_info = rate_limiter.get_usage_info(user_id, user_tier)
logger.debug("Usage info retrieved",
user_id=user_id,
current_usage=usage_info.get('current_usage'),
remaining=usage_info.get('remaining'))
return success_response(usage_info)
except Exception as e:
logger.error("Failed to get usage info",
error=str(e),
exc_info=True)
return error_response(
status=500,
code="USAGE_ERROR",
message="Failed to get usage information"
)

View File

@@ -76,6 +76,7 @@ class RateLimitTier:
ai_calls_per_day: int ai_calls_per_day: int
custom_actions_per_day: int # -1 for unlimited custom_actions_per_day: int # -1 for unlimited
custom_action_char_limit: int custom_action_char_limit: int
max_sessions: int = 1 # Maximum active game sessions allowed
@dataclass @dataclass
@@ -86,6 +87,14 @@ class RateLimitingConfig:
tiers: Dict[str, RateLimitTier] = field(default_factory=dict) tiers: Dict[str, RateLimitTier] = field(default_factory=dict)
@dataclass
class SessionCacheConfig:
"""Session cache configuration for reducing Appwrite API calls."""
enabled: bool = True
ttl_seconds: int = 300 # 5 minutes
redis_db: int = 2 # Separate from RQ (db 0) and rate limiting (db 1)
@dataclass @dataclass
class AuthConfig: class AuthConfig:
"""Authentication configuration.""" """Authentication configuration."""
@@ -104,6 +113,7 @@ class AuthConfig:
name_min_length: int name_min_length: int
name_max_length: int name_max_length: int
email_max_length: int email_max_length: int
session_cache: SessionCacheConfig = field(default_factory=SessionCacheConfig)
@dataclass @dataclass
@@ -229,7 +239,11 @@ class Config:
tiers=rate_limit_tiers tiers=rate_limit_tiers
) )
auth_config = AuthConfig(**config_data['auth']) # Parse auth config with nested session_cache
auth_data = config_data['auth'].copy()
session_cache_data = auth_data.pop('session_cache', {})
session_cache_config = SessionCacheConfig(**session_cache_data) if session_cache_data else SessionCacheConfig()
auth_config = AuthConfig(**auth_data, session_cache=session_cache_config)
session_config = SessionConfig(**config_data['session']) session_config = SessionConfig(**config_data['session'])
marketplace_config = MarketplaceConfig(**config_data['marketplace']) marketplace_config = MarketplaceConfig(**config_data['marketplace'])
cors_config = CORSConfig(**config_data['cors']) cors_config = CORSConfig(**config_data['cors'])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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"

View 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"

View 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"

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -8,7 +8,7 @@ description: >
excel in supporting allies and controlling enemies through clever magic and mental manipulation. excel in supporting allies and controlling enemies through clever magic and mental manipulation.
Choose your art: weave arcane power or bend reality itself. Choose your art: weave arcane power or bend reality itself.
# Base stats (total: 67) # Base stats (total: 67 + luck)
base_stats: base_stats:
strength: 8 # Low physical power strength: 8 # Low physical power
dexterity: 11 # Above average agility dexterity: 11 # Above average agility
@@ -16,11 +16,12 @@ base_stats:
intelligence: 13 # Above average magical power intelligence: 13 # Above average magical power
wisdom: 11 # Above average perception wisdom: 11 # Above average perception
charisma: 14 # High social/performance charisma: 14 # High social/performance
luck: 10 # Knowledge is its own luck
starting_equipment: starting_equipment:
- tome - tome
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -8,7 +8,7 @@ description: >
capable of becoming a guardian angel for their allies or a righteous crusader smiting evil. capable of becoming a guardian angel for their allies or a righteous crusader smiting evil.
Choose your calling: protect the innocent or judge the wicked. Choose your calling: protect the innocent or judge the wicked.
# Base stats (total: 68) # Base stats (total: 68 + luck)
base_stats: base_stats:
strength: 9 # Below average physical power strength: 9 # Below average physical power
dexterity: 9 # Below average agility dexterity: 9 # Below average agility
@@ -16,11 +16,12 @@ base_stats:
intelligence: 12 # Above average magical power intelligence: 12 # Above average magical power
wisdom: 14 # High perception/divine power wisdom: 14 # High perception/divine power
charisma: 13 # Above average social charisma: 13 # Above average social
luck: 11 # Divine favor grants fortune
starting_equipment: starting_equipment:
- rusty_mace - rusty_mace
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -8,7 +8,7 @@ description: >
excel in draining enemies over time or overwhelming foes with undead minions. excel in draining enemies over time or overwhelming foes with undead minions.
Choose your dark art: curse your enemies or raise an army of the dead. Choose your dark art: curse your enemies or raise an army of the dead.
# 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: 14 # High magical power intelligence: 14 # High magical power
wisdom: 11 # Above average perception wisdom: 11 # Above average perception
charisma: 12 # Above average social (commands undead) charisma: 12 # Above average social (commands undead)
luck: 7 # Dark arts come with a cost
starting_equipment: starting_equipment:
- bone_wand - bone_wand
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -8,7 +8,7 @@ description: >
capable of becoming an unyielding shield for their allies or a beacon of healing light. capable of becoming an unyielding shield for their allies or a beacon of healing light.
Choose your oath: defend the weak or redeem the fallen. Choose your oath: defend the weak or redeem the fallen.
# Base stats (total: 67) # Base stats (total: 67 + luck)
base_stats: base_stats:
strength: 12 # Above average physical power strength: 12 # Above average physical power
dexterity: 9 # Below average agility dexterity: 9 # Below average agility
@@ -16,12 +16,13 @@ base_stats:
intelligence: 10 # Average magic intelligence: 10 # Average magic
wisdom: 12 # Above average perception wisdom: 12 # Above average perception
charisma: 11 # Above average social charisma: 11 # Above average social
luck: 9 # Honorable, modest fortune
starting_equipment: starting_equipment:
- rusty_sword - rusty_sword
- rusty_shield - rusty_shield
- cloth_armor - cloth_armor
- rusty_knife - health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

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