Mi2dRPGamEng/engine/render/menu_draw.py

187 lines
8.9 KiB
Python

from __future__ import annotations
from engine.controls_menu import ControlsMenu
from engine.game_modes import CharacterCreationFlow, MainMenu, PauseMenu
from engine.render.ui_draw import LINE_HEIGHT, draw_panel, draw_text, text_width
from engine.ui import AbilityBar, CharacterMenu
from engine.world_creation import WorldCreationMenu
MENU_OPTION_LABELS: dict[str, str] = {
"continue": "Continue",
"new_game": "New Game",
"controls": "Configure Controls",
"resume": "Resume",
"save_and_quit_to_menu": "Save & Quit to Menu",
}
ROW_H = LINE_HEIGHT
PADDING = 16.0
def _draw_option_list(buckets, options: list[str], selected_index: int, x: float, y: float) -> None:
for i, option in enumerate(options):
label = MENU_OPTION_LABELS.get(option, option)
row_y = y + i * ROW_H
if i == selected_index:
draw_panel(buckets, "highlight", x - 6, row_y - 2, text_width("> " + label) + 12, ROW_H)
draw_text(buckets, ("> " if i == selected_index else " ") + label, x, row_y)
def draw_main_menu(buckets, menu: MainMenu, viewport_w: float, viewport_h: float) -> None:
options = menu.options()
panel_w, panel_h = 320.0, PADDING * 2 + ROW_H * (len(options) + 1)
x = (viewport_w - panel_w) / 2
y = (viewport_h - panel_h) / 2
draw_panel(buckets, "background", x, y, panel_w, panel_h)
draw_text(buckets, "Mi2d RPG Engine", x + PADDING, y + PADDING)
_draw_option_list(buckets, options, menu.selected_index, x + PADDING, y + PADDING + ROW_H * 1.5)
def draw_pause_menu(buckets, menu: PauseMenu, viewport_w: float, viewport_h: float) -> None:
options = list(PauseMenu.OPTIONS)
panel_w, panel_h = 320.0, PADDING * 2 + ROW_H * (len(options) + 1)
x = (viewport_w - panel_w) / 2
y = (viewport_h - panel_h) / 2
draw_panel(buckets, "background", x, y, panel_w, panel_h)
draw_text(buckets, "Paused", x + PADDING, y + PADDING)
_draw_option_list(buckets, options, menu.selected_index, x + PADDING, y + PADDING + ROW_H * 1.5)
def draw_character_creation(buckets, flow: CharacterCreationFlow, viewport_w: float, viewport_h: float) -> None:
panel_w, panel_h = 420.0, 220.0
x = (viewport_w - panel_w) / 2
y = (viewport_h - panel_h) / 2
draw_panel(buckets, "background", x, y, panel_w, panel_h)
draw_text(buckets, "New Character", x + PADDING, y + PADDING)
if flow.step == "species":
species = flow.species_list()
current = flow.creator.species_id or "-"
draw_text(buckets, f"Species: < {current} >", x + PADDING, y + PADDING + ROW_H * 2)
draw_text(buckets, f"({flow.species_index + 1}/{max(len(species), 1)})", x + PADDING, y + PADDING + ROW_H * 3)
draw_text(buckets, "Left/Right to choose, Enter to continue", x + PADDING, y + panel_h - PADDING - ROW_H)
else:
draw_text(buckets, f"Species: {flow.creator.species_id}", x + PADDING, y + PADDING + ROW_H * 2)
draw_text(buckets, f"Name: {flow.creator.name}_", x + PADDING, y + PADDING + ROW_H * 3)
draw_text(buckets, "Type a name, Enter to confirm, Esc to go back", x + PADDING, y + panel_h - PADDING - ROW_H)
def draw_world_creation(buckets, menu: WorldCreationMenu, viewport_w: float, viewport_h: float) -> None:
panel_w, panel_h = 480.0, PADDING * 2 + ROW_H * (len(menu.templates) * 2 + 2)
x = (viewport_w - panel_w) / 2
y = (viewport_h - panel_h) / 2
draw_panel(buckets, "background", x, y, panel_w, panel_h)
draw_text(buckets, "Choose a World", x + PADDING, y + PADDING)
selected = menu.selected_index if menu.selected_index is not None else 0
for i, template in enumerate(menu.templates):
row_y = y + PADDING + ROW_H * (1.5 + i * 2)
if i == selected:
draw_panel(buckets, "highlight", x + PADDING - 6, row_y - 2, text_width(template.name) + 12, ROW_H)
draw_text(buckets, template.name, x + PADDING, row_y)
draw_text(buckets, template.description[:60], x + PADDING + 10, row_y + ROW_H)
draw_text(buckets, "Up/Down to choose, Enter to confirm", x + PADDING, y + panel_h - PADDING - ROW_H)
def draw_controls_menu(buckets, menu: ControlsMenu, viewport_w: float, viewport_h: float) -> None:
panel_w = 480.0
panel_h = PADDING * 2 + ROW_H * (len(menu.actions) + 2)
x = (viewport_w - panel_w) / 2
y = max(20.0, (viewport_h - panel_h) / 2)
draw_panel(buckets, "background", x, y, panel_w, panel_h)
draw_text(buckets, "Configure Controls", x + PADDING, y + PADDING)
for i, action in enumerate(menu.actions):
row_y = y + PADDING + ROW_H * (i + 1.5)
keys = ", ".join(menu.keybindings.keys_for_action(action)) or "-"
capturing = menu.awaiting_key and i == menu.selected_index
label = f"{action}: {'press a key...' if capturing else keys}"
if i == menu.selected_index:
draw_panel(buckets, "highlight", x + PADDING - 6, row_y - 2, text_width(label) + 12, ROW_H)
draw_text(buckets, label, x + PADDING, row_y)
footer_y = y + panel_h - PADDING - ROW_H
draw_text(buckets, "Enter to rebind, Esc to go back", x + PADDING, footer_y)
CHARACTER_CARD_W = 260.0
CHARACTER_CARD_H = 200.0
ABILITY_BAR_SLOT_W = 30.0
ABILITY_BAR_SLOT_H = 30.0
ABILITY_BAR_GAP = 3.0
ABILITY_BAR_KEY_LABELS = ("1", "2", "3", "4", "5", "6", "7", "8", "9", "0")
ABILITY_BAR_W = len(ABILITY_BAR_KEY_LABELS) * ABILITY_BAR_SLOT_W + (len(ABILITY_BAR_KEY_LABELS) - 1) * ABILITY_BAR_GAP
def draw_ability_bar(buckets, bar: AbilityBar, viewport_h: float) -> None:
"""Bottom-left action hotbar (numkeys 1-9/0 select a slot, "e"/gp_b activates whichever's
selected - see main.py). engine/ui.py::AbilityBar had never been wired to any rendering,
populated from the player's actual loadout, or hooked up to input consumption before, which
is why it "didn't show" at all.
"""
y = viewport_h - ABILITY_BAR_SLOT_H - PADDING
for i, key_label in enumerate(ABILITY_BAR_KEY_LABELS):
x = PADDING + i * (ABILITY_BAR_SLOT_W + ABILITY_BAR_GAP)
panel_name = "highlight" if i == bar.selected_index else "border"
draw_panel(buckets, panel_name, x, y, ABILITY_BAR_SLOT_W, ABILITY_BAR_SLOT_H)
draw_text(buckets, key_label, x + 2, y + 2, size=10.0, char_advance=7.0)
slot = bar.slots[i]
if slot is not None:
draw_text(buckets, slot.ability_id[:5], x + 1, y + 16, size=8.0, char_advance=6.0)
def _medical_lines(character, registry) -> list[str]:
"""One line per body part (integrity, or REMOVED) plus an indented sub-line for each of its
ailments and organs - the data's all on Character.resolved_body_parts() (engine/character.py)
already, this just flattens it for the medical tab (see draw_character_card).
"""
lines = [f"Blood: {character.blood_percentage:.0f}%"]
for slot, part in sorted(character.resolved_body_parts().items()):
if part is None:
continue
if part.removed:
lines.append(f"{slot}: REMOVED")
continue
lines.append(f"{slot}: {part.integrity:.0f}%")
for ailment in part.ailments:
lines.append(f" {ailment.name} (sev {ailment.severity:.1f})")
for organ in part.organs:
organ_def = registry.organ_defs.get(organ.organ_def_id)
organ_name = organ_def.name if organ_def is not None else organ.organ_def_id
status = "removed" if organ.removed else f"{organ.integrity:.0f}%"
lines.append(f" {organ_name}: {status}")
return lines
def draw_character_card(buckets, character, menu: CharacterMenu, registry, viewport_h: float) -> None:
"""Bio/equipment/medical card, next to the ability bar along the bottom edge (see
draw_ability_bar) - engine/ui.py::CharacterMenu had never been wired to any rendering
before, which is why it "didn't show" - see main.py's toggle_character_card.
"""
x = PADDING + ABILITY_BAR_W + PADDING
y = viewport_h - CHARACTER_CARD_H - PADDING
draw_panel(buckets, "background", x, y, CHARACTER_CARD_W, CHARACTER_CARD_H)
name = character.name or "(unnamed)"
draw_text(buckets, name, x + 10, y + 10)
tab_y = y + 10 + ROW_H
for i, tab in enumerate(CharacterMenu.TABS):
label = f"[{tab}]" if tab == menu.active_tab else f" {tab} "
draw_text(buckets, label, x + 10 + i * 70, tab_y)
body_y = tab_y + ROW_H * 1.5
if menu.active_tab == "bio":
draw_text(buckets, f"Species: {character.species_id or '-'}", x + 10, body_y)
draw_text(buckets, f"Blood: {character.blood_percentage:.0f}%", x + 10, body_y + ROW_H)
for i, (skill_id, value) in enumerate(sorted(character.bio.items())):
draw_text(buckets, f"{skill_id}: {value}", x + 10, body_y + ROW_H * (2 + i))
elif menu.active_tab == "equipment":
for i, piece in enumerate(character.clothing or character.default_clothing):
draw_text(buckets, f"{piece.slot}: {piece.item_def_id}", x + 10, body_y + ROW_H * i)
else:
for i, line in enumerate(_medical_lines(character, registry)):
draw_text(buckets, line, x + 10, body_y + ROW_H * i)