191 lines
7.6 KiB
Python
191 lines
7.6 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 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)
|
|
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()
|