Mi2dRPGamEng/main.py

192 lines
7.7 KiB
Python

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()