725 lines
22 KiB
Python
725 lines
22 KiB
Python
"""Tests for Corpse class and create_corpse factory."""
|
|
|
|
import time
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.container import Container
|
|
from mudlib.corpse import Corpse, create_corpse
|
|
from mudlib.entity import Mob
|
|
from mudlib.mobs import mobs
|
|
from mudlib.thing import Thing
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_mobs():
|
|
"""Clear mobs registry before and after each test."""
|
|
mobs.clear()
|
|
yield
|
|
mobs.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_zone():
|
|
"""Create a test zone for entities."""
|
|
terrain = [["." for _ in range(256)] for _ in range(256)]
|
|
zone = Zone(
|
|
name="testzone",
|
|
width=256,
|
|
height=256,
|
|
toroidal=True,
|
|
terrain=terrain,
|
|
impassable=set(),
|
|
)
|
|
return zone
|
|
|
|
|
|
@pytest.fixture
|
|
def goblin_mob(test_zone):
|
|
"""Create a goblin mob at (5, 10) in the test zone."""
|
|
mob = Mob(
|
|
name="goblin",
|
|
x=5,
|
|
y=10,
|
|
location=test_zone,
|
|
pl=50.0,
|
|
stamina=40.0,
|
|
)
|
|
mobs.append(mob)
|
|
return mob
|
|
|
|
|
|
@pytest.fixture
|
|
def sword():
|
|
"""Create a sword Thing."""
|
|
return Thing(name="sword", description="a rusty sword", portable=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def potion():
|
|
"""Create a potion Thing."""
|
|
return Thing(name="potion", description="a health potion", portable=True)
|
|
|
|
|
|
class TestCorpseClass:
|
|
def test_corpse_is_container_subclass(self):
|
|
"""Corpse is a subclass of Container."""
|
|
assert issubclass(Corpse, Container)
|
|
|
|
def test_corpse_not_portable(self, test_zone):
|
|
"""Corpse is not portable (can't pick up a corpse)."""
|
|
corpse = Corpse(name="test corpse", location=test_zone, x=0, y=0)
|
|
assert corpse.portable is False
|
|
|
|
def test_corpse_always_open(self, test_zone):
|
|
"""Corpse is always open (closed=False)."""
|
|
corpse = Corpse(name="test corpse", location=test_zone, x=0, y=0)
|
|
assert corpse.closed is False
|
|
|
|
def test_corpse_has_decompose_at_field(self, test_zone):
|
|
"""Corpse has decompose_at field (float, monotonic time)."""
|
|
decompose_time = time.monotonic() + 300
|
|
corpse = Corpse(
|
|
name="test corpse",
|
|
location=test_zone,
|
|
x=0,
|
|
y=0,
|
|
decompose_at=decompose_time,
|
|
)
|
|
assert hasattr(corpse, "decompose_at")
|
|
assert isinstance(corpse.decompose_at, float)
|
|
assert corpse.decompose_at == decompose_time
|
|
|
|
|
|
class TestCreateCorpseFactory:
|
|
def test_creates_corpse_at_mob_position(self, goblin_mob, test_zone):
|
|
"""create_corpse creates a corpse at mob's x, y in the zone."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
assert isinstance(corpse, Corpse)
|
|
assert corpse.x == 5
|
|
assert corpse.y == 10
|
|
assert corpse.location is test_zone
|
|
|
|
def test_corpse_name_from_mob(self, goblin_mob, test_zone):
|
|
"""Corpse name is '{mob.name}'s corpse'."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
assert corpse.name == "goblin's corpse"
|
|
|
|
def test_transfers_mob_inventory(self, goblin_mob, test_zone, sword, potion):
|
|
"""Transfers mob's inventory items into the corpse."""
|
|
# Add items to mob's inventory
|
|
sword.move_to(goblin_mob)
|
|
potion.move_to(goblin_mob)
|
|
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
# Items should now be in the corpse
|
|
assert sword in corpse._contents
|
|
assert potion in corpse._contents
|
|
assert sword.location is corpse
|
|
assert potion.location is corpse
|
|
|
|
def test_sets_decompose_at(self, goblin_mob, test_zone):
|
|
"""Sets decompose_at = time.monotonic() + ttl."""
|
|
before = time.monotonic()
|
|
corpse = create_corpse(goblin_mob, test_zone, ttl=300)
|
|
after = time.monotonic()
|
|
|
|
# decompose_at should be within reasonable range
|
|
assert corpse.decompose_at >= before + 300
|
|
assert corpse.decompose_at <= after + 300
|
|
|
|
def test_custom_ttl(self, goblin_mob, test_zone):
|
|
"""create_corpse respects custom ttl parameter."""
|
|
before = time.monotonic()
|
|
corpse = create_corpse(goblin_mob, test_zone, ttl=600)
|
|
after = time.monotonic()
|
|
|
|
assert corpse.decompose_at >= before + 600
|
|
assert corpse.decompose_at <= after + 600
|
|
|
|
def test_calls_despawn_mob(self, goblin_mob, test_zone):
|
|
"""create_corpse calls despawn_mob to remove mob from registry."""
|
|
assert goblin_mob in mobs
|
|
assert goblin_mob.alive is True
|
|
|
|
create_corpse(goblin_mob, test_zone)
|
|
|
|
assert goblin_mob not in mobs
|
|
assert goblin_mob.alive is False
|
|
|
|
def test_returns_corpse(self, goblin_mob, test_zone):
|
|
"""create_corpse returns the created corpse."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
assert isinstance(corpse, Corpse)
|
|
assert corpse.name == "goblin's corpse"
|
|
|
|
def test_empty_inventory(self, goblin_mob, test_zone):
|
|
"""create_corpse with a mob that has no inventory creates empty corpse."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
assert len(corpse._contents) == 0
|
|
assert corpse.name == "goblin's corpse"
|
|
|
|
|
|
class TestCorpseAsContainer:
|
|
def test_can_put_things_in_corpse(self, goblin_mob, test_zone, sword):
|
|
"""Container commands work: Things can be put into corpses."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
# Manually move item into corpse (simulating "put" command)
|
|
sword.move_to(corpse)
|
|
|
|
assert sword in corpse._contents
|
|
assert sword.location is corpse
|
|
|
|
def test_can_take_things_from_corpse(self, goblin_mob, test_zone, sword, potion):
|
|
"""Container commands work: Things can be taken out of corpses."""
|
|
sword.move_to(goblin_mob)
|
|
potion.move_to(goblin_mob)
|
|
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
# Items start in corpse
|
|
assert sword in corpse._contents
|
|
assert potion in corpse._contents
|
|
|
|
# Take sword out (move to zone)
|
|
sword.move_to(test_zone, x=5, y=10)
|
|
|
|
assert sword not in corpse._contents
|
|
assert sword.location is test_zone
|
|
assert potion in corpse._contents
|
|
|
|
def test_corpse_can_accept_things(self, goblin_mob, test_zone, sword):
|
|
"""Corpse.can_accept returns True for Things."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
assert corpse.can_accept(sword) is True
|
|
|
|
def test_corpse_not_portable(self, goblin_mob, test_zone):
|
|
"""Corpse cannot be picked up (portable=False)."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
# Try to move corpse to a mock entity (simulating "get corpse")
|
|
mock_entity = MagicMock()
|
|
mock_entity._contents = []
|
|
|
|
# Corpse is not portable, so entity.can_accept would return False
|
|
# (Entity.can_accept checks obj.portable)
|
|
from mudlib.entity import Entity
|
|
|
|
dummy_entity = Entity(name="dummy", x=0, y=0, location=test_zone)
|
|
assert dummy_entity.can_accept(corpse) is False
|
|
|
|
|
|
class TestCombatDeathCorpse:
|
|
"""Knockouts do not create corpses until a finisher is used."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_corpses(self):
|
|
from mudlib.corpse import active_corpses
|
|
|
|
active_corpses.clear()
|
|
yield
|
|
active_corpses.clear()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mob_knockout_in_combat_does_not_spawn_corpse(self, test_zone):
|
|
"""KO in combat should not create a corpse by itself."""
|
|
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,
|
|
hit_time_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 no corpse spawned yet
|
|
corpses = [
|
|
obj for obj in test_zone.contents_at(5, 10) if isinstance(obj, Corpse)
|
|
]
|
|
assert len(corpses) == 0
|
|
assert mob in mobs
|
|
assert mob.alive is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mob_knockout_keeps_inventory_on_mob(self, test_zone, sword):
|
|
"""KO should not transfer inventory to a corpse until finished."""
|
|
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,
|
|
hit_time_ms=800,
|
|
damage_pct=0.15,
|
|
countered_by=["dodge left"],
|
|
)
|
|
encounter.attack(punch)
|
|
encounter.state = CombatState.RESOLVE
|
|
|
|
# Process combat
|
|
await process_combat()
|
|
|
|
# No corpse yet
|
|
corpses = [
|
|
obj for obj in test_zone.contents_at(5, 10) if isinstance(obj, Corpse)
|
|
]
|
|
assert len(corpses) == 0
|
|
assert sword in mob._contents
|
|
assert sword.location is mob
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_corpse_in_zone_contents_after_ko(self, test_zone):
|
|
"""Zone should not contain corpse from a plain KO."""
|
|
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,
|
|
hit_time_ms=800,
|
|
damage_pct=0.15,
|
|
countered_by=["dodge left"],
|
|
)
|
|
encounter.attack(punch)
|
|
encounter.state = CombatState.RESOLVE
|
|
|
|
# Process combat
|
|
await process_combat()
|
|
|
|
# Verify no corpse 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 == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_snapneck_finisher_spawns_corpse(self, test_zone):
|
|
"""Explicit finisher kill should create a corpse."""
|
|
from unittest.mock import AsyncMock
|
|
|
|
from mudlib.commands.snapneck import cmd_snap_neck
|
|
from mudlib.player import Player, players
|
|
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
attacker = Player(name="hero", x=5, y=10, reader=reader, writer=writer)
|
|
attacker.location = test_zone
|
|
test_zone._contents.append(attacker)
|
|
players[attacker.name] = attacker
|
|
|
|
mob = Mob(
|
|
name="goblin",
|
|
x=5,
|
|
y=10,
|
|
location=test_zone,
|
|
pl=0.0,
|
|
stamina=0.0,
|
|
)
|
|
mobs.append(mob)
|
|
|
|
from mudlib.combat.engine import start_encounter
|
|
|
|
start_encounter(attacker, mob)
|
|
attacker.mode_stack.append("combat")
|
|
await cmd_snap_neck(attacker, "goblin")
|
|
|
|
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"
|
|
players.clear()
|
|
|
|
|
|
class TestCorpseDisplay:
|
|
"""Tests for corpse display in look command."""
|
|
|
|
@pytest.fixture
|
|
def player(self, test_zone):
|
|
"""Create a test player."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from mudlib.player import Player, players
|
|
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
p = Player(
|
|
name="TestPlayer",
|
|
x=5,
|
|
y=10,
|
|
reader=reader,
|
|
writer=writer,
|
|
)
|
|
p.location = test_zone
|
|
test_zone._contents.append(p)
|
|
players[p.name] = p
|
|
yield p
|
|
players.clear()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_corpse_shown_in_look(self, player, test_zone):
|
|
"""Corpse appears as 'X is here.' in look output."""
|
|
from mudlib.commands.look import cmd_look
|
|
|
|
# Create a corpse on player's tile
|
|
Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() + 300,
|
|
)
|
|
|
|
await cmd_look(player, "")
|
|
|
|
# Check output for corpse line
|
|
output = "".join(
|
|
call.args[0] for call in player.writer.write.call_args_list if call.args
|
|
)
|
|
assert "goblin's corpse is here." in output
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_corpse_not_in_ground_items(self, player, test_zone):
|
|
"""Corpse is NOT in 'On the ground:' list, but regular items are."""
|
|
from mudlib.commands.look import cmd_look
|
|
|
|
# Create a corpse and a regular item on player's tile
|
|
Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() + 300,
|
|
)
|
|
Thing(name="sword", location=test_zone, x=5, y=10)
|
|
|
|
await cmd_look(player, "")
|
|
|
|
output = "".join(
|
|
call.args[0] for call in player.writer.write.call_args_list if call.args
|
|
)
|
|
|
|
# Corpse should be shown as "is here", not in ground items
|
|
assert "goblin's corpse is here." in output
|
|
# Sword should be in ground items
|
|
assert "On the ground:" in output
|
|
assert "sword" in output
|
|
# Corpse name should NOT appear in the ground items line
|
|
lines = output.split("\r\n")
|
|
ground_line = next((line for line in lines if "On the ground:" in line), None)
|
|
assert ground_line is not None
|
|
assert "goblin's corpse" not in ground_line
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_corpses(self, player, test_zone):
|
|
"""Multiple corpses each show as separate 'X is here.' lines."""
|
|
from mudlib.commands.look import cmd_look
|
|
|
|
# Create two corpses on player's tile
|
|
Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() + 300,
|
|
)
|
|
Corpse(
|
|
name="orc's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() + 300,
|
|
)
|
|
|
|
await cmd_look(player, "")
|
|
|
|
output = "".join(
|
|
call.args[0] for call in player.writer.write.call_args_list if call.args
|
|
)
|
|
|
|
# Both corpses should appear
|
|
assert "goblin's corpse is here." in output
|
|
assert "orc's corpse is here." in output
|
|
|
|
|
|
class TestDecomposition:
|
|
@pytest.fixture(autouse=True)
|
|
def clear_corpses(self):
|
|
from mudlib.corpse import active_corpses
|
|
|
|
active_corpses.clear()
|
|
yield
|
|
active_corpses.clear()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_expired_corpse_removed(self, test_zone):
|
|
"""Corpse past its decompose_at is removed from the world."""
|
|
from mudlib.corpse import active_corpses, process_decomposing
|
|
|
|
corpse = Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() - 1, # already expired
|
|
)
|
|
active_corpses.append(corpse)
|
|
|
|
await process_decomposing()
|
|
|
|
assert corpse not in active_corpses
|
|
assert corpse.location is None
|
|
assert corpse not in test_zone._contents
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unexpired_corpse_stays(self, test_zone):
|
|
"""Corpse before its decompose_at stays in the world."""
|
|
from mudlib.corpse import active_corpses, process_decomposing
|
|
|
|
corpse = Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() + 300, # far future
|
|
)
|
|
active_corpses.append(corpse)
|
|
|
|
await process_decomposing()
|
|
|
|
assert corpse in active_corpses
|
|
assert corpse.location is test_zone
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decomposition_broadcasts_message(self, test_zone):
|
|
"""Decomposition broadcasts to entities at the same tile."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from mudlib.corpse import active_corpses, process_decomposing
|
|
from mudlib.player import Player
|
|
|
|
# Create player at same position
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
_player = Player(
|
|
name="hero", x=5, y=10, reader=reader, writer=writer, location=test_zone
|
|
)
|
|
|
|
corpse = Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() - 1,
|
|
)
|
|
active_corpses.append(corpse)
|
|
|
|
await process_decomposing()
|
|
|
|
# Check that decomposition message was written
|
|
messages = [call[0][0] for call in writer.write.call_args_list]
|
|
assert any("goblin's corpse decomposes" in msg for msg in messages)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decomposition_only_broadcasts_to_same_tile(self, test_zone):
|
|
"""Decomposition does NOT broadcast to entities at different tiles."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from mudlib.corpse import active_corpses, process_decomposing
|
|
from mudlib.player import Player
|
|
|
|
# Player at different position
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
_player = Player(
|
|
name="faraway", x=50, y=50, reader=reader, writer=writer, location=test_zone
|
|
)
|
|
|
|
corpse = Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() - 1,
|
|
)
|
|
active_corpses.append(corpse)
|
|
|
|
await process_decomposing()
|
|
|
|
# Check that no decomposition message was written
|
|
messages = [call[0][0] for call in writer.write.call_args_list]
|
|
assert not any("goblin's corpse decomposes" in msg for msg in messages)
|
|
|
|
def test_create_corpse_registers_in_active_corpses(self, goblin_mob, test_zone):
|
|
"""create_corpse adds corpse to active_corpses list."""
|
|
from mudlib.corpse import active_corpses
|
|
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
|
|
assert corpse in active_corpses
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_items_in_decomposed_corpse_are_lost(self, test_zone):
|
|
"""Items inside a corpse when it decomposes are removed with it."""
|
|
from mudlib.corpse import active_corpses, process_decomposing
|
|
|
|
corpse = Corpse(
|
|
name="goblin's corpse",
|
|
location=test_zone,
|
|
x=5,
|
|
y=10,
|
|
decompose_at=time.monotonic() - 1,
|
|
)
|
|
sword = Thing(name="sword", location=corpse)
|
|
active_corpses.append(corpse)
|
|
|
|
await process_decomposing()
|
|
|
|
# Corpse is gone
|
|
assert corpse.location is None
|
|
# Sword should also be removed (items rot with the corpse)
|
|
assert sword.location is None
|
|
|
|
|
|
class TestCorpseLoot:
|
|
"""Tests for loot drops in corpses."""
|
|
|
|
def test_create_corpse_with_loot(self, goblin_mob, test_zone):
|
|
"""create_corpse with loot_table rolls loot and adds to corpse."""
|
|
from mudlib.loot import LootEntry
|
|
|
|
loot = [LootEntry(name="gold coin", chance=1.0)]
|
|
corpse = create_corpse(goblin_mob, test_zone, loot_table=loot)
|
|
items = [obj for obj in corpse._contents if isinstance(obj, Thing)]
|
|
assert len(items) == 1
|
|
assert items[0].name == "gold coin"
|
|
|
|
def test_create_corpse_without_loot(self, goblin_mob, test_zone):
|
|
"""create_corpse without loot_table creates empty corpse."""
|
|
corpse = create_corpse(goblin_mob, test_zone)
|
|
assert len(corpse._contents) == 0
|