aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_retribution_mob_death_test.go
blob: cf058209c891faff21e1054a98e46e2ed076e2ad (plain)
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)
		}
	})
}