Spell Casting Architecture¶
This document outlines the architecture for the spell casting system in Shadowdark RPG.
Overview¶
In Shadowdark, spell casting requires a d20 roll plus a spellcasting modifier against a DC of 10 plus the level of the spell. The spellcasting modifier is based on the character's class (INT for Wizards, WIS for Priests, etc.) and can be affected by various talents and abilities.
Core Modules and Responsibilities¶
Stingbatbot.SpellCasting
(Core Module)¶
- Top-level module for spell casting functionality
- Defines the behavior for spell casting modifiers
- Maintains a hardcoded list of implementing modules
- Defines the SpellCasting struct for representing casting checks
- Handles the calculation of spell casting checks
- Applies modifiers from all implementation modules
Stingbatbot.SpellCastingRoll
(New Implementation)¶
- Responsibility: Handle dice rolling for spell casting
- Key Functions:
roll/2
: Main function that performs the spell casting check with optionsbuild_casting_expression/2
: Creates dice expressions for casting checks- Features:
- Advantage/disadvantage support via dice expression modification
- Critical success/failure detection
- Success determination against spell DC
- Structured RollResult output for consistent formatting
- Error handling for dice rolling failures
Stingbatbot.Spell
¶
- Responsibility: Represent spell data
- Data: name, classes, tierByClass, duration, range, description, and other properties
- Key Functions:
get_tier_for_class/2
: Gets the tier for a specific classavailable_to_class?/2
: Checks if a spell is available to a specific class
Stingbatbot.SpellList
¶
- Responsibility: Central registry and lookup service for all spells
- Key Functions:
get_all/0
: Returns all available spellsget_spell/1
: Retrieves a spell by nameget_spells_for_class/1
: Returns all spells available to a specific classget_spells_for_class_and_tier/2
: Returns all spells for a class at a specific tierget_spell_for_class/2
: Retrieves a specific spell for a specific class
SpellCasting Struct¶
%Stingbatbot.SpellCasting{
character: %Character{},
spell: %Spell{},
casting_stat: String.t(), # E.g., "INT", "WIS"
casting_modifier: integer(),
casting_modifiers: [
%{source: String.t(), value: integer()}
],
dc: integer(),
description: String.t()
}
Character Component Modules¶
Spell casting modifiers are implemented by the same character component modules that implement attack modifiers:
Stingbatbot.Ancestry
: Ancestry-specific modules implement spell casting modifiers where applicableStingbatbot.Ancestry.Elf
- Provides arcane affinity bonuses-
Stingbatbot.Ancestry.Kobold
- Provides relevant spell modifiers -
Stingbatbot.Class
: Class-specific modules implement spell casting modifiers where applicable Stingbatbot.Class.Wizard
- Provides INT-based modifiers and arcane specializationsStingbatbot.Class.Priest
- Provides WIS-based modifiers and divine specializations
Static List Architecture¶
Similar to the attack system, spell casting uses a static list architecture where:
- The
Stingbatbot.SpellCasting
module defines a behavior with callbacks for modifying casting checks - Ancestry and class modules implement this behavior
- Implementing modules are explicitly listed in the
@implementation_modules
attribute - The new/2 method takes a character and spell and calls new/3 with a third argument for implemenations list for dependency injection
- When calculating a casting check, all listed modifiers are applied in sequence
This architecture provides several benefits: - Clear separation between the spell casting system and character components - Explicit control over which modules are included and their application order - Single implementation point for each ancestry and class - Simple to understand and maintain
Module Implementation¶
Character component modules implement the spell casting behavior using the use
Stingbatbot.SpellCasting
directive:
- Behavior Implementation:
- Each ancestry and class module uses
use Stingbatbot.SpellCasting
at the top of the module - This sets up the module to implement the required behavior callbacks
-
The module then implements the
apply_stat_modifier/1
andapply_casting_modifiers/1
callbacks -
Manual Registration:
- Modules are manually added to the
@implementation_modules
list in the SpellCasting module - This provides explicit control over which modules are included
-
The execution order is determined by the order in the list
-
Default Implementations:
- The
use
directive provides sensible defaults for the behavior callbacks - When implementing a callback, you must explicitly handle all possible cases
- For class or ancestry-specific modifiers, use pattern matching with a catch-all clause
Example pattern for implementing a class module:
defmodule Stingbatbot.Class.SomeClass do
use Stingbatbot.SpellCasting
# Implement class-specific stat modifier logic
@impl Stingbatbot.SpellCasting
def apply_stat_modifier(%{character: %{class: "SomeClass"}} = spell_casting) do
# Apply class-specific stat modifier logic here
end
# Catch-all for other characters
@impl Stingbatbot.SpellCasting
def apply_stat_modifier(spell_casting), do: spell_casting
# Implement class-specific casting modifiers
@impl Stingbatbot.SpellCasting
def apply_casting_modifiers(%{character: %{class: "SomeClass"}} = spell_casting) do
# Apply class-specific casting modifiers here
end
# Catch-all for other characters
@impl Stingbatbot.SpellCasting
def apply_casting_modifiers(spell_casting), do: spell_casting
end
Note: When implementing a callback using pattern matching, you must provide a catch-all function
clause even though the __using__
macro provides default implementations. This is because defining a
function with any pattern completely replaces the default implementation from the __using__
macro.
Implementation Process¶
The implementation will follow these steps:
Done: Move Stingbatbot.SpellCasting.Spell to Stingbatbot.Spell- Simplify the namespace structure
- Update all references to use the new namespace
-
Ensure all tests pass with the new namespace
-
Done: Create Core SpellCasting Module - Define the SpellCasting struct directly in the module (replacing separate CastingCheck)
- Implement behavior definition and using macro
- Set up the empty @implementation_modules list
- Implement core functions like new/2 and modifier application
-
Setup dependency injection for implemenations using new/3
-
Move and Update Class and Ancestry Modules
- Move existing modules to the proper namespaces:
Done:Stingbatbot.Ancestry.Elf
Done:Stingbatbot.Ancestry.Kobold
Done:Stingbatbot.Class.Wizard
Done:Stingbatbot.Class.Priest
- Add SpellCasting behavior to each module using
use Stingbatbot.SpellCasting
- Implement the
apply_stat_modifier/1
andapply_casting_modifiers/1
callbacks in each module -
Update the
@implementation_modules
list to include these modules -
Update Character Integration
- Update Character module to work with the new spell casting system
-
Ensure proper spell data parsing and access methods
-
Implement SpellCastingRoll Module
- Create new module for handling spell casting rolls
- Implement dice rolling mechanics with dice_module dependency injection
- Support advantage/disadvantage through dice expression modification
- Detect critical success/failure outcomes
- Produce structured RollResult objects for consistent formatting
-
Handle errors gracefully
-
Create TestDice for Testing
- Implement Agent-based TestDice module
- Support dynamic roll function programming
- Provide test isolation for concurrent tests
- Mimic the interface of the real Dice module
Character Integration¶
The Character struct stores spells as structured data and provides helper functions for spell casting:
parse_spells/1
: Creates structured Spell objects from character dataget_spell_castings/1
: Gets casting checks for all known spellsget_spell_casting/2
: Gets a casting check for a specific spell
Implementation Flow¶
- When a new spell casting check is created:
- Base ability modifiers are applied based on character class and stats
- All implementation modules' modifiers are applied in sequence
-
The casting check description is generated
-
Each implementation module:
- Receives the SpellCasting struct as input
- Applies its specific modifiers
-
Returns the modified SpellCasting struct
-
All modifiers are tracked with their source and value for transparency
Spell Casting Roll Flow¶
The spell casting roll process follows these steps:
- A
SpellCasting
struct is created with pre-calculated modifiers - The
SpellCastingRoll.roll/2
function is called with: - The SpellCasting struct
- Options map (can include advantage, disadvantage, and dice_module)
- The appropriate dice expression is built:
- "1d20+{modifier}" for normal rolls
- "2d20kh1+{modifier}" for advantage rolls
- "2d20kl1+{modifier}" for disadvantage rolls
- The dice roll is executed using the provided dice module
- The result is processed to determine:
- Critical success (natural 20)
- Critical failure (natural 1)
- Success/failure against the spell's DC
- A structured RollResult is returned containing:
- The original SpellCasting context
- The dice expression and formatted result
- The total result value
- Critical success/failure status
- Success/failure outcome
- The options used for the roll
Testing Approach¶
The spell casting roll system uses a robust testing approach:
- TestDice Module: Uses an Agent-based approach to store roll functions
- Provides process isolation for concurrent tests
- Supports programmable roll functions for different test scenarios
-
Mimics the interface of the real Dice module
-
Test Setup:
- Creates a realistic character with spells
- Extracts SpellCasting structs from the character
-
Programs the TestDice with specific responses for different scenarios
-
Test Scenarios:
- Basic spell casting
- Advantage/disadvantage mechanics
- Critical success/failure detection
- Success/failure against DC
- Error handling
This approach allows for comprehensive testing of all aspects of the spell casting roll system while maintaining independence from the actual dice rolling service.
Integration with Attack System¶
Both the spell casting and attack systems follow the same architectural pattern: - Shared implementation modules for ancestries and classes - Consistent manual registration mechanism via a hardcoded module list - Similar modifier application process - Direct struct definition in the core module - Consistent RollResult structure for unified formatting
Roll Modifiers Pipeline Pattern¶
In addition to the existing behavior system for calculating static modifiers, the spell casting system will adopt the Roll Modifiers Pipeline Pattern established in the attack system. This powerful architectural pattern allows for flexible, composable modifications to dice roll mechanics themselves, not just the static modifiers applied to rolls.
Core Pattern Application¶
The spell casting system will implement the same callback interface:
@callback apply_roll_modifiers(
context :: any(),
roll_type :: atom(),
components :: RollComponents.t(),
options :: map()
) :: {context :: any(), roll_type :: atom(), RollComponents.t(), options :: map()}
This will enable:
- Dynamic Dice Mechanics: Spell effects that modify dice mechanics (e.g., empowered spells that reroll low dice)
- Class-Specific Modifications: Class features that enhance spell rolls in specific ways (e.g., Wizard schools)
- Situational Adjustments: Modifications based on environment or character state
Pipeline Implementation¶
The spell casting roll pipeline will follow the same structure as the attack roll pipeline:
def build_casting_expression(spell_casting, opts \\ %{}, implementations \\ nil) do
{_spell_casting, _roll_type, roll_components, _opts} = build_casting_roll_components(spell_casting, opts)
|> handle_casting_specialty() # Apply specialty bonuses for schools/domains
|> handle_roll_modifiers(implementations) # Apply modifiers from all implementation modules
build_expression(roll_components, opts)
end
Benefits of Consistent Architecture¶
By applying this pattern consistently across both attack rolls and spell casting:
- Unified Interface: The same callback interface is used throughout
- Knowledge Transfer: Developers familiar with one system can easily work with the other
- Code Reuse: Common utility functions can be shared between systems
- Consistent Extensions: New features can be added to both systems in parallel
- Simplified Testing: Testing patterns can be reused between systems
This architectural consistency ensures that game mechanics work harmoniously across different subsystems and creates a foundation for future extensions like skill checks and saving throws.
Key Design Principles¶
- Clear Separation of Concerns: Spell data vs. casting mechanics vs. ancestry/class rules
- Static List Implementation: Explicit control over included modules and their order
- Unified Architecture: Consistent patterns across different game systems
- Simplified Namespace: Core game concepts at the top level (Spell, Weapon, Character)
- Extensibility: Easy to add new ancestries, classes, and game mechanics
Supported Spellcasting Classes¶
- Wizard (INT)
- Priest (WIS)
Future Enhancements¶
- Actual spell casting roll implementation
- Spell effects system
- Integration with combat for damage spells
- Spell failure consequences
- Spell resource management (e.g., daily limits)
Spell Casting System¶
Overview¶
The Spell Casting System provides a streamlined way for characters to cast spells in Shadowdark RPG. It integrates with the character system to automatically calculate and apply appropriate modifiers based on the character's spellcasting ability, class, and other factors.
This system follows the same architectural patterns established by the Attack Roll System and Ability Check System, leveraging the generic RollResult and RollFormatter modules to maintain a consistent user experience across all roll types.
Command Usage¶
!cast <spell_name> [option1] [option2] ...
spell_name
: Name of the spell to cast (required, case-insensitive partial matching supported)-
If omitted, shows all available spells for the character
-
Options (can be provided in any order after the spell name):
adv
: Cast the spell with advantage (roll twice and take the higher result)dis
: Cast the spell with disadvantage (roll twice and take the lower result)
Examples:
!cast # Lists all available spells
!cast fireball # Cast the Fireball spell
!cast magic missile # Cast the Magic Missile spell
!cast fireball adv # Cast Fireball with advantage
!cast light dis # Cast Light with disadvantage
Note: If both adv
and dis
are specified, they cancel each other out and a normal roll is made.
System Components¶
Core Modules¶
Character Module Extension¶
Responsibility: Provides pre-calculated spell casting data
- Already contains character stats, class, ancestry, and spell data
- Calculates modifiers based on stats and other factors
- Returns SpellCasting structure with pre-calculated modifiers
Key Functions:
- get_spells/1
: Returns a list of all spells known by the character
- get_spell_casting/2
: Returns a pre-calculated SpellCasting structure for a specific spell
SpellCasting¶
Responsibility: Structured data representation of a spell casting check with pre-calculated modifiers
- Contains the spell being cast
- Stores the character making the cast
- Includes pre-calculated modifiers and bonuses from all sources
- Similar to Attack and StatCheck structures
Key Fields:
- character
: The character casting the spell
- spell
: The spell being cast
- casting_stat
: The ability stat used for casting (e.g., INT, WIS, CHA)
- casting_modifier
: The calculated total casting modifier
- dc
: The difficulty class for the spell
- tier
: The spell tier
SpellCastingCommand¶
Responsibility: Processes the !cast
command from Discord
- Implements the
Stingbatbot.Commands.Command
behavior - Located in the
Stingbatbot.Commands
namespace - Registered in the Command Registry for bot access
- Validates the specified spell name
- Extracts options like advantage/disadvantage
- Retrieves the pre-calculated SpellCasting structure from the Character
- Passes the SpellCasting to SpellCastingRoll for dice mechanics
- Uses RollFormatter to format the results
- Sends the formatted message back to Discord via MessageApi
- Follows standard command pattern for error handling and responses
Key Functions:
- name/0
: Returns "cast" as the command name
- shortcut/0
: Returns "c" as the command shortcut
- description/0
: Provides a description of the command
- usage/0
: Provides usage instructions
- execute/2
: Main function that handles command execution, returns {:ok, :sent}
or {:error, reason}
- extract_args/1
: Extracts spell name and options from command arguments
SpellCastingRoll¶
Responsibility: Handles the dice rolling logic for spell casting checks
- Takes a pre-calculated SpellCasting structure
- Creates a die expression using the pre-calculated casting modifier
- Uses the Dice module for actual dice rolling
- Creates structured results for formatting
- Supports advantage/disadvantage mechanics
- Determines success/failure based on spell DC
Key Functions:
- roll/2
: Main function that performs the casting check with the SpellCasting and options
- build_casting_expression/2
: Creates dice expressions with proper modifiers
RollResult Integration¶
The existing RollResult
struct is used to store spell casting results, with:
context
: A%SpellCasting{}
structure with casting detailsroll_expression
: The dice expression (e.g., "1d20+3")roll_result
: The formatted result (e.g., "[15]+3 = 18")roll_total
: The numerical totalcritical
: Status of critical success/failureoutcome
::success
or:failure
based on meeting the DCoptions
: Map of options used for the roll (e.g.,%{advantage: true}
)
RollFormatter Integration¶
The RollFormatter
module handles formatting for SpellCasting contexts:
# Format title for spell casting
def format_title_part(%RollResult{context: %SpellCasting{}} = result) do
character_name = result.context.character.name
spell_name = result.context.spell.name
title = "🧙 #{character_name}'s #{spell_name} Casting"
# Add options if present
option_text = build_option_text(result.options)
if option_text != "", do: "#{title} (#{option_text})", else: title
end
# Format description for spell casting
def format_description_part(%RollResult{context: %SpellCasting{}} = result) do
casting_part = format_casting_part(result)
outcome_part = format_outcome_part(result)
"#{casting_part}\n#{outcome_part}"
end
Data Flow¶
The spell casting process follows these steps:
- User invokes
!cast <spell_name>
command - Bot's command processor identifies "cast" and routes to SpellCastingCommand
- SpellCastingCommand validates the spell and extracts options
- Command retrieves the pre-calculated SpellCasting structure from Character module
- Command passes SpellCasting to SpellCastingRoll with options
- SpellCastingRoll:
- Uses the pre-calculated casting modifier from SpellCasting
- Generates dice expression based on modifiers and options
- Uses Dice module to execute the roll
- Creates a structured RollResult with the SpellCasting as context
- Determines success/failure based on DC
- SpellCastingCommand passes the RollResult to RollFormatter
- RollFormatter converts the result into a user-friendly message
- Message is sent back to Discord via MessageApi
Implementation Details¶
Spell Success Determination¶
Unlike attacks which target an opponent's AC, spell casting in this system is based on meeting or exceeding a DC (Difficulty Class): - Success: Roll total ≥ DC - Failure: Roll total < DC
Advantage Implementation¶
Like other rolls, advantage uses the 2d20kh1
dice notation, which rolls two d20s and keeps the higher result.
Disadvantage Implementation¶
Like other rolls, disadvantage uses the 2d20kl1
dice notation, which rolls two d20s and keeps the lower result.
When both advantage and disadvantage are specified on the same check, they cancel each other out, resulting in a normal d20 roll.
Critical Success/Failure¶
The system detects natural 20s as critical successes and natural 1s as critical failures, marking them in the output with special formatting and emoji.
Extension Points¶
Key extension points in the current architecture:
- SpellCastingRoll can be extended with additional options
- The RollResult structure already supports different types of contexts
- RollFormatter already has pattern matching for different context types
- Roll modifiers framework can be used for class/ancestry features affecting spell casting