Skip to content

Commit

Permalink
Merge pull request #194 from arnauddupuis/arnauddupuis/issue188
Browse files Browse the repository at this point in the history
issue #188: Add serialization capabilities to Animation
  • Loading branch information
arnauddupuis committed Jun 14, 2022
2 parents 87587d1 + 791c535 commit 9b2c729
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 20 deletions.
34 changes: 21 additions & 13 deletions pygamelib/board_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class BoardItem(base.PglBaseObject):
that the pickable, restorable, overlappable and can_move properties are
configurable for all items independently of their type. This fixes an issue with
restorable: only :class:`~Immovable` objects could be restorable. Now all items
can be any combination of these properties. As a developper you are now
can be any combination of these properties. As a developer you are now
encouraged to use the corresponding functions to determine the abilities of an
item.
Expand Down Expand Up @@ -202,6 +202,8 @@ def serialize(self) -> dict:
ret_data["object"] = str(self.__class__)
if self.sprixel is not None:
ret_data["sprixel"] = self.sprixel.serialize()
if self.animation is not None:
ret_data["animation"] = self.animation.serialize()
ret_data["restorable"] = self.restorable()
ret_data["overlappable"] = self.overlappable()
ret_data["pickable"] = self.pickable()
Expand Down Expand Up @@ -268,6 +270,8 @@ def load(cls, data):
value=data["value"],
inventory_space=data["inventory_space"],
)
if "animation" in data.keys() and data["animation"] is not None:
itm.animation = core.Animation.load(data["animation"])
if "particle_emitter" in data.keys() and data["particle_emitter"] is not None:
import pygamelib # noqa: F401

Expand Down Expand Up @@ -307,7 +311,11 @@ def heading(self):
@property
def animation(self):
"""A property to get and set an :class:`~pygamelib.gfx.core.Animation` for
this item."""
this item.
.. Important:: When an animation is set, the item is setting the animation's
parent to itself.
"""
return self.__animation

@animation.setter
Expand Down Expand Up @@ -380,7 +388,7 @@ def store_position(self, row: int, column: int, layer: int = 0):
"""Store the BoardItem position for self access.
The stored position is used for consistency and quick access to the self
postion. It is a redundant information and might not be synchronized.
position. It is a redundant information and might not be synchronized.
:param row: the row of the item in the :class:`~pygamelib.engine.Board`.
:type row: int
Expand Down Expand Up @@ -525,7 +533,7 @@ def collides_with(self, other):
desirable for the pygamelib to assume that 2 items on different layers wont
collide. For example, if a player is over a door, they are on different
layers, but logically speaking they are colliding. The player is overlapping
the door. Therefor, it is the responsibility of the developper to check for
the door. Therefor, it is the responsibility of the developer to check for
layers in collision, if it is important to the game logic.
:param other: The item you want to check for collision.
Expand Down Expand Up @@ -751,7 +759,7 @@ def overlappable(self):
class BoardItemComplexComponent(BoardItem):
"""The default component of a complex item.
It is literrally just a BoardItem but is subclassed for easier identification.
It is literally just a BoardItem but is subclassed for easier identification.
It is however scanning its parent for the item's basic properties (overlappable,
restorable, etc.)
Expand Down Expand Up @@ -944,8 +952,8 @@ def update_sprite(self):
"""
Update the complex item with the current sprite.
.. note:: This method use to need to be called everytime the sprite was changed.
Starting with version 1.3.0, it is no longer a requirement as
.. note:: This method use to need to be called every time the sprite was
changed. Starting with version 1.3.0, it is no longer a requirement as
BoardComplexItem.sprite was turned into a property that takes care of calling
update_sprite().
Expand Down Expand Up @@ -1176,7 +1184,7 @@ def dtmove(self, value):
def serialize(self) -> dict:
"""Serialize the Immovable object.
This returns a dictionnary that contains all the key/value pairs that makes up
This returns a dictionary that contains all the key/value pairs that makes up
the object.
"""
Expand Down Expand Up @@ -1440,7 +1448,7 @@ def add_directional_model(self, direction, model):
Example::
fireball.add_directional_animation(constants.UP, updward_animation)
fireball.add_directional_animation(constants.UP, upward_animation)
"""
if type(direction) is not int:
raise base.PglInvalidTypeException(
Expand Down Expand Up @@ -1778,7 +1786,7 @@ def __init__(
def serialize(self) -> dict:
"""Serialize the Character object.
This returns a dictionnary that contains all the key/value pairs that makes up
This returns a dictionary that contains all the key/value pairs that makes up
the object.
"""
Expand Down Expand Up @@ -2061,7 +2069,7 @@ def serialize(self) -> dict:
"""
Serialize the NPC object.
This returns a dictionnary that contains all the key/value pairs that makes up
This returns a dictionary that contains all the key/value pairs that makes up
the object.
"""
ret_data = super().serialize()
Expand Down Expand Up @@ -2896,9 +2904,9 @@ class Camera(Movable):
board if partial display is enabled.
The Camera object inherits from Movable and can accept an actuator parameter.
However, it is up to the developper to activate the actuators mechanics as the
However, it is up to the developer to activate the actuators mechanics as the
Camera object does not register as a NPC or a Player.
The support for actuators is mainly thought for pre-scripted cutscenes.
The support for actuators is mainly thought for pre-scripted cut-scenes.
Example::
Expand Down
78 changes: 71 additions & 7 deletions pygamelib/gfx/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1814,7 +1814,7 @@ def redraw_screen(game_object):
item = BoardItem(model=Sprite.ALIEN, name='Friendly Alien')
# By default BoardItem does not have any animation, we have to
# explicitely create one
# explicitly create one
item.animation = Animation(display_time=0.1, parent=item,
refresh_screen=redraw_screen)
"""
Expand All @@ -1832,6 +1832,7 @@ def __init__(
self.state = constants.RUNNING
self.display_time = display_time
self.auto_replay = auto_replay
self.parent = None
if frames is None:
frames = []
elif isinstance(frames, SpriteCollection):
Expand All @@ -1856,8 +1857,70 @@ def __init__(
self.refresh_screen = refresh_screen
self.__dtanimate = 0.0

def serialize(self):
"""
Serialize the Animation object.
The `refresh_screen` callback function is not serialized. Neither is the parent.
:return: A dictionary containing the Animation object's data.
:rtype: dict
"""
ret_data = {}
ret_data["display_time"] = self.display_time
ret_data["auto_replay"] = self.auto_replay
if isinstance(self.frames[0], Sprite):
ret_data["frame_type"] = "sprite"
elif isinstance(self.frames[0], Sprixel):
ret_data["frame_type"] = "sprixel"
else:
ret_data["frame_type"] = "str"
ret_data["frames"] = []
for frame in self.frames:
if isinstance(frame, Sprite) or isinstance(frame, Sprixel):
ret_data["frames"].append(frame.serialize())
else:
ret_data["frames"].append(frame)
ret_data["_frame_index"] = self._frame_index
ret_data["_initial_index"] = self._initial_index
return ret_data

@classmethod
def load(cls, data):
"""
Load a serialized Animation object.
:param data: The serialized Animation object.
:type data: dict
:return: The loaded Animation object.
:rtype: :class:`Animation`
"""
# Start by constructing a default Animation object (because we have some
# specific cases to handle)
obj = cls()
# Unrelated note: all this function's code after this line has been written by
# Github's Copilot... This is really a time saver.
obj.display_time = data["display_time"]
obj.auto_replay = data["auto_replay"]
if data["frame_type"] == "sprite":
obj.frames = []
for frame in data["frames"]:
obj.frames.append(Sprite.load(frame))
elif data["frame_type"] == "sprixel":
obj.frames = []
for frame in data["frames"]:
obj.frames.append(Sprixel.load(frame))
else:
obj.frames = data["frames"]
obj._frame_index = data["_frame_index"]
obj._initial_index = data["_initial_index"]
return obj

@property
def dtanimate(self):
"""
The time elapsed since the last frame was displayed.
"""
return self.__dtanimate

@dtanimate.setter
Expand Down Expand Up @@ -1908,7 +1971,7 @@ def add_frame(self, frame):
Raise an exception if frame is not a string.
:param frame: The frame to add to the animation.
:type frame: str
:type frame: str|:class:`Sprite`|:class:`Sprixel`
:raise: :class:`pygamelib.base.PglInvalidTypeException`
Example::
Expand Down Expand Up @@ -2006,7 +2069,7 @@ def next_frame(self):
"""Update the parent's model, sprixel or sprite with the next frame of the
animation.
That method takes care of automatically replaying the animation if the
That method takes care of automatically resetting the animation if the
last frame is reached if the state is constants.RUNNING.
If the the state is PAUSED it still update the parent.model
Expand Down Expand Up @@ -2088,10 +2151,11 @@ def play_all(self):
ctrl = 0
while ctrl < len(self.frames):
if self.dtanimate >= self.display_time:
# Dirty but that's a current limitation: to restore stuff on the board's
# overlapped matrix, we need to either move or replace an item after
# updating the sprite. This is only for sprites that have null items but
# we don't want to let any one slip.
# Dirty but that's a current limitation: to really update a complex item
# on the board, we need to either move or replace an item after
# updating the sprite. This is mostly for sprites that have null items
# but we don't want to let any one slip. An item on a Screen is not
# concerned by that.
# Also: this is convoluted...
if (
pgl_isinstance(
Expand Down
79 changes: 79 additions & 0 deletions tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,85 @@ def test_dtdisplay(self):
with self.assertRaises(pgl_base.PglInvalidTypeException):
a.dtanimate = "1.0"

def test_serialization_with_string(self):
a = gfx_core.Animation(display_time=0.5)
a.add_frame("-o-")
a.add_frame("\\o-")
a.add_frame("\\o\\")
a.add_frame("|o|")
self.assertIsNotNone(a.serialize())
b = gfx_core.Animation.load(a.serialize())
self.assertEqual(b.display_time, 0.5)
self.assertEqual(len(b.frames), 4)
self.assertEqual(b.frames[0], "-o-")
self.assertEqual(a.serialize(), b.serialize())

def test_serialization_with_sprixel(self):
a = gfx_core.Animation(display_time=0.5)
a.add_frame(gfx_core.Sprixel("-o-"))
a.add_frame(gfx_core.Sprixel("\\o-"))
a.add_frame(gfx_core.Sprixel("\\o\\"))
a.add_frame(gfx_core.Sprixel("|o|"))
self.assertIsNotNone(a.serialize())
b = gfx_core.Animation.load(a.serialize())
self.assertEqual(b.display_time, 0.5)
self.assertEqual(len(b.frames), 4)
self.assertEqual(b.frames[0], gfx_core.Sprixel("-o-"))
self.assertEqual(a.serialize(), b.serialize())

def test_serialization_with_sprite(self):
spr = gfx_core.Sprite(
sprixels=[
[
gfx_core.Sprixel.cyan_rect(),
gfx_core.Sprixel.red_rect(),
gfx_core.Sprixel.green_rect(),
],
[
gfx_core.Sprixel.yellow_rect(),
gfx_core.Sprixel.blue_rect(),
gfx_core.Sprixel.white_rect(),
],
]
)
blue_spr = gfx_core.Sprite(
sprixels=[
[
gfx_core.Sprixel.blue_rect(),
gfx_core.Sprixel.blue_rect(),
gfx_core.Sprixel.blue_rect(),
],
[
gfx_core.Sprixel.blue_rect(),
gfx_core.Sprixel.blue_rect(),
gfx_core.Sprixel.blue_rect(),
],
]
)
red_spr = gfx_core.Sprite(
sprixels=[
[
gfx_core.Sprixel.red_rect(),
gfx_core.Sprixel.red_rect(),
gfx_core.Sprixel.red_rect(),
],
[
gfx_core.Sprixel.red_rect(),
gfx_core.Sprixel.red_rect(),
gfx_core.Sprixel.red_rect(),
],
]
)
a = gfx_core.Animation(display_time=0.5)
a.add_frame(spr)
a.add_frame(red_spr)
a.add_frame(blue_spr)
self.assertIsNotNone(a.serialize())
b = gfx_core.Animation.load(a.serialize())
self.assertEqual(b.display_time, 0.5)
self.assertEqual(len(b.frames), 3)
self.assertEqual(a.serialize(), b.serialize())


if __name__ == "__main__":
unittest.main()
15 changes: 15 additions & 0 deletions tests/test_boardItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,21 @@ def test_camera(self):
self.assertEqual(cam.column, 34)
self.assertFalse(cam.has_inventory())

def test_bi_serialization(self):
b = board_items.BoardItem(
animation=gfx_core.Animation(
display_time=0.42, frames=["Q", "W", "E", "R", "T", "Y"]
)
)
self.assertEqual(b.animation.display_time, 0.42)
self.assertEqual(b.animation.frames, ["Q", "W", "E", "R", "T", "Y"])
data = b.serialize()
self.assertEqual(data["animation"]["display_time"], 0.42)
b2 = board_items.BoardItem.load(data)
self.assertEqual(b2.animation.display_time, 0.42)
self.assertEqual(b2.animation.frames, ["Q", "W", "E", "R", "T", "Y"])
self.assertEqual(b.serialize(), b2.serialize())


if __name__ == "__main__":
unittest.main()

0 comments on commit 9b2c729

Please sign in to comment.