Skip to content

Add gravestone entity on player death with storable inventory#354

Merged
dmccoystephenson merged 5 commits into
mainfrom
copilot/add-gravestone-feature
Apr 23, 2026
Merged

Add gravestone entity on player death with storable inventory#354
dmccoystephenson merged 5 commits into
mainfrom
copilot/add-gravestone-feature

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 21, 2026

On player death, items were scattered individually across the room, requiring tedious one-by-one retrieval after respawn. This PR replaces that behavior with a Gravestone entity that holds all dropped items and returns them in bulk on right-click.

New entities

  • StorableInventory (src/entity/storableInventory.py) — mixin that owns an Inventory and exposes getStoredInventory(); decoupled for reuse by the future Chest entity
  • Gravestone (src/entity/gravestone.py) — solid, non-pickupable, non-craftable; extends DrawableEntity + StorableInventory
  • Placeholder asset: assets/images/gravestone.png

Death behavior (worldScreen.py)

respawnPlayer() now transfers the full player inventory into a Gravestone's stored inventory and places it at the player's last location instead of scattering items. No gravestone is spawned if the inventory was empty.

Interaction

Right-clicking a Gravestone in the world transfers all stored items to the player's inventory and removes the gravestone. If any item doesn't fit, the gravestone is left in place with "Inventory full" status — no partial transfer. The capacity pre-check (_inventoryCanFitAll) uses InventorySlot.getMaxStackSize() so it stays in sync with the inventory's actual stacking limits.

Persistence

RoomJsonReaderWriter serializes/deserializes Gravestone entities with their full stored inventory contents:

{
  "entityClass": "Gravestone",
  "storedInventory": {
    "inventorySlots": [
      { "slotIndex": 0, "slotContents": [{ "entityClass": "Apple", ... }] }
    ]
  }
}

New helpers: _generateJsonForStoredInventory, _restoreStoredInventory, _createStoredItem. On load, _restoreStoredInventory logs an error if an item cannot be placed (e.g. corrupt save), and _createStoredItem raises ValueError for unknown entity classes rather than silently dropping items.

Tests

  • tests/entity/test_gravestone.py — initialization, storage isolation, multi-item storage
  • tests/screen/test_worldScreen_gravestone.py — spawn on death, empty-inventory guard, item retrieval, full-inventory guard
  • tests/world/test_roomJsonReaderWriter.py — 4 new cases covering JSON generation, deserialization, and round-trip fidelity

Copilot AI linked an issue Apr 21, 2026 that may be closed by this pull request
7 tasks
Agent-Logs-Url: https://github.com/Preponderous-Software/roam/sessions/cbcee017-2333-496f-ad1b-cddf1334f80e

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copilot AI changed the title [WIP] Add gravestone feature for item recovery on player death Add gravestone entity on player death with storable inventory Apr 21, 2026
Copilot AI requested a review from dmccoystephenson April 21, 2026 03:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “gravestone” drop mechanic so player-death inventory recovery is done in one interaction instead of scattered pickups, with room persistence support.

Changes:

  • Introduces StorableInventory mixin + Gravestone entity (with placeholder sprite).
  • Updates WorldScreen.respawnPlayer() to drop a gravestone containing the player’s inventory and adds right-click interaction to retrieve items.
  • Extends RoomJsonReaderWriter (and tests) to serialize/deserialize gravestones with their stored inventory.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/entity/storableEntity.py Adds a reusable stored-inventory mixin for entities.
src/entity/gravestone.py Implements the Gravestone entity as a solid DrawableEntity with storage.
src/screen/worldScreen.py Drops gravestone on death and adds gravestone retrieval via place-action interaction.
src/world/roomJsonReaderWriter.py Persists gravestone stored inventory via new helper methods.
tests/entity/test_gravestone.py Unit tests for gravestone initialization and storage behavior.
tests/screen/test_worldScreen_gravestone.py Tests death-drop and interaction behavior in WorldScreen.
tests/world/test_roomJsonReaderWriter.py Adds serialization/deserialization + round-trip tests for gravestones.
assets/images/gravestone.png Placeholder sprite asset for the gravestone.
CHANGELOG.md Documents the feature and adds learning-log notes.

Comment thread src/screen/worldScreen.py Outdated
Comment on lines +763 to +767
for item in list(slot.getContents()):
if not self.player.getInventory().placeIntoFirstAvailableInventorySlot(
item
):
self.status.set("Inventory full")
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_interactWithGravestone() places item objects into the player's inventory without removing them from the gravestone slots, and if placement fails partway through it returns without rolling back already-moved items. This violates the "no partial transfer" behavior and can duplicate items (some end up in player inventory while still remaining in the gravestone). Consider making the transfer atomic: pre-check capacity for all items, or track moved items and rollback on failure, and only remove/clear the gravestone inventory after a fully successful transfer.

Copilot uses AI. Check for mistakes.
Comment thread src/world/roomJsonReaderWriter.py Outdated
constructor = self.entityConstructors.get(entityClass)
if constructor is None:
_logger.warning("unknown stored item class", entityClass=entityClass)
return None
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_createStoredItem() logs and returns None for unknown stored item classes, which will silently drop items from a saved gravestone on load. For persistence, it’s safer to fail loudly (e.g., raise a ValueError like generateEntityFromJson does) or otherwise surface the corruption, rather than losing player items without notice.

Suggested change
return None
raise ValueError(f"unknown stored item class: {entityClass}")

Copilot uses AI. Check for mistakes.
Comment thread tests/entity/test_gravestone.py Outdated
Comment on lines +1 to +3
from src.entity.gravestone import Gravestone
from src.entity.apple import Apple
from src.entity.oakWood import OakWood
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test imports entities via the src.entity... package path, while the game code imports via entity... (because src is on pytest's pythonpath). Mixing these can create two distinct class objects for the same entity and break isinstance checks. Prefer importing from entity.gravestone import Gravestone / from entity.apple import Apple / etc. consistently in tests.

Suggested change
from src.entity.gravestone import Gravestone
from src.entity.apple import Apple
from src.entity.oakWood import OakWood
from entity.gravestone import Gravestone
from entity.apple import Apple
from entity.oakWood import OakWood

Copilot uses AI. Check for mistakes.
os.environ["SDL_AUDIODRIVER"] = "dummy"
import pygame
import pytest
from unittest.mock import MagicMock, patch
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import: patch is imported but never used in this test module. Please remove it to keep the test code clean (and to avoid autoflake/linters flagging it).

Suggested change
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock

Copilot uses AI. Check for mistakes.
from entity.gravestone import Gravestone
from entity.oakWood import OakWood
from inventory.inventory import Inventory
from lib.pyenvlib.grid import Grid
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import: Grid is imported but never used in this test module. Please remove it to keep imports minimal.

Suggested change
from lib.pyenvlib.grid import Grid

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot You are a coding agent tasked with resolving all open comments and review threads
on this pull request. Work through every comment systematically and apply the
appropriate code changes.


Instructions

1. Discover all comments

  • Read every review thread, inline comment, and general PR comment
  • Include comments from: Copilot, human reviewers, and any other bots
  • Do not skip comments marked as suggestions — apply them if they make sense

2. For each comment, do the following:

  • Understand what is being requested or flagged
  • Locate the relevant file(s) and line(s)
  • Apply the fix directly in code
  • If a Copilot suggestion diff is provided, apply it (or an equivalent fix)
  • If the comment is ambiguous, use your best judgment and reply in the PR thread or review comment explaining what you did; only add a code comment when explicitly requested or when future maintainers would benefit from it
  • After addressing a comment, reply to the thread summarising what you did and mark it as resolved if your environment supports it

3. Types of issues to handle:

  • Code quality / style issues (naming, formatting, readability)
  • Logic bugs or edge cases flagged by reviewers
  • Security concerns (e.g. input validation, secrets, injection risks)
  • Performance suggestions
  • Missing or incomplete error handling
  • Test coverage gaps
  • Documentation / comment improvements
  • Dependency or import issues

4. Do NOT:

  • Dismiss or ignore any comment without acting on it — note that "acting" can also mean replying to the thread with a rationale (e.g., won't fix, already addressed elsewhere) and leaving the code unchanged when appropriate
  • Leave TODO comments as a substitute for fixing the issue
  • Break existing tests while addressing comments
  • Change functionality beyond the scope of what the comment asks for

After making changes

  • Ensure all existing tests still pass
  • Add or update tests if a comment explicitly asks for them, or if a bug fix warrants it
  • Double-check that every open thread has been addressed, replied to, and resolved

Context

  • This PR may contain Copilot-generated review comments — treat these with the same priority as human comments
  • If two comments conflict with each other, prefer the human reviewer's intent and document the conflict and resolution in the PR conversation or commit message, not in a code comment
  • Commit changes in logical groups if there are many unrelated fixes

Agent-Logs-Url: https://github.com/Preponderous-Software/roam/sessions/bbdb32d6-b3ff-4d16-9c02-192d9355ab8f

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 21, 2026

@copilot You are a coding agent tasked with resolving all open comments and review threads
on this pull request. Work through every comment systematically and apply the
appropriate code changes....

Addressed all four review threads in ff07cd6:

  1. Atomic gravestone transfer — replaced the partial-transfer loop with a _inventoryCanFitAll() pre-check that simulates stacking (slot-space accounting by item name) without calling entityClass(), so it works for all item types including Bear and YoungCrop. Only if every item fits does the actual transfer proceed; otherwise "Inventory full" is returned with the gravestone untouched.
  2. Unknown stored item class_createStoredItem() now raises ValueError instead of logging and returning None, matching the behaviour of generateEntityFromJson.
  3. Import paths in test_gravestone.py — switched from src.entity.* to bare entity.* imports to match source module paths.
  4. Unused imports — removed patch and Grid from test_worldScreen_gravestone.py.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modify name of file to match class name 'StorableInventory' and update any references in code/docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ae80ca9 — renamed storableEntity.pystorableInventory.py and updated the import in gravestone.py and the reference in CHANGELOG.md.

…lass name

Agent-Logs-Url: https://github.com/Preponderous-Software/roam/sessions/b4c56a29-4b2c-432c-862e-80489f38a43c

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Comment thread src/screen/worldScreen.py
Comment on lines +779 to +803
def _inventoryCanFitAll(self, inventory, items):
"""Return True if all items can be placed into inventory without overflow."""
maxStack = 20
# Available space in existing non-empty slots keyed by item name
slotSpace = {}
freeSlots = 0
for slot in inventory.getInventorySlots():
if slot.isEmpty():
freeSlots += 1
else:
name = slot.getContents()[0].getName()
available = maxStack - slot.getNumItems()
if available > 0:
slotSpace[name] = slotSpace.get(name, 0) + available
for item in items:
name = item.getName()
if slotSpace.get(name, 0) > 0:
slotSpace[name] -= 1
elif freeSlots > 0:
freeSlots -= 1
# Opening a new slot gives (maxStack - 1) additional spaces
slotSpace[name] = slotSpace.get(name, 0) + (maxStack - 1)
else:
return False
return True
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_inventoryCanFitAll() hardcodes maxStack = 20. Inventory stacking limits already live on InventorySlot.getMaxStackSize() (and are used elsewhere like InventoryScreen.craftRecipe). Using the slot’s getMaxStackSize() (or a shared helper) here avoids this logic silently diverging if stack size ever changes.

Copilot uses AI. Check for mistakes.
Comment thread src/world/roomJsonReaderWriter.py Outdated
for itemJson in slotJson.get("slotContents", []):
item = self._createStoredItem(itemJson)
if item is not None:
inventory.placeIntoFirstAvailableInventorySlot(item)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_restoreStoredInventory() ignores the boolean return from placeIntoFirstAvailableInventorySlot(). If the JSON ever contains more items than the stored inventory can hold (corrupt save, future inventory size changes, etc.), items will be silently dropped during load. Consider checking the return value and raising/logging an error so data loss is visible.

Suggested change
inventory.placeIntoFirstAvailableInventorySlot(item)
itemPlaced = inventory.placeIntoFirstAvailableInventorySlot(item)
if not itemPlaced:
_logger.error(
"failed to restore stored inventory item %s (%s) from saved slot %s: "
"no inventory space available",
itemJson.get("entityClass"),
itemJson.get("entityId"),
slotJson.get("slotIndex"),
)

Copilot uses AI. Check for mistakes.
Comment thread tests/entity/test_gravestone.py Outdated
Comment on lines +36 to +42
stone1 = Gravestone()
stone2 = Gravestone()

stone1.getStoredInventory().placeIntoFirstAvailableInventorySlot(Apple())

assert stone1.getStoredInventory().getNumItems() == 1
assert stone2.getStoredInventory().getNumItems() == 0
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable names stone1/stone2 in this gravestone test are misleading (they’re Gravestone instances). Renaming them to gravestone1/gravestone2 would make the test intent clearer and avoid confusion when reading failures.

Suggested change
stone1 = Gravestone()
stone2 = Gravestone()
stone1.getStoredInventory().placeIntoFirstAvailableInventorySlot(Apple())
assert stone1.getStoredInventory().getNumItems() == 1
assert stone2.getStoredInventory().getNumItems() == 0
gravestone1 = Gravestone()
gravestone2 = Gravestone()
gravestone1.getStoredInventory().placeIntoFirstAvailableInventorySlot(Apple())
assert gravestone1.getStoredInventory().getNumItems() == 1
assert gravestone2.getStoredInventory().getNumItems() == 0

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +10
from inventory.inventory import Inventory


# @author Copilot
# @since April 21st, 2026
class StorableInventory:
"""Mixin that provides an internal Inventory for entities that can store items.

Intended to be used by Gravestone and, in the future, Chest entities.
"""
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions a new StorableInventory living in src/entity/storableEntity.py, but the implementation added here is src/entity/storableInventory.py. Aligning the PR description (or the filename) will prevent confusion for reviewers and future contributors searching for the class.

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot You are a coding agent tasked with resolving all open comments and review threads
on this pull request. Work through every comment systematically and apply the
appropriate code changes.


Instructions

1. Discover all comments

  • Read every review thread, inline comment, and general PR comment
  • Include comments from: Copilot, human reviewers, and any other bots
  • Do not skip comments marked as suggestions — apply them if they make sense

2. For each comment, do the following:

  • Understand what is being requested or flagged
  • Locate the relevant file(s) and line(s)
  • Apply the fix directly in code
  • If a Copilot suggestion diff is provided, apply it (or an equivalent fix)
  • If the comment is ambiguous, use your best judgment and reply in the PR thread or review comment explaining what you did; only add a code comment when explicitly requested or when future maintainers would benefit from it
  • After addressing a comment, reply to the thread summarising what you did and mark it as resolved if your environment supports it

3. Types of issues to handle:

  • Code quality / style issues (naming, formatting, readability)
  • Logic bugs or edge cases flagged by reviewers
  • Security concerns (e.g. input validation, secrets, injection risks)
  • Performance suggestions
  • Missing or incomplete error handling
  • Test coverage gaps
  • Documentation / comment improvements
  • Dependency or import issues

4. Do NOT:

  • Dismiss or ignore any comment without acting on it — note that "acting" can also mean replying to the thread with a rationale (e.g., won't fix, already addressed elsewhere) and leaving the code unchanged when appropriate
  • Leave TODO comments as a substitute for fixing the issue
  • Break existing tests while addressing comments
  • Change functionality beyond the scope of what the comment asks for

After making changes

  • Ensure all existing tests still pass
  • Add or update tests if a comment explicitly asks for them, or if a bug fix warrants it
  • Double-check that every open thread has been addressed, replied to, and resolved

Context

  • This PR may contain Copilot-generated review comments — treat these with the same priority as human comments
  • If two comments conflict with each other, prefer the human reviewer's intent and document the conflict and resolution in the PR conversation or commit message, not in a code comment
  • Commit changes in logical groups if there are many unrelated fixes

Agent-Logs-Url: https://github.com/Preponderous-Software/roam/sessions/6af43ff9-aee8-4883-a3c0-991f3c47572a

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 21, 2026

@copilot You are a coding agent tasked with resolving all open comments and review threads
on this pull request. Work through every comment systematically and apply the
appropriate code changes....

Addressed all open review threads in c09b2fe:

  1. _inventoryCanFitAll hardcoded stack size — replaced maxStack = 20 with maxStack = slot.getMaxStackSize() read during slot iteration, so the check stays in sync with InventorySlot.
  2. _restoreStoredInventory ignores placement result — now checks the bool returned by placeIntoFirstAvailableInventorySlot() and logs an error (with item class, entity ID, and slot index) if placement fails.
  3. stone1/stone2 variable names — renamed to gravestone1/gravestone2 in test_gravestone.py.
  4. PR description vs filename — the description referenced storableEntity.py; the file was already renamed to storableInventory.py in the previous commit (ae80ca9), so no further code change was needed.

@dmccoystephenson dmccoystephenson marked this pull request as ready for review April 23, 2026 05:05
@dmccoystephenson dmccoystephenson merged commit 7dc3bff into main Apr 23, 2026
1 check passed
@dmccoystephenson dmccoystephenson deleted the copilot/add-gravestone-feature branch April 23, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add 'gravestone' feature

3 participants