Skip to content

Monster Attack System

Overview

The Monster Attack System provides a way to handle monster attacks in the game, from parsing attack descriptions to rolling attacks in combat. It consists of four main components:

  1. MonsterAttack - Represents and handles monster attack data
  2. Monster schema - Stores monster data including parsed attacks
  3. MonsterAttackRoll - Handles rolling attacks and damage with critical hit support
  4. MonsterAttackCommand - Allows DMs to roll monster attacks in initiative

MonsterAttack Module

The MonsterAttack module defines the structure and handling of monster attacks.

Struct Definition

defmodule Stingbatbot.Monsters.MonsterAttack do
  defstruct [
    name: nil,      # e.g., "tentacle", "bite"
    count: 1,       # number of attacks (e.g., 2 for multiattack)
    range: nil,     # e.g., "near", "close/near"
    bonus: 0,       # attack bonus (e.g., +5)
    damage: nil,    # damage dice (e.g., "1d8", "2d6")
    effect: nil,    # special effect (e.g., "curse")
    conjunction: nil # "and" or "or" if part of a compound attack
  ]
end

Key Functions

  • parse/1 - Parse a single attack description into a MonsterAttack struct
  • parse_all/1 - Parse multiple attacks from a string containing attacks separated by "or" or "and", preserving the conjunctions

Note: Map conversion functions (from_map/1, from_maps/1, to_map/1) have been moved to the Stingbatbot.Monsters.Maps module for better separation of concerns.

Example Usage

# Parse a single attack (returns a list with one element)
[attack] = Monsters.parse_attack_string("2 tentacle (near) +5 (1d8 + curse)")

# Parse multiple attacks with conjunctions
attacks = Monsters.parse_attack_string("2 tentacle (near) +5 (1d8 + curse) or 1 bite +4 (2d6)")
# The first attack will have conjunction: "or"

# Convert from database format (using Maps module)
attack = Stingbatbot.Monsters.attack_from_map(%{
  "name" => "tentacle",
  "count" => 2,
  "range" => "near",
  "bonus" => 5,
  "damage" => "1d8",
  "effect" => "curse",
  "conjunction" => "or"
})

Monster Schema

The Monster schema stores monster data including their attacks. Attacks are stored as JSONB in the database.

Attack Storage

schema "monsters" do
  # ... other fields ...
  field :attacks, {:array, :map}, default: []  # Structured attacks stored as JSONB
end

Schema Design Philosophy

The Monster schema is designed to be pure and focused on validation only. Data transformation logic has been moved to the Stingbatbot.Monsters.Operations module to maintain clear separation of concerns.

Key Changes: - Schema changeset only handles validation - Attack parsing moved to prepare_monster_attrs/1 in Operations module - Talent processing moved to Operations module - Schema remains focused on data integrity

Data Transformation Pipeline

The monster system uses a data transformation pipeline to keep the schema focused on validation:

Operations Module (Stingbatbot.Monsters.Operations)

Responsibility: Database operations and data transformation

Maps Module (Stingbatbot.Monsters.Maps)

Responsibility: Struct/map conversions for JSONB storage

The Maps module handles all conversions between Elixir structs and plain maps for database storage:

# Convert MonsterAttack struct to map for storage
attack_map = Stingbatbot.Monsters.attack_to_map(attack_struct)

# Convert map back to MonsterAttack struct
attack_struct = Stingbatbot.Monsters.attack_from_map(attack_map)

# Batch conversion
attack_structs = Stingbatbot.Monsters.attacks_from_maps(attack_maps)

MonsterAttackRoll Module

The MonsterAttackRoll module handles the actual rolling of monster attacks, including critical hits and damage calculations.

Key Functions

  • roll/2 - Roll a single attack with optional dice_api parameter for testing

Critical Hit Handling

When rolling an attack: 1. The attack roll is performed first 2. If the attack roll indicates a critical hit (natural 20): - The damage expression is modified to double the number of dice - For example, "1d6" becomes "2d6" on a critical hit 3. The modified damage expression is then rolled

Example Usage

# Roll a single attack
{:ok, roll_result} = MonsterAttackRoll.roll(monster, attack)

# Roll with test dice for predictable results
{:ok, roll_result} = MonsterAttackRoll.roll(monster, attack, dice_api: TestDice)

MonsterAttackCommand

The MonsterAttackCommand allows DMs to roll attacks for monsters in initiative.

Command Usage

!ma <combatant_name> <attack_name>

Flow

  1. Command Parsing
  2. Split args into combatant_name and attack_name
  3. Validate DM permissions
  4. Handle empty/missing arguments

  5. Combatant Selection

  6. Use Initiatives.get_combatant_by_name to find the combatant
  7. Handles cases:

    • No initiative found
    • No combatants found
    • No matching combatant
    • Multiple matching combatants
  8. Attack Selection

  9. Get monster data from combatant.stats
  10. Convert attack maps to MonsterAttack structs using Stingbatbot.Monsters.attacks_from_maps/1
  11. Find matching attack by name
  12. Handle cases:

    • No attacks available
    • No matching attack found
    • Multiple matching attacks
  13. Attack Rolling

  14. Create MonsterAttackRoll with the selected attack
  15. Roll the attack
  16. Format the result

  17. Response

  18. Create embed with:
    • Title: Combatant name and attack name
    • Description: Attack roll results
    • Color: Red for monsters

Example Usage

DM: !ma Angry Goblin Bite
Bot: [embed] Angry Goblin - Bite
     Attack Roll: 1d20+3 (15) = 18
     Damage: 1d6+1 (4) = 5

DM: !ma Angry Goblin
Bot: Please specify an attack. Available attacks: Bite, Claw, Shortbow

DM: !ma Goblin
Bot: Multiple goblins found: Angry Goblin, Hungry Goblin, Sneaky Goblin
    Please be more specific.

Public API Usage

The monster system provides a clean public API through the Stingbatbot.Monsters context module:

# Create a monster with automatic attack parsing
changeset = Stingbatbot.Monsters.create_changeset(%{
  name: "Goblin",
  atk_description: "1 dagger +2 (1d4)"
})

# Convert attack maps to structs for combat
attacks = Stingbatbot.Monsters.attacks_from_maps(monster.attacks)

# Convert structs to maps for storage
attack_maps = Enum.map(attacks, &Stingbatbot.Monsters.attack_to_map/1)

Implementation Notes

  1. Data Flow
  2. Attack descriptions are parsed into MonsterAttack structs during monster creation/update
  3. Structs are converted to maps for JSONB storage
  4. Maps are converted back to structs when needed for rolling attacks
  5. Critical hits are handled by doubling the damage dice

  6. Architecture Benefits

  7. Separation of Concerns: Schema focuses on validation, Operations handles transformation
  8. Maintainability: Clear module responsibilities and clean APIs
  9. Extensibility: Easy to add new functionality without modifying existing code
  10. Testability: Each module can be tested in isolation

  11. Error Handling

  12. Handle missing or invalid attack descriptions
  13. Handle missing or invalid attack data in JSONB
  14. Handle cases where attack parsing fails
  15. Provide clear error messages for DMs

  16. Future Considerations

  17. Add validation for attack data
  18. Add support for more complex attack patterns
  19. Add support for conditional effects
  20. Add support for saving throws
  21. Add support for area effects