Browse Source

Code principal

Jeu + éditeur
master
nfx 1 month ago
parent
commit
4adfc0944d
  1. BIN
      1.dat
  2. BIN
      2.dat
  3. 274
      dev_edit.py
  4. 21
      json_to_bin.py
  5. 169
      lvl_edit.py
  6. 8
      lvl_template.json
  7. 163
      main.py
  8. 378
      objects.py

BIN
1.dat

Binary file not shown.

BIN
2.dat

Binary file not shown.

274
dev_edit.py

@ -0,0 +1,274 @@
"""
LEVEL EDITOR(prototype)
The file that allows to create and edit levels
THIS PROGRAM IS DEPRECATED, USE LVL_EDIT.PY TO EDIT LEVELS
"""
import pyxel
import json
import shutil
CELL_SIZE = 20
GRID_X = 11
GRID_Y = 6
WIN_X = CELL_SIZE*GRID_X
WIN_Y = CELL_SIZE*GRID_Y
def clamp(n: int, a: int, b: int):
"""
:param n: The number to be clamped
:param a: The minimum limit
:param b: The maximum limit
:return: The clamped number
Returns the provided number n clamped between a and b
"""
if n < a:
return a
if n > b:
return b
return n
def get_file_content(lvl: int) -> list:
"""
:param lvl: (int) The id of the file to read
:return: (list) The level content of the file
Reads a level file content and returns its data
"""
file = open(f"lvl/{lvl}.json", "r", encoding="utf-8")
decoder = json.JSONDecoder()
content = decoder.decode(file.read())
file.close()
return content
def can_load(lvl: int) -> bool:
"""
:param lvl: (int) The id of the file to check for
:return: Returns if the level file exists and can be loaded
"""
try:
file = get_file_content(lvl)
return len(file) == GRID_Y
except FileNotFoundError:
return False
class LevelManager:
def __init__(self):
self.level = 0 # the id of the currently loaded stage
self.cache = [] # the current stage state is stored there
self.tileTypes = { # helps with tile identifiers
"empty": 0,
"wall": 1,
"player": 3,
"exit": 2,
"switch_blue": 4,
"s_wall_blue": 5
}
def load(self, lvl: int):
"""
:param lvl: (int) the level id
Reloads the game using the specified level
"""
self.level = lvl
self.cache = get_file_content(lvl)
def save(self, lvl: int):
"""
:param lvl: (int) the level id
Saves the cache under the specified level id
"""
file = open(f"lvl/{lvl}.json", "w", encoding="utf-8")
encoder = json.JSONEncoder()
file.write(encoder.encode(self.cache))
file.close()
def update_tile(self, position: tuple[int, int], val: int):
"""
:param position: (tuple) The position of the tile to modify
:param val: (int) The new value of the tile
Updates the specified tile from the cache to the specified value
"""
self.cache[position[1]][position[0]] = val
def get_tile(self, position: tuple[int, int]) -> int:
"""
:param position: (tuple) the position of the requested tile
:return: (int) the value of the tile
Fetches the current value of the specified tile or -1 if out of range
"""
if not self.is_in_range(position):
return -1
return self.cache[position[1]][position[0]]
def is_in_range(self, position: tuple[int, int]) -> bool:
"""
:param position: (tuple) The position that will be checked
:return: (bool) indicates whether the said position is in map bounds
Checks if the given position is within the map range(a level should have been loaded first)
"""
if len(self.cache) == 0:
return False
if position[0] < 0 or position[1] < 0:
return False
if position[1] > GRID_Y-1:
return False
if position[0] > GRID_X-1:
return False
return True
def move_tile(self, tile_a, tile_b):
"""
:param tile_a: (tuple) The position of the tile to be moved
:param tile_b: (tuple) The position of the target tile
Moves the content of tile_a to tile_b(overwrites tile_b and sets tile_a to 0)
This is meant for player movement
"""
if tile_a != tile_b:
content = self.get_tile(tile_a)
self.update_tile(tile_b, content)
self.update_tile(tile_a, self.tileTypes['empty'])
def find_object(self, elt: int) -> tuple[int, int]:
"""
:param elt: (int) The type of element to find on the map(use self.tileTypes)
:return: (tuple) The position of the tile(or (-1, -1) if not found)
Finds the first instance of the provided element on the board, and returns its position
"""
for y in range(GRID_Y):
for x in range(GRID_X):
if self.cache[y][x] == elt:
return x, y
return -1, -1
def find_player(self): # finds the player in the stage grid and returns its coordinates
return self.find_object(self.tileTypes['player'])
def input_cmd():
cmd_txt = input("cmd => ").split(" ")
if cmd_txt[0] == "load" and len(cmd_txt) > 1 and cmd_txt[1].isnumeric() and can_load(int(cmd_txt[1])):
print(f"Now editing level {cmd_txt[1]}")
runner.level.load(int(cmd_txt[1]))
elif cmd_txt[0] == "create" and len(cmd_txt) > 1 and cmd_txt[1].isnumeric():
print(f"Now editing level {cmd_txt[1]}")
shutil.copyfile("lvl_template.json", f'lvl/{cmd_txt[1]}.json')
runner.level.load(int(cmd_txt[1]))
elif cmd_txt[0] == "save":
runner.level.save(runner.level.level)
print(f"Saved successfully")
else:
print("Invalid Command")
class Editor:
def __init__(self):
self.level = LevelManager() # An instance that manages level data and navigating
self.isInit = False # Safety attribute that prevents init from running twice
self.cursor = (0, 0) # Describes the mouse position(snapped to grid)
self.toolToggle = 0
self.toolToggleRotation = [
self.level.tileTypes['wall'],
self.level.tileTypes['exit'],
self.level.tileTypes['switch_blue'],
self.level.tileTypes['s_wall_blue']
]
self.element_display = [
"empty",
"wall",
"exit",
"player",
"blue switch",
"blue switch wall"
]
def on_click(self): # toggles the presence of an element at cursor position
tile = self.level.get_tile(self.cursor)
if tile != self.toolToggleRotation[self.toolToggle] and tile != self.level.tileTypes['player']:
self.level.update_tile(self.cursor, self.toolToggleRotation[self.toolToggle])
elif tile != self.level.tileTypes['player']:
self.level.update_tile(self.cursor, self.level.tileTypes['empty'])
def move_player(self): # moves the player to the cursor(overwrites said tile)
if self.cursor[0] >= 0 and self.cursor[1] >= 0:
plr_pos = self.level.find_player()
self.level.move_tile(plr_pos, self.cursor)
def tool_rotation(self): # Changes the element that will be placed on click
next_index = self.toolToggle + 1
if next_index >= len(self.toolToggleRotation):
self.toolToggle = 0
else:
self.toolToggle = next_index
new_element = self.toolToggleRotation[self.toolToggle]
print(f"Now placing {self.element_display[new_element]}")
# manages controls and tools
def upd(self):
mx, my = pyxel.mouse_x, pyxel.mouse_y
self.cursor = (mx//CELL_SIZE, my//CELL_SIZE)
if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
self.on_click()
elif pyxel.btnp(pyxel.KEY_E):
self.move_player()
elif pyxel.btnp(pyxel.KEY_A):
self.tool_rotation()
elif pyxel.btnp(pyxel.KEY_C):
input_cmd()
# utility methods that draws the cursor to the mouse position
def draw_cursor(self):
x, y = self.cursor
center_x = x*CELL_SIZE+CELL_SIZE//2
center_y = y*CELL_SIZE+CELL_SIZE//2
pyxel.circb(center_x, center_y, 5, 4)
# self-explanatory, manages what's seen on screen
def display(self):
pyxel.cls(0)
for y in range(GRID_Y):
for x in range(GRID_X):
tile = self.level.get_tile((x, y))
if tile == self.level.tileTypes['wall']:
pyxel.rect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE, 7)
elif tile == self.level.tileTypes['exit']:
pyxel.rect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE, 11)
elif tile == self.level.tileTypes['player']:
pyxel.circ(x*CELL_SIZE+CELL_SIZE//2, y*CELL_SIZE+CELL_SIZE//2, CELL_SIZE//2-1, 8)
elif tile == self.level.tileTypes['switch_blue']:
pyxel.circ(x*CELL_SIZE+CELL_SIZE//2, y*CELL_SIZE+CELL_SIZE//2, CELL_SIZE//3-1, 12)
elif tile == self.level.tileTypes['s_wall_blue']:
pyxel.rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE, 12)
self.draw_cursor()
def init(self, lvl: int): # opens the editor window and loads the specified stage
if not self.isInit:
self.isInit = True
self.level.load(lvl)
pyxel.init(WIN_X, WIN_Y, title="ice_walker editor")
pyxel.mouse(True)
pyxel.run(self.upd, self.display)
def start_editor():
runner.init(int(lvl_id))
if __name__ == "__main__":
lvl_id = input("Please enter the id of the level to load: ")
if lvl_id.isnumeric():
print(f"Now editing level {lvl_id}")
runner = Editor()
start_editor()
else:
print("Invalid level id provided, please rerun and try again")

21
json_to_bin.py

@ -0,0 +1,21 @@
"""
Converts json level data files into binary level data
"""
import json
from os import listdir
from os.path import isfile, join
LVL_DIR = "lvl/"
BIN_DIR = "lvl_bin/"
if __name__ == "__main__":
files = [f for f in listdir(LVL_DIR) if isfile(join(LVL_DIR, f))]
decoder = json.JSONDecoder()
for v in files:
file = open(join(LVL_DIR, v), "r", encoding="utf-8")
content = decoder.decode(file.read())
newfile = open(join(BIN_DIR, v.replace(".json", ".dat")), "wb")
bin_content = b''
for line in content:
bin_content += bytes(line+[255])
newfile.write(bin_content)

169
lvl_edit.py

@ -0,0 +1,169 @@
"""
ICE WALKER LEVEL EDITOR
Runs a level editor which can be used to create and edit levels
"""
import objects
import json
import pyxel
CELL_SIZE = 8
GRID_X = 11
GRID_Y = 6
WIN_X = CELL_SIZE*GRID_X
WIN_Y = CELL_SIZE*GRID_Y
def load_file(lvl: int) -> list:
"""
:param lvl: (int) The id of the level to load
:return: (list) The loaded level from the file
Reads the corresponding level file and returns its loaded content
"""
file = open(f"lvl/{lvl}.json", "r", encoding="utf-8")
decoder = json.JSONDecoder()
return decoder.decode(file.read())
def can_load(lvl: int) -> bool:
"""
:param lvl: (int) The id of the file to check for
:return: (bool) Returns if the level file exists and can be loaded
Tests if a level can be loaded, returns the result as a boolean
"""
try:
file = load_file(lvl)
return len(file) == GRID_Y
except FileNotFoundError:
return False
class LevelManager:
"""
This class is responsible for handling game data
"""
def __init__(self):
self.current_level = 0
self.cache = []
def load_level(self, lvl: int):
"""
:param lvl: (int) The id of the level to load
Loads the specified level and stores it into the cache
"""
if can_load(lvl):
self.cache = load_file(lvl)
self.current_level = lvl
print(f"editing level {lvl}")
def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None:
"""
:param collection: (Collection) The collection to load the objects into
:return: The player object created by this method
Loads the level elements using the loaded cache, returns the new player instance
"""
new_player = None
for y in range(len(self.cache)):
for x in range(len(self.cache[y])):
element_id = self.cache[y][x]
elt = objects.get_type_from_id(element_id)
if elt:
new = elt()
new.position = objects.V2(x, y)
collection.add(new)
if element_id == 3:
new_player = new
return new_player
class Game:
"""
This class manages the game runtime
"""
def __init__(self):
self.level = None # An instance of the LevelManager class
self.main_collection = None # An instance of Collection, usually the main one
self.editor_collection = None # An instance of Collection used by the editor and ignored while saving
self.player = None # The current instance of the Player object
self.cursor = objects.Cursor()
self._drag = None # The current object being dragged in the editor
self._dragging = False # Determines is the mouse is currently in the dragging state
def _upd(self):
if self.player:
change_level = 0
# Update cursor position
mx, my = pyxel.mouse_x, pyxel.mouse_y
self.cursor.position = objects.clamp_in_range(objects.V2(mx//CELL_SIZE, my//CELL_SIZE))
if pyxel.btnp(pyxel.KEY_LEFT): # Level switch controls
change_level = -1
elif pyxel.btnp(pyxel.KEY_RIGHT):
change_level = 1
elif pyxel.btnp(pyxel.KEY_DELETE): # Object management controls
obj = self.main_collection.get_at_position(self.cursor.position)
if obj and obj.can_be_deleted:
self.main_collection.remove(obj, True)
if change_level != 0: # Clear cache and load another level
self._drag = None
self._dragging = False
self.level.load_level(self.level.current_level + change_level)
self.main_collection.clear_all()
new_player = self.level.load_collection_from_cache(self.main_collection)
self.player = new_player
self.main_collection.add(self.editor_collection)
return # Not handle the rest because not necessary
if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT):
if not self._dragging:
self._dragging = True
self._drag = self.main_collection.get_at_position(self.cursor.position)
elif self._drag:
self._drag.position = self.cursor.position
else:
self._dragging = False
self._drag = None
def _display(self):
pyxel.cls(0)
for v in self.main_collection.get_objects(): # Render all active objects
if isinstance(v, objects.MapObject):
v.draw()
elif isinstance(v, objects.Collection) and v.visible: # Render embedded collections, if visible
for embed_v in v.get_objects():
embed_v.draw()
# Draw cursor
pyxel.rect(pyxel.mouse_x, pyxel.mouse_y, 1, 1, 5)
def run(self, lvl_m: LevelManager, main_c: objects.Collection, editor_c: objects.Collection):
"""
:param lvl_m: (LevelManager) The instance of LevelManager to assign with the game instance
:param main_c: (Collection) The primary collection containing the entirety of the game
:param editor_c: (Collection) The editor's collection
Initializes and runs the editor
"""
self.level = lvl_m
self.main_collection = main_c
self.editor_collection = editor_c
editor_c.add(self.cursor)
main_c.add(editor_c)
pyxel.init(WIN_X, WIN_Y, title="ice_walker")
pyxel.run(self._upd, self._display)
if __name__ == "__main__":
main_collection = objects.Collection()
editor_collection = objects.Collection()
level = LevelManager() # Load the first level of the game
level.load_level(1)
player = level.load_collection_from_cache(main_collection)
game = Game() # Initializes the game engine
game.player = player
game.run(level, main_collection, editor_collection)

8
lvl_template.json

@ -0,0 +1,8 @@
[
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

163
main.py

@ -0,0 +1,163 @@
"""
ICE WALKER GAME RUNNER
The main file that runs the game itself
"""
import objects
import json
import pyxel
import pathlib
CELL_SIZE = 8
GRID_X = 11
GRID_Y = 6
WIN_X = CELL_SIZE*GRID_X
WIN_Y = CELL_SIZE*GRID_Y
def load_file_json(lvl: int) -> list:
"""
:param lvl: (int) The id of the level to load
:return: (list) The loaded level from the file
Reads the corresponding level file and returns its loaded content
"""
file = open(f"lvl/{lvl}.json", "r", encoding="utf-8")
decoder = json.JSONDecoder()
return decoder.decode(file.read())
def load_file(lvl: int) -> list:
"""
:param lvl: (int) The id of the level to load
:return: (list) The loaded level from the file
Reads the corresponding level file and returns its loaded content
"""
file = open(f"lvl_bin/{lvl}.dat", "rb")
content = file.read()
res = []
mem = []
for v in content:
if int(v) == 255:
res.append(mem)
mem = []
else:
mem.append(int(v))
return res
def can_load(lvl: int) -> bool:
"""
:param lvl: (int) The id of the file to check for
:return: (bool) Returns if the level file exists and can be loaded
Tests if a level can be loaded, returns the result as a boolean
"""
path = pathlib.Path(f"lvl_bin/{lvl}.dat")
return path.is_file()
class LevelManager:
"""
This class is responsible for handling game data
"""
def __init__(self):
self.current_level = 0
self.cache = []
def load_level(self, lvl: int):
"""
:param lvl: (int) The id of the level to load
Loads the specified level and stores it into the cache
"""
if can_load(lvl):
self.cache = load_file(lvl)
self.current_level = lvl
def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None:
"""
:param collection: (Collection) The collection to load the objects into
:return: The player object created by this method
Loads the level elements using the loaded cache, returns the new player instance
"""
new_player = None
for y in range(len(self.cache)):
for x in range(len(self.cache[y])):
element_id = self.cache[y][x]
elt = objects.get_type_from_id(element_id)
if elt:
new = elt()
new.position = objects.V2(x, y)
collection.add(new)
if element_id == 3:
new_player = new
return new_player
class Game:
"""
This class manages the game runtime
"""
def __init__(self):
self.level = None # An instance of the LevelManager class
self.main_collection = None # An instance of Collection, usually the main one
self.player = None # The current instance of the Player object
def _upd(self):
if self.player:
if pyxel.btnp(pyxel.KEY_R): # Reset the current level
self.main_collection.clear_all()
self.player = self.level.load_collection_from_cache(self.main_collection)
direction = objects.V2()
if pyxel.btnp(pyxel.KEY_Z): # Determine move direction
direction.Y = -1
elif pyxel.btnp(pyxel.KEY_S):
direction.Y = 1
elif pyxel.btnp(pyxel.KEY_Q):
direction.X = -1
elif pyxel.btnp(pyxel.KEY_D):
direction.X = 1
if not direction.is_zero(): # Handles the actual movement and collisions
new_pos, colliders = self.main_collection.ray_cast(self.player.position, direction, self.player)
self.player.position = new_pos
for v in colliders:
v.touched(self.player)
if self.main_collection.exits == 0: # Handles when the level was cleared
self.level.load_level(self.level.current_level + 1)
self.main_collection.clear_all()
new_player = self.level.load_collection_from_cache(self.main_collection)
self.player = new_player
def _display(self):
pyxel.cls(0)
for v in self.main_collection.get_objects(): # Render all active objects
if isinstance(v, objects.MapObject):
v.draw()
elif isinstance(v, objects.Collection) and v.visible: # Render embedded collections, if visible
for embed_v in v.get_objects():
embed_v.draw()
def run(self, lvl_m: LevelManager, main_c: objects.Collection):
"""
:param lvl_m: (LevelManager) The instance of LevelManager to assign with the game instance
:param main_c: (Collection) The primary collection containing the entirety of the game
Initializes and runs the game
"""
self.level = lvl_m
self.main_collection = main_c
pyxel.init(WIN_X, WIN_Y, title="ice_walker")
pyxel.run(self._upd, self._display)
if __name__ == "__main__":
main_collection = objects.Collection()
level = LevelManager() # Load the first level of the game
level.load_level(1)
player = level.load_collection_from_cache(main_collection)
game = Game() # Initializes the game engine
game.player = player
game.run(level, main_collection)

378
objects.py

@ -0,0 +1,378 @@
"""
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
Loading…
Cancel
Save