diff --git a/game/event.go b/game/event.go index 42b3756..5e6b776 100644 --- a/game/event.go +++ b/game/event.go @@ -212,21 +212,63 @@ func armorName(floor int) string { } func (s *GameSession) triggerEvent() { + event := PickRandomEvent() + s.addLog(fmt.Sprintf("Event: %s — %s", event.Name, event.Description)) + + // Auto-resolve with a random choice + choice := event.Choices[rand.Intn(len(event.Choices))] + outcome := choice.Resolve(s.state.FloorNum) + s.addLog(fmt.Sprintf(" → %s: %s", choice.Label, outcome.Description)) + + // Pick a random alive player to apply the outcome + var alive []*entity.Player for _, p := range s.state.Players { - if p.IsDead() { - continue + if !p.IsDead() { + alive = append(alive, p) } - if rand.Float64() < 0.5 { - baseDmg := 10 + s.state.FloorNum - dmg := baseDmg + rand.Intn(baseDmg/2+1) - p.TakeDamage(dmg) - s.addLog(fmt.Sprintf("Trap! %s takes %d damage", p.Name, dmg)) + } + if len(alive) == 0 { + return + } + target := alive[rand.Intn(len(alive))] + + if outcome.HPChange > 0 { + before := target.HP + target.Heal(outcome.HPChange) + s.addLog(fmt.Sprintf(" %s heals %d HP", target.Name, target.HP-before)) + } else if outcome.HPChange < 0 { + target.TakeDamage(-outcome.HPChange) + s.addLog(fmt.Sprintf(" %s takes %d damage", target.Name, -outcome.HPChange)) + } + + if outcome.GoldChange != 0 { + target.Gold += outcome.GoldChange + if target.Gold < 0 { + target.Gold = 0 + } + if outcome.GoldChange > 0 { + s.addLog(fmt.Sprintf(" %s gains %d gold", target.Name, outcome.GoldChange)) } else { - baseHeal := 15 + s.state.FloorNum - heal := baseHeal + rand.Intn(baseHeal/2+1) - before := p.HP - p.Heal(heal) - s.addLog(fmt.Sprintf("Blessing! %s heals %d HP", p.Name, p.HP-before)) + s.addLog(fmt.Sprintf(" %s loses %d gold", target.Name, -outcome.GoldChange)) + } + } + + if outcome.ItemDrop { + if len(target.Inventory) < s.cfg.Game.InventoryLimit { + floor := s.state.FloorNum + if rand.Float64() < 0.5 { + bonus := 3 + rand.Intn(6) + floor/3 + item := entity.Item{Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus} + target.Inventory = append(target.Inventory, item) + s.addLog(fmt.Sprintf(" %s found %s (ATK+%d)", target.Name, item.Name, item.Bonus)) + } else { + bonus := 2 + rand.Intn(4) + floor/4 + item := entity.Item{Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus} + target.Inventory = append(target.Inventory, item) + s.addLog(fmt.Sprintf(" %s found %s (DEF+%d)", target.Name, item.Name, item.Bonus)) + } + } else { + s.addLog(fmt.Sprintf(" %s's inventory is full!", target.Name)) } } } diff --git a/game/random_event.go b/game/random_event.go new file mode 100644 index 0000000..0c1083e --- /dev/null +++ b/game/random_event.go @@ -0,0 +1,247 @@ +package game + +import "math/rand" + +// EventOutcome describes the result of choosing an event option. +type EventOutcome struct { + HPChange int + GoldChange int + ItemDrop bool + Description string +} + +// EventChoice represents a single choice the player can make during an event. +type EventChoice struct { + Label string + Resolve func(floor int) EventOutcome +} + +// RandomEvent represents a random event with multiple choices. +type RandomEvent struct { + Name string + Description string + Choices []EventChoice +} + +// GetRandomEvents returns all 8 defined random events. +func GetRandomEvents() []RandomEvent { + return []RandomEvent{ + { + Name: "altar", + Description: "You discover an ancient altar glowing with strange energy.", + Choices: []EventChoice{ + { + Label: "Pray at the altar", + Resolve: func(floor int) EventOutcome { + if rand.Float64() < 0.6 { + heal := 15 + floor*2 + return EventOutcome{HPChange: heal, Description: "The altar blesses you with healing light."} + } + dmg := 10 + floor + return EventOutcome{HPChange: -dmg, Description: "The altar's energy lashes out at you!"} + }, + }, + { + Label: "Offer gold", + Resolve: func(floor int) EventOutcome { + cost := 10 + floor + return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "You offer gold and receive a divine gift."} + }, + }, + { + Label: "Walk away", + Resolve: func(floor int) EventOutcome { + return EventOutcome{Description: "You leave the altar undisturbed."} + }, + }, + }, + }, + { + Name: "fountain", + Description: "A shimmering fountain bubbles in the center of the room.", + Choices: []EventChoice{ + { + Label: "Drink from the fountain", + Resolve: func(floor int) EventOutcome { + heal := 20 + floor*2 + return EventOutcome{HPChange: heal, Description: "The water rejuvenates you!"} + }, + }, + { + Label: "Toss a coin", + Resolve: func(floor int) EventOutcome { + if rand.Float64() < 0.5 { + gold := 15 + floor*3 + return EventOutcome{GoldChange: gold, Description: "The fountain rewards your generosity!"} + } + return EventOutcome{GoldChange: -5, Description: "The coin sinks and nothing happens."} + }, + }, + }, + }, + { + Name: "merchant", + Description: "A hooded merchant appears from the shadows.", + Choices: []EventChoice{ + { + Label: "Trade gold for healing", + Resolve: func(floor int) EventOutcome { + cost := 15 + floor + heal := 25 + floor*2 + return EventOutcome{HPChange: heal, GoldChange: -cost, Description: "The merchant sells you a healing draught."} + }, + }, + { + Label: "Buy a mystery item", + Resolve: func(floor int) EventOutcome { + cost := 20 + floor*2 + return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "The merchant hands you a wrapped package."} + }, + }, + { + Label: "Decline", + Resolve: func(floor int) EventOutcome { + return EventOutcome{Description: "The merchant vanishes into the shadows."} + }, + }, + }, + }, + { + Name: "trap_room", + Description: "The floor is covered with suspicious pressure plates.", + Choices: []EventChoice{ + { + Label: "Carefully navigate", + Resolve: func(floor int) EventOutcome { + if rand.Float64() < 0.5 { + return EventOutcome{Description: "You skillfully avoid all the traps!"} + } + dmg := 8 + floor + return EventOutcome{HPChange: -dmg, Description: "You trigger a trap and take damage!"} + }, + }, + { + Label: "Rush through", + Resolve: func(floor int) EventOutcome { + dmg := 5 + floor/2 + gold := 10 + floor*2 + return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "You take minor damage but find hidden gold!"} + }, + }, + }, + }, + { + Name: "shrine", + Description: "A glowing shrine hums with divine power.", + Choices: []EventChoice{ + { + Label: "Kneel and pray", + Resolve: func(floor int) EventOutcome { + heal := 30 + floor*2 + return EventOutcome{HPChange: heal, Description: "The shrine fills you with renewed vigor!"} + }, + }, + { + Label: "Take the offering", + Resolve: func(floor int) EventOutcome { + gold := 20 + floor*3 + dmg := 15 + floor + return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "You steal the offering but anger the spirits!"} + }, + }, + }, + }, + { + Name: "chest", + Description: "An ornate chest sits in the corner of the room.", + Choices: []EventChoice{ + { + Label: "Open carefully", + Resolve: func(floor int) EventOutcome { + if rand.Float64() < 0.7 { + gold := 15 + floor*2 + return EventOutcome{GoldChange: gold, Description: "The chest contains a pile of gold!"} + } + dmg := 12 + floor + return EventOutcome{HPChange: -dmg, Description: "The chest was a mimic! It bites you!"} + }, + }, + { + Label: "Smash it open", + Resolve: func(floor int) EventOutcome { + return EventOutcome{ItemDrop: true, Description: "You smash the chest and find equipment inside!"} + }, + }, + { + Label: "Leave it", + Resolve: func(floor int) EventOutcome { + return EventOutcome{Description: "Better safe than sorry."} + }, + }, + }, + }, + { + Name: "ghost", + Description: "A spectral figure materializes before you.", + Choices: []EventChoice{ + { + Label: "Speak with the ghost", + Resolve: func(floor int) EventOutcome { + gold := 10 + floor*2 + return EventOutcome{GoldChange: gold, Description: "The ghost thanks you for listening and rewards you."} + }, + }, + { + Label: "Attack the ghost", + Resolve: func(floor int) EventOutcome { + if rand.Float64() < 0.4 { + return EventOutcome{ItemDrop: true, Description: "The ghost drops a spectral weapon as it fades!"} + } + dmg := 15 + floor + return EventOutcome{HPChange: -dmg, Description: "The ghost retaliates with ghostly fury!"} + }, + }, + }, + }, + { + Name: "mushroom", + Description: "Strange glowing mushrooms grow in clusters here.", + Choices: []EventChoice{ + { + Label: "Eat a mushroom", + Resolve: func(floor int) EventOutcome { + r := rand.Float64() + if r < 0.33 { + heal := 20 + floor*2 + return EventOutcome{HPChange: heal, Description: "The mushroom tastes great and heals you!"} + } else if r < 0.66 { + dmg := 10 + floor + return EventOutcome{HPChange: -dmg, Description: "The mushroom was poisonous!"} + } + gold := 10 + floor + return EventOutcome{GoldChange: gold, Description: "The mushroom gives you strange visions... and gold falls from above!"} + }, + }, + { + Label: "Collect and sell", + Resolve: func(floor int) EventOutcome { + gold := 8 + floor + return EventOutcome{GoldChange: gold, Description: "You carefully harvest the mushrooms for sale."} + }, + }, + { + Label: "Ignore them", + Resolve: func(floor int) EventOutcome { + return EventOutcome{Description: "You wisely avoid the mysterious fungi."} + }, + }, + }, + }, + } +} + +// PickRandomEvent returns a random event from the list. +func PickRandomEvent() RandomEvent { + events := GetRandomEvents() + return events[rand.Intn(len(events))] +} diff --git a/game/random_event_test.go b/game/random_event_test.go new file mode 100644 index 0000000..844b9ad --- /dev/null +++ b/game/random_event_test.go @@ -0,0 +1,100 @@ +package game + +import "testing" + +func TestGetRandomEvents(t *testing.T) { + events := GetRandomEvents() + if len(events) < 8 { + t.Errorf("Expected at least 8 random events, got %d", len(events)) + } + for _, e := range events { + if len(e.Choices) < 2 { + t.Errorf("Event %q should have at least 2 choices, got %d", e.Name, len(e.Choices)) + } + if e.Name == "" { + t.Error("Event name should not be empty") + } + if e.Description == "" { + t.Errorf("Event %q description should not be empty", e.Name) + } + } +} + +func TestResolveChoice(t *testing.T) { + events := GetRandomEvents() + + // Test altar "Walk away" — always safe + var altar RandomEvent + for _, e := range events { + if e.Name == "altar" { + altar = e + break + } + } + if altar.Name == "" { + t.Fatal("altar event not found") + } + // The third choice (index 2) is "Walk away" — should have no HP/gold change + outcome := altar.Choices[2].Resolve(5) + if outcome.HPChange != 0 || outcome.GoldChange != 0 { + t.Errorf("Walk away should have no changes, got HP=%d Gold=%d", outcome.HPChange, outcome.GoldChange) + } + + // Test fountain "Drink" — always heals + var fountain RandomEvent + for _, e := range events { + if e.Name == "fountain" { + fountain = e + break + } + } + if fountain.Name == "" { + t.Fatal("fountain event not found") + } + drinkOutcome := fountain.Choices[0].Resolve(10) + if drinkOutcome.HPChange <= 0 { + t.Errorf("Drinking from fountain should heal, got HP=%d", drinkOutcome.HPChange) + } + + // Test shrine "Kneel and pray" — always heals + var shrine RandomEvent + for _, e := range events { + if e.Name == "shrine" { + shrine = e + break + } + } + if shrine.Name == "" { + t.Fatal("shrine event not found") + } + prayOutcome := shrine.Choices[0].Resolve(5) + if prayOutcome.HPChange <= 0 { + t.Errorf("Praying at shrine should heal, got HP=%d", prayOutcome.HPChange) + } + + // Test chest "Smash it open" — always gives item + var chest RandomEvent + for _, e := range events { + if e.Name == "chest" { + chest = e + break + } + } + if chest.Name == "" { + t.Fatal("chest event not found") + } + smashOutcome := chest.Choices[1].Resolve(5) + if !smashOutcome.ItemDrop { + t.Error("Smashing chest should always give an item drop") + } +} + +func TestPickRandomEvent(t *testing.T) { + event := PickRandomEvent() + if event.Name == "" { + t.Error("PickRandomEvent should return a valid event") + } + if len(event.Choices) < 2 { + t.Errorf("Picked event should have at least 2 choices, got %d", len(event.Choices)) + } +}