feat: add combo skill system with 5 combos
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
combat/combo.go
Normal file
91
combat/combo.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
45
combat/combo_test.go
Normal file
45
combat/combo_test.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
46
game/turn.go
46
game/turn.go
@@ -213,6 +213,41 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
return
|
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 {
|
if len(intents) > 0 && len(s.state.Monsters) > 0 {
|
||||||
results := combat.ResolveAttacks(intents, s.state.Monsters, s.cfg.Game.CoopBonus)
|
results := combat.ResolveAttacks(intents, s.state.Monsters, s.cfg.Game.CoopBonus)
|
||||||
for i, r := range results {
|
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
|
// Award gold only for monsters that JUST died this turn
|
||||||
for i, m := range s.state.Monsters {
|
for i, m := range s.state.Monsters {
|
||||||
if m.IsDead() && aliveBeforeTurn[i] {
|
if m.IsDead() && aliveBeforeTurn[i] {
|
||||||
|
|||||||
Reference in New Issue
Block a user