Combat
github.com/mechanical-lich/ml-rogue-lib/pkg/rlcombat
A D&D-style melee combat pipeline covering to-hit rolls, damage calculation, resistances, weaknesses, and status effect transfer. All functions are stateless — wire them into your game systems or player action handlers.
Constants
const DefaultDamageType = "bludgeoning"
Used when an entity’s StatsComponent.BaseDamageType is empty.
Functions
Hit
func Hit(level rlworld.LevelInterface, entity, entityHit *ecs.Entity, swap bool)
Performs a full melee attack from entity against entityHit.
Pipeline:
- If the two entities are friendly (
IsFriendlyreturnstrue) andswapistrue, their positions are exchanged and the function returns. - Requires
StatsComponenton both entities andHealthComponenton the defender — returns early otherwise. - Rolls
1d20 + Dex modifier + inventory attack bonusvsdefender AC + inventory defense bonus. - On a hit: calls
InflictDamage, thenApplyStatusEffects. - On a miss: posts a tagged “missed” message to MLGE’s message log.
- Always calls
TriggerDefensesonentityHit.
InflictDamage
func InflictDamage(attacker, defender *ecs.Entity)
Rolls damage and applies it to the defender’s HealthComponent.
Pipeline:
- Calls
GetAttackDiceto obtain the dice string, damage type, and Strength modifier. - Rolls the dice using MLGE’s
dice.ParseDiceRequest. - Halves damage if the defender has a matching resistance (via
StatsComponent.Resistancesor equipped armor). - Doubles damage if the defender has a matching weakness.
- Enforces a minimum of 1 damage.
- Posts a tagged “combat” message with attacker name, damage amount, and damage type.
GetAttackDice
func GetAttackDice(entity *ecs.Entity) (dice string, damageType string, modifier int)
Returns the attack dice expression, damage type, and Strength-based modifier for an entity. If the entity has an InventoryComponent with a weapon equipped in RightHand, the weapon’s dice and damage type override the entity’s base stats. Inventory attack bonuses are added to the modifier.
IsFriendly
func IsFriendly(attacker, defender *ecs.Entity) bool
Returns true if both entities share the same non-empty DescriptionComponent.Faction. Used by Hit to swap friendly entities instead of attacking them.
TriggerDefenses
func TriggerDefenses(defender *ecs.Entity, attackerX, attackerY int)
Notifies the defender that it was attacked:
- Sets
DefensiveAIComponent.Attacked = trueand records attacker coordinates. - Sets
AIMemoryComponent.Attacked = trueand records attacker coordinates. - Adds an
AlertedComponent(duration 120) if not already present.
ApplyStatusEffects
func ApplyStatusEffects(attacker, defender *ecs.Entity)
Transfers status effects from attacker to defender on a successful hit. Currently: if the attacker has PoisonousComponent, a PoisonedComponent is added to the defender (if not already poisoned).
GetModifier
func GetModifier(stat int) int
Returns the D&D-style ability modifier: (stat - 10) / 2.
IsInArrowPath
func IsInArrowPath(aX, aY, tX, tY, maxRange int) bool
Returns true if the target at (tX,tY) is reachable from (aX,aY) via a straight or diagonal line of at most maxRange tiles. Useful for determining whether a ranged attack is geometrically possible without a full ray-cast.
HasResistance
func HasResistance(defender *ecs.Entity, damageType string) bool
Returns true if the entity resists damageType — either via StatsComponent.Resistances or via ArmorComponent.Resistances on any equipped item in the legacy InventoryComponent slots.
HasWeakness
func HasWeakness(defender *ecs.Entity, damageType string) bool
Returns true if the entity is weak to damageType via StatsComponent.Weaknesses.
Resistances and Weaknesses
Resistances and weaknesses are stored as []string on StatsComponent. Equipped armor pieces may also contribute resistances via ArmorComponent.Resistances. The damage type is matched case-sensitively.
// Common damage type strings (not constants — define your own as needed).
"bludgeoning"
"slashing"
"piercing"
"fire"
"cold"
"poison"
Usage Example
import "github.com/mechanical-lich/ml-rogue-lib/pkg/rlcombat"
// Player bumps into an enemy entity.
func onPlayerBump(level rlworld.LevelInterface, player, target *ecs.Entity) {
rlcombat.Hit(level, player, target, false)
// Check if target died.
if target.HasComponent(rlcomponents.Dead) {
// award XP, play sound, etc.
}
}
rlbodycombat
github.com/mechanical-lich/ml-rogue-lib/pkg/rlcombat/rlbodyCombat
An extended combat pipeline that routes damage to individual body parts when the defender has a BodyComponent. Falls back to the legacy HealthComponent path for entities without a body. Import this package in place of rlcombat when using the body system.
Hit
func Hit(level rlworld.LevelInterface, entity, entityHit *ecs.Entity, swap bool) bool
Performs a full melee attack. Returns true if the attack was executed.
Pipeline:
- If both entities share a faction and
swapistrue, their positions are exchanged; returnsfalse. - Returns
falseif either entity lacksStatsComponent, or if the defender has neitherBodyComponentnorHealthComponent. - Rolls
1d20 + Dex modifier + weapon attack bonusvsdefender AC + armor defense bonus. - Natural 20 is always a critical hit (doubles damage).
- On a hit:
- If the defender has a
BodyComponent, a random non-amputated part is chosen. Damage is applied viaapplyBodyPartDamage, which setsBrokenandAmputatedflags and checksKillsWhen*. - If all parts are amputated, the entity is marked dead immediately (a fully-amputated entity cannot survive a hit).
- If a lethal condition is met,
HealthComponent.Healthis set to0and aDeadComponentis added. - A
CombatEventis queued with full hit details. ApplyStatusEffectsis called.
- If the defender has a
- On a miss: a “missed” message is posted and a miss
CombatEventis queued. - Always calls
TriggerDefenseson the defender.
Damage routing summary:
| Defender state | Damage target |
|---|---|
Has BodyComponent, parts available | Random non-amputated body part |
Has BodyComponent, all parts amputated | Entity marked dead |
No BodyComponent, has HealthComponent | HealthComponent |
| Neither | Attack is invalid; returns false |
CombatEvent
type CombatEvent struct {
X, Y, Z int // world position of the attacker
AttackerName string
DefenderName string
Damage int // 0 on a miss
DamageType string
BodyPart string // empty on miss or health-only hit
Miss bool
Crit bool
Broken bool
Amputated bool
}
const CombatEventType event.EventType = "CombatEvent"
Posted to MLGE’s queued event system on every attack resolution. Register a listener to drive visual effects, floating damage numbers, or sound cues:
import (
rlbodycombat "github.com/mechanical-lich/ml-rogue-lib/pkg/rlcombat/rlbodyCombat"
"github.com/mechanical-lich/mlge/event"
)
type fxHandler struct{}
func (h *fxHandler) HandleEvent(e event.EventData) error {
ce, ok := e.(rlbodycombat.CombatEvent)
if !ok || ce.Miss {
return nil
}
spawnHitParticle(ce.X, ce.Y, ce.Z, ce.DamageType, ce.Crit)
return nil
}
// At startup:
event.GetQueuedInstance().RegisterListener(&fxHandler{}, rlbodycombat.CombatEventType)
Usage Example
import (
rlbodycombat "github.com/mechanical-lich/ml-rogue-lib/pkg/rlcombat/rlbodyCombat"
"github.com/mechanical-lich/ml-rogue-lib/pkg/rlcomponents"
)
// Entity with a body takes damage; vitals check kills it.
func onPlayerBump(level rlworld.LevelInterface, player, target *ecs.Entity) {
rlbodycombat.Hit(level, player, target, false)
if target.HasComponent(rlcomponents.Dead) {
// drop loot, award XP, play death sound
}
}