Source code for world_objects.item

"""
Physical dropped item entity management.

This module manages the instantiation, 3D physics, collision handling, and rendering of
items that pop out of broken blocks. The `ItemManager` utilizes a strict First-In-First-Out
(FIFO) cap to forcefully limit active entities, guaranteeing smooth framerates regardless of
how many blocks are exploded concurrently.
"""

import math
import random
from typing import Any, List

import pygame as pg
from pyglm import glm

from meshes.item_mesh import ItemMesh
from meshes.obj_mesh import ObjMesh
from profiler import global_profiler
from settings import (
    GRAVITY,
    ITEM_ENTITY_CAP,
    ITEM_PICKUP_DELAY,
    ITEM_PICKUP_RADIUS,
    ITEM_RENDER_DISTANCE_SQUARED,
    ITEM_SCALE,
    ITEM_SPAWN_VELOCITY_MULTIPLIER,
    STICK,
    WOODEN_PICKAXE,
    get_path,
)


[docs] class Item: """ Represents a physical, dropped 3D item entity in the world. Handles gravity, sliding friction, bouncing, and player pickup detection. Items are spawned when blocks are broken or when dropped from the inventory. Args: app (Any): The main application instance. position (Any): A PyGLM vec3 or tuple representing the initial world spawn coordinates. voxel_id (int): The block or item UID that dictates its visual mesh and inventory value. """ @global_profiler.profile_func('Item_Init') def __init__(self, app: Any, position: Any, voxel_id: Any) -> None: """ Spawns an item bursting out of the specified position with a randomized velocity, and applies a short pickup delay to prevent instant re-collection. """ # Store a reference to the main application instance to access global state self.app: Any = app # Offset the initial spawn position by 0.5 on all axes to center it within the broken block's coordinate space self.position: Any = glm.vec3(position) + 0.5 # Generate a randomized ejection velocity to make the item burst outwards # X and Z axes get a random spread, while Y gets a slight upward bump self.velocity: Any = glm.vec3( (random.random() - 0.5) * ITEM_SPAWN_VELOCITY_MULTIPLIER, 0.005, (random.random() - 0.5) * ITEM_SPAWN_VELOCITY_MULTIPLIER, ) # Handle type conversion for voxel_id, which may come in as raw bytes from the SQLite database self.voxel_id: int = int.from_bytes(voxel_id, 'little') if isinstance(voxel_id, bytes) else int(voxel_id) # Initialize the baseline Y-axis rotation (yaw) for the item's spinning animation self.rotation: float = 0.0 # Set the global scaling factor for how large dropped items appear in the world self.scale: float = ITEM_SCALE # Flag to track whether this item should be garbage collected (e.g., if picked up or fallen into the void) self.is_dead: bool = False # Calculate the absolute timestamp (in milliseconds) when this item becomes eligible for pickup self.pickup_delay: int = pg.time.get_ticks() + ITEM_PICKUP_DELAY
[docs] @global_profiler.profile_func('Item_Update') def update(self) -> None: """ Applies continuous gravity and velocity updates, handles simple ground collisions, and destroys the item if it falls into the void or is collected by the player. """ # Apply gravitational acceleration downwards, scaled by the time since the last frame self.velocity.y += GRAVITY * self.app.delta_time # Integrate the velocity vector into the current position to move the item through space self.position += self.velocity * self.app.delta_time # Retrieve a reference to the world state to check for block collisions world: Any = self.app.scene.world # Calculate the integer coordinate directly below the item to check if it has hit the floor check_position: Any = glm.ivec3(self.position.x, self.position.y - self.scale / 2, self.position.z) # Query the voxel handler to see if the block at check_position is solid (non-zero ID) if world.voxel_handler.get_voxel_id(check_position)[0]: # If a collision is detected, snap the item's Y position to rest precisely on top of the block surface self.position.y = check_position.y + 1.0 + self.scale / 2 # Apply a harsh dampening factor (friction) to the X and Z velocities so the item stops sliding self.velocity.x *= 0.8 self.velocity.z *= 0.8 # Nullify the vertical velocity completely since it is now resting on the ground self.velocity.y = 0 # Check if the item has fallen below the world boundary (-10 Y) and mark it for deletion elif self.position.y < -10: self.is_dead = True # Increment the item's yaw rotation continuously based on elapsed time to create a spinning effect self.rotation += 0.003 * self.app.delta_time # Ensure the mandatory pickup cooldown period has elapsed before checking for player collision if pg.time.get_ticks() > self.pickup_delay: # Calculate the Euclidean distance between the item's center and the player's center if glm.distance(self.position, self.app.player.position) < ITEM_PICKUP_RADIUS: # Attempt to add the item's voxel ID to the player's inventory if self.app.player.add_item(self.voxel_id): # If the inventory accepted the item, mark this entity for destruction self.is_dead = True # Trigger the auditory feedback for a successful item pickup self.app.sounds.play_place_block()
[docs] @global_profiler.profile_func('Item_GetModelMatrix') def get_model_matrix(self) -> Any: """ Returns the transformation matrix required to position, rotate, and scale the 3D item for rendering. """ # Calculate a time-based bobbing offset using a continuous sine wave based on the system clock # This provides the classic 3D hovering/floating effect for dropped items bobbing_offset = math.sin(pg.time.get_ticks() * 0.003) * 0.1 # Apply the computed bobbing offset to the item's absolute Y position bob_position = glm.vec3(self.position.x, self.position.y + bobbing_offset, self.position.z) # Initialize a 4x4 identity matrix and apply a translation transformation to move the model to its world coordinates model_matrix: Any = glm.translate(glm.mat4(), bob_position) # Apply a rotational transformation around the global Y-axis (up vector) using the accumulated rotation angle model_matrix = glm.rotate(model_matrix, self.rotation, glm.vec3(0, 1, 0)) # Finally, apply a uniform scaling transformation to shrink the model to the defined ITEM_SCALE size return glm.scale(model_matrix, glm.vec3(self.scale))
[docs] class ItemManager: """ Manages all active Item entities in the scene. Handles updating physics, batched rendering, and enforcing an entity cap to prevent performance degradation from extreme item quantities. Args: app (Any): The main application instance. """ @global_profiler.profile_func('ItemManager_Init') def __init__(self, app: Any) -> None: """ Initializes the item list and pre-loads the meshes required to render blocks and 3D models like pickaxes or sticks. """ # Retain the application context self.app: Any = app # Initialize an empty list that will act as the live entity pool for all dropped items self.items: List[Item] = [] # Instantiate the generic cubic mesh used for standard voxel block drops self.mesh: Any = ItemMesh(app) # Load the custom 3D Wavefront (.obj) model specifically for the stick item self.stick_mesh: Any = ObjMesh(app, get_path('assets/models/items/stick/stick.obj')) # Load the custom 3D Wavefront (.obj) model specifically for the wooden pickaxe item self.pickaxe_mesh: Any = ObjMesh(app, get_path('assets/models/items/wooden-pickaxe/wooden_pickaxe.obj'))
[docs] @global_profiler.profile_func('ItemManager_AddItem') def add_item(self, position: Any, voxel_id: int) -> None: """ Spawns a new item entity into the world. Enforces a First-In-First-Out (FIFO) limit to automatically despawn old items if too many are active at once. """ # Check if adding a new item would exceed the hardcoded global entity cap if len(self.items) > ITEM_ENTITY_CAP: # Forcefully remove the oldest item in the list (index 0) to maintain strict memory and CPU bounds self.items.pop(0) # Instantiate a new Item object at the designated coordinates and append it to the active pool self.items.append(Item(self.app, position, voxel_id))
[docs] @global_profiler.profile_func('ItemManager_LoadItem') def load_item( self, voxel_id: int, position_x: float, position_y: float, position_z: float, velocity_x: float, velocity_y: float, velocity_z: float, ) -> None: """ Restores a previously saved item entity into the world with its exact former position and velocity to bypass the random spawn burst. """ # Enforce the exact same entity cap logic as add_item to prevent save-scumming overload if len(self.items) > ITEM_ENTITY_CAP: self.items.pop(0) # Instantiate a shell Item object at the origin item: Item = Item(self.app, (0, 0, 0), voxel_id) # Overwrite the random initial position with the exact coordinates loaded from the database item.position = glm.vec3(position_x, position_y, position_z) # Overwrite the random burst velocity with the preserved momentum vector item.velocity = glm.vec3(velocity_x, velocity_y, velocity_z) # Append the restored item to the active pool self.items.append(item)
[docs] @global_profiler.profile_func('ItemManager_Update') def update(self) -> None: """ Updates physics for all active items and removes ones marked as dead. """ # Iterate through every active item entity and invoke its internal physics/logic update step for item in self.items: item.update() # Rebuild the active items list, filtering out any entities that have flagged themselves as dead self.items = [item for item in self.items if not item.is_dead]
[docs] @global_profiler.profile_func('ItemManager_Render') def render(self) -> None: """ Renders all items that fall within the specific item render distance. """ # Temporarily disable OpenGL backface culling because the items spin, exposing all geometric faces self.app.ctx.disable(self.app.ctx.CULL_FACE) # Cache the player's current world position to calculate render distances efficiently player_position: Any = self.app.player.position # Iterate through the entire active item pool for the render pass for item in self.items: # Perform a fast squared-distance check to cull items that are too far away to be visible if glm.distance2(item.position, player_position) > ITEM_RENDER_DISTANCE_SQUARED: continue # Route the item to its appropriate 3D mesh based on its unique voxel ID if item.voxel_id == STICK: mesh = self.stick_mesh elif item.voxel_id == WOODEN_PICKAXE: mesh = self.pickaxe_mesh else: # Default to the generic cubic block mesh mesh = self.mesh # Calculate the final Model matrix containing all position, rotation, and scaling data # and write it directly into the shader's 'm_model' uniform mesh.program['m_model'].write(item.get_model_matrix()) # If the shader requires texture indexing (for blocks), pass the voxel ID uniform if 'voxel_id' in mesh.program: mesh.program['voxel_id'] = int(item.voxel_id) # Issue the final OpenGL draw call for this specific item mesh mesh.render()