from __future__ import division
from collections import namedtuple
from copy import copy
from math import atan2, cos, degrees, radians, pi, sin, sqrt
from construct import Container
from bravo.beta.packets import make_packet
def f(self, other):
return self._replace(x=op(self.x, other.x), y=op(self.y, other.y),
[docs]class Position(namedtuple("Position", "x, y, z")):
The coordinates pointing to an entity.
Positions are *always* stored as integer absolute pixel coordinates.
__add__ = _combinator(operator.add)
__sub__ = _combinator(operator.sub)
__mul__ = _combinator(operator.mul)
__div__ = _combinator(operator.div)
[docs] def from_player(cls, x, y, z):
Create a ``Position`` from floating-point block coordinates.
return cls(int(x * 32), int(y * 32), int(z * 32))
[docs] def to_player(self):
Return this position as floating-point block coordinates.
return self.x / 32, self.y / 32, self.z / 32
[docs] def to_block(self):
Return this position as block coordinates.
return int(self.x // 32), int(self.y // 32), int(self.z // 32)
return int(self.x // 32 // 16), int(self.z // 32 // 16)
[docs] def distance(self, other):
Return the distance between this position and another, in absolute
dx = (self.x - other.x)**2
dy = (self.y - other.y)**2
dz = (self.z - other.z)**2
return int(sqrt(dx + dy + dz))
[docs] def heading(self, other):
Return the heading from this position to another, in radians.
This is a wrapper for the common atan2() expression found in games,
meant to help encapsulate semantics and keep copy-paste errors from
theta = atan2(self.z - other.z, self.x - other.x) + pi / 2
if theta < 0:
theta += pi * 2
[docs]class Orientation(namedtuple("Orientation", "theta, phi")):
The angles corresponding to the heading of an entity.
Theta and phi are very much like the theta and phi of spherical
coordinates, except that phi's zero is perpendicular to the XZ-plane
rather than pointing straight up or straight down.
Orientation is stored in floating-point radians, for simplicity of
computation. Unfortunately, no wire protocol speaks radians, so several
conversion methods are provided for sanity and convenience.
The ``from_degs()`` and ``to_degs()`` methods provide integer degrees.
This form is called "yaw and pitch" by protocol documentation.
[docs] def from_degs(cls, yaw, pitch):
Create an ``Orientation`` from integer degrees.
return cls(radians(yaw) % (pi * 2), radians(pitch))
[docs] def to_degs(self):
Return this orientation as integer degrees.
return int(round(degrees(self.theta))), int(round(degrees(self.phi)))
[docs] def to_fracs(self):
Return this orientation as fractions of a byte.
yaw = int(self.theta * 255 / (2 * pi)) % 256
pitch = int(self.phi * 255 / (2 * pi)) % 256
return yaw, pitch
The position and orientation of an entity.
# Position in pixels.
self.pos = Position(0, 0, 0)
# Start with a relatively sane stance.
self.stance = 1.0
# Orientation, in radians.
self.ori = Orientation(0.0, 0.0)
# Whether we are in the air.
self.grounded = False
[docs] def at_block(cls, x, y, z):
Pinpoint a location at a certain block.
This constructor is intended to aid in pinpointing locations at a
specific block rather than forcing users to do the pixel<->block maths
themselves. Admittedly, the maths in question aren't hard, but there's
no reason to avoid this encapsulation.
location = cls()
location.pos = Position(x * 32, y * 32, z * 32)
return "<Location(%s, (%d, %d (+%.6f), %d), (%.2f, %.2f))>" % (
"grounded" if self.grounded else "midair", self.pos.x, self.pos.y,
self.stance - self.pos.y, self.pos.z, self.ori.theta,
__str__ = __repr__
[docs] def clamp(self):
Force this location to be sane.
Forces the position and orientation to be sane, then fixes up
location-specific things, like stance.
:returns: bool indicating whether this location had to be altered
clamped = False
y = self.pos.y
# Clamp Y. We take precautions here and forbid things to go up past
# the top of the world; this tend to strand entities up in the sky
# where they cannot get down. We also forbid entities from falling
# past bedrock.
# TODO: Fix me, I'm broken
# XXX how am I broken?
if not (32 * 1) <= y:
y = max(y, 32 * 1)
self.pos = self.pos._replace(y=y)
clamped = True
# Stance is the current jumping position, plus a small offset of
# around 0.1. In the Alpha server, it must between 0.1 and 1.65, or
# the anti-grounded code kicks the client. In the Beta server, though,
# the clamp is different. Experimentally, the stance can range from
# 1.5 (crouching) to 2.4 (jumping). At this point, we enforce some
# sanity on our client, and force the stance to a reasonable value.
fy = y / 32
if not 1.5 < (self.stance - fy) < 2.4:
# Standard standing stance is 1.62.
self.stance = fy + 1.62
clamped = True
[docs] def save_to_packet(self):
Returns a position/look/grounded packet.
# Get our position.
x, y, z = self.pos.to_player()
# Grab orientation.
yaw, pitch = self.ori.to_degs()
# Note: When this packet is sent from the server, the 'y' and 'stance' fields are swapped.
position = Container(x=x, y=self.stance, z=z, stance=y)
orientation = Container(rotation=yaw, pitch=pitch)
grounded = Container(grounded=self.grounded)
packet = make_packet("location", position=position,
[docs] def distance(self, other):
Return the distance between this location and another location.
[docs] def in_front_of(self, distance):
Return a ``Location`` a certain number of blocks in front of this
The orientation of the returned location is identical to this
:param int distance: the number of blocks by which to offset this
other = copy(self)
distance *= 32
# Do some trig to put the other location a few blocks ahead of the
# player in the direction they are facing. Note that all three
# coordinates are "misnamed;" the unit circle actually starts at (0,
# 1) and goes *backwards* towards (-1, 0).
x = int(self.pos.x - distance * sin(self.ori.theta))
z = int(self.pos.z + distance * cos(self.ori.theta))
other.pos = other.pos._replace(x=x, z=z)