Browse Source

Transférer les fichiers vers ''

master
nfx 4 weeks ago
parent
commit
8efb003a67
  1. 138
      lvl_edit.py
  2. 16
      main.py
  3. 60
      objects.py

138
lvl_edit.py

@ -6,43 +6,91 @@
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
WIN_X = CELL_SIZE * GRID_X
WIN_Y = CELL_SIZE * GRID_Y
def load_file(lvl: int) -> list:
def load_file_json(lvl: int) -> list[list[int]]:
"""
: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")
file = open(f"lvl_json/{lvl}.json", "r", encoding="utf-8")
decoder = json.JSONDecoder()
return decoder.decode(file.read())
def parse_file(content: bytes) -> list[list[int]]:
"""
:param content: (bytes) The raw content of the level file
:return: (list) The level data converted into a table
"""
res = []
mem = []
for v in content:
if int(v) == 255:
res.append(mem)
mem = []
else:
mem.append(int(v))
return res
def load_template() -> list[list[int]]:
"""
:return: (list) The content of the template level file
Loads the template json file and returns its content
"""
file = open("lvl_template.dat", 'rb')
return parse_file(file.read())
def load_file(lvl: int) -> list[list[int]]:
"""
: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}.dat", "rb")
return parse_file(file.read())
def save_file(lvl: int, data: list):
"""
:param lvl: (int) The id of the level to save
:param data: (list) The data to encode and save
Saves the given data into a level binary
"""
content = b''
for line in data:
content += bytes(line + [255])
file = open(f"lvl/{lvl}.dat", "wb")
file.write(content)
file.close()
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
path = pathlib.Path(f"lvl/{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 = []
@ -55,7 +103,11 @@ class LevelManager:
if can_load(lvl):
self.cache = load_file(lvl)
self.current_level = lvl
print(f"editing level {lvl}")
# print(f"editing level {lvl}")
elif lvl > 0:
self.cache = load_template()
self.current_level = lvl
# print(f"editing level {lvl}")
def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None:
"""
@ -76,28 +128,69 @@ class LevelManager:
new_player = new
return new_player
def build_data_from_collection(self, collection: objects.Collection):
"""
:param collection: (Collection) The collection to build the level data from
Turns the collection into a level data table and stores it into the cache
"""
# Build an empty data
res = [[0 for _ in range(GRID_X)] for _ in range(GRID_Y)]
# Iterate through the collection's objects and store their id into the level data at their locations
for v in collection.get_objects():
if isinstance(v, objects.MapObject):
res[v.position.Y][v.position.X] = v.ID
self.cache = res
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.status = objects.EditText()
self._drag = None # The current object being dragged in the editor
self._dragging = False # Determines is the mouse is currently in the dragging state
self.place_object_id = 0 # The object placed upon right click.
self.current_preview_display = None # The object that is being rendered as a preview(for obj selection)
self.preview_last_changed = 0 # The frame on which the preview object has been last set
def _set_preview_display(self, obj: objects.MapObject | None = None):
"""
:param obj: (MapObject | None) The object to display as a preview
Parents the provided object into the editor collection to use as a temporary preview
"""
if self.current_preview_display is not None:
self.editor_collection.remove(self.current_preview_display, True)
if obj is not None:
self.current_preview_display = obj
self.editor_collection.add(obj)
self.preview_last_changed = pyxel.frame_count
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))
self.cursor.position = objects.clamp_in_range(objects.V2(mx // CELL_SIZE, my // CELL_SIZE))
if pyxel.mouse_wheel == 1: # Next Element
self.place_object_id = objects.editor_next_number(self.place_object_id)
self._set_preview_display(objects.editor_preview_objects[self.place_object_id])
elif pyxel.mouse_wheel == -1: # Previous Element
self.place_object_id = objects.editor_previous_number(self.place_object_id)
self._set_preview_display(objects.editor_preview_objects[self.place_object_id])
if pyxel.frame_count-self.preview_last_changed >= 30 and self.current_preview_display:
self._set_preview_display() # Remove temporary preview once timer's reached 30 frames
if pyxel.btnp(pyxel.KEY_LEFT): # Level switch controls
change_level = -1
@ -107,11 +200,23 @@ class Game:
obj = self.main_collection.get_at_position(self.cursor.position)
if obj and obj.can_be_deleted:
self.main_collection.remove(obj, True)
elif pyxel.btnp(pyxel.MOUSE_BUTTON_RIGHT): # Insert an object
constructor = objects.editor_place_elements[self.place_object_id]
new_elt = constructor()
new_elt.position = self.cursor.position
self.main_collection.add(new_elt)
self.main_collection.on_top(self.editor_collection)
elif pyxel.btnp(pyxel.KEY_S) and pyxel.btn(pyxel.KEY_SHIFT): # Save the level file
self.level.build_data_from_collection(self.main_collection)
save_file(self.level.current_level, self.level.cache)
self.status.set_text(f"Saved {self.level.current_level}")
# print(f"Level saved as level {self.level.current_level}")
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.status.set_text(f"Editing {self.level.current_level}")
self.main_collection.clear_all()
new_player = self.level.load_collection_from_cache(self.main_collection)
self.player = new_player
@ -125,6 +230,16 @@ class Game:
elif self._drag:
self._drag.position = self.cursor.position
else:
if self._drag:
overlap = self.main_collection.get_at_position(self._drag.position, [self._drag])
# The level data doesn't support multiple objects at the same location
if overlap and overlap.can_be_deleted:
# If the object it is overlapping with can be normally deleted, delete it
self.main_collection.remove(overlap, True)
elif overlap:
# If the object it is overlapping with cant be normally deleted,
# deletes the dragged object instead
self.main_collection.remove(self._drag, True)
self._dragging = False
self._drag = None
@ -151,6 +266,7 @@ class Game:
self.main_collection = main_c
self.editor_collection = editor_c
editor_c.add(self.cursor)
editor_c.add(self.status)
main_c.add(editor_c)
pyxel.init(WIN_X, WIN_Y, title="ice_walker")
pyxel.run(self._upd, self._display)

16
main.py

@ -6,7 +6,7 @@
import objects
import json
import pyxel
import pathlib
from os.path import exists
CELL_SIZE = 8
GRID_X = 11
@ -22,7 +22,7 @@ def load_file_json(lvl: int) -> list:
: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")
file = open(f"lvl_json/{lvl}.json", "r", encoding="utf-8")
decoder = json.JSONDecoder()
return decoder.decode(file.read())
@ -33,7 +33,7 @@ def load_file(lvl: int) -> list:
: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")
file = open(f"lvl/{lvl}.dat", "rb")
content = file.read()
res = []
mem = []
@ -52,8 +52,7 @@ def can_load(lvl: int) -> bool:
: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()
return exists(f"lvl/{lvl}.dat")
class LevelManager:
@ -69,9 +68,12 @@ class LevelManager:
:param lvl: (int) The id of the level to load
Loads the specified level and stores it into the cache
"""
print(f"Loading level {lvl}")
if can_load(lvl):
self.cache = load_file(lvl)
self.current_level = lvl
else:
print("level doesn't exist")
def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None:
"""
@ -104,10 +106,6 @@ class Game:
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

60
objects.py

@ -18,6 +18,7 @@ WIN_Y = CELL_SIZE*GRID_Y
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
@ -195,9 +196,9 @@ class Collection:
res.append(v)
return res
def on_top(self, obj: MapObject):
def on_top(self, obj):
"""
:param obj: (MapObject) the object to put in front
: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)
"""
@ -236,7 +237,6 @@ class Collection:
if isinstance(v, MapObject):
v.collection = None
self._instances = []
self.exits = 0
class Switch(MapObject):
@ -355,6 +355,27 @@ class Cursor(MapObject):
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 = [
@ -376,3 +397,36 @@ def get_type_from_id(element_id: int):
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

Loading…
Cancel
Save