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.
|
This should be called each game loop tick to advance combat state machines.
|
||||||
"""
|
"""
|
||||||
|
from mudlib.player import Player
|
||||||
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|
||||||
for encounter in active_encounters[:]: # Copy list to allow modification
|
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.attacker.send("Combat has fizzled out.\r\n")
|
||||||
await encounter.defender.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):
|
for entity in (encounter.attacker, encounter.defender):
|
||||||
if isinstance(entity, Player) and entity.mode == "combat":
|
if isinstance(entity, Player) and entity.mode == "combat":
|
||||||
entity.mode_stack.pop()
|
entity.mode_stack.pop()
|
||||||
|
|
@ -149,8 +149,6 @@ async def process_combat() -> None:
|
||||||
await viewer.send(msg + "\r\n")
|
await viewer.send(msg + "\r\n")
|
||||||
|
|
||||||
# Send vitals update after damage resolution
|
# Send vitals update after damage resolution
|
||||||
from mudlib.player import Player
|
|
||||||
|
|
||||||
for entity in (encounter.attacker, encounter.defender):
|
for entity in (encounter.attacker, encounter.defender):
|
||||||
if isinstance(entity, Player):
|
if isinstance(entity, Player):
|
||||||
send_char_vitals(entity)
|
send_char_vitals(entity)
|
||||||
|
|
@ -168,6 +166,17 @@ async def process_combat() -> None:
|
||||||
loser = encounter.attacker
|
loser = encounter.attacker
|
||||||
winner = encounter.defender
|
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
|
# Despawn mob losers, send victory/defeat messages
|
||||||
if isinstance(loser, Mob):
|
if isinstance(loser, Mob):
|
||||||
from mudlib.corpse import create_corpse
|
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
|
# Pop combat mode from both entities if they're Players
|
||||||
from mudlib.player import Player
|
|
||||||
|
|
||||||
attacker = encounter.attacker
|
attacker = encounter.attacker
|
||||||
if isinstance(attacker, Player) and attacker.mode == "combat":
|
if isinstance(attacker, Player) and attacker.mode == "combat":
|
||||||
attacker.mode_stack.pop()
|
attacker.mode_stack.pop()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING, Any
|
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
|
# Global registry of connected players
|
||||||
players: dict[str, Player] = {}
|
players: dict[str, Player] = {}
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,11 @@ def save_player(player: Player) -> None:
|
||||||
Args:
|
Args:
|
||||||
player: Player instance to save
|
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
|
# Serialize inventory as JSON list of thing names
|
||||||
inventory_names = [obj.name for obj in player.contents if isinstance(obj, Thing)]
|
inventory_names = [obj.name for obj in player.contents if isinstance(obj, Thing)]
|
||||||
inventory_json = json.dumps(inventory_names)
|
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