1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
package game
// Regression: Dead Man's Switch retribution can zero a mob inside killPlayer,
// after endCombat has already taken the player-death branch and returned.
// That left the mob at HP 0 in MobStore: filtered out of MobsInRoom (HP > 0)
// and skipped by SeedMobs, it vanished from the room until restart, with no
// drops and no respawn. Retribution kills must route through the same
// mob-death helper the normal attack path uses.
import (
"testing"
"thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
const retributionMobYAML = `id: retribution_target
name: test target
combat:
aggressive: false
attack_types:
- crush
kind: combat
respawn_ticks: 1
size: medium
stats:
attack: 1
defense: 1
hp: 1
max_melee_hit: 1
speed: 5
strength: 1
drops:
remains: bones
`
func TestRetributionKillRunsMobDeathPath(t *testing.T) {
dir := t.TempDir()
seedWorld(t, dir)
writeDataFile(t, dir, "items", "bones.yaml", "id: bones\nname: bones\nstackable: false\nvalue: 1\n")
writeDataFile(t, dir, "techs", "retribution.yaml",
"name: Dead Man's Switch\nlevel: 46\neffects:\n retribution_pct: 0.25\n")
writeDataFile(t, dir, "mobs", "retribution_target.yaml", retributionMobYAML)
g := newHardeningGame(t, dir)
g.RunOnOwner(func() {
g.MobStore.ReloadDefs(dir)
def, err := g.MobStore.LoadDef("retribution_target")
if err != nil {
t.Fatalf("load mob def: %v", err)
}
mob := g.MobStore.SpawnTransient(def, &behavior.SpawnMobConfig{ID: "retribution_target"}, 2, "")
// A normal seeded mob, not a trigger despawn: respawn must restore it.
mob.SpawnedByTrigger = false
mob.RespawnTicks = 1
mob.Drops.Remains = "bones"
mob.HP = 1
mob.MaxHP = 1
p := player.New("alice")
p.RoomID = 2
p.HP = 0
p.ActivateTech("retribution")
conn := newChanConn()
sess := &net.Session{Conn: conn, Player: p}
sess.SetState(net.StateGame)
g.endCombat(sess, p, mob)
if mob.HP != 0 {
t.Fatalf("mob HP = %d, want 0 after retribution", mob.HP)
}
if ground := g.World.GroundItems(2); ground["bones"] != 1 {
t.Fatalf("retribution kill did not run the drops path: ground=%v", ground)
}
// The death path schedules a respawn; one tick must restore the mob
// in its home room rather than leave it gone at HP 0.
g.Ticks.Tick()
if mob.HP != mob.MaxHP || mob.HP <= 0 {
t.Fatalf("mob did not respawn: HP=%d MaxHP=%d", mob.HP, mob.MaxHP)
}
if mob.RoomID != 2 {
t.Fatalf("mob respawned in room %d, want 2", mob.RoomID)
}
})
}
|