Add decomposition timer with broadcast and game loop integration
This commit is contained in:
parent
0fbd63a1f7
commit
189f8ac273
2 changed files with 71 additions and 18 deletions
|
|
@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|||
|
||||
from mudlib.container import Container
|
||||
from mudlib.entity import Mob
|
||||
from mudlib.loot import LootEntry, roll_loot
|
||||
from mudlib.mobs import despawn_mob
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
|
@ -28,16 +29,19 @@ class Corpse(Container):
|
|||
decompose_at: float = 0.0
|
||||
|
||||
|
||||
def create_corpse(mob: Mob, zone: Zone, ttl: int = 300) -> Corpse:
|
||||
def create_corpse(
|
||||
mob: Mob, zone: Zone, ttl: int = 300, loot_table: list[LootEntry] | None = None
|
||||
) -> Corpse:
|
||||
"""Create a corpse from a defeated mob.
|
||||
|
||||
Args:
|
||||
mob: The mob that died
|
||||
zone: The zone where the corpse will be placed
|
||||
ttl: Time to live in seconds (default 300)
|
||||
loot_table: Optional loot table to roll for additional items
|
||||
|
||||
Returns:
|
||||
The created corpse with the mob's inventory
|
||||
The created corpse with the mob's inventory and loot
|
||||
"""
|
||||
# Create corpse at mob's position
|
||||
corpse = Corpse(
|
||||
|
|
@ -52,6 +56,11 @@ def create_corpse(mob: Mob, zone: Zone, ttl: int = 300) -> Corpse:
|
|||
for item in list(mob._contents):
|
||||
item.move_to(corpse)
|
||||
|
||||
# Roll and add loot
|
||||
if loot_table:
|
||||
for item in roll_loot(loot_table):
|
||||
item.move_to(corpse)
|
||||
|
||||
# Remove mob from world
|
||||
despawn_mob(mob)
|
||||
|
||||
|
|
@ -69,13 +78,16 @@ async def process_decomposing() -> None:
|
|||
if now >= corpse.decompose_at:
|
||||
# Broadcast to entities at the same tile
|
||||
zone = corpse.location
|
||||
if isinstance(zone, Zone):
|
||||
if isinstance(zone, Zone) and corpse.x is not None and corpse.y is not None:
|
||||
from mudlib.entity import Entity
|
||||
|
||||
for obj in zone.contents_at(corpse.x, corpse.y):
|
||||
if isinstance(obj, Entity):
|
||||
await obj.send(f"{corpse.name} decomposes.\r\n")
|
||||
|
||||
# Clear contents — items rot with the corpse
|
||||
for item in list(corpse._contents):
|
||||
item.move_to(None)
|
||||
# Remove corpse from world
|
||||
corpse.move_to(None)
|
||||
active_corpses.remove(corpse)
|
||||
|
|
|
|||
|
|
@ -221,6 +221,14 @@ class TestCorpseAsContainer:
|
|||
class TestCombatDeathCorpse:
|
||||
"""Tests for corpse spawning when a mob dies in combat."""
|
||||
|
||||
@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_death_in_combat_spawns_corpse(self, test_zone):
|
||||
"""Mob death in combat spawns a corpse at mob's position."""
|
||||
|
|
@ -572,14 +580,19 @@ class TestDecomposition:
|
|||
@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
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from mudlib.corpse import active_corpses, process_decomposing
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.player import Player
|
||||
|
||||
# Create entity at same position
|
||||
entity = Entity(name="hero", x=5, y=10, location=test_zone)
|
||||
entity.send = AsyncMock()
|
||||
# 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",
|
||||
|
|
@ -592,19 +605,26 @@ class TestDecomposition:
|
|||
|
||||
await process_decomposing()
|
||||
|
||||
entity.send.assert_called_once_with("goblin's corpse decomposes.\r\n")
|
||||
# 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
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from mudlib.corpse import active_corpses, process_decomposing
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.player import Player
|
||||
|
||||
# Entity at different position
|
||||
entity = Entity(name="faraway", x=50, y=50, location=test_zone)
|
||||
entity.send = AsyncMock()
|
||||
# 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",
|
||||
|
|
@ -617,7 +637,9 @@ class TestDecomposition:
|
|||
|
||||
await process_decomposing()
|
||||
|
||||
entity.send.assert_not_called()
|
||||
# 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."""
|
||||
|
|
@ -639,12 +661,31 @@ class TestDecomposition:
|
|||
y=10,
|
||||
decompose_at=time.monotonic() - 1,
|
||||
)
|
||||
_sword = Thing(name="sword", location=corpse)
|
||||
sword = Thing(name="sword", location=corpse)
|
||||
active_corpses.append(corpse)
|
||||
|
||||
await process_decomposing()
|
||||
|
||||
# Corpse is gone
|
||||
assert corpse.location is None
|
||||
# Sword's location is still the corpse (orphaned)
|
||||
# This is expected — items rot with the corpse
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue