Skip to content

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 options
  • build_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 class
  • available_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 spells
  • get_spell/1: Retrieves a spell by name
  • get_spells_for_class/1: Returns all spells available to a specific class
  • get_spells_for_class_and_tier/2: Returns all spells for a class at a specific tier
  • get_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 applicable
  • Stingbatbot.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 specializations
  • Stingbatbot.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:

  1. The Stingbatbot.SpellCasting module defines a behavior with callbacks for modifying casting checks
  2. Ancestry and class modules implement this behavior
  3. Implementing modules are explicitly listed in the @implementation_modules attribute
  4. The new/2 method takes a character and spell and calls new/3 with a third argument for implemenations list for dependency injection
  5. 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:

  1. Behavior Implementation:
  2. Each ancestry and class module uses use Stingbatbot.SpellCasting at the top of the module
  3. This sets up the module to implement the required behavior callbacks
  4. The module then implements the apply_stat_modifier/1 and apply_casting_modifiers/1 callbacks

  5. Manual Registration:

  6. Modules are manually added to the @implementation_modules list in the SpellCasting module
  7. This provides explicit control over which modules are included
  8. The execution order is determined by the order in the list

  9. Default Implementations:

  10. The use directive provides sensible defaults for the behavior callbacks
  11. When implementing a callback, you must explicitly handle all possible cases
  12. 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:

  1. Done: Move Stingbatbot.SpellCasting.Spell to Stingbatbot.Spell
  2. Simplify the namespace structure
  3. Update all references to use the new namespace
  4. Ensure all tests pass with the new namespace

  5. Done: Create Core SpellCasting Module

  6. Define the SpellCasting struct directly in the module (replacing separate CastingCheck)
  7. Implement behavior definition and using macro
  8. Set up the empty @implementation_modules list
  9. Implement core functions like new/2 and modifier application
  10. Setup dependency injection for implemenations using new/3

  11. Move and Update Class and Ancestry Modules

  12. Move existing modules to the proper namespaces:
    • Done: Stingbatbot.Ancestry.Elf
    • Done: Stingbatbot.Ancestry.Kobold
    • Done: Stingbatbot.Class.Wizard
    • Done: Stingbatbot.Class.Priest
  13. Add SpellCasting behavior to each module using use Stingbatbot.SpellCasting
  14. Implement the apply_stat_modifier/1 and apply_casting_modifiers/1 callbacks in each module
  15. Update the @implementation_modules list to include these modules

  16. Update Character Integration

  17. Update Character module to work with the new spell casting system
  18. Ensure proper spell data parsing and access methods

  19. Implement SpellCastingRoll Module

  20. Create new module for handling spell casting rolls
  21. Implement dice rolling mechanics with dice_module dependency injection
  22. Support advantage/disadvantage through dice expression modification
  23. Detect critical success/failure outcomes
  24. Produce structured RollResult objects for consistent formatting
  25. Handle errors gracefully

  26. Create TestDice for Testing

  27. Implement Agent-based TestDice module
  28. Support dynamic roll function programming
  29. Provide test isolation for concurrent tests
  30. 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 data
  • get_spell_castings/1: Gets casting checks for all known spells
  • get_spell_casting/2: Gets a casting check for a specific spell

Implementation Flow

  1. When a new spell casting check is created:
  2. Base ability modifiers are applied based on character class and stats
  3. All implementation modules' modifiers are applied in sequence
  4. The casting check description is generated

  5. Each implementation module:

  6. Receives the SpellCasting struct as input
  7. Applies its specific modifiers
  8. Returns the modified SpellCasting struct

  9. All modifiers are tracked with their source and value for transparency

Spell Casting Roll Flow

The spell casting roll process follows these steps:

  1. A SpellCasting struct is created with pre-calculated modifiers
  2. The SpellCastingRoll.roll/2 function is called with:
  3. The SpellCasting struct
  4. Options map (can include advantage, disadvantage, and dice_module)
  5. The appropriate dice expression is built:
  6. "1d20+{modifier}" for normal rolls
  7. "2d20kh1+{modifier}" for advantage rolls
  8. "2d20kl1+{modifier}" for disadvantage rolls
  9. The dice roll is executed using the provided dice module
  10. The result is processed to determine:
  11. Critical success (natural 20)
  12. Critical failure (natural 1)
  13. Success/failure against the spell's DC
  14. A structured RollResult is returned containing:
  15. The original SpellCasting context
  16. The dice expression and formatted result
  17. The total result value
  18. Critical success/failure status
  19. Success/failure outcome
  20. The options used for the roll

Testing Approach

The spell casting roll system uses a robust testing approach:

  1. TestDice Module: Uses an Agent-based approach to store roll functions
  2. Provides process isolation for concurrent tests
  3. Supports programmable roll functions for different test scenarios
  4. Mimics the interface of the real Dice module

  5. Test Setup:

  6. Creates a realistic character with spells
  7. Extracts SpellCasting structs from the character
  8. Programs the TestDice with specific responses for different scenarios

  9. Test Scenarios:

  10. Basic spell casting
  11. Advantage/disadvantage mechanics
  12. Critical success/failure detection
  13. Success/failure against DC
  14. 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:

  1. Dynamic Dice Mechanics: Spell effects that modify dice mechanics (e.g., empowered spells that reroll low dice)
  2. Class-Specific Modifications: Class features that enhance spell rolls in specific ways (e.g., Wizard schools)
  3. 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:

  1. Unified Interface: The same callback interface is used throughout
  2. Knowledge Transfer: Developers familiar with one system can easily work with the other
  3. Code Reuse: Common utility functions can be shared between systems
  4. Consistent Extensions: New features can be added to both systems in parallel
  5. 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

  1. Clear Separation of Concerns: Spell data vs. casting mechanics vs. ancestry/class rules
  2. Static List Implementation: Explicit control over included modules and their order
  3. Unified Architecture: Consistent patterns across different game systems
  4. Simplified Namespace: Core game concepts at the top level (Spell, Weapon, Character)
  5. 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 details
  • roll_expression: The dice expression (e.g., "1d20+3")
  • roll_result: The formatted result (e.g., "[15]+3 = 18")
  • roll_total: The numerical total
  • critical: Status of critical success/failure
  • outcome: :success or :failure based on meeting the DC
  • options: 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:

  1. User invokes !cast <spell_name> command
  2. Bot's command processor identifies "cast" and routes to SpellCastingCommand
  3. SpellCastingCommand validates the spell and extracts options
  4. Command retrieves the pre-calculated SpellCasting structure from Character module
  5. Command passes SpellCasting to SpellCastingRoll with options
  6. SpellCastingRoll:
  7. Uses the pre-calculated casting modifier from SpellCasting
  8. Generates dice expression based on modifiers and options
  9. Uses Dice module to execute the roll
  10. Creates a structured RollResult with the SpellCasting as context
  11. Determines success/failure based on DC
  12. SpellCastingCommand passes the RollResult to RollFormatter
  13. RollFormatter converts the result into a user-friendly message
  14. 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