Wire kill/death tracking into combat engine
Increment player.kills and player.mob_kills on mob defeat, player.deaths on player defeat. Session time accumulation via accumulate_play_time helper.
This commit is contained in:
parent
a398227814
commit
e31af53577
4 changed files with 184 additions and 6 deletions
|
|
@ -84,6 +84,8 @@ async def process_combat() -> None:
|
|||
|
||||
This should be called each game loop tick to advance combat state machines.
|
||||
"""
|
||||
from mudlib.player import Player
|
||||
|
||||
now = time.monotonic()
|
||||
|
||||
for encounter in active_encounters[:]: # Copy list to allow modification
|
||||
|
|
@ -92,8 +94,6 @@ async def process_combat() -> None:
|
|||
await encounter.attacker.send("Combat has fizzled out.\r\n")
|
||||
await encounter.defender.send("Combat has fizzled out.\r\n")
|
||||
|
||||
from mudlib.player import Player
|
||||
|
||||
for entity in (encounter.attacker, encounter.defender):
|
||||
if isinstance(entity, Player) and entity.mode == "combat":
|
||||
entity.mode_stack.pop()
|
||||
|
|
@ -149,8 +149,6 @@ async def process_combat() -> None:
|
|||
await viewer.send(msg + "\r\n")
|
||||
|
||||
# Send vitals update after damage resolution
|
||||
from mudlib.player import Player
|
||||
|
||||
for entity in (encounter.attacker, encounter.defender):
|
||||
if isinstance(entity, Player):
|
||||
send_char_vitals(entity)
|
||||
|
|
@ -168,6 +166,17 @@ async def process_combat() -> None:
|
|||
loser = encounter.attacker
|
||||
winner = encounter.defender
|
||||
|
||||
# Track kill/death stats
|
||||
if isinstance(winner, Player):
|
||||
winner.kills += 1
|
||||
if isinstance(loser, Mob):
|
||||
winner.mob_kills[loser.name] = (
|
||||
winner.mob_kills.get(loser.name, 0) + 1
|
||||
)
|
||||
|
||||
if isinstance(loser, Player):
|
||||
loser.deaths += 1
|
||||
|
||||
# Despawn mob losers, send victory/defeat messages
|
||||
if isinstance(loser, Mob):
|
||||
from mudlib.corpse import create_corpse
|
||||
|
|
@ -191,8 +200,6 @@ async def process_combat() -> None:
|
|||
)
|
||||
|
||||
# Pop combat mode from both entities if they're Players
|
||||
from mudlib.player import Player
|
||||
|
||||
attacker = encounter.attacker
|
||||
if isinstance(attacker, Player) and attacker.mode == "combat":
|
||||
attacker.mode_stack.pop()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -90,5 +91,16 @@ class Player(Entity):
|
|||
)
|
||||
|
||||
|
||||
def accumulate_play_time(player: Player) -> None:
|
||||
"""Accumulate play time since session start and reset timer.
|
||||
|
||||
Args:
|
||||
player: The player to update
|
||||
"""
|
||||
if player.session_start > 0:
|
||||
player.play_time_seconds += time.monotonic() - player.session_start
|
||||
player.session_start = time.monotonic()
|
||||
|
||||
|
||||
# Global registry of connected players
|
||||
players: dict[str, Player] = {}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,11 @@ def save_player(player: Player) -> None:
|
|||
Args:
|
||||
player: Player instance to save
|
||||
"""
|
||||
# Accumulate play time before saving
|
||||
from mudlib.player import accumulate_play_time
|
||||
|
||||
accumulate_play_time(player)
|
||||
|
||||
# Serialize inventory as JSON list of thing names
|
||||
inventory_names = [obj.name for obj in player.contents if isinstance(obj, Thing)]
|
||||
inventory_json = json.dumps(inventory_names)
|
||||
|
|
|
|||
154
tests/test_kill_tracking.py
Normal file
154
tests/test_kill_tracking.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
"""Tests for kill and death tracking in combat."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from mudlib.combat.engine import (
|
||||
process_combat,
|
||||
start_encounter,
|
||||
)
|
||||
from mudlib.combat.moves import CombatMove
|
||||
from mudlib.entity import Mob
|
||||
from mudlib.player import accumulate_play_time
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def punch_move():
|
||||
"""Create a basic punch move for testing."""
|
||||
return CombatMove(
|
||||
name="punch right",
|
||||
move_type="attack",
|
||||
stamina_cost=5.0,
|
||||
timing_window_ms=800,
|
||||
damage_pct=0.15,
|
||||
countered_by=[],
|
||||
resolve_hit="{attacker} hits {defender}!",
|
||||
resolve_miss="{defender} dodges!",
|
||||
announce="{attacker} punches!",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_player_kills_mob_increments_stats(player, test_zone, punch_move):
|
||||
"""Player kills mob -> kills incremented, mob_kills tracked."""
|
||||
# Create a goblin mob
|
||||
goblin = Mob(name="goblin", x=0, y=0)
|
||||
goblin.location = test_zone
|
||||
test_zone._contents.append(goblin)
|
||||
|
||||
# Start encounter
|
||||
encounter = start_encounter(player, goblin)
|
||||
|
||||
# Execute attack
|
||||
encounter.attack(punch_move)
|
||||
|
||||
# Advance past telegraph (0.3s) + window (0.8s)
|
||||
encounter.tick(time.monotonic() + 0.31) # -> WINDOW
|
||||
encounter.tick(time.monotonic() + 1.2) # -> RESOLVE
|
||||
|
||||
# Set defender to very low pl so damage kills them
|
||||
goblin.pl = 1.0
|
||||
|
||||
# Process combat (this will resolve and end encounter)
|
||||
await process_combat()
|
||||
|
||||
# Verify stats
|
||||
assert player.kills == 1
|
||||
assert player.mob_kills["goblin"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_player_killed_by_mob_increments_deaths(player, test_zone, punch_move):
|
||||
"""Player killed by mob -> deaths incremented."""
|
||||
# Create a goblin mob
|
||||
goblin = Mob(name="goblin", x=0, y=0)
|
||||
goblin.location = test_zone
|
||||
test_zone._contents.append(goblin)
|
||||
|
||||
# Start encounter with mob as attacker
|
||||
encounter = start_encounter(goblin, player)
|
||||
|
||||
# Execute attack
|
||||
encounter.attack(punch_move)
|
||||
|
||||
# Advance to RESOLVE
|
||||
encounter.tick(time.monotonic() + 0.31)
|
||||
encounter.tick(time.monotonic() + 1.2)
|
||||
|
||||
# Set player to low pl so they die
|
||||
player.pl = 1.0
|
||||
|
||||
# Process combat
|
||||
await process_combat()
|
||||
|
||||
# Verify deaths incremented
|
||||
assert player.deaths == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_kills_accumulate(player, test_zone, punch_move):
|
||||
"""After killing 3 goblins, player.kills == 3, player.mob_kills["goblin"] == 3."""
|
||||
for _ in range(3):
|
||||
# Create goblin
|
||||
goblin = Mob(name="goblin", x=0, y=0)
|
||||
goblin.location = test_zone
|
||||
test_zone._contents.append(goblin)
|
||||
|
||||
# Create and resolve encounter
|
||||
encounter = start_encounter(player, goblin)
|
||||
encounter.attack(punch_move)
|
||||
encounter.tick(time.monotonic() + 0.31)
|
||||
encounter.tick(time.monotonic() + 1.2)
|
||||
goblin.pl = 1.0
|
||||
|
||||
await process_combat()
|
||||
|
||||
# Verify accumulated kills
|
||||
assert player.kills == 3
|
||||
assert player.mob_kills["goblin"] == 3
|
||||
|
||||
|
||||
def test_session_time_tracking(player):
|
||||
"""Session time tracking accumulates correctly."""
|
||||
# Set session start to a known time
|
||||
start_time = time.monotonic()
|
||||
player.session_start = start_time
|
||||
|
||||
# Simulate 5 seconds passing
|
||||
time.sleep(0.01) # Small real delay to ensure monotonic() advances
|
||||
player.session_start = start_time # Reset for predictable test
|
||||
|
||||
# Mock time to be 5 seconds later
|
||||
import unittest.mock
|
||||
|
||||
with unittest.mock.patch("time.monotonic", return_value=start_time + 5.0):
|
||||
accumulate_play_time(player)
|
||||
|
||||
# Should have accumulated 5 seconds
|
||||
assert player.play_time_seconds == 5.0
|
||||
|
||||
# Session start should be reset to current time
|
||||
with unittest.mock.patch("time.monotonic", return_value=start_time + 5.0):
|
||||
assert player.session_start == start_time + 5.0
|
||||
|
||||
|
||||
def test_accumulate_play_time_multiple_sessions(player):
|
||||
"""Multiple accumulation calls should add up correctly."""
|
||||
start_time = time.monotonic()
|
||||
player.session_start = start_time
|
||||
|
||||
import unittest.mock
|
||||
|
||||
# First accumulation: 3 seconds
|
||||
with unittest.mock.patch("time.monotonic", return_value=start_time + 3.0):
|
||||
accumulate_play_time(player)
|
||||
|
||||
assert player.play_time_seconds == 3.0
|
||||
|
||||
# Second accumulation: 2 more seconds (from reset point)
|
||||
with unittest.mock.patch("time.monotonic", return_value=start_time + 5.0):
|
||||
accumulate_play_time(player)
|
||||
|
||||
# Should have 3 + 2 = 5 total
|
||||
assert player.play_time_seconds == 5.0
|
||||
Loading…
Reference in a new issue