Wire corpse spawning into combat death handling
When a mob dies in combat, create_corpse is called to spawn a corpse at the mob's position with the mob's inventory transferred. This replaces the direct despawn_mob call, making combat deaths leave lootable corpses behind. The fallback to despawn_mob is kept if the mob somehow has no zone.
This commit is contained in:
parent
4f487d5178
commit
487e316629
2 changed files with 199 additions and 2 deletions
|
|
@ -170,6 +170,13 @@ async def process_combat() -> None:
|
||||||
|
|
||||||
# Despawn mob losers, send victory/defeat messages
|
# Despawn mob losers, send victory/defeat messages
|
||||||
if isinstance(loser, Mob):
|
if isinstance(loser, Mob):
|
||||||
|
from mudlib.corpse import create_corpse
|
||||||
|
from mudlib.zone import Zone
|
||||||
|
|
||||||
|
zone = loser.location
|
||||||
|
if isinstance(zone, Zone):
|
||||||
|
create_corpse(loser, zone)
|
||||||
|
else:
|
||||||
from mudlib.mobs import despawn_mob
|
from mudlib.mobs import despawn_mob
|
||||||
|
|
||||||
despawn_mob(loser)
|
despawn_mob(loser)
|
||||||
|
|
|
||||||
|
|
@ -216,3 +216,193 @@ class TestCorpseAsContainer:
|
||||||
|
|
||||||
dummy_entity = Entity(name="dummy", x=0, y=0, location=test_zone)
|
dummy_entity = Entity(name="dummy", x=0, y=0, location=test_zone)
|
||||||
assert dummy_entity.can_accept(corpse) is False
|
assert dummy_entity.can_accept(corpse) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCombatDeathCorpse:
|
||||||
|
"""Tests for corpse spawning when a mob dies in combat."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mob_death_in_combat_spawns_corpse(self, test_zone):
|
||||||
|
"""Mob death in combat spawns a corpse at mob's position."""
|
||||||
|
from mudlib.combat.encounter import CombatState
|
||||||
|
from mudlib.combat.engine import (
|
||||||
|
active_encounters,
|
||||||
|
process_combat,
|
||||||
|
start_encounter,
|
||||||
|
)
|
||||||
|
from mudlib.combat.moves import CombatMove
|
||||||
|
from mudlib.entity import Entity
|
||||||
|
|
||||||
|
# Clear active encounters
|
||||||
|
active_encounters.clear()
|
||||||
|
|
||||||
|
# Create a weak mob
|
||||||
|
mob = Mob(
|
||||||
|
name="goblin",
|
||||||
|
x=5,
|
||||||
|
y=10,
|
||||||
|
location=test_zone,
|
||||||
|
pl=1.0,
|
||||||
|
stamina=40.0,
|
||||||
|
)
|
||||||
|
mobs.append(mob)
|
||||||
|
|
||||||
|
# Create attacker
|
||||||
|
attacker = Entity(
|
||||||
|
name="hero",
|
||||||
|
x=5,
|
||||||
|
y=10,
|
||||||
|
location=test_zone,
|
||||||
|
pl=100.0,
|
||||||
|
stamina=50.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start encounter
|
||||||
|
encounter = start_encounter(attacker, mob)
|
||||||
|
|
||||||
|
# Set up a lethal move
|
||||||
|
punch = CombatMove(
|
||||||
|
name="punch right",
|
||||||
|
move_type="attack",
|
||||||
|
stamina_cost=5.0,
|
||||||
|
timing_window_ms=800,
|
||||||
|
damage_pct=0.15,
|
||||||
|
countered_by=["dodge left"],
|
||||||
|
)
|
||||||
|
encounter.attack(punch)
|
||||||
|
encounter.state = CombatState.RESOLVE
|
||||||
|
|
||||||
|
# Process combat to trigger resolve
|
||||||
|
await process_combat()
|
||||||
|
|
||||||
|
# Check for corpse at mob's position
|
||||||
|
corpses = [
|
||||||
|
obj for obj in test_zone.contents_at(5, 10) if isinstance(obj, Corpse)
|
||||||
|
]
|
||||||
|
assert len(corpses) == 1
|
||||||
|
assert corpses[0].name == "goblin's corpse"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mob_death_transfers_inventory_to_corpse(self, test_zone, sword):
|
||||||
|
"""Mob death transfers inventory to corpse."""
|
||||||
|
from mudlib.combat.encounter import CombatState
|
||||||
|
from mudlib.combat.engine import (
|
||||||
|
active_encounters,
|
||||||
|
process_combat,
|
||||||
|
start_encounter,
|
||||||
|
)
|
||||||
|
from mudlib.combat.moves import CombatMove
|
||||||
|
from mudlib.entity import Entity
|
||||||
|
|
||||||
|
# Clear active encounters
|
||||||
|
active_encounters.clear()
|
||||||
|
|
||||||
|
# Create a weak mob with inventory
|
||||||
|
mob = Mob(
|
||||||
|
name="goblin",
|
||||||
|
x=5,
|
||||||
|
y=10,
|
||||||
|
location=test_zone,
|
||||||
|
pl=1.0,
|
||||||
|
stamina=40.0,
|
||||||
|
)
|
||||||
|
mobs.append(mob)
|
||||||
|
sword.move_to(mob)
|
||||||
|
|
||||||
|
# Create attacker
|
||||||
|
attacker = Entity(
|
||||||
|
name="hero",
|
||||||
|
x=5,
|
||||||
|
y=10,
|
||||||
|
location=test_zone,
|
||||||
|
pl=100.0,
|
||||||
|
stamina=50.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start encounter and kill mob
|
||||||
|
encounter = start_encounter(attacker, mob)
|
||||||
|
punch = CombatMove(
|
||||||
|
name="punch right",
|
||||||
|
move_type="attack",
|
||||||
|
stamina_cost=5.0,
|
||||||
|
timing_window_ms=800,
|
||||||
|
damage_pct=0.15,
|
||||||
|
countered_by=["dodge left"],
|
||||||
|
)
|
||||||
|
encounter.attack(punch)
|
||||||
|
encounter.state = CombatState.RESOLVE
|
||||||
|
|
||||||
|
# Process combat
|
||||||
|
await process_combat()
|
||||||
|
|
||||||
|
# Find corpse
|
||||||
|
corpses = [
|
||||||
|
obj for obj in test_zone.contents_at(5, 10) if isinstance(obj, Corpse)
|
||||||
|
]
|
||||||
|
assert len(corpses) == 1
|
||||||
|
corpse = corpses[0]
|
||||||
|
|
||||||
|
# Verify sword is in corpse
|
||||||
|
assert sword in corpse._contents
|
||||||
|
assert sword.location is corpse
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_corpse_appears_in_zone_contents(self, test_zone):
|
||||||
|
"""Corpse appears in zone.contents_at after mob death."""
|
||||||
|
from mudlib.combat.encounter import CombatState
|
||||||
|
from mudlib.combat.engine import (
|
||||||
|
active_encounters,
|
||||||
|
process_combat,
|
||||||
|
start_encounter,
|
||||||
|
)
|
||||||
|
from mudlib.combat.moves import CombatMove
|
||||||
|
from mudlib.entity import Entity
|
||||||
|
|
||||||
|
# Clear active encounters
|
||||||
|
active_encounters.clear()
|
||||||
|
|
||||||
|
# Create a weak mob
|
||||||
|
mob = Mob(
|
||||||
|
name="goblin",
|
||||||
|
x=5,
|
||||||
|
y=10,
|
||||||
|
location=test_zone,
|
||||||
|
pl=1.0,
|
||||||
|
stamina=40.0,
|
||||||
|
)
|
||||||
|
mobs.append(mob)
|
||||||
|
|
||||||
|
# Create attacker
|
||||||
|
attacker = Entity(
|
||||||
|
name="hero",
|
||||||
|
x=5,
|
||||||
|
y=10,
|
||||||
|
location=test_zone,
|
||||||
|
pl=100.0,
|
||||||
|
stamina=50.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start encounter and kill mob
|
||||||
|
encounter = start_encounter(attacker, mob)
|
||||||
|
punch = CombatMove(
|
||||||
|
name="punch right",
|
||||||
|
move_type="attack",
|
||||||
|
stamina_cost=5.0,
|
||||||
|
timing_window_ms=800,
|
||||||
|
damage_pct=0.15,
|
||||||
|
countered_by=["dodge left"],
|
||||||
|
)
|
||||||
|
encounter.attack(punch)
|
||||||
|
encounter.state = CombatState.RESOLVE
|
||||||
|
|
||||||
|
# Process combat
|
||||||
|
await process_combat()
|
||||||
|
|
||||||
|
# Verify corpse is in zone contents
|
||||||
|
contents = list(test_zone.contents_at(5, 10))
|
||||||
|
corpse_count = sum(1 for obj in contents if isinstance(obj, Corpse))
|
||||||
|
assert corpse_count == 1
|
||||||
|
|
||||||
|
# Verify it's the goblin's corpse
|
||||||
|
corpse = next(obj for obj in contents if isinstance(obj, Corpse))
|
||||||
|
assert corpse.name == "goblin's corpse"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue