mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Replaces the single-roll probability system for dungeon and arena combat with a multi-phase simulation engine where gear, buffs, pets, and NPCs are meaningful mechanical inputs. Adds consumable items (auto-crafted from forage/mine/fish drops), monster abilities for T2+ enemies, and Dragon Quest-style combat narrative with phased message delivery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRenderCombatLog_ProducesMessages(t *testing.T) {
|
|
player := Combatant{
|
|
Name: "Hero",
|
|
Stats: CombatStats{MaxHP: 100, Attack: 20, Defense: 10, Speed: 8, CritRate: 0.05},
|
|
Mods: CombatModifiers{DamageReduct: 1.0},
|
|
IsPlayer: true,
|
|
}
|
|
enemy := Combatant{
|
|
Name: "Goblin",
|
|
Stats: CombatStats{MaxHP: 60, Attack: 12, Defense: 5, Speed: 6, CritRate: 0.03},
|
|
Mods: CombatModifiers{DamageReduct: 1.0},
|
|
}
|
|
|
|
result := SimulateCombat(player, enemy, defaultCombatPhases)
|
|
msgs := RenderCombatLog(result, "Hero", "Goblin")
|
|
|
|
if len(msgs) < 1 {
|
|
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestRenderCombatLog_PhaseHeaders(t *testing.T) {
|
|
player := Combatant{
|
|
Name: "Hero",
|
|
Stats: CombatStats{MaxHP: 100, Attack: 20, Defense: 10, Speed: 8, CritRate: 0.05},
|
|
Mods: CombatModifiers{DamageReduct: 1.0},
|
|
IsPlayer: true,
|
|
}
|
|
enemy := Combatant{
|
|
Name: "Rat",
|
|
Stats: CombatStats{MaxHP: 40, Attack: 8, Defense: 3, Speed: 4},
|
|
Mods: CombatModifiers{DamageReduct: 1.0},
|
|
}
|
|
|
|
result := SimulateCombat(player, enemy, defaultCombatPhases)
|
|
msgs := RenderCombatLog(result, "Hero", "Rat")
|
|
|
|
hasOpening := false
|
|
for _, m := range msgs {
|
|
if strings.Contains(m, "Opening") {
|
|
hasOpening = true
|
|
}
|
|
}
|
|
if !hasOpening {
|
|
t.Error("should have an Opening phase header")
|
|
}
|
|
}
|
|
|
|
func TestRenderCombatLog_WinPhaseMessages(t *testing.T) {
|
|
result := CombatResult{
|
|
PlayerWon: true,
|
|
PlayerStartHP: 100,
|
|
EnemyStartHP: 50,
|
|
PlayerEndHP: 80,
|
|
EnemyEndHP: 0,
|
|
TotalRounds: 3,
|
|
Closeness: 0.2,
|
|
Events: []CombatEvent{
|
|
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 25, EnemyHP: 25, PlayerHP: 100},
|
|
{Round: 2, Phase: "clash", Actor: "player", Action: "hit", Damage: 25, EnemyHP: 0, PlayerHP: 80},
|
|
},
|
|
}
|
|
|
|
msgs := RenderCombatLog(result, "Hero", "Slime")
|
|
|
|
if len(msgs) < 1 {
|
|
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
|
|
}
|
|
// Phase messages should contain damage numbers
|
|
found := false
|
|
for _, m := range msgs {
|
|
if strings.Contains(m, "25") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("phase messages should contain damage values")
|
|
}
|
|
}
|
|
|
|
func TestRenderCombatLog_LossPhaseMessages(t *testing.T) {
|
|
result := CombatResult{
|
|
PlayerWon: false,
|
|
PlayerStartHP: 100,
|
|
EnemyStartHP: 80,
|
|
PlayerEndHP: 0,
|
|
EnemyEndHP: 40,
|
|
TotalRounds: 4,
|
|
Closeness: 0.5,
|
|
Events: []CombatEvent{
|
|
{Round: 1, Phase: "opening", Actor: "enemy", Action: "hit", Damage: 50, PlayerHP: 50, EnemyHP: 80},
|
|
{Round: 2, Phase: "clash", Actor: "enemy", Action: "hit", Damage: 50, PlayerHP: 0, EnemyHP: 80},
|
|
},
|
|
}
|
|
|
|
msgs := RenderCombatLog(result, "Hero", "Dragon")
|
|
if len(msgs) < 1 {
|
|
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestRenderCombatLogArena_ProducesPhaseMessages(t *testing.T) {
|
|
result := CombatResult{
|
|
PlayerWon: true,
|
|
PlayerStartHP: 100,
|
|
EnemyStartHP: 60,
|
|
PlayerEndHP: 70,
|
|
EnemyEndHP: 0,
|
|
TotalRounds: 3,
|
|
Closeness: 0.3,
|
|
Events: []CombatEvent{
|
|
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 30, EnemyHP: 30, PlayerHP: 100},
|
|
{Round: 2, Phase: "clash", Actor: "player", Action: "hit", Damage: 30, EnemyHP: 0, PlayerHP: 70},
|
|
},
|
|
}
|
|
|
|
msgs := RenderCombatLogArena(result, "Hero", "Ratticus")
|
|
if len(msgs) < 1 {
|
|
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestRenderCombatLog_ConsumableEvents(t *testing.T) {
|
|
result := CombatResult{
|
|
PlayerWon: true,
|
|
PlayerStartHP: 100,
|
|
EnemyStartHP: 60,
|
|
PlayerEndHP: 80,
|
|
EnemyEndHP: 0,
|
|
Events: []CombatEvent{
|
|
{Round: 0, Phase: "pre_combat", Actor: "consumable", Action: "use_consumable", Desc: "Goblin Grease"},
|
|
{Round: 0, Phase: "pre_combat", Actor: "consumable", Action: "flat_damage", Damage: 8, PlayerHP: 100, EnemyHP: 52},
|
|
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 30, PlayerHP: 100, EnemyHP: 22},
|
|
{Round: 2, Phase: "clash", Actor: "player", Action: "hit", Damage: 22, PlayerHP: 80, EnemyHP: 0},
|
|
},
|
|
}
|
|
|
|
msgs := RenderCombatLog(result, "Hero", "Imp")
|
|
|
|
found := false
|
|
for _, m := range msgs {
|
|
if strings.Contains(m, "Goblin Grease") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("should render consumable use event mentioning item name")
|
|
}
|
|
}
|
|
|
|
func TestRenderCombatLog_MonsterAbilityEvents(t *testing.T) {
|
|
result := CombatResult{
|
|
PlayerWon: true,
|
|
PlayerStartHP: 100,
|
|
EnemyStartHP: 60,
|
|
PlayerEndHP: 50,
|
|
EnemyEndHP: 0,
|
|
Events: []CombatEvent{
|
|
{Round: 1, Phase: "opening", Actor: "enemy", Action: "armor_break", Damage: 0, PlayerHP: 100, EnemyHP: 60},
|
|
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 30, PlayerHP: 100, EnemyHP: 30},
|
|
{Round: 2, Phase: "clash", Actor: "enemy", Action: "hit", Damage: 25, PlayerHP: 75, EnemyHP: 30},
|
|
{Round: 2, Phase: "clash", Actor: "enemy", Action: "poison_tick", Damage: 5, PlayerHP: 70, EnemyHP: 30},
|
|
{Round: 3, Phase: "decisive", Actor: "player", Action: "crit", Damage: 30, PlayerHP: 50, EnemyHP: 0},
|
|
},
|
|
}
|
|
|
|
msgs := RenderCombatLog(result, "Hero", "Wyrm")
|
|
|
|
hasArmorBreak := false
|
|
hasPoison := false
|
|
for _, m := range msgs {
|
|
if strings.Contains(m, "armor") || strings.Contains(m, "Armor") || strings.Contains(m, "defense") || strings.Contains(m, "Defense") {
|
|
hasArmorBreak = true
|
|
}
|
|
if strings.Contains(m, "poison") || strings.Contains(m, "Poison") || strings.Contains(m, "toxin") || strings.Contains(m, "Venom") || strings.Contains(m, "venom") {
|
|
hasPoison = true
|
|
}
|
|
}
|
|
if !hasArmorBreak {
|
|
t.Error("should render armor_break event")
|
|
}
|
|
if !hasPoison {
|
|
t.Error("should render poison_tick event")
|
|
}
|
|
}
|