Handle door navigation and turnarounds

This commit is contained in:
Tanner
2026-11-07 21:25:18 +00:00
parent be5fe260e3
commit c65705fef8
6 changed files with 497 additions and 10 deletions
+257
View File
@@ -666,6 +666,8 @@ class LegacyGame:
# migrated actions use the 26.3 packet classes imported above.
class Game(LegacyGame):
ARM_SWING_INTERVAL = 0.1
DOOR_AUTO_OPEN_GRACE = 0.25
DOOR_TURN_DURATION = 0.3
def __init__(self, global_state):
self.g = global_state
@@ -673,6 +675,9 @@ class Game(LegacyGame):
self.break_deadline = None
self.breaking_state = None
self.next_swing_at = 0.0
self.door_navigation = None
self.g.yaw_target_override = None
self.g.unopenable_doors = self.g.get('unopenable_doors') or set()
from mosfet.protocol.packets import SetHealthPacket
# Health is a protocol-777 packet, not the legacy UpdateHealthPacket.
@@ -725,6 +730,258 @@ class Game(LegacyGame):
self.sequence += 1
return self.sequence
def _door_info(self, position):
state_id = self.g.world.block_at(*position)
if state_id not in blocks.DOOR_IDS:
return None
properties = blocks.PROPS.get(state_id, {})
lower = tuple(position)
if properties.get('half') == 'upper':
lower = utils.padd(lower, path.BLOCK_BELOW)
lower_state = self.g.world.block_at(*lower)
if lower_state in blocks.DOOR_IDS:
state_id = lower_state
properties = blocks.PROPS.get(state_id, {})
return lower, state_id, properties
def _toggle_door(self, lower, state_id):
position = utils.pint(self.g.pos)
dx, dz = position[0] - lower[0], position[2] - lower[2]
if abs(dx) > abs(dz):
if dx >= 0:
face, cursor = BlockFace.EAST, (0.95, 0.5, 0.5)
else:
face, cursor = BlockFace.WEST, (0.05, 0.5, 0.5)
elif abs(dz) > 0:
if dz >= 0:
face, cursor = BlockFace.SOUTH, (0.5, 0.5, 0.95)
else:
face, cursor = BlockFace.NORTH, (0.5, 0.5, 0.05)
else:
face, cursor = BlockFace.TOP, (0.5, 0.95, 0.5)
boxes = blocks.COLLISION_BOXES.get(state_id, ())
if boxes:
box = boxes[0]
cursor = ((box[0] + box[3]) / 2,
(box[1] + box[4]) / 2,
(box[2] + box[5]) / 2)
self.place_block(lower, face, cursor=cursor)
def _look_at_with_yaw_separation(self, target, fallback=None):
"""Keep target yaw far enough from the player to avoid the near-zero guard."""
player = self.g.pos
player_x = player.x if hasattr(player, 'x') else player[0]
player_z = player.z if hasattr(player, 'z') else player[2]
center = [target[0] + 0.5, target[1] + 0.5, target[2] + 0.5]
dx, dz = player_x - center[0], player_z - center[2]
distance = utils.hypot(dx, dz)
if distance < 0.8:
if distance < 1e-6 and fallback is not None:
dx, dz = fallback
distance = utils.hypot(dx, dz)
if distance > 1e-6:
scale = 0.8 / distance
center[0] = player_x - dx * scale
center[2] = player_z - dz * scale
return tuple(value - 0.5 for value in center)
def _door_yaw_target(self, look_at):
target = vector.Point3D(look_at)
target.x += 0.5
target.z += 0.5
position = self.g.pos
if not hasattr(position, 'x'):
position = vector.Point3D(position)
direction = position - target
direction.y = 0
if direction.length() <= 0.6:
return self.g.yaw
return direction.normalized().signedAngleDeg(
other=bot.YAW_ANGLE_DIR, ref=bot.YAW_ANGLE_REF)
def _begin_door_turn(self, navigation, target, fallback=None):
look_at = self._look_at_with_yaw_separation(target, fallback)
yaw = self.g.yaw
target_yaw = self._door_yaw_target(look_at)
delta = (target_yaw - yaw + 180) % 360 - 180
navigation['yaw_origin'] = yaw
navigation['yaw_door_target'] = yaw + delta
self.g.yaw_target_override = navigation['yaw_door_target']
self.g.look_at = look_at
def _begin_door_return(self, navigation, now):
navigation['phase'] = 'turning_back'
navigation['deadline'] = now + self.DOOR_TURN_DURATION
self.g.yaw_target_override = navigation.get('yaw_origin')
target = navigation.get('forward_look_at')
self.g.look_at = (self._look_at_with_yaw_separation(target)
if target is not None else
navigation['previous_look_at'])
def _restore_door_look(self, navigation):
self.g.look_at = navigation['previous_look_at']
self.g.yaw_target_override = None
self.door_navigation = None
def handle_path_doors(self):
"""Open closed wooden doors in the active route and close them after crossing."""
now = time.monotonic()
navigation = self.door_navigation
if navigation is not None:
lower = navigation['position']
info = self._door_info(lower)
is_open = bool(info and info[2].get('open') == 'true')
if navigation['phase'] == 'checking':
if (info is None or
info[1] not in blocks.OPENABLE_DOOR_IDS):
self._restore_door_look(navigation)
return False
if is_open:
navigation['phase'] = 'crossing'
navigation['opened_by_bot'] = False
navigation['saw_open'] = True
self.g.look_at = navigation['previous_look_at']
return False
if now >= navigation['deadline']:
navigation['phase'] = 'opening'
navigation['opened_by_bot'] = True
navigation['deadline'] = now + 2.0
print('[door] opening on path at', lower, flush=True)
self._toggle_door(lower, info[1])
return True
if navigation['phase'] == 'opening':
if not self.g.path:
self._restore_door_look(navigation)
return False
if is_open:
navigation['phase'] = 'crossing'
navigation['saw_open'] = True
self.g.look_at = navigation['previous_look_at']
return False
if now > navigation['deadline']:
upper = utils.padd(lower, path.BLOCK_ABOVE)
self.g.unopenable_doors.update((lower, upper))
goal = navigation['goal']
self._restore_door_look(navigation)
current = utils.pint(self.g.pos)
self.g.path = (self.g.world.path_to_place(current, goal)
or [])
print('Door did not open; routing around', lower,
flush=True)
return False
return True
if navigation['phase'] == 'crossing':
if navigation['after'] is not None and \
utils.pint(self.g.pos) == navigation['after']:
if navigation.get('saw_open', False):
navigation['phase'] = 'turning_to_close'
navigation['deadline'] = (
now + self.DOOR_TURN_DURATION)
navigation['close_door'] = (
navigation.get('opened_by_bot', False) and
is_open and info is not None and
info[2].get('powered') != 'true')
after = navigation['after']
fallback = ((after[0] - lower[0],
after[2] - lower[2])
if after is not None else None)
self._begin_door_turn(navigation, lower, fallback)
return True
self._restore_door_look(navigation)
return False
if not is_open:
# If a pressure plate or redstone opened it, wait briefly
# for its state update rather than toggling it closed.
if (info is not None and
info[1] in blocks.OPENABLE_DOOR_IDS and
lower in self.g.path):
navigation['phase'] = 'checking'
navigation['opened_by_bot'] = False
navigation['deadline'] = (
now + self.DOOR_AUTO_OPEN_GRACE)
return True
if (navigation.get('saw_open') and
navigation['after'] is not None):
return False
self._restore_door_look(navigation)
return False
if navigation['phase'] == 'turning_to_close':
if now >= navigation['deadline']:
if (navigation.get('close_door') and is_open and
info is not None and
info[2].get('powered') != 'true'):
navigation['phase'] = 'closing'
navigation['deadline'] = now + 2.0
print('[door] closing after crossing at', lower,
flush=True)
self._toggle_door(lower, info[1])
else:
self._begin_door_return(navigation, now)
return True
if navigation['phase'] == 'closing':
if not is_open or now > navigation['deadline']:
self._begin_door_return(navigation, now)
return True
if navigation['phase'] == 'turning_back':
if now >= navigation['deadline']:
self._restore_door_look(navigation)
return False
return True
if not self.g.path:
return False
current = utils.pint(self.g.pos)
path_index = 0
if (len(self.g.path) > 1 and
tuple(self.g.path[0]) == current):
path_index = 1
if path_index >= len(self.g.path):
return False
info = self._door_info(tuple(self.g.path[path_index]))
if info is None:
return False
lower, state_id, properties = info
is_open = properties.get('open') == 'true'
if (not is_open and state_id not in blocks.OPENABLE_DOOR_IDS) or \
utils.phyp(current, lower) > 3:
return False
after_index = path_index + 1
after = (tuple(self.g.path[after_index])
if after_index < len(self.g.path) else None)
if after_index + 1 < len(self.g.path):
forward_look_at = tuple(self.g.path[after_index + 1])
elif after is not None:
forward_look_at = tuple(
after[i] + 2 * (after[i] - lower[i]) for i in range(3))
else:
forward_look_at = None
navigation = {
'position': lower,
'after': after,
'goal': tuple(self.g.path[-1]),
'phase': 'crossing' if is_open else 'checking',
'deadline': now + self.DOOR_AUTO_OPEN_GRACE,
'opened_by_bot': False,
'saw_open': is_open,
'close_door': False,
'previous_look_at': self.g.get('look_at'),
'forward_look_at': forward_look_at,
}
self.door_navigation = navigation
if not is_open:
self.g.look_at = lower
return True
return False
def animate(self):
"""Ask the 26.3 server to broadcast a main-hand punch animation."""
self._send_action(PunchPacket())