def on_update(self, update: ppb_events.Update, signal): if self.health <= 0: update.scene.remove(self) update.scene.add( RunOnceAnimation( position=self.position, life_span=0.25, image=Animation( "shooter/resources/explosions/player/sprite_{1..7}.png", 24), end_event=shooter_events.PlayerDied(), size=2)) signal(ppb_events.PlaySound(sounds["dead"])) controller = update.controls self.heading = Vector(controller.get("horizontal"), controller.get("vertical")) if self.heading: self.heading = self.heading.normalize() self.move(update.time_delta)
def on_pre_render(self, event, signal): core_position = event.scene.player.position min_x = int((core_position.x // 10) * 10) min_y = int((core_position.y // 10) * 10) cells = list( product(range(min_x - 10, min_x + 20, 10), range(min_y - 10, min_y + 20, 10))) for c_x, c_y in cells: if (c_x, c_y) in self.created_zones: continue _seed = self.seed + f"{c_x}-{c_y}" seed(_seed) results = choices(self.choices, weights=self.weights, k=100) self.created_zones[(c_x, c_y)] = { "spawned": True, "classes": results } for (x, y), cls in zip( product(range(c_x, c_x + 10), range(c_y, c_y + 10)), results): if cls is not None: event.scene.add(cls(pos=Vector(x, y)))
class Rotatable: """ A simple rotation mixin. Can be included with sprites. """ _rotation = 0 # This is necessary to make facing do the thing while also being adjustable. #: The baseline vector, representing the "front" of the sprite basis = Vector(0, -1) # Considered making basis private, the only reason to do so is to # discourage people from relying on it as data. @property def facing(self): """ The direction the "front" is facing """ return Vector(*self.basis).rotate(self.rotation).normalize() @facing.setter def facing(self, value): self.rotation = self.basis.angle(value) @property def rotation(self): """ The amount the sprite is rotated, in degrees """ return self._rotation @rotation.setter def rotation(self, value): self._rotation = value % 360 def rotate(self, degrees): """ Rotate the sprite by a given angle (in degrees). """ self.rotation += degrees
def bottom(self): self._attribute_gate(BOTTOM, [TOP, BOTTOM]) return Vector(self.value, self.parent.bottom.value)
class BaseSprite(EventMixin): """ The base Sprite class. All sprites should inherit from this (directly or indirectly). Attributes: * image (str): The image file * resource_path (pathlib.Path): The path that image is relative to * position: Location of the sprite * facing: The direction of the "top" of the sprite (rendering only) * size: The width/height of the sprite (sprites are square) """ image = None resource_path = None position: Vector = Vector(0, 0) facing: Vector = Vector(0, -1) _size: Union[int, float] = 1 _offset_value = None def __init__(self, **kwargs): super().__init__() # Make these instance properties with fresh instances # Don't use Vector.convert() because we need copying self.position = Vector(*self.position) self.facing = Vector(*self.facing) # Initialize things for k, v in kwargs.items(): # Abbreviations if k == 'pos': k = 'position' # Castings if k in ('position', 'facing'): v = Vector(*v) # Vector.convert() when that ships. setattr(self, k, v) # Trigger some calculations self.size = self.size @property def center(self) -> Vector: return self.position @center.setter def center(self, value: Sequence[float]): x = value[0] y = value[1] self.position.x = x self.position.y = y @property def left(self) -> Side: return Side(self, LEFT) @left.setter def left(self, value: float): self.position.x = value + self._offset_value @property def right(self) -> Side: return Side(self, RIGHT) @right.setter def right(self, value): self.position.x = value - self._offset_value @property def top(self): return Side(self, TOP) @top.setter def top(self, value): self.position.y = value + self._offset_value @property def bottom(self): return Side(self, BOTTOM) @bottom.setter def bottom(self, value): self.position.y = value - self._offset_value @property def size(self) -> Union[int, float]: return self._size @size.setter def size(self, value: Union[int, float]): self._size = value self._offset_value = self._size / 2 def rotate(self, degrees: Number): self.facing.rotate(degrees) def __image__(self): if self.image is None: self.image = f"{type(self).__name__.lower()}.png" return self.image def __resource_path__(self): if self.resource_path is None: self.resource_path = Path(realpath(getfile(type(self)))).absolute().parent return self.resource_path
def right(self): self._attribute_gate(RIGHT, [LEFT, RIGHT]) return Vector(self.parent.right.value, self.value)
def test_bottom_bottom(self): self.assertRaises(AttributeError, getattr, self.sprite.bottom, "bottom") self.assertRaises(AttributeError, setattr, self.sprite.bottom, "bottom", Vector(1, 1))
def test_top_top(self): self.assertRaises(AttributeError, getattr, self.sprite.top, "top") self.assertRaises(AttributeError, setattr, self.sprite.top, "top", Vector(1, 1))
def test_right_left(self): self.assertRaises(AttributeError, getattr, self.sprite.right, "left") self.assertRaises(AttributeError, setattr, self.sprite.right, "left", Vector(1, 1))
def bottom(self) -> Vector: """ Get the corner vector """ self._attribute_gate(BOTTOM, [TOP, BOTTOM]) return Vector(float(self), float(self.parent.bottom))
def top(self) -> Vector: """ Get the corner vector """ self._attribute_gate(TOP, [TOP, BOTTOM]) return Vector(float(self), float(self.parent.top))
def bottom(self, value): self.position = Vector(self.position.x, value + self._offset_value)
def top(self, value): self.position = Vector(self.position.x, value - self._offset_value)
def right(self, value): self.position = Vector(value - self._offset_value, self.position.y)
def left(self, value: float): self.position = Vector(value + self._offset_value, self.position.y)
class MoverMixin(ppb.Sprite): velocity = Vector(0, 0) def on_update(self, update, signal): self.position += self.velocity * update.time_delta
def test_pos(self): self.assertEqual(self.sprite.position, Vector(0, 0)) self.assertEqual(self.wide_sprite.position, Vector(2, 2))
def right(self) -> Vector: """ Get the corner vector """ self._attribute_gate(RIGHT, [LEFT, RIGHT]) return Vector(float(self.parent.right), float(self))
def test_top_center(self): self.assertEqual(self.sprite.top.center, Vector(0, -0.5)) self.sprite.top.center = (1, 1) self.assertEqual(self.sprite.top.center, Vector(1, 1))
def viewport_height(self, value: Union[int, float]): self._viewport_height = value self.viewport_offset = Vector(self.viewport_width / 2, value / 2)
def test_bottom_center(self): self.assertEqual(self.sprite.bottom.center, Vector(0, 0.5)) self.sprite.bottom.center = (1, 1) self.assertEqual(self.sprite.bottom.center, Vector(1, 1))
class TestRotatable(Rotatable): _rotation = 180 basis = Vector(0, 1)
class TestSprite(BaseSprite): position = Vector(4, 2)
def test_camera_viewport(): cam = Camera(viewport=(0, 0, 800, 600)) assert cam.point_in_viewport(Vector(400, 400)) assert not cam.point_in_viewport(Vector(900, 600)) assert cam.viewport_offset == Vector(400, 300)
def center(self): if self.side in (TOP, BOTTOM): return Vector(self.parent.center.x, self.value) else: return Vector(self.value, self.parent.center.y)
def test_camera_point_in_viewport_not_at_origin(): cam = Camera(viewport=(100, 100, 800, 600)) assert cam.point_in_viewport(Vector(150, 650)) assert cam.point_in_viewport(Vector(899, 300)) assert not cam.point_in_viewport(Vector(50, 50)) assert not cam.point_in_viewport(Vector(901, 600))
def top(self): self._attribute_gate(TOP, [TOP, BOTTOM]) return Vector(self.value, self.parent.top.value)
def test_camera_move(): cam = Camera() cam.position = Vector(500, 500) assert cam.position == Vector(500, 500) cam.position += Vector(100, 100) assert cam.position == Vector(600, 600)
def left(self): self._attribute_gate(LEFT, [LEFT, RIGHT]) return Vector(self.parent.left.value, self.value)
def __init__(self, position=Vector(2, 2)): super().__init__() self.size = 2 self.position = position