from __future__ import annotations import random import time from pathlib import Path import glfw from engine.aim import snap_to_8way from engine.camera import OrthoCamera from engine.character import Character from engine.config import KeyBindings from engine.db import WorldRepository from engine.defs import DefRegistry from engine.entity import EntityPosition from engine.gamepad import GamepadBindings, GamepadState 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_placeholder_textures from engine.render.device import configure_context, request_device from engine.render.pipeline import QuadPipeline from engine.render.renderer import Renderer from engine.render.texture import TextureCache from engine.render.window import Window from engine.timing import GameClock from engine.world import World from resources.logic.actions import activate_hand from resources.logic.health_ticks import apply_bleeding_tick from resources.logic.organs import apply_organ_interactions_tick from resources.logic.steering import advance_all_ship_rotations, at_helm, try_steer_ship 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" BASE_MOVE_SPEED = 4.0 # tiles/second at full (two healthy legs') speed factor def spawn_player(registry: DefRegistry) -> Character: entity_def = registry.get_entity_def("player") assert entity_def.species_id is not None player = Character( entity_id="player-1", name=entity_def.name, def_id=entity_def.id, position=EntityPosition("demo_world", 4, 4, 0), species_id=entity_def.species_id, gender="nonbinary", default_body_parts=registry.new_default_body_parts(entity_def.species_id), default_clothing=registry.new_default_clothing(entity_def.id), ) player.wield_item("right_hand", "staff_oak", registry) if entity_def.inventory_size is not None: player.inventory = Inventory(*entity_def.inventory_size) player.inventory.place(ItemInstance("staff-1", "staff_oak"), registry.get_item_def("staff_oak"), 0, 0) player.inventory.place(ItemInstance("sandwich-1", "sandwich"), registry.get_item_def("sandwich"), 1, 0) return player def build_fresh_world(registry: DefRegistry, map_loader: MapLoader) -> World: world = World(registry=registry) demo_world = map_loader.load("demo_world.json") world.add_map(demo_world) for embedding in demo_world.embeddings: world.add_map(embedding.child_map) player = spawn_player(registry) world.add_entity(player) world.player = player return world def main() -> None: registry = DefRegistry.load(DEFS_DIR) generate_placeholder_textures(registry, TEXTURES_DIR) DB_PATH.parent.mkdir(parents=True, exist_ok=True) repo = WorldRepository.open(DB_PATH) map_loader = MapLoader(DEFS_DIR / "maps") world = repo.load_world(registry, map_loader) if world is None: world = build_fresh_world(registry, map_loader) print("No save found; started a fresh world.") else: print(f"Loaded save: player at {world.player.position}.") 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()[0], window.canvas.get_physical_size()[1]) keybindings = KeyBindings.load(KEYBINDINGS_PATH) gamepad_bindings = GamepadBindings.load(CONTROLLER_BINDINGS_PATH) input_state = InputState(keybindings) clock = GameClock(tick_interval=1.0) last_frame_time = time.perf_counter() rng = random.Random() def on_event(event: dict) -> None: if event.get("event_type") == "close": repo.save_world(world) print("Saved world on close.") else: input_state.handle_event(event) window.canvas.add_event_handler(on_event, "key_down", "key_up", "pointer_down", "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 poll_gamepad() -> None: # A gamepad is polled (not event-driven like key_down/key_up), so this just feeds # whatever's plugged into joystick slot 1 into the same action dispatch as keyboard/ # mouse each frame - see InputState.apply_gamepad_state for the edge detection. 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: input_state.apply_gamepad_state(GamepadState.from_glfw(raw), gamepad_bindings) def draw_frame() -> None: nonlocal last_frame_time wall_now = time.perf_counter() dt = wall_now - last_frame_time last_frame_time = wall_now poll_gamepad() 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) player = world.player if isinstance(player, Character) and player.position is not None: player.is_blocking = input_state.is_blocking move_dx, move_dy, _ = input_state.current_move_vector() if move_dx or move_dy: if at_helm(player, world) is not None: # 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) leap = input_state.consume_leap() if leap is not None: world.try_leap(player, *leap) 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) if isinstance(player, Character) and player.position is not None: camera.set_target(player.position.x + 0.5, player.position.y + 0.5) renderer.render_frame(context, world, camera) window.canvas.request_draw(draw_frame) window.run() if __name__ == "__main__": main()