from collections import defaultdict from pathlib import Path import pytest from engine.character import Character from engine.character_creator import CharacterCreator from engine.controls_menu import ControlsMenu from engine.defs import DefRegistry from engine.game_modes import CharacterCreationFlow, MainMenu, PauseMenu from engine.inventory import Inventory from engine.item import ItemInstance from engine.render import menu_draw from engine.ui import AbilityBar, AbilityBarSlot, CharacterMenu from engine.world_creation import WorldCreationMenu DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" @pytest.fixture() def registry(): return DefRegistry.load(DEFS_DIR) def new_buckets(): return defaultdict(list) def test_draw_main_menu_emits_a_background_panel_and_option_text(): buckets = new_buckets() menu_draw.draw_main_menu(buckets, MainMenu(has_existing_save=True), 800, 600) assert buckets["ui/panel/background.png"] assert any(k.startswith("ui/font/") for k in buckets) def test_draw_pause_menu_emits_content(): buckets = new_buckets() menu_draw.draw_pause_menu(buckets, PauseMenu(), 800, 600) assert buckets["ui/panel/background.png"] def test_draw_character_creation_species_step(registry): buckets = new_buckets() flow = CharacterCreationFlow(CharacterCreator(registry)) menu_draw.draw_character_creation(buckets, flow, 800, 600) assert buckets["ui/panel/background.png"] def test_draw_character_creation_name_step(registry): buckets = new_buckets() flow = CharacterCreationFlow(CharacterCreator(registry)) flow.advance() flow.type_char("V") menu_draw.draw_character_creation(buckets, flow, 800, 600) assert buckets["ui/panel/background.png"] def test_draw_character_creation_password_step_masks_the_typed_password(registry): from engine.render.ui_draw import glyph_texture_path buckets = new_buckets() flow = CharacterCreationFlow(CharacterCreator(registry)) flow.advance() # species -> name flow.type_char("V") flow.advance() # name -> password flow.type_char("q") # "q" appears in none of this screen's own labels/footer text menu_draw.draw_character_creation(buckets, flow, 800, 600) assert buckets["ui/panel/background.png"] assert glyph_texture_path("*") in buckets # the typed password renders masked assert glyph_texture_path("q") not in buckets # the raw character never appears def test_draw_character_creation_defaults_to_new_character_title(registry): from engine.render.ui_draw import glyph_texture_path buckets = new_buckets() flow = CharacterCreationFlow(CharacterCreator(registry)) menu_draw.draw_character_creation(buckets, flow, 800, 600) assert glyph_texture_path("P") not in buckets # "P" appears in no default-screen text def test_draw_character_creation_accepts_a_custom_title(registry): from engine.render.ui_draw import glyph_texture_path buckets = new_buckets() flow = CharacterCreationFlow(CharacterCreator(registry)) menu_draw.draw_character_creation(buckets, flow, 800, 600, title="Spawn NPC") assert buckets[glyph_texture_path("P")] # from "NPC" - only present with the custom title def test_draw_world_creation_lists_templates_and_highlights_selection(): buckets = new_buckets() menu = WorldCreationMenu() menu.select(0) menu_draw.draw_world_creation(buckets, menu, 800, 600) assert buckets["ui/panel/background.png"] assert buckets["ui/panel/highlight.png"] assert any(k.startswith("ui/font/") for k in buckets) def test_draw_world_creation_with_nothing_selected_defaults_to_first_row(): buckets = new_buckets() menu_draw.draw_world_creation(buckets, WorldCreationMenu(), 800, 600) assert buckets["ui/panel/highlight.png"] def test_draw_controls_menu_highlights_selected_row(): buckets = new_buckets() from engine.config import KeyBindings menu = ControlsMenu(KeyBindings.load(Path("/nonexistent"))) menu_draw.draw_controls_menu(buckets, menu, 800, 600) assert buckets["ui/panel/highlight.png"] def test_draw_ability_bar_highlights_the_selected_slot(): buckets = new_buckets() bar = AbilityBar() bar.assign(2, AbilityBarSlot(kind="hand", source_id="right_hand", ability_id="time_warp_sphere")) bar.select(2) menu_draw.draw_ability_bar(buckets, bar, 600) assert buckets["ui/panel/highlight.png"] assert buckets["ui/panel/border.png"] assert any(k.startswith("ui/font/") for k in buckets) def test_draw_ability_bar_with_nothing_selected_has_no_highlight(): buckets = new_buckets() menu_draw.draw_ability_bar(buckets, AbilityBar(), 600) assert "ui/panel/highlight.png" not in buckets assert buckets["ui/panel/border.png"] def test_draw_character_card_bio_tab(registry): buckets = new_buckets() character = Character( name="Vex", species_id="human", default_body_parts=registry.new_default_body_parts("human") ) menu_draw.draw_character_card(buckets, character, CharacterMenu(), registry, 600) assert buckets["ui/panel/background.png"] assert any(k.startswith("ui/font/") for k in buckets) def test_draw_character_card_equipment_tab(registry): buckets = new_buckets() character = Character( name="Vex", species_id="human", default_body_parts=registry.new_default_body_parts("human") ) card_menu = CharacterMenu() card_menu.select_tab("equipment") menu_draw.draw_character_card(buckets, character, card_menu, registry, 600) assert buckets["ui/panel/background.png"] def test_draw_character_card_medical_tab_lists_body_parts_and_organs(registry): buckets = new_buckets() character = Character( name="Vex", species_id="human", default_body_parts=registry.new_default_body_parts("human") ) card_menu = CharacterMenu() card_menu.select_tab("medical") menu_draw.draw_character_card(buckets, character, card_menu, registry, 600) assert buckets["ui/panel/background.png"] assert any(k.startswith("ui/font/") for k in buckets) def test_medical_lines_omits_log_section_when_empty(registry): character = Character(species_id="human", default_body_parts=registry.new_default_body_parts("human")) lines = menu_draw._medical_lines(character, registry) assert "Log:" not in lines def test_medical_lines_includes_recent_log_entries_newest_first(registry): character = Character(species_id="human", default_body_parts=registry.new_default_body_parts("human")) character.log_medical_event("first event") character.log_medical_event("second event") lines = menu_draw._medical_lines(character, registry) assert "Log:" in lines log_index = lines.index("Log:") assert lines[log_index + 1] == " second event" assert lines[log_index + 2] == " first event" def test_medical_lines_caps_log_display_count(registry): character = Character(species_id="human", default_body_parts=registry.new_default_body_parts("human")) for i in range(10): character.log_medical_event(f"event {i}") lines = menu_draw._medical_lines(character, registry) log_index = lines.index("Log:") log_lines = lines[log_index + 1 :] assert len(log_lines) == menu_draw.MEDICAL_LOG_DISPLAY_COUNT assert log_lines[0] == " event 9" def test_draw_character_card_medical_tab_shows_removed_parts_and_ailments(registry): from engine.character import Ailment buckets = new_buckets() character = Character( name="Vex", species_id="human", default_body_parts=registry.new_default_body_parts("human") ) torso = character.get_or_create_body_part_override("torso") torso.ailments.append(Ailment(id="infection", name="Infection", severity=0.5)) leg = character.get_or_create_body_part_override("left_leg") leg.removed = True card_menu = CharacterMenu() card_menu.select_tab("medical") menu_draw.draw_character_card(buckets, character, card_menu, registry, 600) assert buckets["ui/panel/background.png"] # --- character card hit-testing + inventory grid ------------------------------------------------ def test_character_card_rect_matches_ability_bar_offset(): x, y, w, h = menu_draw.character_card_rect(600) assert x == menu_draw.PADDING + menu_draw.ABILITY_BAR_W + menu_draw.PADDING assert y == 600 - menu_draw.CHARACTER_CARD_H - menu_draw.PADDING assert (w, h) == (menu_draw.CHARACTER_CARD_W, menu_draw.CHARACTER_CARD_H) def test_point_in_rect_true_inside_false_outside(): rect = (10.0, 20.0, 100.0, 50.0) assert menu_draw.point_in_rect(50, 40, rect) is True assert menu_draw.point_in_rect(5, 40, rect) is False assert menu_draw.point_in_rect(50, 100, rect) is False def test_inventory_cell_at_finds_the_top_left_cell(): inventory = Inventory(6, 5) origin = (100.0, 200.0) cell = menu_draw.inventory_cell_at(inventory, origin, origin[0] + 1, origin[1] + 1) assert cell == (0, 0) def test_inventory_cell_at_none_outside_the_grid(): inventory = Inventory(6, 5) assert menu_draw.inventory_cell_at(inventory, (100.0, 200.0), 0, 0) is None def test_draw_inventory_grid_highlights_occupied_cells(registry): buckets = new_buckets() inventory = Inventory(2, 1) inventory.place(ItemInstance("item-1", "bandage"), registry.get_item_def("bandage"), 0, 0) menu_draw.draw_inventory_grid(buckets, inventory, (10.0, 10.0)) assert buckets["ui/panel/highlight.png"] assert buckets["ui/panel/border.png"] assert any(k.startswith("ui/font/") for k in buckets) def test_draw_inventory_grid_blanks_the_dragged_cell(registry): buckets = new_buckets() inventory = Inventory(1, 1) inventory.place(ItemInstance("item-1", "bandage"), registry.get_item_def("bandage"), 0, 0) menu_draw.draw_inventory_grid(buckets, inventory, (10.0, 10.0), dragging_item_id="item-1") assert "ui/panel/highlight.png" not in buckets # dragged cell drawn as blank "border" instead assert buckets["ui/panel/border.png"] def test_draw_drag_ghost_emits_a_panel_and_label(): buckets = new_buckets() menu_draw.draw_drag_ghost(buckets, "bandage", 100.0, 100.0) assert buckets["ui/panel/highlight.png"] assert any(k.startswith("ui/font/") for k in buckets) # --- equipment tab rows + backpack popup card ------------------------------------------------- def test_equipment_row_at_finds_a_worn_slot_row(registry): from engine.character import ClothingPiece character = Character( species_id="human", default_body_parts=registry.new_default_body_parts("human"), clothing=[ClothingPiece("back", "backpack_basic")], ) x, y, _w, _h = menu_draw.character_card_rect(600) body_y = y + 10 + menu_draw.ROW_H + menu_draw.ROW_H * 1.5 assert menu_draw.equipment_row_at(character, 600, x + 20, body_y + 1) == "back" def test_equipment_row_at_finds_the_pockets_row_after_worn_items(registry): from engine.character import ClothingPiece character = Character( species_id="human", default_body_parts=registry.new_default_body_parts("human"), clothing=[ClothingPiece("back", "backpack_basic")], ) character.inventory = Inventory(4, 4) x, y, _w, _h = menu_draw.character_card_rect(600) body_y = y + 10 + menu_draw.ROW_H + menu_draw.ROW_H * 1.5 pockets_row_y = body_y + menu_draw.ROW_H # second row, after "back" from engine.ui import CharacterMenu assert menu_draw.equipment_row_at(character, 600, x + 20, pockets_row_y + 1) == CharacterMenu.POCKETS_SLOT def test_equipment_row_at_none_below_the_last_row(registry): from engine.character import ClothingPiece character = Character( species_id="human", default_body_parts=registry.new_default_body_parts("human"), clothing=[ClothingPiece("back", "backpack_basic")], ) x, y, _w, _h = menu_draw.character_card_rect(600) body_y = y + 10 + menu_draw.ROW_H + menu_draw.ROW_H * 1.5 below_the_single_row = body_y + menu_draw.ROW_H * 5 assert menu_draw.equipment_row_at(character, 600, x + 20, below_the_single_row) is None def test_backpack_popup_rect_sized_to_the_inventory_and_anchored_above_the_card(): inventory = Inventory(3, 2) card_x, card_y, _w, _h = menu_draw.character_card_rect(600) rect = menu_draw.backpack_popup_rect(inventory, 600) x, y, w, h = rect assert x == card_x assert y < card_y # sits above the character card, not overlapping it cell = menu_draw.INVENTORY_CELL_SIZE + menu_draw.INVENTORY_CELL_GAP assert w == inventory.width * cell + 20.0 assert h == inventory.height * cell + 20.0 + menu_draw.ROW_H def test_draw_backpack_popup_does_nothing_when_closed(registry): from engine.ui import CharacterMenu buckets = new_buckets() character = Character(species_id="human", default_body_parts=registry.new_default_body_parts("human")) menu_draw.draw_backpack_popup(buckets, character, CharacterMenu(), registry, 600) assert buckets == {} def test_draw_backpack_popup_renders_the_open_pockets_inventory(registry): from engine.ui import CharacterMenu buckets = new_buckets() character = Character(species_id="human", default_body_parts=registry.new_default_body_parts("human")) character.inventory = Inventory(2, 1) menu = CharacterMenu() menu.toggle_backpack(CharacterMenu.POCKETS_SLOT, character, registry) menu_draw.draw_backpack_popup(buckets, character, menu, registry, 600) assert buckets["ui/panel/background.png"] assert buckets["ui/panel/border.png"] assert any(k.startswith("ui/font/") for k in buckets)