100 lines
4.6 KiB
Python
100 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from engine.character import Character
|
|
from engine.map import MapEmbedding, rotated_size
|
|
from engine.world import World
|
|
|
|
|
|
def at_helm(character: Character, world: World) -> MapEmbedding | None:
|
|
"""Returns the embedding a character can steer, if they're standing on a helm tile of an
|
|
embedded map (e.g. a ship's wheel) - None otherwise.
|
|
"""
|
|
if character.position is None:
|
|
return None
|
|
map_ = world.maps[character.position.map_id]
|
|
embedding = map_.parent_embedding
|
|
if embedding is None:
|
|
return None
|
|
tile = map_.get_tile(character.position.x, character.position.y, character.position.z)
|
|
room_def = world.registry.get_tile_layer_def("room", tile.layer_id("room"))
|
|
if room_def is None or not room_def.is_helm:
|
|
return None
|
|
return embedding
|
|
|
|
|
|
def _footprint_is_clear(world: World, embedding: MapEmbedding, anchor: tuple[int, int, int], rotation: int) -> bool:
|
|
parent = embedding.parent_map
|
|
width, height = rotated_size(embedding.child_map.width, embedding.child_map.height, rotation)
|
|
for ly in range(height):
|
|
for lx in range(width):
|
|
px, py, pz = anchor[0] + lx, anchor[1] + ly, anchor[2]
|
|
if not parent.in_bounds(px, py, pz):
|
|
return False
|
|
if not parent.get_tile(px, py, pz).is_walkable(world.registry):
|
|
return False
|
|
return True
|
|
|
|
|
|
def try_steer_ship(character: Character, world: World, dx: int, dy: int, dz: int = 0) -> bool:
|
|
"""Moves the embedded map the character is helming by one step, if the destination footprint
|
|
is clear in the parent map. Entities aboard need no updates - only the anchor changes.
|
|
"""
|
|
embedding = at_helm(character, world)
|
|
if embedding is None:
|
|
return False
|
|
new_anchor = (embedding.anchor[0] + dx, embedding.anchor[1] + dy, embedding.anchor[2] + dz)
|
|
if not _footprint_is_clear(world, embedding, new_anchor, embedding.rotation):
|
|
return False
|
|
embedding.move_to(new_anchor)
|
|
return True
|
|
|
|
|
|
def try_turn_ship(character: Character, world: World, quarter_turns: int) -> bool:
|
|
"""Sets the helmed ship's desired heading, quarter_turns further from its current desired
|
|
rotation (so two consecutive "turn right" commands queue up a full 180, same as if the
|
|
player held the control down). The ship doesn't snap to the new heading - it turns toward
|
|
it at its own max_rotation_speed, one quarter-turn at a time; see advance_ship_rotation,
|
|
called once per game tick, for the actual stepping. Returns whether the eventual target
|
|
orientation's footprint is clear right now - if the world changes before the ship gets
|
|
there, advance_ship_rotation simply pauses rather than forcing through a collision.
|
|
"""
|
|
embedding = at_helm(character, world)
|
|
if embedding is None:
|
|
return False
|
|
new_desired = (embedding.desired_rotation + quarter_turns) % 4
|
|
if not _footprint_is_clear(world, embedding, embedding.anchor, new_desired):
|
|
return False
|
|
embedding.desired_rotation = new_desired
|
|
return True
|
|
|
|
|
|
def advance_ship_rotation(embedding: MapEmbedding, world: World, now: float) -> bool:
|
|
"""Steps `embedding` one quarter-turn closer to its desired_rotation, if enough time has
|
|
passed since the last step (per its own max_rotation_speed) and the intermediate footprint
|
|
is clear. Turns the shorter way around (e.g. 0 -> 3 is one counter-clockwise step, not
|
|
three clockwise ones). No-op (returns False) if already at the desired heading, still on
|
|
cooldown, or the next step would collide - a blocked ship just waits, it doesn't give up on
|
|
the desired heading.
|
|
"""
|
|
if embedding.rotation == embedding.desired_rotation:
|
|
return False
|
|
if now < embedding.rotation_ready_at:
|
|
return False
|
|
delta = (embedding.desired_rotation - embedding.rotation) % 4
|
|
step = -1 if delta == 3 else 1 # 3 quarter-turns one way == 1 the other way, take the short one
|
|
next_rotation = (embedding.rotation + step) % 4
|
|
if not _footprint_is_clear(world, embedding, embedding.anchor, next_rotation):
|
|
return False
|
|
embedding.rotate_to(next_rotation)
|
|
embedding.rotation_ready_at = now + 90.0 / embedding.max_rotation_speed
|
|
return True
|
|
|
|
|
|
def advance_all_ship_rotations(world: World, now: float) -> None:
|
|
"""Advances every embedding in every map in the world one step, if it's due - call once per
|
|
game tick from the main loop (same spot as World.expire_time_warp_zones).
|
|
"""
|
|
for map_ in world.maps.values():
|
|
for embedding in map_.embeddings:
|
|
advance_ship_rotation(embedding, world, now)
|