Cookbook structure: - lib/: Reusable component library (Button, StatBar, AnimationChain, etc.) - primitives/: Demo apps for individual components - features/: Demo apps for complex features (animation chaining, shaders) - apps/: Complete mini-applications (calculator, dialogue system) - automation/: Screenshot capture utilities API signature updates applied: - on_enter/on_exit/on_move callbacks now only receive (pos) per #230 - on_cell_enter/on_cell_exit callbacks only receive (cell_pos) per #230 - Animation chain library uses Timer-based sequencing (unaffected by #229) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2daebc84b5
commit
55f6ea9502
41 changed files with 8493 additions and 0 deletions
563
tests/cookbook/lib/item_manager.py
Normal file
563
tests/cookbook/lib/item_manager.py
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Item Manager - Coordinates item pickup/drop across multiple widgets
|
||||
|
||||
This module provides:
|
||||
- ItemManager: Central coordinator for item transfers
|
||||
- ItemSlot: Frame-based equipment slot widget
|
||||
- Item: Data class for item properties
|
||||
|
||||
The manager allows entities to move between:
|
||||
- Grid widgets (inventory, shop displays)
|
||||
- ItemSlot widgets (equipment slots)
|
||||
- The cursor (when held)
|
||||
|
||||
Usage:
|
||||
manager = ItemManager()
|
||||
manager.register_grid("inventory", inventory_grid)
|
||||
manager.register_slot("weapon", weapon_slot)
|
||||
|
||||
# Widgets call manager methods on click:
|
||||
manager.pickup_from_grid("inventory", cell_pos)
|
||||
manager.drop_to_slot("weapon")
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
|
||||
class ItemEntity(mcrfpy.Entity):
|
||||
"""Entity subclass that can hold item data."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.item = None # Will be set after adding to grid
|
||||
|
||||
|
||||
class Item:
|
||||
"""Data class representing an item's properties."""
|
||||
|
||||
def __init__(self, sprite_index, name, **stats):
|
||||
"""
|
||||
Args:
|
||||
sprite_index: Texture sprite index
|
||||
name: Display name
|
||||
**stats: Arbitrary stats (atk, def, int, price, etc.)
|
||||
"""
|
||||
self.sprite_index = sprite_index
|
||||
self.name = name
|
||||
self.stats = stats
|
||||
|
||||
def __repr__(self):
|
||||
return f"Item({self.name}, sprite={self.sprite_index})"
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
return self.stats.get('price', 0)
|
||||
|
||||
@property
|
||||
def atk(self):
|
||||
return self.stats.get('atk', 0)
|
||||
|
||||
@property
|
||||
def def_(self):
|
||||
return self.stats.get('def', 0)
|
||||
|
||||
@property
|
||||
def slot_type(self):
|
||||
"""Equipment slot this item fits in (weapon, shield, etc.)"""
|
||||
return self.stats.get('slot_type', None)
|
||||
|
||||
|
||||
class ItemSlot(mcrfpy.Frame):
|
||||
"""A frame that can hold a single item (for equipment slots)."""
|
||||
|
||||
def __init__(self, pos, size=(64, 64), slot_type=None,
|
||||
empty_color=(60, 60, 70), valid_color=(60, 100, 60),
|
||||
invalid_color=(100, 60, 60), filled_color=(80, 80, 90)):
|
||||
"""
|
||||
Args:
|
||||
pos: Position tuple (x, y)
|
||||
size: Size tuple (w, h)
|
||||
slot_type: Type of items this slot accepts (e.g., 'weapon', 'shield')
|
||||
empty_color: Color when empty and not hovered
|
||||
valid_color: Color when valid item is hovering
|
||||
invalid_color: Color when invalid item is hovering
|
||||
filled_color: Color when containing an item
|
||||
"""
|
||||
super().__init__(pos, size, fill_color=empty_color, outline=2, outline_color=(100, 100, 120))
|
||||
|
||||
self.slot_type = slot_type
|
||||
self.empty_color = empty_color
|
||||
self.valid_color = valid_color
|
||||
self.invalid_color = invalid_color
|
||||
self.filled_color = filled_color
|
||||
|
||||
# Item stored in this slot
|
||||
self.item = None
|
||||
self.item_sprite = None
|
||||
|
||||
# Manager reference (set by manager.register_slot)
|
||||
self.manager = None
|
||||
self.slot_name = None
|
||||
|
||||
# Setup sprite for displaying item
|
||||
sprite_scale = min(size[0], size[1]) / 16.0 * 0.8 # 80% of slot size
|
||||
self.item_sprite = mcrfpy.Sprite(
|
||||
pos=(size[0] * 0.1, size[1] * 0.1),
|
||||
texture=mcrfpy.default_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
self.item_sprite.scale = sprite_scale
|
||||
self.item_sprite.visible = False
|
||||
self.children.append(self.item_sprite)
|
||||
|
||||
# Setup events
|
||||
self.on_click = self._on_click
|
||||
self.on_enter = self._on_enter
|
||||
self.on_exit = self._on_exit
|
||||
|
||||
def set_item(self, item):
|
||||
"""Set the item in this slot."""
|
||||
self.item = item
|
||||
if item:
|
||||
self.item_sprite.sprite_index = item.sprite_index
|
||||
self.item_sprite.visible = True
|
||||
self.fill_color = self.filled_color
|
||||
else:
|
||||
self.item_sprite.visible = False
|
||||
self.fill_color = self.empty_color
|
||||
|
||||
def clear(self):
|
||||
"""Remove item from slot."""
|
||||
self.set_item(None)
|
||||
|
||||
def can_accept(self, item):
|
||||
"""Check if this slot can accept the given item."""
|
||||
if self.slot_type is None:
|
||||
return True # Accepts anything
|
||||
if item is None:
|
||||
return True
|
||||
return item.slot_type == self.slot_type
|
||||
|
||||
def _on_click(self, pos, button, action):
|
||||
if action != "start" or button != "left":
|
||||
return
|
||||
if self.manager:
|
||||
self.manager.handle_slot_click(self.slot_name)
|
||||
|
||||
def _on_enter(self, pos, *args):
|
||||
if self.manager and self.manager.held_item:
|
||||
if self.can_accept(self.manager.held_item):
|
||||
self.fill_color = self.valid_color
|
||||
else:
|
||||
self.fill_color = self.invalid_color
|
||||
|
||||
def _on_exit(self, pos, *args):
|
||||
if self.item:
|
||||
self.fill_color = self.filled_color
|
||||
else:
|
||||
self.fill_color = self.empty_color
|
||||
|
||||
|
||||
class ItemManager:
|
||||
"""Coordinates item pickup and placement across multiple widgets."""
|
||||
|
||||
def __init__(self, scene):
|
||||
"""
|
||||
Args:
|
||||
scene: The mcrfpy.Scene to add cursor elements to
|
||||
"""
|
||||
self.scene = scene
|
||||
self.grids = {} # name -> (grid, {(x,y): Item})
|
||||
self.slots = {} # name -> ItemSlot
|
||||
|
||||
# Currently held item state
|
||||
self.held_item = None
|
||||
self.held_source = None # ('grid', name, pos) or ('slot', name)
|
||||
self.held_entity = None # For grid sources, the original entity
|
||||
|
||||
# Cursor sprite setup
|
||||
self.cursor_frame = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(64, 64),
|
||||
fill_color=(0, 0, 0, 0),
|
||||
outline=0
|
||||
)
|
||||
self.cursor_sprite = mcrfpy.Sprite(
|
||||
pos=(0, 0),
|
||||
texture=mcrfpy.default_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
self.cursor_sprite.scale = 4.0
|
||||
self.cursor_sprite.visible = False
|
||||
self.cursor_frame.children.append(self.cursor_sprite)
|
||||
scene.children.append(self.cursor_frame)
|
||||
|
||||
# Callbacks for UI updates
|
||||
self.on_pickup = None # Called when item picked up
|
||||
self.on_drop = None # Called when item dropped
|
||||
self.on_cancel = None # Called when pickup cancelled
|
||||
|
||||
def register_grid(self, name, grid, items=None):
|
||||
"""Register a grid for item management.
|
||||
|
||||
Args:
|
||||
name: Unique name for this grid
|
||||
grid: mcrfpy.Grid instance
|
||||
items: Optional dict of {(x, y): Item} for initial items
|
||||
"""
|
||||
item_map = items or {}
|
||||
|
||||
# Add a color layer for highlighting if not present
|
||||
color_layer = grid.add_layer('color', z_index=-1)
|
||||
|
||||
self.grids[name] = (grid, item_map, color_layer)
|
||||
|
||||
# Setup grid event handlers
|
||||
grid.on_click = lambda pos, btn, act: self._on_grid_click(name, pos, btn, act)
|
||||
grid.on_cell_enter = lambda cell_pos: self._on_grid_cell_enter(name, cell_pos)
|
||||
grid.on_move = self._on_move
|
||||
|
||||
def register_slot(self, name, slot):
|
||||
"""Register an ItemSlot for item management.
|
||||
|
||||
Args:
|
||||
name: Unique name for this slot
|
||||
slot: ItemSlot instance
|
||||
"""
|
||||
self.slots[name] = slot
|
||||
slot.manager = self
|
||||
slot.slot_name = name
|
||||
|
||||
def add_item_to_grid(self, grid_name, pos, item):
|
||||
"""Add an item to a grid at the specified position.
|
||||
|
||||
Args:
|
||||
grid_name: Name of registered grid
|
||||
pos: (x, y) cell position
|
||||
item: Item instance
|
||||
|
||||
Returns:
|
||||
True if successful, False if cell occupied
|
||||
"""
|
||||
if grid_name not in self.grids:
|
||||
return False
|
||||
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
|
||||
if pos in item_map:
|
||||
return False # Cell occupied
|
||||
|
||||
# Create entity
|
||||
entity = ItemEntity()
|
||||
grid.entities.append(entity)
|
||||
entity.grid_pos = pos # Use grid_pos for tile coordinates
|
||||
entity.sprite_index = item.sprite_index
|
||||
entity.item = item # Store item reference on our subclass
|
||||
item_map[pos] = item
|
||||
|
||||
return True
|
||||
|
||||
def get_item_at(self, grid_name, pos):
|
||||
"""Get item at grid position, or None."""
|
||||
if grid_name not in self.grids:
|
||||
return None
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
return item_map.get(pos)
|
||||
|
||||
def _get_entity_at(self, grid_name, pos):
|
||||
"""Get entity at grid position."""
|
||||
grid, _, _ = self.grids[grid_name]
|
||||
for entity in grid.entities:
|
||||
gp = entity.grid_pos
|
||||
if (int(gp[0]), int(gp[1])) == pos:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def _on_grid_click(self, grid_name, pos, button, action):
|
||||
"""Handle click on a registered grid."""
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
if button == "right":
|
||||
self.cancel_pickup()
|
||||
return
|
||||
|
||||
if button != "left":
|
||||
return
|
||||
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
|
||||
# Convert screen pos to cell
|
||||
cell_size = 16 * grid.zoom
|
||||
x = int((pos[0] - grid.x) / cell_size)
|
||||
y = int((pos[1] - grid.y) / cell_size)
|
||||
grid_w, grid_h = grid.grid_size
|
||||
|
||||
if not (0 <= x < grid_w and 0 <= y < grid_h):
|
||||
return
|
||||
|
||||
cell_pos = (x, y)
|
||||
|
||||
if self.held_item is None:
|
||||
# Try to pick up
|
||||
if cell_pos in item_map:
|
||||
self._pickup_from_grid(grid_name, cell_pos)
|
||||
else:
|
||||
# Try to drop
|
||||
if cell_pos not in item_map:
|
||||
self._drop_to_grid(grid_name, cell_pos)
|
||||
|
||||
def _pickup_from_grid(self, grid_name, pos):
|
||||
"""Pick up item from grid cell."""
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
item = item_map[pos]
|
||||
entity = self._get_entity_at(grid_name, pos)
|
||||
|
||||
if entity:
|
||||
entity.visible = False
|
||||
|
||||
self.held_item = item
|
||||
self.held_source = ('grid', grid_name, pos)
|
||||
self.held_entity = entity
|
||||
|
||||
# Setup cursor
|
||||
self.cursor_sprite.sprite_index = item.sprite_index
|
||||
self.cursor_sprite.visible = True
|
||||
|
||||
# Highlight source cell
|
||||
color_layer.set(pos, (255, 255, 100, 200))
|
||||
|
||||
if self.on_pickup:
|
||||
self.on_pickup(item, grid_name, pos)
|
||||
|
||||
def _drop_to_grid(self, grid_name, pos):
|
||||
"""Drop held item to grid cell."""
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
|
||||
# Check if same grid and moving item
|
||||
if self.held_source[0] == 'grid':
|
||||
source_grid_name = self.held_source[1]
|
||||
source_pos = self.held_source[2]
|
||||
|
||||
# Remove from source
|
||||
source_grid, source_map, source_color_layer = self.grids[source_grid_name]
|
||||
if source_pos in source_map:
|
||||
del source_map[source_pos]
|
||||
|
||||
# Clear source highlight
|
||||
source_color_layer.set(source_pos, (0, 0, 0, 0))
|
||||
|
||||
# Move or recreate entity
|
||||
if grid_name == source_grid_name and self.held_entity:
|
||||
# Same grid - just move entity
|
||||
self.held_entity.grid_pos = pos
|
||||
self.held_entity.visible = True
|
||||
else:
|
||||
# Different grid - remove old, create new
|
||||
if self.held_entity:
|
||||
# Remove from source grid
|
||||
for i, e in enumerate(source_grid.entities):
|
||||
if e is self.held_entity:
|
||||
source_grid.entities.pop(i)
|
||||
break
|
||||
|
||||
# Create in target grid
|
||||
entity = ItemEntity()
|
||||
grid.entities.append(entity)
|
||||
entity.grid_pos = pos # Use grid_pos for tile coordinates
|
||||
entity.sprite_index = self.held_item.sprite_index
|
||||
entity.item = self.held_item
|
||||
self.held_entity = None
|
||||
|
||||
elif self.held_source[0] == 'slot':
|
||||
# Moving from slot to grid
|
||||
slot_name = self.held_source[1]
|
||||
slot = self.slots[slot_name]
|
||||
slot.clear()
|
||||
|
||||
# Create entity in grid
|
||||
entity = ItemEntity()
|
||||
grid.entities.append(entity)
|
||||
entity.grid_pos = pos # Use grid_pos for tile coordinates
|
||||
entity.sprite_index = self.held_item.sprite_index
|
||||
entity.item = self.held_item
|
||||
|
||||
# Add to target grid's item map
|
||||
item_map[pos] = self.held_item
|
||||
|
||||
if self.on_drop:
|
||||
self.on_drop(self.held_item, grid_name, pos)
|
||||
|
||||
# Clear held state
|
||||
self.cursor_sprite.visible = False
|
||||
self.held_item = None
|
||||
self.held_source = None
|
||||
self.held_entity = None
|
||||
|
||||
def handle_slot_click(self, slot_name):
|
||||
"""Handle click on a registered slot."""
|
||||
slot = self.slots[slot_name]
|
||||
|
||||
if self.held_item is None:
|
||||
# Try to pick up from slot
|
||||
if slot.item:
|
||||
self._pickup_from_slot(slot_name)
|
||||
else:
|
||||
# Try to drop to slot
|
||||
if slot.can_accept(self.held_item):
|
||||
self._drop_to_slot(slot_name)
|
||||
|
||||
def _pickup_from_slot(self, slot_name):
|
||||
"""Pick up item from slot."""
|
||||
slot = self.slots[slot_name]
|
||||
item = slot.item
|
||||
|
||||
self.held_item = item
|
||||
self.held_source = ('slot', slot_name)
|
||||
self.held_entity = None
|
||||
|
||||
slot.clear()
|
||||
|
||||
# Setup cursor
|
||||
self.cursor_sprite.sprite_index = item.sprite_index
|
||||
self.cursor_sprite.visible = True
|
||||
|
||||
if self.on_pickup:
|
||||
self.on_pickup(item, slot_name, None)
|
||||
|
||||
def _drop_to_slot(self, slot_name):
|
||||
"""Drop held item to slot."""
|
||||
slot = self.slots[slot_name]
|
||||
|
||||
# If slot has item, swap
|
||||
old_item = slot.item
|
||||
slot.set_item(self.held_item)
|
||||
|
||||
# Clean up source
|
||||
if self.held_source[0] == 'grid':
|
||||
source_grid_name = self.held_source[1]
|
||||
source_pos = self.held_source[2]
|
||||
source_grid, source_map, source_color_layer = self.grids[source_grid_name]
|
||||
|
||||
# Remove from source grid
|
||||
if source_pos in source_map:
|
||||
del source_map[source_pos]
|
||||
|
||||
# Clear highlight
|
||||
source_color_layer.set(source_pos, (0, 0, 0, 0))
|
||||
|
||||
# Remove entity
|
||||
if self.held_entity:
|
||||
for i, e in enumerate(source_grid.entities):
|
||||
if e is self.held_entity:
|
||||
source_grid.entities.pop(i)
|
||||
break
|
||||
|
||||
# If swapping, put old item in source position
|
||||
if old_item:
|
||||
self.add_item_to_grid(source_grid_name, source_pos, old_item)
|
||||
|
||||
elif self.held_source[0] == 'slot':
|
||||
# Slot to slot swap
|
||||
source_slot_name = self.held_source[1]
|
||||
source_slot = self.slots[source_slot_name]
|
||||
if old_item:
|
||||
source_slot.set_item(old_item)
|
||||
|
||||
if self.on_drop:
|
||||
self.on_drop(self.held_item, slot_name, None)
|
||||
|
||||
# Clear held state
|
||||
self.cursor_sprite.visible = False
|
||||
self.held_item = None
|
||||
self.held_source = None
|
||||
self.held_entity = None
|
||||
|
||||
def cancel_pickup(self):
|
||||
"""Cancel current pickup and return item to source."""
|
||||
if not self.held_item:
|
||||
return
|
||||
|
||||
if self.held_source[0] == 'grid':
|
||||
grid_name = self.held_source[1]
|
||||
pos = self.held_source[2]
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
|
||||
# Restore entity visibility
|
||||
if self.held_entity:
|
||||
self.held_entity.visible = True
|
||||
|
||||
# Restore item map
|
||||
item_map[pos] = self.held_item
|
||||
|
||||
# Clear highlight
|
||||
color_layer.set(pos, (0, 0, 0, 0))
|
||||
|
||||
elif self.held_source[0] == 'slot':
|
||||
slot_name = self.held_source[1]
|
||||
slot = self.slots[slot_name]
|
||||
slot.set_item(self.held_item)
|
||||
|
||||
if self.on_cancel:
|
||||
self.on_cancel(self.held_item)
|
||||
|
||||
# Clear held state
|
||||
self.cursor_sprite.visible = False
|
||||
self.held_item = None
|
||||
self.held_source = None
|
||||
self.held_entity = None
|
||||
|
||||
def _on_grid_cell_enter(self, grid_name, cell_pos):
|
||||
"""Handle cell hover on registered grid."""
|
||||
if not self.held_item:
|
||||
return
|
||||
|
||||
grid, item_map, color_layer = self.grids[grid_name]
|
||||
x, y = int(cell_pos[0]), int(cell_pos[1])
|
||||
|
||||
# Don't highlight source cell
|
||||
if (self.held_source[0] == 'grid' and
|
||||
self.held_source[1] == grid_name and
|
||||
self.held_source[2] == (x, y)):
|
||||
return
|
||||
|
||||
if (x, y) in item_map:
|
||||
color_layer.set((x, y), (255, 100, 100, 200)) # Red - occupied
|
||||
else:
|
||||
color_layer.set((x, y), (100, 255, 100, 200)) # Green - available
|
||||
|
||||
def _on_move(self, pos, *args):
|
||||
"""Update cursor position."""
|
||||
if self.cursor_sprite.visible:
|
||||
self.cursor_frame.x = pos[0] - 32
|
||||
self.cursor_frame.y = pos[1] - 32
|
||||
|
||||
|
||||
# Predefined items using the texture sprites
|
||||
ITEM_DATABASE = {
|
||||
'buckler': Item(101, "Buckler", def_=1, slot_type='shield', price=15),
|
||||
'shield': Item(102, "Shield", def_=2, slot_type='shield', price=30),
|
||||
'shortsword': Item(103, "Shortsword", atk=1, slot_type='weapon', price=20),
|
||||
'longsword': Item(104, "Longsword", atk=2, slot_type='weapon', price=40),
|
||||
'cleaver': Item(105, "Cleaver", atk=3, slot_type='weapon', two_handed=True, price=60),
|
||||
'buster': Item(106, "Buster Sword", atk=4, slot_type='weapon', two_handed=True, price=100),
|
||||
'training_sword': Item(107, "Training Sword", atk=2, slot_type='weapon', two_handed=True, price=25),
|
||||
'hammer': Item(117, "Hammer", atk=2, slot_type='weapon', price=35),
|
||||
'double_axe': Item(118, "Double Axe", atk=5, slot_type='weapon', two_handed=True, price=120),
|
||||
'axe': Item(119, "Axe", atk=3, slot_type='weapon', price=50),
|
||||
'wand': Item(129, "Wand", atk=1, int_=4, slot_type='weapon', price=45),
|
||||
'staff': Item(130, "Staff", atk=1, int_=7, slot_type='weapon', two_handed=True, price=80),
|
||||
'spear': Item(131, "Spear", atk=4, range_=1, slot_type='weapon', two_handed=True, price=55),
|
||||
'cloudy_potion': Item(113, "Cloudy Potion", slot_type='consumable', price=5),
|
||||
'str_potion': Item(114, "Strength Potion", slot_type='consumable', price=25),
|
||||
'health_potion': Item(115, "Health Potion", slot_type='consumable', price=15),
|
||||
'mana_potion': Item(116, "Mana Potion", slot_type='consumable', price=15),
|
||||
'lesser_cloudy': Item(125, "Lesser Cloudy Potion", slot_type='consumable', price=3),
|
||||
'lesser_str': Item(126, "Lesser Strength Potion", slot_type='consumable', price=12),
|
||||
'lesser_health': Item(127, "Lesser Health Potion", slot_type='consumable', price=8),
|
||||
'lesser_mana': Item(128, "Lesser Mana Potion", slot_type='consumable', price=8),
|
||||
}
|
||||
|
||||
|
||||
def get_item(name):
|
||||
"""Get an item from the database by name."""
|
||||
return ITEM_DATABASE.get(name)
|
||||
Loading…
Add table
Add a link
Reference in a new issue