Mi2dRPGamEng/main.py

685 lines
31 KiB
Python

from __future__ import annotations
import random
import time
from collections import defaultdict
from pathlib import Path
import glfw
from engine.ai import AIController
from engine.aim import snap_to_8way
from engine.camera import OrthoCamera
from engine.character import Character
from engine.character_creator import NPC_SELECTABLE_SPECIES, CharacterCreator
from engine.config import KeyBindings
from engine.controls_menu import ControlsMenu
from engine.db import WorldRepository
from engine.defs import DefRegistry
from engine.dev_config import DevConfig
from engine.dev_tools import DevSpawnMenu, DevTurretMenu
from engine.entity import EntityPosition
from engine.gamepad import GamepadBindings, GamepadButtonEdgeTracker, GamepadState
from engine.game_modes import CharacterCreationFlow, MainMenu, Mode, PauseMenu
from engine.input import InputState
from engine.inventory import Inventory
from engine.item import ItemInstance
from engine.map_loader import MapLoader
from engine.placeholder_textures import generate_depth_shadow_texture, generate_placeholder_textures
from engine.render.device import configure_context, request_device
from engine.render.menu_draw import (
backpack_grid_origin,
backpack_popup_rect,
character_card_rect,
draw_ability_bar,
draw_backpack_popup,
draw_character_card,
draw_character_creation,
draw_controls_menu,
draw_drag_ghost,
draw_main_menu,
draw_pause_menu,
draw_world_creation,
equipment_row_at,
inventory_cell_at,
point_in_rect,
)
from engine.render.pipeline import QuadPipeline
from engine.render.renderer import Renderer
from engine.render.texture import TextureCache
from engine.render.ui_draw import draw_text
from engine.render.window import Window
from engine.timing import GameClock
from engine.ui import AbilityBar, CharacterMenu, populate_ability_bar
from engine.ui_assets import generate_ui_textures
from engine.world import World
from engine.world_creation import WorldCreationMenu, build_world_from_template, find_spawn_position
from resources.logic.actions import activate_ability_bar_slot, activate_hand
from resources.logic.ai import run_ai_tick
from resources.logic.contagion import apply_flu_contagion_tick
from resources.logic.falling import apply_fall_damage
from resources.logic.health_ticks import apply_bleeding_tick, apply_boost_expiry_tick
from resources.logic.organs import apply_organ_interactions_tick
from resources.logic.pickup import drop_item, try_pickup
from resources.logic.steering import advance_all_ship_rotations, at_helm, try_steer_ship
from resources.logic.swimming import in_water, try_swim_vertical
from resources.logic.turrets import spawn_turret
ROOT_DIR = Path(__file__).resolve().parent
DEFS_DIR = ROOT_DIR / "resources" / "defs"
TEXTURES_DIR = ROOT_DIR / "resources" / "textures"
DB_PATH = ROOT_DIR / "data" / "world.sqlite3"
KEYBINDINGS_PATH = ROOT_DIR / "config" / "keybindings.conf"
CONTROLLER_BINDINGS_PATH = ROOT_DIR / "config" / "controller_bindings.conf"
DEV_CONFIG_PATH = ROOT_DIR / "config" / "dev.conf"
BASE_MOVE_SPEED = 4.0 # tiles/second at full (two healthy legs') speed factor
DEFAULT_TILES_VISIBLE_Y = 12.0
HELM_TILES_VISIBLE_Y = 20.0 # wider view while steering a ship, so more of it is in frame
# Raw gamepad button -> the same synthetic key strings the menu handlers already accept from
# keyboard input (see handle_menu_key) - lets a controller drive every menu without a second
# set of menu-navigation logic. Only consulted outside Mode.PLAYING; during play, the dpad/A/B
# stay bound to gameplay actions via GamepadBindings/controller_bindings.conf as before.
GAMEPAD_MENU_KEY_MAP: dict[str, str] = {
"gp_dpad_up": "ArrowUp",
"gp_dpad_down": "ArrowDown",
"gp_dpad_left": "ArrowLeft",
"gp_dpad_right": "ArrowRight",
"gp_a": "Enter",
"gp_b": "Escape",
}
def equip_starting_loadout(character: Character, registry: DefRegistry) -> None:
"""A fresh character's starter gear - the same loadout spawn_player used to hand out
directly, now applied to whichever Character actually becomes the player (built via
engine/character_creator.py::CharacterCreator at "new game" time rather than a fixed
template), so newly created characters aren't dropped into the world with empty hands.
"""
character.wield_item("right_hand", "staff_oak", registry)
entity_def = registry.get_entity_def("player")
if entity_def.inventory_size is not None and character.inventory is None:
character.inventory = Inventory(*entity_def.inventory_size)
character.inventory.place(ItemInstance("staff-1", "staff_oak"), registry.get_item_def("staff_oak"), 0, 0)
character.inventory.place(ItemInstance("sandwich-1", "sandwich"), registry.get_item_def("sandwich"), 1, 0)
def spawn_new_character_into_world(world: World, registry: DefRegistry, player: Character, rng: random.Random) -> bool:
"""Drops a freshly-created character into `world` at a random walkable, dry, unroofed
spot (see engine/world_creation.py::find_spawn_position) and equips their starter gear.
Returns whether it succeeded - False (world untouched) only if the map has nowhere to
stand at all, which shouldn't happen for any of AVAILABLE_WORLD_TEMPLATES.
"""
root_map = next((m for m in world.maps.values() if m.parent_embedding is None), None)
if root_map is None:
return False
spawn = find_spawn_position(root_map, registry, rng)
if spawn is None:
return False
player.position = spawn
equip_starting_loadout(player, registry)
world.add_entity(player)
world.player = player
return True
class AppState:
"""Everything main()'s frame loop mutates from one frame to the next: which screen is
active (see engine/game_modes.py::Mode) and the live instance of whichever pure-state menu
class that screen is currently driving. Not itself reusable outside this module - it's just
the integration point tying together the several previously-unwired menu/flow classes.
"""
def __init__(self, registry: DefRegistry, world: World | None):
self.mode = Mode.MAIN_MENU
self.world = world
self.main_menu = MainMenu(has_existing_save=world is not None)
self.char_flow: CharacterCreationFlow | None = None
self.pending_character: Character | None = None # built, awaiting a world to spawn into
self.npc_spawn_flow: CharacterCreationFlow | None = None # dev-mode NPC spawner (see Mode.NPC_SPAWN)
self.world_creation_menu: WorldCreationMenu | None = None
self.controls_menu: ControlsMenu | None = None
self.pause_menu: PauseMenu | None = None
self.return_mode: Mode = Mode.MAIN_MENU # where Escape from the controls menu goes back to
self.character_card = CharacterMenu()
self.character_card_visible = False
self.dragging_item: tuple[str, str] | None = None # (item_id, def_id) picked up off the inventory grid
self.ability_bar = AbilityBar()
self.dev_spawn_menu = DevSpawnMenu(registry)
self.dev_turret_menu = DevTurretMenu(registry)
def main() -> None:
registry = DefRegistry.load(DEFS_DIR)
generate_placeholder_textures(registry, TEXTURES_DIR)
generate_depth_shadow_texture(TEXTURES_DIR)
generate_ui_textures(TEXTURES_DIR)
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
repo = WorldRepository.open(DB_PATH)
map_loader = MapLoader(DEFS_DIR / "maps")
empty_world = World(registry=registry) # menu backdrop before any world has ever been made
keybindings = KeyBindings.load(KEYBINDINGS_PATH)
gamepad_bindings = GamepadBindings.load(CONTROLLER_BINDINGS_PATH)
dev_config = DevConfig.load(DEV_CONFIG_PATH)
input_state = InputState(keybindings)
menu_gamepad = GamepadButtonEdgeTracker()
clock = GameClock(tick_interval=1.0)
rng = random.Random()
window = Window()
adapter, device = request_device(window.canvas)
context, texture_format = configure_context(window.canvas, adapter, device)
pipeline = QuadPipeline(device, texture_format)
texture_cache = TextureCache(device, TEXTURES_DIR)
renderer = Renderer(device, pipeline, texture_cache, registry)
camera = OrthoCamera(*window.canvas.get_physical_size(), tiles_visible_y=DEFAULT_TILES_VISIBLE_Y)
last_frame_time = time.perf_counter()
saved_world = repo.load_world(registry, map_loader)
if saved_world is not None:
print(f"Found a save: player at {saved_world.player.position}.")
state = AppState(registry, saved_world)
def enter_playing() -> None:
input_state.clear_held_state()
state.mode = Mode.PLAYING
def leave_playing() -> None:
input_state.clear_held_state()
def trigger_pause() -> None:
leave_playing()
state.pause_menu = PauseMenu()
state.mode = Mode.PAUSED
def resolve_action(key: str, modifiers) -> str | None:
if key and "Shift" in modifiers:
action = keybindings.action_for_key(f"shift+{key}")
if action is not None:
return action
return keybindings.action_for_key(key)
def handle_main_menu_key(key: str) -> None:
menu = state.main_menu
if key == "ArrowUp":
menu.move(-1)
elif key == "ArrowDown":
menu.move(1)
elif key == "Enter":
choice = menu.selected()
if choice == "continue":
enter_playing()
elif choice == "new_game":
state.char_flow = CharacterCreationFlow(CharacterCreator(registry))
state.mode = Mode.CHARACTER_CREATE
elif choice == "controls":
state.return_mode = Mode.MAIN_MENU
state.controls_menu = ControlsMenu(keybindings)
state.mode = Mode.CONTROLS
def handle_character_create_key(key: str) -> None:
flow = state.char_flow
assert flow is not None
if flow.step == "species":
if key == "ArrowLeft":
flow.cycle_species(-1)
elif key == "ArrowRight":
flow.cycle_species(1)
elif key == "Enter":
flow.advance()
elif key == "Escape":
state.char_flow = None
state.mode = Mode.MAIN_MENU
return
if key == "Enter":
if flow.advance():
character = flow.creator.build_character()
assert character is not None
state.pending_character = character
state.char_flow = None
state.world_creation_menu = WorldCreationMenu()
state.world_creation_menu.select(0)
state.mode = Mode.WORLD_CREATE
elif key == "Escape":
flow.go_back()
elif key == "Backspace":
flow.backspace()
elif len(key) == 1:
flow.type_char(key)
def handle_npc_spawn_key(key: str) -> None:
"""Dev-mode-only NPC spawner: the exact same species/name CharacterCreationFlow as "new
game" (see handle_character_create_key above), just pooled over every species instead of
only the player-selectable ones (see NPC_SELECTABLE_SPECIES), and confirming spawns the
built Character into the current world instead of handing off to world creation.
"""
flow = state.npc_spawn_flow
assert flow is not None
if flow.step == "species":
if key == "ArrowLeft":
flow.cycle_species(-1)
elif key == "ArrowRight":
flow.cycle_species(1)
elif key == "Enter":
flow.advance()
elif key == "Escape":
state.npc_spawn_flow = None
enter_playing()
return
if key == "Enter":
if flow.advance():
npc = flow.creator.build_character()
assert npc is not None
player = state.world.player if state.world is not None else None
if isinstance(player, Character) and player.position is not None and state.world is not None:
dx, dy, dz = current_aim_direction(player)
npc.position = EntityPosition(
player.position.map_id, player.position.x + dx, player.position.y + dy, player.position.z + dz
)
npc.ai = AIController(behavior_id="wander")
state.world.add_entity(npc)
state.npc_spawn_flow = None
enter_playing()
elif key == "Escape":
flow.go_back()
elif key == "Backspace":
flow.backspace()
elif len(key) == 1:
flow.type_char(key)
def handle_world_create_key(key: str) -> None:
menu = state.world_creation_menu
assert menu is not None
if key == "ArrowUp":
menu.move(-1)
elif key == "ArrowDown":
menu.move(1)
elif key == "Escape":
state.world_creation_menu = None
state.pending_character = None
state.mode = Mode.MAIN_MENU
elif key == "Enter":
template = menu.selected_template()
character = state.pending_character
if template is not None and character is not None:
world = build_world_from_template(template, registry, map_loader)
if spawn_new_character_into_world(world, registry, character, rng):
state.world = world
state.main_menu = MainMenu(has_existing_save=True)
state.pending_character = None
state.world_creation_menu = None
enter_playing()
def handle_controls_key(key: str) -> None:
menu = state.controls_menu
assert menu is not None
if menu.awaiting_key:
if key == "Escape":
menu.cancel_rebind()
else:
menu.capture_key(key)
return
if key == "ArrowUp":
menu.move(-1)
elif key == "ArrowDown":
menu.move(1)
elif key == "Enter":
menu.begin_rebind()
elif key == "Escape":
menu.save(KEYBINDINGS_PATH)
state.controls_menu = None
state.mode = state.return_mode
def handle_pause_key(key: str) -> None:
menu = state.pause_menu
assert menu is not None
if key == "ArrowUp":
menu.move(-1)
elif key == "ArrowDown":
menu.move(1)
elif key == "Escape":
state.pause_menu = None
enter_playing()
elif key == "Enter":
choice = menu.selected()
if choice == "resume":
state.pause_menu = None
enter_playing()
elif choice == "controls":
state.return_mode = Mode.PAUSED
state.controls_menu = ControlsMenu(keybindings)
state.mode = Mode.CONTROLS
elif choice == "save_and_quit_to_menu":
if state.world is not None:
repo.save_world(state.world)
print("Saved world.")
state.world = None
state.pause_menu = None
state.main_menu = MainMenu(has_existing_save=True)
state.mode = Mode.MAIN_MENU
def handle_menu_key(key: str) -> None:
if state.mode == Mode.MAIN_MENU:
handle_main_menu_key(key)
elif state.mode == Mode.CHARACTER_CREATE:
handle_character_create_key(key)
elif state.mode == Mode.WORLD_CREATE:
handle_world_create_key(key)
elif state.mode == Mode.CONTROLS:
handle_controls_key(key)
elif state.mode == Mode.PAUSED:
handle_pause_key(key)
elif state.mode == Mode.NPC_SPAWN:
handle_npc_spawn_key(key)
def handle_playing_key_down(event: dict) -> None:
key = event.get("key", "")
action = resolve_action(key, event.get("modifiers", ()))
if action == "pause":
trigger_pause()
return
if action == "toggle_character_card":
state.character_card_visible = not state.character_card_visible
if not state.character_card_visible:
state.dragging_item = None
state.character_card.close_backpack_popup()
return
if action == "cycle_character_tab":
if state.character_card_visible:
state.character_card.cycle_tab()
return
if action == "toggle_dev_mode":
dev_config.toggle()
dev_config.save(DEV_CONFIG_PATH)
return
if dev_config.enabled and action == "dev_cycle_spawn":
state.dev_spawn_menu.cycle(1)
return
if dev_config.enabled and action == "dev_spawn_object":
player = state.world.player if state.world is not None else None
if isinstance(player, Character):
state.dev_spawn_menu.spawn_into(player)
return
if dev_config.enabled and action == "dev_spawn_npc_menu":
leave_playing()
state.npc_spawn_flow = CharacterCreationFlow(CharacterCreator(registry, selectable_species=NPC_SELECTABLE_SPECIES))
state.mode = Mode.NPC_SPAWN
return
if dev_config.enabled and action == "dev_cycle_turret":
state.dev_turret_menu.cycle(1)
return
if dev_config.enabled and action == "dev_place_turret":
player = state.world.player if state.world is not None else None
entity_def_id = state.dev_turret_menu.selected_entity_def_id()
if isinstance(player, Character) and player.position is not None and entity_def_id is not None and state.world is not None:
dx, dy, dz = current_aim_direction(player)
position = EntityPosition(
player.position.map_id, player.position.x + dx, player.position.y + dy, player.position.z + dz
)
# hostile to the placing player by default - a dev/debug placement is meant to
# be immediately testable (see it track and fire), not require setting up
# faction tags first; see resources/logic/turrets.py::is_hostile.
turret = spawn_turret(registry, entity_def_id, position, hostile_entity_ids=(player.entity_id,))
state.world.add_entity(turret)
return
input_state.handle_event(event)
def handle_character_card_pointer_event(event_type: str, event: dict) -> bool:
"""Hit-tests a pointer_down/pointer_up against the open character card and (if one is
open) its backpack popup, driving two gestures: clicking an equipment-tab row toggles
that container's popup open/closed (see CharacterMenu.toggle_backpack), and dragging a
cell out of an open popup and releasing outside both the card and the popup drops it on
the ground (see resources/logic/pickup.py::drop_item). Returns whether the event was
consumed by the card/popup (so on_event knows not to also feed it to gameplay's
InputState - e.g. a click landing on the card shouldn't also register as a hand
activation in the world behind it).
"""
player = state.world.player if state.world is not None else None
if not isinstance(player, Character):
return False
menu = state.character_card
_viewport_w, viewport_h = window.canvas.get_physical_size()
px, py = event.get("x", 0.0), event.get("y", 0.0)
open_inventory = menu.resolve_open_inventory(player, registry)
if event_type == "pointer_down":
if open_inventory is not None:
popup_rect = backpack_popup_rect(open_inventory, viewport_h)
if point_in_rect(px, py, popup_rect):
cell = inventory_cell_at(open_inventory, backpack_grid_origin(popup_rect), px, py)
if cell is not None:
placed = open_inventory.item_at(*cell)
if placed is not None:
state.dragging_item = (placed.item.item_id, placed.item.def_id)
return True
if not point_in_rect(px, py, character_card_rect(viewport_h)):
return False
if menu.active_tab == "equipment":
slot = equipment_row_at(player, viewport_h, px, py)
if slot is not None:
menu.toggle_backpack(slot, player, registry)
return True
if event_type == "pointer_up":
if state.dragging_item is None:
return False
item_id, def_id = state.dragging_item
state.dragging_item = None
on_card = point_in_rect(px, py, character_card_rect(viewport_h))
on_popup = open_inventory is not None and point_in_rect(
px, py, backpack_popup_rect(open_inventory, viewport_h)
)
if not on_card and not on_popup:
drop_item(player, state.world, item_id, def_id, open_inventory, registry)
return True
return False
def on_event(event: dict) -> None:
event_type = event.get("event_type")
if event_type == "close":
if state.world is not None:
repo.save_world(state.world)
print("Saved world on close.")
return
if state.mode != Mode.PLAYING:
if event_type == "key_down":
handle_menu_key(event.get("key", ""))
return
if state.character_card_visible and event_type in ("pointer_down", "pointer_up"):
if handle_character_card_pointer_event(event_type, event):
return
if event_type == "key_down":
handle_playing_key_down(event)
else:
input_state.handle_event(event)
window.canvas.add_event_handler(
on_event, "key_down", "key_up", "pointer_down", "pointer_up", "pointer_move", "close"
)
def current_aim_direction(player: Character) -> tuple[int, int, int]:
assert player.position is not None
if input_state.pointer_pos is None:
return input_state.facing
wx, wy = camera.screen_to_world(*input_state.pointer_pos)
dx, dy = snap_to_8way(wx - (player.position.x + 0.5), wy - (player.position.y + 0.5))
return dx, dy, 0
def read_gamepad_state() -> GamepadState | None:
# A gamepad is polled (not event-driven like key_down/key_up), so this reads whatever's
# plugged into joystick slot 1 fresh each frame - see InputState.apply_gamepad_state for
# gameplay's own edge detection, and GAMEPAD_MENU_KEY_MAP/menu_gamepad above for menu
# navigation's separate one (both need to run off the same one snapshot per frame).
if glfw.joystick_present(glfw.JOYSTICK_1) and glfw.joystick_is_gamepad(glfw.JOYSTICK_1):
raw = glfw.get_gamepad_state(glfw.JOYSTICK_1)
if raw is not None:
return GamepadState.from_glfw(raw)
return None
def draw_frame() -> None:
nonlocal last_frame_time
wall_now = time.perf_counter()
dt = wall_now - last_frame_time
last_frame_time = wall_now
viewport_w, viewport_h = window.canvas.get_physical_size()
camera.resize(viewport_w, viewport_h)
ui_buckets: dict[str, list[float]] = defaultdict(list)
gamepad_state = read_gamepad_state()
# Always fed, regardless of mode, so the edge set reflects real button transitions - if
# this only ran while a menu was open, a button already held when a menu opens would
# look "just pressed" the moment play resumes and the tracker starts watching again.
gamepad_menu_edges = menu_gamepad.newly_pressed(gamepad_state) if gamepad_state is not None else set()
# Mode transitions from gamepad input are applied before branching on state.mode below,
# same as keyboard's on_event (which always runs between frames) - otherwise a gp_start
# pause would still fall through and run/render one more frame of gameplay first.
if state.mode == Mode.PLAYING:
if "gp_start" in gamepad_menu_edges:
trigger_pause()
elif "gp_back" in gamepad_menu_edges:
state.character_card_visible = not state.character_card_visible
if not state.character_card_visible:
state.dragging_item = None
state.character_card.close_backpack_popup()
else:
for button in gamepad_menu_edges:
key = GAMEPAD_MENU_KEY_MAP.get(button)
if key is not None:
handle_menu_key(key)
if state.mode == Mode.PLAYING and state.world is not None:
if gamepad_state is not None:
input_state.apply_gamepad_state(gamepad_state, gamepad_bindings)
world = state.world
ticks = clock.advance(dt)
world.expire_time_warp_zones(clock.total_time)
advance_all_ship_rotations(world, clock.total_time)
if ticks:
for entity in world.entities.values():
if isinstance(entity, Character):
apply_bleeding_tick(entity, registry, ticks)
apply_organ_interactions_tick(entity, registry, ticks)
apply_boost_expiry_tick(entity, clock.total_time)
run_ai_tick(entity, world, registry, clock.total_time, rng)
apply_flu_contagion_tick(world, ticks, rng)
player = world.player
if isinstance(player, Character) and player.position is not None:
player.is_blocking = input_state.is_blocking
helming = at_helm(player, world) is not None
camera.set_zoom_target(HELM_TILES_VISIBLE_Y if helming else DEFAULT_TILES_VISIBLE_Y)
move_dx, move_dy, _ = input_state.current_move_vector()
if move_dx or move_dy:
if helming:
# steering a ship stays a discrete, one-press-per-step affair - the
# ship's own grid structure doesn't need continuous sub-tile positioning
move = input_state.consume_move()
if move is not None:
try_steer_ship(player, world, *move)
else:
speed = player.effective_speed(BASE_MOVE_SPEED, registry)
slow = world.speed_multiplier_at(
player.entity_id, player.position.map_id, player.position.x, player.position.y,
player.position.z, clock.total_time,
)
if speed > 0:
world.try_move_continuous(player, move_dx, move_dy, 0, distance=speed * slow * dt)
swimming = in_water(player, world)
leap = input_state.consume_leap()
if leap is not None:
if swimming:
try_swim_vertical(player, world, 1) # space surfaces while submerged
else:
world.try_leap(player, *leap)
if input_state.consume_swim_down():
try_swim_vertical(player, world, -1)
fallen = world.consume_fall_distance(player.entity_id)
if fallen > 0:
apply_fall_damage(player, registry, fallen, rng)
if input_state.consume_pickup():
try_pickup(player, world, registry)
hand = input_state.consume_hand_activation()
if hand is not None:
activate_hand(
player, world, registry, hand, current_aim_direction(player), rng=rng, now=clock.total_time
)
populate_ability_bar(state.ability_bar, player, registry)
ability_index = input_state.consume_ability_select()
if ability_index is not None:
state.ability_bar.select(ability_index)
if input_state.consume_activate_ability_bar():
selected = state.ability_bar.selected_slot()
if selected is not None:
activate_ability_bar_slot(
player, world, registry, selected, current_aim_direction(player),
rng=rng, now=clock.total_time,
)
camera.set_target(player.position.x + 0.5, player.position.y + 0.5)
camera.update_zoom(dt)
if isinstance(player, Character):
draw_ability_bar(ui_buckets, state.ability_bar, viewport_h)
if state.character_card_visible:
draw_character_card(ui_buckets, player, state.character_card, registry, viewport_h)
dragging_id = state.dragging_item[0] if state.dragging_item is not None else None
draw_backpack_popup(ui_buckets, player, state.character_card, registry, viewport_h, dragging_id)
if state.dragging_item is not None and input_state.pointer_pos is not None:
draw_drag_ghost(ui_buckets, state.dragging_item[1], *input_state.pointer_pos)
if dev_config.enabled:
draw_text(ui_buckets, "DEV MODE", viewport_w - 130, 10)
draw_text(ui_buckets, f"spawn: {state.dev_spawn_menu.selected_item_id()}", viewport_w - 260, 30)
draw_text(ui_buckets, f"turret: {state.dev_turret_menu.selected_entity_def_id()}", viewport_w - 260, 50)
renderer.render_frame(context, world, camera, ui_buckets)
return
backdrop = state.world if state.world is not None else empty_world
if state.mode == Mode.MAIN_MENU:
draw_main_menu(ui_buckets, state.main_menu, viewport_w, viewport_h)
elif state.mode == Mode.CHARACTER_CREATE:
assert state.char_flow is not None
draw_character_creation(ui_buckets, state.char_flow, viewport_w, viewport_h)
elif state.mode == Mode.WORLD_CREATE:
assert state.world_creation_menu is not None
draw_world_creation(ui_buckets, state.world_creation_menu, viewport_w, viewport_h)
elif state.mode == Mode.CONTROLS:
assert state.controls_menu is not None
draw_controls_menu(ui_buckets, state.controls_menu, viewport_w, viewport_h)
elif state.mode == Mode.PAUSED:
assert state.pause_menu is not None
draw_pause_menu(ui_buckets, state.pause_menu, viewport_w, viewport_h)
elif state.mode == Mode.NPC_SPAWN:
assert state.npc_spawn_flow is not None
draw_character_creation(ui_buckets, state.npc_spawn_flow, viewport_w, viewport_h, title="Spawn NPC")
renderer.render_frame(context, backdrop, camera, ui_buckets)
window.canvas.request_draw(draw_frame)
window.run()
if __name__ == "__main__":
main()