import pygame from math import radians, sin, cos, atan2, degrees, sqrt from . import game from . import loaders from . import rect from . import spellcheck ANCHORS = { 'x': { 'left': 0.0, 'center': 0.5, 'middle': 0.5, 'right': 1.0, }, 'y': { 'top': 0.0, 'center': 0.5, 'middle': 0.5, 'bottom': 1.0, } } def calculate_anchor(value, dim, total): if isinstance(value, str): try: return total * ANCHORS[dim][value] except KeyError: raise ValueError( '%r is not a valid %s-anchor name' % (value, dim) ) return float(value) # These are methods (of the same name) on pygame.Rect SYMBOLIC_POSITIONS = set(( "topleft", "bottomleft", "topright", "bottomright", "midtop", "midleft", "midbottom", "midright", "center", )) # Provides more meaningful default-arguments e.g. for display in IDEs etc. POS_TOPLEFT = None ANCHOR_CENTER = None def transform_anchor(ax, ay, w, h, angle): """Transform anchor based upon a rotation of a surface of size w x h.""" theta = -radians(angle) sintheta = sin(theta) costheta = cos(theta) # Dims of the transformed rect tw = abs(w * costheta) + abs(h * sintheta) th = abs(w * sintheta) + abs(h * costheta) # Offset of the anchor from the center cax = ax - w * 0.5 cay = ay - h * 0.5 # Rotated offset of the anchor from the center rax = cax * costheta - cay * sintheta ray = cax * sintheta + cay * costheta return ( tw * 0.5 + rax, th * 0.5 + ray ) class Actor: EXPECTED_INIT_KWARGS = SYMBOLIC_POSITIONS DELEGATED_ATTRIBUTES = [a for a in dir(rect.ZRect) if not a.startswith("_")] _anchor = _anchor_value = (0, 0) _angle = 0.0 def __init__(self, image, pos=POS_TOPLEFT, anchor=ANCHOR_CENTER, **kwargs): self._handle_unexpected_kwargs(kwargs) self.__dict__["_rect"] = rect.ZRect((0, 0), (0, 0)) # Initialise it at (0, 0) for size (0, 0). # We'll move it to the right place and resize it later self.image = image self._init_position(pos, anchor, **kwargs) def __getattr__(self, attr): if attr in self.__class__.DELEGATED_ATTRIBUTES: return getattr(self._rect, attr) else: return object.__getattribute__(self, attr) def __setattr__(self, attr, value): """Assign rect attributes to the underlying rect.""" if attr in self.__class__.DELEGATED_ATTRIBUTES: return setattr(self._rect, attr, value) else: # Ensure data descriptors are set normally return object.__setattr__(self, attr, value) def __iter__(self): return iter(self._rect) def _handle_unexpected_kwargs(self, kwargs): unexpected_kwargs = set(kwargs.keys()) - self.EXPECTED_INIT_KWARGS if not unexpected_kwargs: return for found, suggested in spellcheck.compare( unexpected_kwargs, self.EXPECTED_INIT_KWARGS): raise TypeError( "Unexpected keyword argument '{}' (did you mean '{}'?)".format( found, suggested)) def _init_position(self, pos, anchor, **kwargs): if anchor is None: anchor = ("center", "center") self.anchor = anchor symbolic_pos_args = { k: kwargs[k] for k in kwargs if k in SYMBOLIC_POSITIONS} if not pos and not symbolic_pos_args: # No positional information given, use sensible top-left default self.topleft = (0, 0) elif pos and symbolic_pos_args: raise TypeError("'pos' argument cannot be mixed with 'topleft', 'topright' etc. argument.") elif pos: self.pos = pos else: self._set_symbolic_pos(symbolic_pos_args) def _set_symbolic_pos(self, symbolic_pos_dict): if len(symbolic_pos_dict) == 0: raise TypeError("No position-setting keyword arguments ('topleft', 'topright' etc) found.") if len(symbolic_pos_dict) > 1: raise TypeError("Only one 'topleft', 'topright' etc. argument is allowed.") setter_name, position = symbolic_pos_dict.popitem() setattr(self, setter_name, position) @property def anchor(self): return self._anchor_value @anchor.setter def anchor(self, val): self._anchor_value = val self._calc_anchor() def _calc_anchor(self): ax, ay = self._anchor_value ow, oh = self._orig_surf.get_size() ax = calculate_anchor(ax, 'x', ow) ay = calculate_anchor(ay, 'y', oh) self._untransformed_anchor = ax, ay if self._angle == 0.0: self._anchor = self._untransformed_anchor else: self._anchor = transform_anchor(ax, ay, ow, oh, self._angle) @property def angle(self): return self._angle @angle.setter def angle(self, angle): self._angle = angle self._surf = pygame.transform.rotate(self._orig_surf, angle) p = self.pos self.width, self.height = self._surf.get_size() w, h = self._orig_surf.get_size() ax, ay = self._untransformed_anchor self._anchor = transform_anchor(ax, ay, w, h, angle) self.pos = p @property def pos(self): px, py = self.topleft ax, ay = self._anchor return px + ax, py + ay @pos.setter def pos(self, pos): px, py = pos ax, ay = self._anchor self.topleft = px - ax, py - ay @property def x(self): ax = self._anchor[0] return self.left + ax @x.setter def x(self, px): self.left = px - self._anchor[0] @property def y(self): ay = self._anchor[1] return self.top + ay @y.setter def y(self, py): self.top = py - self._anchor[1] @property def image(self): return self._image_name @image.setter def image(self, image): self._image_name = image self._orig_surf = self._surf = loaders.images.load(image) self._update_pos() def _update_pos(self): p = self.pos self.width, self.height = self._surf.get_size() self._calc_anchor() self.pos = p def draw(self): game.screen.blit(self._surf, self.topleft) def angle_to(self, target): """Return the angle from this actors position to target, in degrees.""" if isinstance(target, Actor): tx, ty = target.pos else: tx, ty = target myx, myy = self.pos dx = tx - myx dy = myy - ty # y axis is inverted from mathematical y in Pygame return degrees(atan2(dy, dx)) def distance_to(self, target): """Return the distance from this actor's pos to target, in pixels.""" if isinstance(target, Actor): tx, ty = target.pos else: tx, ty = target myx, myy = self.pos dx = tx - myx dy = ty - myy return sqrt(dx * dx + dy * dy)