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.
432 lines
16 KiB
432 lines
16 KiB
""" |
|
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 |
|
It is not meant to be mutable |
|
""" |
|
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): |
|
""" |
|
:param obj: (MapObject | Collection) 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 = [] |
|
|
|
|
|
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) |
|
|
|
|
|
class EditText(MapObject): |
|
def __init__(self): |
|
super().__init__() |
|
self.ID = -2 |
|
self._txt = "" |
|
self._frame_count = 0 |
|
self.frame_limit = 30 |
|
|
|
def draw(self): |
|
if len(self._txt) > 0: |
|
self._frame_count += 1 |
|
if self._frame_count > self.frame_limit: |
|
self._txt = "" |
|
return |
|
pyxel.text(self.position.X * CELL_SIZE, self.position.Y * CELL_SIZE, self._txt, 7, None) |
|
|
|
def set_text(self, txt: str): |
|
self._txt = txt |
|
self._frame_count = 0 |
|
|
|
|
|
# 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 |
|
|
|
|
|
# Gives the editor the elements that can be placed using right mouse button |
|
editor_place_elements = [] |
|
editor_preview_objects = [] |
|
|
|
for element in loadable_elements: |
|
prev_object = element() |
|
if prev_object.can_be_placed: |
|
editor_place_elements.append(element) |
|
editor_preview_objects.append(prev_object) |
|
|
|
|
|
def editor_next_number(current: int) -> int: |
|
""" |
|
:param current: (int) The current index used by the editor |
|
:return: (int) The next index of the editor_place_elements list |
|
Used by the editor to determine the next element that can be placed using the mouse wheel |
|
""" |
|
if current >= len(editor_place_elements)-1: |
|
return 0 |
|
return current + 1 |
|
|
|
|
|
def editor_previous_number(current: int) -> int: |
|
""" |
|
:param current: (int) The current index used by the editor |
|
:return: (int) The previous index of the editor_place_elements list |
|
Used by the editor to determine the previous element that can be placed using the mouse wheel |
|
""" |
|
if current <= 0: |
|
return len(editor_place_elements) - 1 |
|
return current - 1
|
|
|