You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
379 lines
14 KiB
379 lines
14 KiB
1 month ago
|
"""
|
||
|
ICE WALKER OBJECTS LIBRARY
|
||
|
Defines all the objets and their behavior
|
||
|
"""
|
||
|
|
||
|
import pyxel
|
||
|
|
||
|
CELL_SIZE = 8
|
||
|
GRID_X = 11
|
||
|
GRID_Y = 6
|
||
|
|
||
|
WIN_X = CELL_SIZE*GRID_X
|
||
|
WIN_Y = CELL_SIZE*GRID_Y
|
||
|
|
||
|
# Superclass and other classes definitions
|
||
|
|
||
|
|
||
|
class V2:
|
||
|
"""
|
||
|
This object represents a grid point
|
||
|
"""
|
||
|
def __init__(self, x=0, y=0):
|
||
|
self.X = x # The horizontal position of the object
|
||
|
self.Y = y # The vertical position of the object
|
||
|
|
||
|
def __eq__(self, other): # equality operator handler
|
||
|
if isinstance(other, V2):
|
||
|
return self.X == other.X and self.Y == other.Y
|
||
|
return NotImplemented
|
||
|
|
||
|
def __add__(self, other): # add operator handler
|
||
|
if isinstance(other, V2):
|
||
|
return V2(self.X+other.X, self.Y+other.Y)
|
||
|
return NotImplemented
|
||
|
|
||
|
def is_zero(self) -> bool: # returns True if object's X and Y are 0
|
||
|
return self.X == 0 and self.Y == 0
|
||
|
|
||
|
|
||
|
# static functions
|
||
|
|
||
|
def is_in_range(position: V2) -> bool:
|
||
|
"""
|
||
|
:param position: (V2) The grid position to check for
|
||
|
:return: (bool) False is the given position is out of range
|
||
|
Returns whether the given position is within the map range
|
||
|
"""
|
||
|
if position.X < 0 or position.X > GRID_X-1: # Check horizontal range
|
||
|
return False
|
||
|
if position.Y < 0 or position.Y > GRID_Y-1: # Check vertical range
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
|
||
|
def clamp_in_range(position: V2) -> V2:
|
||
|
"""
|
||
|
:param position: (V2) The grid position to check for
|
||
|
:return: (V2) The position to process
|
||
|
Returns a clamped version of the given position, so it is within the map range
|
||
|
"""
|
||
|
x, y = position.X, position.Y
|
||
|
if position.X < 0: # Check horizontal range
|
||
|
x = 0
|
||
|
elif position.X > GRID_X-1:
|
||
|
x = GRID_X-1
|
||
|
if position.Y < 0: # Check vertical range
|
||
|
y = 0
|
||
|
elif position.Y > GRID_Y-1:
|
||
|
y = GRID_Y-1
|
||
|
return V2(x, y)
|
||
|
|
||
|
|
||
|
class MapObject:
|
||
|
"""
|
||
|
The base class for objects within the game.
|
||
|
unlike Collections, MapObjects can't hold any children.
|
||
|
Should not be constructed directly.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
self.collisions = False # Determines if the object can collide with the player
|
||
|
self.ID = 0 # The object data reference
|
||
|
self.display_name = "empty" # How the object is called in the editor
|
||
|
self.runID = "empty" # How the object type will be referenced as internally
|
||
|
self.position = V2() # The grid position of the object
|
||
|
self.can_be_placed = True # Determines if the object can be placed in the editor
|
||
|
self.can_be_deleted = True # Determines if the object can be deleted in the editor
|
||
|
self.collection = None # The collection the object belongs to
|
||
|
|
||
|
def draw(self): # This method will be called every frame by the game to render the object
|
||
|
pass
|
||
|
|
||
|
def removed(self): # This method is called right before the objet is deleted
|
||
|
pass
|
||
|
|
||
|
def touched(self, collider: 'MapObject'): # Handles collision reactions
|
||
|
pass
|
||
|
|
||
|
|
||
|
class Collection:
|
||
|
"""
|
||
|
Collections objects are responsible for handling MapObjects in a running game.
|
||
|
Collections can be embedded to each other, making a folder logic
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
self._instances = [] # The MapObjects and Collections this Collection holds.
|
||
|
self.exits = 0 # The number of exits currently present on the map, should be read only.
|
||
|
self.visible = True # If embedded, setting this to False will skip the render of the objects it contains.
|
||
|
|
||
|
def find_by_type(self, type_object) -> MapObject | None:
|
||
|
"""
|
||
|
:param type_object: The type of the object to check for
|
||
|
:return: An object of the given type_object type
|
||
|
Returns the first object of the type_object type found in the running game
|
||
|
"""
|
||
|
for v in self._instances:
|
||
|
if isinstance(v, type_object):
|
||
|
return v
|
||
|
return None
|
||
|
|
||
|
def _locate(self, object_to_locate) -> int:
|
||
|
"""
|
||
|
:param object_to_locate: (MapObject | Collection) The object to look for in the running game
|
||
|
:return: (int) The index of the object in the data table
|
||
|
Finds the reference to object_to_locate in the DataModel instances table, returns -1 if not found
|
||
|
"""
|
||
|
for i in range(len(self._instances)):
|
||
|
if self._instances[i] == object_to_locate:
|
||
|
return i
|
||
|
return -1
|
||
|
|
||
|
def remove(self, object_to_remove: MapObject, silent: bool = False):
|
||
|
"""
|
||
|
:param object_to_remove: (MapObject) The object to remove from the running game
|
||
|
:param silent: (bool) If true, does not call "MapObject.removed"
|
||
|
Finds and removes the given object from the running game after calling "MapObject.removed" if not silent
|
||
|
"""
|
||
|
inst_index = self._locate(object_to_remove)
|
||
|
if inst_index >= 0:
|
||
|
if isinstance(object_to_remove, Collection):
|
||
|
# We remove embedded collections directly because they don't have any data we need to remove first
|
||
|
self._instances.remove(object_to_remove)
|
||
|
return
|
||
|
if not silent:
|
||
|
object_to_remove.removed()
|
||
|
object_to_remove.collection = None
|
||
|
if object_to_remove.ID == 2: # Count one exit less if what we're removing is an Exit object(which has ID 2)
|
||
|
self.exits -= 1
|
||
|
|
||
|
self._instances.remove(object_to_remove)
|
||
|
|
||
|
def add(self, object_to_add):
|
||
|
"""
|
||
|
:param object_to_add: (MapObject) The object to add to the collection
|
||
|
Adds the given object to the collection
|
||
|
"""
|
||
|
if isinstance(object_to_add, MapObject) and object_to_add.collection is None:
|
||
|
object_to_add.collection = self
|
||
|
if object_to_add.ID == 2:
|
||
|
self.exits += 1
|
||
|
self._instances.append(object_to_add)
|
||
|
elif isinstance(object_to_add, Collection): # Collections can be embedded into another one
|
||
|
self._instances.append(object_to_add)
|
||
|
else:
|
||
|
print("Cannot add an object to a collection when it already belongs to one")
|
||
|
|
||
|
def get_at_position(self, position: V2, exclude: list | None = None) -> MapObject | None:
|
||
|
"""
|
||
|
:param position: (V2) The position of the object to find
|
||
|
:param exclude: (list) The objects to ignore while doing the search
|
||
|
:return: (MapObject | None) The first object found at the given position
|
||
|
Returns the first object with a matching position, or None if not found
|
||
|
"""
|
||
|
if exclude is None or len(exclude) == 0:
|
||
|
for v in self._instances:
|
||
|
if isinstance(v, MapObject) and v.position == position:
|
||
|
return v
|
||
|
else:
|
||
|
for v in self._instances:
|
||
|
if isinstance(v, MapObject) and v.position == position and v not in exclude:
|
||
|
return v
|
||
|
return None
|
||
|
|
||
|
def get_objects(self, object_filter: list | None = None) -> list:
|
||
|
"""
|
||
|
:param object_filter: (list) A filter which will exclude the objects it contains from the result
|
||
|
:return: (list) A list of objects in the running game
|
||
|
Returns a list of all the objects currently in the running game
|
||
|
Excludes objects in filter
|
||
|
"""
|
||
|
if object_filter is None or len(object_filter) == 0:
|
||
|
return list(self._instances) # avoid filtering using an empty filter
|
||
|
res = []
|
||
|
for v in self._instances:
|
||
|
if v not in object_filter:
|
||
|
res.append(v)
|
||
|
return res
|
||
|
|
||
|
def on_top(self, obj: MapObject):
|
||
|
"""
|
||
|
:param obj: (MapObject) the object to put in front
|
||
|
Changes the order in the Collection's internal list so the provided object gets rendered last(and on top
|
||
|
of everything else)
|
||
|
"""
|
||
|
if obj in self._instances:
|
||
|
self._instances.remove(obj)
|
||
|
self._instances.append(obj)
|
||
|
|
||
|
def ray_cast(self, origin: V2, direction: V2, collide_object: MapObject | None = None) -> tuple[V2, list]:
|
||
|
"""
|
||
|
:param origin: (V2) The origin grid position
|
||
|
:param direction: (V2) The direction snapped to grid
|
||
|
:param collide_object: (MapObject) The collider associated with the ray
|
||
|
:return: (V2, list) The position right before hitting an object with collisions and the objects it collided with
|
||
|
Casts a ray in the current map and returns the position right next to the object it hits
|
||
|
Also returns what collided at the end and the object present at the returned position
|
||
|
Note that the collision behaviors are only active if collide_object is set, so calling this without
|
||
|
specifying a collide_object will return an empty list
|
||
|
"""
|
||
|
new_position = origin + direction
|
||
|
hitting_object = self.get_at_position(new_position, [collide_object])
|
||
|
if (hitting_object and hitting_object.collisions) or not is_in_range(new_position):
|
||
|
colliders = [] # Since multiple objects can collide at once, the colliders are returned as a list
|
||
|
if hitting_object and collide_object: # Check collisions with what the ray cast has hit
|
||
|
colliders.append(hitting_object)
|
||
|
object_overlap = self.get_at_position(origin, [collide_object])
|
||
|
if object_overlap and collide_object: # Check for an object which is already present at the result position
|
||
|
colliders.append(object_overlap)
|
||
|
return origin, colliders
|
||
|
return self.ray_cast(new_position, direction, collide_object) # Recursive case for next step
|
||
|
|
||
|
def clear_all(self):
|
||
|
"""
|
||
|
Removes every object from the collection without calling removed() on them
|
||
|
"""
|
||
|
for v in self._instances: # Remove the collection tag inside the objects before actually clearing
|
||
|
if isinstance(v, MapObject):
|
||
|
v.collection = None
|
||
|
self._instances = []
|
||
|
self.exits = 0
|
||
|
|
||
|
|
||
|
class Switch(MapObject):
|
||
|
"""
|
||
|
The base class for switches.
|
||
|
Switches removes the Switch Walls of the same color when deleted(when the player lands on it).
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
# new properties
|
||
|
self.color_id = 12 # Defines the color associated with this switch
|
||
|
|
||
|
def draw(self):
|
||
|
pyxel.circ(self.position.X * CELL_SIZE + 4, self.position.Y * CELL_SIZE + 4, CELL_SIZE // 3 - 1, self.color_id)
|
||
|
|
||
|
def removed(self):
|
||
|
for v in self.collection.get_objects([self]):
|
||
|
if isinstance(v, SwitchWall) and v.color_id == self.color_id:
|
||
|
self.collection.remove(v)
|
||
|
|
||
|
def touched(self, collider: MapObject):
|
||
|
if collider.ID == 3: # Check if the player is the collider, as it can't only be that.
|
||
|
self.collection.remove(self)
|
||
|
|
||
|
|
||
|
class SwitchWall(MapObject):
|
||
|
"""
|
||
|
A variant of Wall which is influenced by SwitchBlue objects.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.collisions = True
|
||
|
self.display_name = "Switch Wall"
|
||
|
self.color_id = 12 # Defines the switch color associated with the wall
|
||
|
|
||
|
def draw(self):
|
||
|
pyxel.rect(self.position.X * CELL_SIZE, self.position.Y * CELL_SIZE, CELL_SIZE, CELL_SIZE, self.color_id)
|
||
|
|
||
|
|
||
|
# Object classes definitions
|
||
|
|
||
|
class Wall(MapObject):
|
||
|
"""
|
||
|
Represents a wall on the map.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.collisions = True
|
||
|
self.ID = 1
|
||
|
self.display_name = "Wall"
|
||
|
|
||
|
def draw(self):
|
||
|
pyxel.rect(self.position.X * CELL_SIZE, self.position.Y * CELL_SIZE, CELL_SIZE, CELL_SIZE, 7)
|
||
|
|
||
|
|
||
|
class Exit(MapObject):
|
||
|
"""
|
||
|
Represents an "exit".
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.ID = 2
|
||
|
self.display_name = "Objective Point"
|
||
|
|
||
|
def draw(self):
|
||
|
pyxel.rect(self.position.X * CELL_SIZE, self.position.Y * CELL_SIZE, CELL_SIZE, CELL_SIZE, 11)
|
||
|
|
||
|
def touched(self, collider: MapObject):
|
||
|
if collider.ID == 3: # Check if the player is the collider, as it can't only be that.
|
||
|
self.collection.remove(self)
|
||
|
|
||
|
|
||
|
class Player(MapObject):
|
||
|
"""
|
||
|
Represents the player on the map.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.ID = 3
|
||
|
self.display_name = "Player"
|
||
|
self.can_be_placed = False
|
||
|
self.can_be_deleted = False
|
||
|
|
||
|
def draw(self):
|
||
|
pyxel.circ(self.position.X * CELL_SIZE + 4, self.position.Y * CELL_SIZE + 4, 3, 8)
|
||
|
|
||
|
|
||
|
class SwitchBlue(Switch):
|
||
|
"""
|
||
|
The blue variant of the Switch object.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
# We don't do anything else than constructing the super class because the default color is blue
|
||
|
super().__init__()
|
||
|
self.ID = 4
|
||
|
|
||
|
|
||
|
class SwitchWallBlue(SwitchWall):
|
||
|
"""
|
||
|
The blue variant of the SwitchWall object.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
# Same as for SwitchBlue
|
||
|
super().__init__()
|
||
|
self.ID = 5
|
||
|
|
||
|
|
||
|
# Editor objects
|
||
|
|
||
|
class Cursor(MapObject):
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.ID = -1
|
||
|
|
||
|
def draw(self):
|
||
|
pyxel.circb(self.position.X * CELL_SIZE + 4, self.position.Y * CELL_SIZE + 4, 2, 4)
|
||
|
|
||
|
|
||
|
# Wrapping the above classes into a list for the loader to use
|
||
|
|
||
|
loadable_elements = [
|
||
|
Wall,
|
||
|
Exit,
|
||
|
Player,
|
||
|
SwitchBlue,
|
||
|
SwitchWallBlue
|
||
|
]
|
||
|
|
||
|
|
||
|
def get_type_from_id(element_id: int):
|
||
|
"""
|
||
|
:param element_id: (int) The type id of the element to look for
|
||
|
:return: (MapObject | None) The constructor of the element with the matching type id
|
||
|
Finds an object type with a matching type id and returns it
|
||
|
"""
|
||
|
for v in loadable_elements:
|
||
|
if v().ID == element_id:
|
||
|
return v
|
||
|
return None
|