Source code for ui.hud

"""
Heads-Up Display (HUD) elements and dynamic overlays for the game.

This module constructs the in-game overlay, rendering the crosshair, the
interactive drag-and-drop inventory, the hotbar with survival statistics,
the 3D view-bobbing held item, and the F3 debug screen.
"""

from typing import Any, Dict, List, Tuple

import moderngl as mgl
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 (
    ASPECT_RATIO,
    CHUNK_SIZE,
    FONT_SIZE_DEBUG,
    GLASS,
    GLOWSTONE,
    HELD_BLOCK_ROT_X,
    HELD_BLOCK_ROT_Y,
    HELD_BLOCK_SCALE,
    HELD_ITEM_BOB_OFFSET_X_MULT,
    HELD_ITEM_BOB_OFFSET_Y_MULT,
    HELD_ITEM_PLACE_SWING_OFFSET_Y,
    HELD_ITEM_PLACE_SWING_ROTATION_X,
    HELD_ITEM_POS,
    HELD_ITEM_SWING_OFFSET_Y,
    HELD_ITEM_SWING_OFFSET_Z,
    HELD_ITEM_SWING_ROT_X,
    HELD_PICKAXE_POS_OFFSET,
    HELD_PICKAXE_ROT_X,
    HELD_PICKAXE_ROT_Z,
    HELD_PICKAXE_SCALE,
    HELD_STICK_POS_OFFSET,
    HELD_STICK_ROT_X,
    HELD_STICK_ROT_Z,
    HELD_STICK_SCALE,
    HOTBAR_SCALE,
    HOTBAR_SIZE,
    HOTBAR_Y,
    INVENTORY_SIZE,
    SAND,
    SLOT_SCALE,
    STICK,
    STONE,
    STONE_BRICKS,
    SURVIVAL,
    UI_BG_COLOR,
    UI_SLOT_BG_COLOR,
    UI_SLOT_HOVER_COLOR,
    UI_SLOT_SELECTED_BG_COLOR,
    UI_SLOT_SELECTED_FRAME_COLOR,
    WINDOW_RESOLUTION,
    WOOD,
    WOOD_PLANKS,
    WOODEN_PICKAXE,
    get_path,
)

from .meshes import BlockIconMesh, CrosshairMesh, UIColorMesh, UITextMesh
from .text import TextRenderer


[docs] class Crosshair: """ Renders a simple fixed crosshair at the center of the screen. Args: app (Any): The main application context. """ @global_profiler.profile_func('Crosshair_Init') def __init__(self, app: Any) -> None: """ Initialize the `Crosshair` renderer for the given app context. Prepares the mesh used to draw the centered crosshair. """ # Process logic block self.app: Any = app self.mesh: Any = CrosshairMesh(app)
[docs] @global_profiler.profile_func('Crosshair_Render') def render(self) -> None: """Issues the draw call to render the crosshair mesh.""" # Dispatch render call to GPU self.mesh.render()
[docs] class Hotbar: """ Renders the bottom-screen hotbar, including the transparent slot backgrounds, active selection frame, 3D block/item icons, stack counts, and survival status bars. Args: app (Any): The main application context. """ @global_profiler.profile_func('Hotbar_Init') def __init__(self, app: Any) -> None: """ Initialize the `Hotbar` HUD element and its rendering resources. Pre-computes text textures for stack counts and caches rendering helpers. """ # Process logic block self.app: Any = app self.block_mesh: Any = BlockIconMesh(app) self.color_mesh: Any = UIColorMesh(app) self.text_mesh: Any = UITextMesh(app) self.text_renderer: Any = TextRenderer(app) # Process logic block self.count_textures: Dict[int, Any] = {i: self.text_renderer.get_texture(str(i)) for i in range(1, 65)} self.cached_health_str: str = '' self.health_tex: Any = None self.cached_hunger_str: str = '' self.hunger_tex: Any = None # Process logic block self.cached_oxy_str: str = '' self.oxy_tex: Any = None
[docs] @global_profiler.profile_func('Hotbar_Render') def render(self) -> None: """Dynamically draws the hotbar slots, items, counts, and survival bars.""" # Process logic block player: Any = self.app.player s: float = HOTBAR_SCALE slot_s: float = SLOT_SCALE gap: float = 0.01 x_spacing: float = (slot_s * 2 + gap) / ASPECT_RATIO # Process logic block start_x: float = -4 * x_spacing y: float = HOTBAR_Y # Execute loop iteration for i in range(HOTBAR_SIZE): x: float = start_x + i * x_spacing is_selected: bool = i == player.hotbar_index if is_selected: sel_s: float = slot_s + 0.006 self.color_mesh.program['u_scale'] = (sel_s / ASPECT_RATIO, sel_s) self.color_mesh.program['u_offset'] = (x, y) self.color_mesh.program['u_color'] = UI_SLOT_SELECTED_FRAME_COLOR self.color_mesh.render() self.color_mesh.program['u_scale'] = (slot_s / ASPECT_RATIO, slot_s) self.color_mesh.program['u_color'] = UI_SLOT_SELECTED_BG_COLOR self.color_mesh.render() else: self.color_mesh.program['u_scale'] = (slot_s / ASPECT_RATIO, slot_s) self.color_mesh.program['u_offset'] = (x, y) self.color_mesh.program['u_color'] = UI_SLOT_BG_COLOR self.color_mesh.render() for i in range(HOTBAR_SIZE): voxel_id: int = player.inventory[i] if voxel_id != 0: if voxel_id in (STICK, WOODEN_PICKAXE): self.text_mesh.program['u_scale'] = (s / ASPECT_RATIO, s) self.text_mesh.program['u_offset'] = (start_x + i * x_spacing, y) self.text_mesh.program['u_texture_0'] = 5 if voxel_id == STICK else 6 self.text_mesh.render() self.text_mesh.program['u_texture_0'] = 4 else: self.block_mesh.program['u_scale'] = (s / ASPECT_RATIO, s) self.block_mesh.program['u_offset'] = (start_x + i * x_spacing, y) self.block_mesh.program['voxel_id'] = voxel_id self.block_mesh.render() for i in range(HOTBAR_SIZE): count: int = player.inventory_counts[i] if count > 0: tex: Any = self.count_textures.get(count) if tex: tex.use(location=4) tex_w: int = tex.size[0] tex_h: int = tex.size[1] scale_y: float = 0.025 scale_x: float = scale_y * (tex_w / tex_h) / ASPECT_RATIO offset_x: float = start_x + i * x_spacing + 0.015 offset_y: float = y - 0.025 self.text_mesh.program['u_scale'] = (scale_x, scale_y) self.text_mesh.program['u_offset'] = (offset_x, offset_y) self.text_mesh.render() # Handle conditional branching if player.game_mode == SURVIVAL: def draw_bar( ratio: float, offset_x: float, offset_y: float, bg_color: Tuple[float, float, float, float], fg_color: Tuple[float, float, float, float], tex: Any, ) -> None: # Initialize and update variables self.color_mesh.program['u_scale'] = (0.2, 0.01) self.color_mesh.program['u_offset'] = (offset_x, offset_y) self.color_mesh.program['u_color'] = bg_color # Dispatch render call to GPU self.color_mesh.render() # Handle conditional branching if ratio > 0: self.color_mesh.program['u_scale'] = (0.2 * ratio, 0.01) self.color_mesh.program['u_offset'] = (offset_x - 0.2 * (1.0 - ratio), offset_y) self.color_mesh.program['u_color'] = fg_color self.color_mesh.render() # Execute expression statement tex.use(location=4) # Process logic block scale_y: float = 0.015 scale_x: float = scale_y * (tex.size[0] / tex.size[1]) / ASPECT_RATIO # Initialize and update variables self.text_mesh.program['u_scale'] = (scale_x, scale_y) self.text_mesh.program['u_offset'] = (offset_x, offset_y) # Dispatch render call to GPU self.text_mesh.render() health_ratio: float = max(0.0, float(player.health) / player.max_health) health_str: str = f'HP: {int(player.health)}/{player.max_health}' if health_str != self.cached_health_str or self.health_tex is None: if self.health_tex: self.health_tex.release() self.health_tex = self.text_renderer.get_dynamic_texture(health_str) self.cached_health_str = health_str draw_bar(health_ratio, -0.22, y + 0.08, (0.1, 0.1, 0.1, 0.8), (0.8, 0.1, 0.1, 0.9), self.health_tex) hunger_ratio: float = max(0.0, float(player.hunger) / player.max_hunger) hunger_str: str = f'Food: {int(player.hunger)}/{player.max_hunger}' if hunger_str != self.cached_hunger_str or self.hunger_tex is None: if self.hunger_tex: self.hunger_tex.release() self.hunger_tex = self.text_renderer.get_dynamic_texture(hunger_str) self.cached_hunger_str = hunger_str draw_bar(hunger_ratio, 0.22, y + 0.08, (0.1, 0.1, 0.1, 0.8), (0.8, 0.5, 0.1, 0.9), self.hunger_tex) if player.oxygen < player.max_oxygen: oxy_ratio: float = max(0.0, float(player.oxygen) / player.max_oxygen) oxy_str: str = f'O2: {int(player.oxygen)}/{player.max_oxygen}' if oxy_str != self.cached_oxy_str or self.oxy_tex is None: if self.oxy_tex: self.oxy_tex.release() self.oxy_tex = self.text_renderer.get_dynamic_texture(oxy_str) self.cached_oxy_str = oxy_str draw_bar(oxy_ratio, 0.22, y + 0.12, (0.1, 0.1, 0.1, 0.8), (0.1, 0.6, 0.9, 0.9), self.oxy_tex)
[docs] class HeldBlock: """ Renders the 3D model of the currently equipped item or block in the player's hand. Includes procedural view bobbing and swinging animations for mining/placing. Args: app (Any): The main application context. """ @global_profiler.profile_func('HeldBlock_Init') def __init__(self, app: Any) -> None: """ Initialize the held item/block renderer and supporting meshes. Loads meshes for stick and pickaxe models used for special-item rendering. """ # Process logic block self.app: Any = app self.mesh: Any = ItemMesh(app) self.stick_mesh: Any = ObjMesh(app, get_path('assets/models/items/stick/stick.obj'), texture_id=5) self.pickaxe_mesh: Any = ObjMesh( app, get_path('assets/models/items/wooden-pickaxe/wooden_pickaxe.obj'), texture_id=6 )
[docs] @global_profiler.profile_func('HeldBlock_Render') def render(self) -> None: """Applies transformation matrices to simulate hand movement and renders the item.""" # Process logic block player: Any = self.app.player voxel_id: int = player.inventory[player.hotbar_index] # Handle conditional branching if voxel_id == 0: return # Process logic block bob_offset_y: float = glm.sin(player.step_counter) * HELD_ITEM_BOB_OFFSET_Y_MULT bob_offset_x: float = glm.cos(player.step_counter * 0.5) * HELD_ITEM_BOB_OFFSET_X_MULT swing_offset_y: float = 0.0 swing_offset_z: float = 0.0 swing_rotation_x: float = 0.0 # Handle conditional branching if player.mining_time > 0.0: swing_val: float = max(0.0, float(glm.sin(player.mining_time * 0.03))) swing_offset_y = swing_val * HELD_ITEM_SWING_OFFSET_Y swing_offset_z = -swing_val * HELD_ITEM_SWING_OFFSET_Z swing_rotation_x = swing_val * HELD_ITEM_SWING_ROT_X else: time_since_place: int = pg.time.get_ticks() - player.interaction_timer if time_since_place < player.interaction_delay: progress: float = time_since_place / player.interaction_delay swing_val = float(glm.sin(progress * glm.pi())) swing_offset_y = swing_val * HELD_ITEM_PLACE_SWING_OFFSET_Y swing_rotation_x = swing_val * HELD_ITEM_PLACE_SWING_ROTATION_X # Process logic block position: Any = HELD_ITEM_POS + glm.vec3(bob_offset_x, bob_offset_y - swing_offset_y, swing_offset_z) model_matrix: Any = glm.inverse(player.m_view) # Initialize and update variables model_matrix = glm.translate(model_matrix, position) # Handle conditional branching if voxel_id == STICK: model_matrix = glm.translate(model_matrix, HELD_STICK_POS_OFFSET) model_matrix = glm.rotate(model_matrix, HELD_STICK_ROT_X - swing_rotation_x, glm.vec3(1, 0, 0)) model_matrix = glm.rotate(model_matrix, HELD_STICK_ROT_Z, glm.vec3(0, 0, 1)) model_matrix = glm.scale(model_matrix, HELD_STICK_SCALE) mesh = self.stick_mesh elif voxel_id == WOODEN_PICKAXE: model_matrix = glm.translate(model_matrix, HELD_PICKAXE_POS_OFFSET) model_matrix = glm.rotate(model_matrix, HELD_PICKAXE_ROT_X - swing_rotation_x, glm.vec3(1, 0, 0)) model_matrix = glm.rotate(model_matrix, HELD_PICKAXE_ROT_Z, glm.vec3(0, 0, 1)) model_matrix = glm.scale(model_matrix, HELD_PICKAXE_SCALE) mesh = self.pickaxe_mesh else: model_matrix = glm.rotate(model_matrix, HELD_BLOCK_ROT_X - swing_rotation_x, glm.vec3(1, 0, 0)) model_matrix = glm.rotate(model_matrix, HELD_BLOCK_ROT_Y, glm.vec3(0, 1, 0)) model_matrix = glm.scale(model_matrix, HELD_BLOCK_SCALE) mesh = self.mesh # Execute expression statement mesh.program['m_proj'].write(player.m_proj) mesh.program['m_view'].write(player.m_view) mesh.program['m_model'].write(model_matrix) # Handle conditional branching if 'voxel_id' in mesh.program: mesh.program['voxel_id'] = voxel_id # Dispatch render call to GPU self.app.ctx.disable(mgl.DEPTH_TEST) self.app.ctx.enable(mgl.CULL_FACE) mesh.render() self.app.ctx.enable(mgl.DEPTH_TEST)
[docs] class InventoryUI: """ Manages the full player inventory and crafting grid interface. Handles drag-and-drop item management, stack splitting, and crafting matrix evaluation. Args: app (Any): The main application context. """ @global_profiler.profile_func('InventoryUI_Init') def __init__(self, app: Any) -> None: """ Initialize the full `InventoryUI` including crafting, slots and tooltips. Caches geometry and text textures required for interactive inventory rendering. """ # Process logic block self.app: Any = app self.block_mesh: Any = BlockIconMesh(app) self.color_mesh: Any = UIColorMesh(app) self.text_mesh: Any = UITextMesh(app) self.text_renderer: Any = TextRenderer(app) # Process logic block self.drag_id: int = 0 self.drag_count: int = 0 self.drag_start_pos: Tuple[int, int] = (0, 0) self.tooltip_texture: Any = None self.last_hover_name: str = '' # Process logic block self._cached_aspect: float = ASPECT_RATIO self._slot_positions: Dict[int, Tuple[float, float]] = {} self.count_textures: Dict[int, Any] = {i: self.text_renderer.get_texture(str(i)) for i in range(1, 65)}
[docs] @global_profiler.profile_func('InventoryUI_UpdateCrafting') def update_crafting(self) -> None: """Evaluates the 2x2 crafting grid and updates the output slot if a valid recipe matches.""" # Process logic block player: Any = self.app.player grid: Tuple[int, ...] = tuple(player.inventory[36:40]) recipes: Dict[Tuple[int, ...], Tuple[int, int]] = { (WOOD, 0, 0, 0): (WOOD_PLANKS, 4), (0, WOOD, 0, 0): (WOOD_PLANKS, 4), (0, 0, WOOD, 0): (WOOD_PLANKS, 4), (0, 0, 0, WOOD): (WOOD_PLANKS, 4), (WOOD_PLANKS, 0, WOOD_PLANKS, 0): (STICK, 4), (0, WOOD_PLANKS, 0, WOOD_PLANKS): (STICK, 4), (WOOD_PLANKS, WOOD_PLANKS, STICK, 0): (WOODEN_PICKAXE, 1), (SAND, SAND, SAND, SAND): (GLOWSTONE, 4), (SAND, 0, 0, 0): (GLASS, 1), (0, SAND, 0, 0): (GLASS, 1), (0, 0, SAND, 0): (GLASS, 1), (0, 0, 0, SAND): (GLASS, 1), (STONE, STONE, STONE, STONE): (STONE_BRICKS, 4), } # Handle conditional branching if grid in recipes: player.inventory[40], player.inventory_counts[40] = recipes[grid] else: player.inventory[40], player.inventory_counts[40] = (0, 0)
[docs] @global_profiler.profile_func('InventoryUI_GetSlotPos') def get_slot_pos(self, i: int) -> Tuple[float, float]: """Calculates and caches the 2D screen coordinate for a specific inventory slot.""" # Handle conditional branching if self._cached_aspect != ASPECT_RATIO: self._slot_positions.clear() self._cached_aspect = ASPECT_RATIO if i in self._slot_positions: return self._slot_positions[i] # Process logic block gap: float = 0.01 x_spacing: float = (SLOT_SCALE * 2 + gap) / ASPECT_RATIO y_spacing: float = SLOT_SCALE * 2 + gap # Handle conditional branching if i < HOTBAR_SIZE: col: int = i % HOTBAR_SIZE x = -4 * x_spacing + col * x_spacing y = HOTBAR_Y elif i < 36: col = i % HOTBAR_SIZE row: int = 2 - (i - HOTBAR_SIZE) // HOTBAR_SIZE y = HOTBAR_Y + (row + 1.5) * y_spacing x = -4 * x_spacing + col * x_spacing elif i < 40: grid_idx: int = i - 36 x = 0.5 * x_spacing + grid_idx % 2 * x_spacing y = HOTBAR_Y + (6.0 - grid_idx // 2) * y_spacing else: x = 3.0 * x_spacing y = HOTBAR_Y + 5.5 * y_spacing # Initialize and update variables self._slot_positions[i] = (x, y) # Return computed result return (x, y)
[docs] @global_profiler.profile_func('InventoryUI_GetSlotAtMouse') def get_slot_at_mouse(self, mouse_pos: Tuple[int, int]) -> int: """Returns the ID of the inventory slot currently hovered by the mouse cursor.""" # Process logic block mouse_x: float = mouse_pos[0] / WINDOW_RESOLUTION.x * 2.0 - 1.0 mouse_y: float = 1.0 - mouse_pos[1] / WINDOW_RESOLUTION.y * 2.0 slot_w: float = SLOT_SCALE / ASPECT_RATIO slot_h: float = SLOT_SCALE # Execute loop iteration for i in range(INVENTORY_SIZE): slot_pos: Tuple[float, float] = self.get_slot_pos(i) slot_x: float = slot_pos[0] slot_y: float = slot_pos[1] if slot_x - slot_w < mouse_x < slot_x + slot_w and slot_y - slot_h < mouse_y < slot_y + slot_h: return i # Return computed result return -1
[docs] @global_profiler.profile_func('InventoryUI_GetClosestValidSlot') def get_closest_valid_slot(self, mouse_pos: Tuple[int, int], drag_id: int, drag_count: int) -> int: """Finds the closest valid drop target slot during a drag-and-drop operation.""" # Process logic block mouse_x: float = mouse_pos[0] / WINDOW_RESOLUTION.x * 2.0 - 1.0 mouse_y: float = 1.0 - mouse_pos[1] / WINDOW_RESOLUTION.y * 2.0 best_index: int = -1 best_distance_sq: float = float('inf') gap: float = 0.01 # Process logic block y_spacing: float = SLOT_SCALE * 2 + gap max_distance_sq: float = (y_spacing * 1.5) ** 2 player: Any = self.app.player # Execute loop iteration for i in range(INVENTORY_SIZE): if i == 40: continue slot_pos: Tuple[float, float] = self.get_slot_pos(i) slot_x: float = slot_pos[0] slot_y: float = slot_pos[1] delta_x: float = (mouse_x - slot_x) * ASPECT_RATIO delta_y: float = mouse_y - slot_y distance_sq: float = delta_x * delta_x + delta_y * delta_y if distance_sq < best_distance_sq and distance_sq < max_distance_sq: slot_id: int = player.inventory[i] if slot_id == 0 or (slot_id == drag_id and player.inventory_counts[i] < 64): best_index = i best_distance_sq = distance_sq # Return computed result return best_index
[docs] @global_profiler.profile_func('InventoryUI_HandleEvent') def handle_event(self, event: Any) -> None: """Processes left/right mouse clicks for selecting, splitting, and merging item stacks.""" # Handle conditional branching if event.type == pg.MOUSEBUTTONDOWN: i: int = self.get_slot_at_mouse(pg.mouse.get_pos()) if i != -1: player: Any = self.app.player slot_id: int = player.inventory[i] slot_count: int = player.inventory_counts[i] if event.button == 1: if i == 40: if slot_id != 0: can_take: bool = False if self.drag_id == 0: self.drag_id = slot_id self.drag_count = slot_count can_take = True elif self.drag_id == slot_id and self.drag_count + slot_count <= 64: self.drag_count += slot_count can_take = True if can_take: for c in range(36, 40): if player.inventory_counts[c] > 0: player.inventory_counts[c] -= 1 if player.inventory_counts[c] <= 0: player.inventory[c] = 0 self.drag_start_pos = pg.mouse.get_pos() elif self.drag_id == 0: if slot_id != 0: self.drag_id = slot_id self.drag_count = slot_count player.inventory[i] = 0 player.inventory_counts[i] = 0 self.drag_start_pos = pg.mouse.get_pos() elif slot_id == 0: player.inventory[i] = self.drag_id player.inventory_counts[i] = self.drag_count self.drag_id = 0 self.drag_count = 0 elif slot_id == self.drag_id: space: int = 64 - slot_count if space >= self.drag_count: player.inventory_counts[i] += self.drag_count self.drag_id = 0 self.drag_count = 0 else: player.inventory_counts[i] = 64 self.drag_count -= space else: player.inventory[i], self.drag_id = (self.drag_id, slot_id) player.inventory_counts[i], self.drag_count = (self.drag_count, slot_count) self.drag_start_pos = pg.mouse.get_pos() self.update_crafting() elif event.button == 3: if i != 40: if self.drag_id == 0: if slot_id != 0: half: int = slot_count - slot_count // 2 self.drag_id = slot_id self.drag_count = half player.inventory_counts[i] -= half if player.inventory_counts[i] <= 0: player.inventory[i] = 0 self.drag_start_pos = pg.mouse.get_pos() elif slot_id == 0: player.inventory[i] = self.drag_id player.inventory_counts[i] = 1 self.drag_count -= 1 if self.drag_count <= 0: self.drag_id = 0 elif slot_id == self.drag_id and slot_count < 64: player.inventory_counts[i] += 1 self.drag_count -= 1 if self.drag_count <= 0: self.drag_id = 0 self.update_crafting() elif event.type == pg.MOUSEBUTTONUP: if event.button == 1 and self.drag_id != 0: mouse_pos: Tuple[int, int] = pg.mouse.get_pos() delta_x: int = mouse_pos[0] - self.drag_start_pos[0] delta_y: int = mouse_pos[1] - self.drag_start_pos[1] if delta_x * delta_x + delta_y * delta_y > 100: i = self.get_closest_valid_slot(mouse_pos, self.drag_id, self.drag_count) if i != -1: player = self.app.player slot_id = player.inventory[i] slot_count = player.inventory_counts[i] if slot_id == 0: player.inventory[i] = self.drag_id player.inventory_counts[i] = self.drag_count self.drag_id = 0 self.drag_count = 0 elif slot_id == self.drag_id: space = 64 - slot_count if space >= self.drag_count: player.inventory_counts[i] += self.drag_count self.drag_id = 0 self.drag_count = 0 else: player.inventory_counts[i] = 64 self.drag_count -= space self.update_crafting()
[docs] @global_profiler.profile_func('InventoryUI_Close') def close(self) -> None: """Cleans up the inventory screen, ejecting active crafting items back into the world.""" # Process logic block player: Any = self.app.player # Execute loop iteration for i in range(36, 40): if player.inventory[i] != 0: for _ in range(player.inventory_counts[i]): if not player.add_item(player.inventory[i]): self.app.scene.item_manager.add_item(player.position, player.inventory[i]) player.inventory[i], player.inventory_counts[i] = (0, 0) # Handle conditional branching if self.drag_id != 0: while self.drag_count > 0: if not player.add_item(self.drag_id): for _ in range(self.drag_count): self.app.scene.item_manager.add_item(player.position, self.drag_id) break self.drag_count -= 1 self.drag_id = 0 self.drag_count = 0 # Execute expression statement self.update_crafting() # Handle conditional branching if self.tooltip_texture: self.tooltip_texture.release() self.tooltip_texture = None self.last_hover_name = ''
[docs] @global_profiler.profile_func('InventoryUI_Render') def render(self) -> None: """Issues draw calls for the entire inventory UI, background, and floating tooltip items.""" # Process logic block gap: float = 0.01 x_spacing: float = (SLOT_SCALE * 2 + gap) / ASPECT_RATIO y_spacing: float = SLOT_SCALE * 2 + gap background_width: float = 4.5 * x_spacing + 0.02 background_height: float = 3.0 * y_spacing + 0.02 # Initialize and update variables self.color_mesh.program['u_scale'] = (background_width, background_height) self.color_mesh.program['u_offset'] = (0.0, HOTBAR_Y + 4.0 * y_spacing) self.color_mesh.program['u_color'] = UI_BG_COLOR # Dispatch render call to GPU self.color_mesh.render() # Process logic block player: Any = self.app.player hover_idx: int = self.get_slot_at_mouse(pg.mouse.get_pos()) # Execute loop iteration for i in range(INVENTORY_SIZE): slot_pos: Tuple[float, float] = self.get_slot_pos(i) x: float = slot_pos[0] y: float = slot_pos[1] scale: float = SLOT_SCALE if i == hover_idx: self.color_mesh.program['u_scale'] = ((scale + 0.005) / ASPECT_RATIO, scale + 0.005) self.color_mesh.program['u_offset'] = (x, y) self.color_mesh.program['u_color'] = UI_SLOT_HOVER_COLOR self.color_mesh.render() self.color_mesh.program['u_scale'] = (scale / ASPECT_RATIO, scale) self.color_mesh.program['u_offset'] = (x, y) self.color_mesh.program['u_color'] = UI_SLOT_BG_COLOR self.color_mesh.render() voxel_id: int = player.inventory[i] if voxel_id != 0: if voxel_id in (STICK, WOODEN_PICKAXE): self.text_mesh.program['u_scale'] = (HOTBAR_SCALE / ASPECT_RATIO, HOTBAR_SCALE) self.text_mesh.program['u_offset'] = (x, y) self.text_mesh.program['u_texture_0'] = 5 if voxel_id == STICK else 6 self.text_mesh.render() self.text_mesh.program['u_texture_0'] = 4 else: self.block_mesh.program['u_scale'] = (HOTBAR_SCALE / ASPECT_RATIO, HOTBAR_SCALE) self.block_mesh.program['u_offset'] = (x, y) self.block_mesh.program['voxel_id'] = voxel_id self.block_mesh.render() count: int = player.inventory_counts[i] if count > 0: texture: Any = self.count_textures.get(count) if texture: texture.use(location=4) scale_y: float = 0.025 scale_x: float = scale_y * (texture.size[0] / texture.size[1]) / ASPECT_RATIO self.text_mesh.program['u_scale'] = (scale_x, scale_y) self.text_mesh.program['u_offset'] = (x + 0.015, y - 0.025) self.text_mesh.render() # Handle conditional branching if self.drag_id != 0: mouse_pos: Tuple[int, int] = pg.mouse.get_pos() mouse_x: float = mouse_pos[0] / WINDOW_RESOLUTION.x * 2.0 - 1.0 mouse_y: float = 1.0 - mouse_pos[1] / WINDOW_RESOLUTION.y * 2.0 if self.drag_id in (STICK, WOODEN_PICKAXE): self.text_mesh.program['u_scale'] = (HOTBAR_SCALE / ASPECT_RATIO, HOTBAR_SCALE) self.text_mesh.program['u_offset'] = (mouse_x, mouse_y) self.text_mesh.program['u_texture_0'] = 5 if self.drag_id == STICK else 6 self.text_mesh.render() self.text_mesh.program['u_texture_0'] = 4 else: self.block_mesh.program['u_scale'] = (HOTBAR_SCALE / ASPECT_RATIO, HOTBAR_SCALE) self.block_mesh.program['u_offset'] = (mouse_x, mouse_y) self.block_mesh.program['voxel_id'] = self.drag_id self.block_mesh.render() if self.drag_count > 0: drag_texture: Any = self.count_textures.get(self.drag_count) if drag_texture: drag_texture.use(location=4) drag_scale_y: float = 0.025 drag_scale_x: float = drag_scale_y * (drag_texture.size[0] / drag_texture.size[1]) / ASPECT_RATIO self.text_mesh.program['u_scale'] = (drag_scale_x, drag_scale_y) self.text_mesh.program['u_offset'] = (mouse_x + 0.015, mouse_y - 0.025) self.text_mesh.render() if hover_idx != -1 and self.drag_id == 0: hover_id: int = player.inventory[hover_idx] if hover_id != 0: item_names: Dict[int, str] = { 1: 'Sand', 2: 'Grass', 3: 'Dirt', 4: 'Stone', 5: 'Wood', 6: 'Leaves', 7: 'Wood Planks', 9: 'Glass', 10: 'Glowstone', 20: 'Stick', 21: 'Wooden Pickaxe', } name: str = item_names.get(hover_id, f'Item ID: {hover_id}') tooltip_mouse_pos: Tuple[int, int] = pg.mouse.get_pos() tooltip_mouse_x: float = tooltip_mouse_pos[0] / WINDOW_RESOLUTION.x * 2.0 - 1.0 tooltip_mouse_y: float = 1.0 - tooltip_mouse_pos[1] / WINDOW_RESOLUTION.y * 2.0 if name != self.last_hover_name: if self.tooltip_texture: self.tooltip_texture.release() self.tooltip_texture = self.text_renderer.get_dynamic_texture(name) self.last_hover_name = name tooltip_texture_current: Any = self.tooltip_texture tooltip_texture_current.use(location=4) tooltip_scale_y: float = 0.025 tooltip_scale_x: float = ( tooltip_scale_y * (tooltip_texture_current.size[0] / tooltip_texture_current.size[1]) / ASPECT_RATIO ) self.color_mesh.program['u_scale'] = (tooltip_scale_x + 0.01, tooltip_scale_y + 0.01) self.color_mesh.program['u_offset'] = ( tooltip_mouse_x + tooltip_scale_x + 0.02, tooltip_mouse_y - tooltip_scale_y - 0.02, ) self.color_mesh.program['u_color'] = (0.05, 0.05, 0.05, 0.95) self.color_mesh.render() self.text_mesh.program['u_scale'] = (tooltip_scale_x, tooltip_scale_y) self.text_mesh.program['u_offset'] = ( tooltip_mouse_x + tooltip_scale_x + 0.02, tooltip_mouse_y - tooltip_scale_y - 0.02, ) self.text_mesh.render()
[docs] class DebugOverlay: """ Displays an on-screen overlay with performance metrics, player coordinates, targeted block info, and current game mode (F3 menu). Args: app (Any): The main application context. """ @global_profiler.profile_func('DebugOverlay_Init') def __init__(self, app: Any) -> None: """ Initialize the debug overlay that displays performance and positional info. Sets up the debug font and texture caching used when updating the overlay. """ # Process logic block self.app: Any = app self.font: pg.font.Font = pg.font.SysFont('arial', FONT_SIZE_DEBUG, bold=True) self.text_mesh: Any = UITextMesh(app) self.dynamic_texture: Any = None self.last_update: int = 0
[docs] @global_profiler.profile_func('DebugOverlay_Render') def render(self) -> None: """Compiles and renders the performance statistics and positional data overlay.""" # Process logic block current_time: int = pg.time.get_ticks() # Handle conditional branching if current_time - self.last_update > 250 or self.dynamic_texture is None: self.last_update = current_time player: Any = self.app.player handler: Any = self.app.scene.world.voxel_handler fps: float = self.app.clock.get_fps() pos_x: float = float(player.position.x) pos_y: float = float(player.position.y) pos_z: float = float(player.position.z) chunk_x: int = int(pos_x // CHUNK_SIZE) chunk_y: int = int(pos_y // CHUNK_SIZE) chunk_z: int = int(pos_z // CHUNK_SIZE) yaw: float = float(glm.degrees(player.yaw) % 360) pitch: float = float(glm.degrees(player.pitch)) target: str = 'Air' if handler.voxel_id: target = f'ID: {handler.voxel_id} at {int(handler.voxel_world_pos.x)} {int(handler.voxel_world_pos.y)} {int(handler.voxel_world_pos.z)}' lines: List[str] = [ f'Pyrite (FPS: {fps:.0f})', f'XYZ: {pos_x:.3f} / {pos_y:.5f} / {pos_z:.3f}', f'Chunk: {chunk_x} {chunk_y} {chunk_z}', f'Facing: Yaw {yaw:.1f} Pitch {pitch:.1f}', f'Time: {self.app.world_session_time:.2f}', f'Target Block: {target}', f'Game Mode: {("Survival" if player.game_mode == SURVIVAL else "Creative")}', ] surfaces: List[pg.Surface] = [] for line in lines: shadow: pg.Surface = self.font.render(line, True, (60, 60, 60)) text: pg.Surface = self.font.render(line, True, (220, 220, 220)) merged: pg.Surface = pg.Surface((text.get_width() + 2, text.get_height() + 2), pg.SRCALPHA) merged.blit(shadow, (2, 2)) merged.blit(text, (0, 0)) surfaces.append(merged) max_width: int = max((s.get_width() for s in surfaces)) total_height: int = sum((s.get_height() for s in surfaces)) background_surface: pg.Surface = pg.Surface((max_width + 10, total_height + 10), pg.SRCALPHA) background_surface.fill((0, 0, 0, 120)) current_y: int = 5 for s in surfaces: background_surface.blit(s, (5, current_y)) current_y += s.get_height() if self.dynamic_texture: self.dynamic_texture.release() self.dynamic_texture = self.app.ctx.texture( background_surface.get_size(), 4, pg.image.tobytes(background_surface, 'RGBA', True) ) self.dynamic_texture.filter = (mgl.NEAREST, mgl.NEAREST) # Execute expression statement self.dynamic_texture.use(location=4) # Process logic block texture_width: int = self.dynamic_texture.size[0] texture_height: int = self.dynamic_texture.size[1] scale_y: float = texture_height / WINDOW_RESOLUTION.y scale_x: float = texture_width / WINDOW_RESOLUTION.x x_offset: float = -1.0 + scale_x # Process logic block y_offset: float = 1.0 - scale_y # Initialize and update variables self.text_mesh.program['u_scale'] = (scale_x, scale_y) self.text_mesh.program['u_offset'] = (x_offset, y_offset) # Dispatch render call to GPU self.text_mesh.render()