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 DevFactionMenu, 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_faction_editor, 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.faction_edit_menu = DevFactionMenu() # dev-mode faction tag editor (see Mode.FACTION_EDIT) self.faction_edit_target: Character | None = None # whose factions faction_edit_menu is currently showing 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_faction_edit_key(key: str) -> None: """Dev-mode-only faction tag editor - see DevFactionMenu (engine/dev_tools.py) for the add/remove/select logic, main.py's dev_edit_player_factions/dev_edit_target_factions for how this mode gets entered (and which Character it's pointed at). """ menu = state.faction_edit_menu target = state.faction_edit_target assert target is not None if key == "ArrowUp": menu.cycle_selected(target, -1) elif key == "ArrowDown": menu.cycle_selected(target, 1) elif key == "Enter": menu.add_tag(target) elif key == "Delete": menu.remove_selected(target) elif key == "Backspace": menu.backspace() elif key == "Escape": state.faction_edit_target = None enter_playing() elif len(key) == 1: menu.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) elif state.mode == Mode.FACTION_EDIT: handle_faction_edit_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 if dev_config.enabled and action == "dev_edit_player_factions": player = state.world.player if state.world is not None else None if isinstance(player, Character): leave_playing() state.faction_edit_target = player state.faction_edit_menu = DevFactionMenu() state.mode = Mode.FACTION_EDIT return if dev_config.enabled and action == "dev_edit_target_factions": 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) ahead = (player.position.x + dx, player.position.y + dy, player.position.z + dz) target = state.world.entity_at(player.position.map_id, *ahead) if isinstance(target, Character): leave_playing() state.faction_edit_target = target state.faction_edit_menu = DevFactionMenu() state.mode = Mode.FACTION_EDIT 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") elif state.mode == Mode.FACTION_EDIT: assert state.faction_edit_target is not None draw_faction_editor(ui_buckets, state.faction_edit_menu, state.faction_edit_target, viewport_w, viewport_h) renderer.render_frame(context, backdrop, camera, ui_buckets) window.canvas.request_draw(draw_frame) window.run() if __name__ == "__main__": main()