diff --git a/combat/combo.go b/combat/combo.go new file mode 100644 index 0000000..67c252c --- /dev/null +++ b/combat/combo.go @@ -0,0 +1,91 @@ +package combat + +import "github.com/tolelom/catacombs/entity" + +type ComboAction struct { + Class entity.Class + ActionType string // "attack", "skill", "item" +} + +type ComboEffect struct { + DamageMultiplier float64 // multiplied onto each AttackIntent.Multiplier + BonusDamage int // added to each AttackIntent.PlayerATK + HealAll int // heal all players after resolution + Message string // shown in combat log +} + +type ComboDef struct { + Name string + Required []ComboAction + Effect ComboEffect +} + +var comboDefs = []ComboDef{ + { + Name: "Ice Shatter", + Required: []ComboAction{ + {Class: entity.ClassMage, ActionType: "skill"}, + {Class: entity.ClassWarrior, ActionType: "attack"}, + }, + Effect: ComboEffect{DamageMultiplier: 1.5, Message: "💥 ICE SHATTER! Frozen enemies shatter!"}, + }, + { + Name: "Holy Assault", + Required: []ComboAction{ + {Class: entity.ClassHealer, ActionType: "skill"}, + {Class: entity.ClassWarrior, ActionType: "attack"}, + }, + Effect: ComboEffect{DamageMultiplier: 1.3, HealAll: 10, Message: "✨ HOLY ASSAULT! Blessed strikes heal the party!"}, + }, + { + Name: "Shadow Strike", + Required: []ComboAction{ + {Class: entity.ClassRogue, ActionType: "skill"}, + {Class: entity.ClassMage, ActionType: "attack"}, + }, + Effect: ComboEffect{DamageMultiplier: 1.4, Message: "🗡️ SHADOW STRIKE! Magical shadows amplify the attack!"}, + }, + { + Name: "Full Assault", + Required: []ComboAction{ + {Class: entity.ClassWarrior, ActionType: "attack"}, + {Class: entity.ClassMage, ActionType: "attack"}, + {Class: entity.ClassRogue, ActionType: "attack"}, + }, + Effect: ComboEffect{DamageMultiplier: 1.3, BonusDamage: 5, Message: "⚔️ FULL ASSAULT! Combined attack overwhelms!"}, + }, + { + Name: "Restoration", + Required: []ComboAction{ + {Class: entity.ClassHealer, ActionType: "skill"}, + {Class: entity.ClassRogue, ActionType: "item"}, + }, + Effect: ComboEffect{HealAll: 20, Message: "💚 RESTORATION! Combined healing surges!"}, + }, +} + +func DetectCombos(actions map[string]ComboAction) []ComboDef { + var triggered []ComboDef + for _, combo := range comboDefs { + if matchesCombo(combo.Required, actions) { + triggered = append(triggered, combo) + } + } + return triggered +} + +func matchesCombo(required []ComboAction, actions map[string]ComboAction) bool { + for _, req := range required { + found := false + for _, act := range actions { + if act.Class == req.Class && act.ActionType == req.ActionType { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/combat/combo_test.go b/combat/combo_test.go new file mode 100644 index 0000000..c4c897f --- /dev/null +++ b/combat/combo_test.go @@ -0,0 +1,45 @@ +package combat + +import ( + "testing" + + "github.com/tolelom/catacombs/entity" +) + +func TestDetectCombo_IceShatter(t *testing.T) { + actions := map[string]ComboAction{ + "mage": {Class: entity.ClassMage, ActionType: "skill"}, + "warrior": {Class: entity.ClassWarrior, ActionType: "attack"}, + } + combos := DetectCombos(actions) + if len(combos) == 0 { + t.Fatal("expected Ice Shatter combo") + } + if combos[0].Name != "Ice Shatter" { + t.Errorf("expected 'Ice Shatter', got %q", combos[0].Name) + } +} + +func TestDetectCombo_NoMatch(t *testing.T) { + actions := map[string]ComboAction{ + "w1": {Class: entity.ClassWarrior, ActionType: "attack"}, + "w2": {Class: entity.ClassWarrior, ActionType: "attack"}, + } + combos := DetectCombos(actions) + if len(combos) != 0 { + t.Errorf("expected no combos, got %d", len(combos)) + } +} + +func TestDetectCombo_Multiple(t *testing.T) { + // Healer skill + Warrior attack + Rogue item → Holy Assault + Restoration + actions := map[string]ComboAction{ + "healer": {Class: entity.ClassHealer, ActionType: "skill"}, + "warrior": {Class: entity.ClassWarrior, ActionType: "attack"}, + "rogue": {Class: entity.ClassRogue, ActionType: "item"}, + } + combos := DetectCombos(actions) + if len(combos) != 2 { + t.Errorf("expected 2 combos, got %d", len(combos)) + } +} diff --git a/game/turn.go b/game/turn.go index 51a0303..0390a32 100644 --- a/game/turn.go +++ b/game/turn.go @@ -213,6 +213,41 @@ func (s *GameSession) resolvePlayerActions() { return } + // Combo detection: build action map and apply combo effects before resolving attacks + comboActions := make(map[string]combat.ComboAction) + for _, p := range s.state.Players { + if p.IsOut() { + continue + } + action, ok := s.actions[p.Fingerprint] + if !ok { + continue + } + var actionType string + switch action.Type { + case ActionAttack: + actionType = "attack" + case ActionSkill: + actionType = "skill" + case ActionItem: + actionType = "item" + default: + continue + } + comboActions[p.Fingerprint] = combat.ComboAction{Class: p.Class, ActionType: actionType} + } + + combos := combat.DetectCombos(comboActions) + for _, combo := range combos { + s.addLog(combo.Effect.Message) + for i := range intents { + if combo.Effect.DamageMultiplier > 0 { + intents[i].Multiplier *= combo.Effect.DamageMultiplier + } + intents[i].PlayerATK += combo.Effect.BonusDamage + } + } + if len(intents) > 0 && len(s.state.Monsters) > 0 { results := combat.ResolveAttacks(intents, s.state.Monsters, s.cfg.Game.CoopBonus) for i, r := range results { @@ -234,6 +269,17 @@ func (s *GameSession) resolvePlayerActions() { } } + // Apply combo HealAll effects after attack resolution + for _, combo := range combos { + if combo.Effect.HealAll > 0 { + for _, p := range s.state.Players { + if !p.IsOut() { + p.Heal(combo.Effect.HealAll) + } + } + } + } + // Award gold only for monsters that JUST died this turn for i, m := range s.state.Monsters { if m.IsDead() && aliveBeforeTurn[i] {