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:
MonsterAttack
- Represents and handles monster attack dataMonster
schema - Stores monster data including parsed attacksMonsterAttackRoll
- Handles rolling attacks and damage with critical hit supportMonsterAttackCommand
- 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 structparse_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¶
- Command Parsing
- Split args into combatant_name and attack_name
- Validate DM permissions
-
Handle empty/missing arguments
-
Combatant Selection
- Use
Initiatives.get_combatant_by_name
to find the combatant -
Handles cases:
- No initiative found
- No combatants found
- No matching combatant
- Multiple matching combatants
-
Attack Selection
- Get monster data from combatant.stats
- Convert attack maps to MonsterAttack structs using
Stingbatbot.Monsters.attacks_from_maps/1
- Find matching attack by name
-
Handle cases:
- No attacks available
- No matching attack found
- Multiple matching attacks
-
Attack Rolling
- Create MonsterAttackRoll with the selected attack
- Roll the attack
-
Format the result
-
Response
- 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¶
- Data Flow
- Attack descriptions are parsed into MonsterAttack structs during monster creation/update
- Structs are converted to maps for JSONB storage
- Maps are converted back to structs when needed for rolling attacks
-
Critical hits are handled by doubling the damage dice
-
Architecture Benefits
- Separation of Concerns: Schema focuses on validation, Operations handles transformation
- Maintainability: Clear module responsibilities and clean APIs
- Extensibility: Easy to add new functionality without modifying existing code
-
Testability: Each module can be tested in isolation
-
Error Handling
- Handle missing or invalid attack descriptions
- Handle missing or invalid attack data in JSONB
- Handle cases where attack parsing fails
-
Provide clear error messages for DMs
-
Future Considerations
- Add validation for attack data
- Add support for more complex attack patterns
- Add support for conditional effects
- Add support for saving throws
- Add support for area effects