248 lines
8.0 KiB
Python
248 lines
8.0 KiB
Python
import json
|
|
import os
|
|
|
|
from ..constants import tileSize
|
|
from .geometry import Aabb
|
|
from .tiles import (
|
|
AIR,
|
|
asciiToTile,
|
|
tileToAscii,
|
|
asciiEntities,
|
|
tileRegistry,
|
|
)
|
|
|
|
|
|
class EntitySpawn:
|
|
__slots__ = ("kind", "x", "y", "meta")
|
|
|
|
def __init__(self, kind, x, y, meta=None):
|
|
self.kind = kind
|
|
self.x = float(x)
|
|
self.y = float(y)
|
|
self.meta = meta if meta is not None else {}
|
|
|
|
def asDict(self):
|
|
record = {"kind": self.kind, "x": self.x, "y": self.y}
|
|
if self.meta:
|
|
record["meta"] = self.meta
|
|
return record
|
|
|
|
|
|
class PlatformSpawn:
|
|
__slots__ = ("x", "y", "w", "h", "toX", "toY", "speed", "oneWay")
|
|
|
|
def __init__(self, x, y, w, h, toX, toY, speed, oneWay=False):
|
|
self.x = float(x)
|
|
self.y = float(y)
|
|
self.w = float(w)
|
|
self.h = float(h)
|
|
self.toX = float(toX)
|
|
self.toY = float(toY)
|
|
self.speed = float(speed)
|
|
self.oneWay = bool(oneWay)
|
|
|
|
def asDict(self):
|
|
return {
|
|
"x": self.x,
|
|
"y": self.y,
|
|
"w": self.w,
|
|
"h": self.h,
|
|
"toX": self.toX,
|
|
"toY": self.toY,
|
|
"speed": self.speed,
|
|
"oneWay": self.oneWay,
|
|
}
|
|
|
|
|
|
class Level:
|
|
def __init__(self, name, collision, background=None, foreground=None):
|
|
self.name = name
|
|
self.collision = collision
|
|
self.rows = len(collision)
|
|
self.cols = len(collision[0]) if collision else 0
|
|
self.background = background
|
|
self.foreground = foreground
|
|
self.playerSpawn = (tileSize, tileSize)
|
|
self.enemies = []
|
|
self.items = []
|
|
self.checkpoints = []
|
|
self.platforms = []
|
|
self.exit = None
|
|
|
|
def pixelWidth(self):
|
|
return self.cols * tileSize
|
|
|
|
def pixelHeight(self):
|
|
return self.rows * tileSize
|
|
|
|
def inBounds(self, col, row):
|
|
return 0 <= col < self.cols and 0 <= row < self.rows
|
|
|
|
def tileNameAt(self, col, row):
|
|
if row < 0 or row >= self.rows:
|
|
return AIR
|
|
if col < 0 or col >= self.cols:
|
|
return AIR
|
|
return self.collision[row][col]
|
|
|
|
def defAt(self, col, row):
|
|
return tileRegistry[self.tileNameAt(col, row)]
|
|
|
|
def solidAt(self, col, row):
|
|
if col < 0 or col >= self.cols:
|
|
return True
|
|
if row < 0 or row >= self.rows:
|
|
return False
|
|
return tileRegistry[self.collision[row][col]].solid
|
|
|
|
def oneWayAt(self, col, row):
|
|
return self.defAt(col, row).oneWay
|
|
|
|
def hazardAt(self, col, row):
|
|
return self.defAt(col, row).hazard
|
|
|
|
def ladderAt(self, col, row):
|
|
return self.defAt(col, row).ladder
|
|
|
|
def slopeAt(self, col, row):
|
|
return self.defAt(col, row).slope
|
|
|
|
def hazardDamageAt(self, col, row):
|
|
return self.defAt(col, row).damage
|
|
|
|
def rectFor(self, col, row):
|
|
return Aabb(col * tileSize, row * tileSize, tileSize, tileSize)
|
|
|
|
def setTile(self, col, row, name):
|
|
if self.inBounds(col, row):
|
|
self.collision[row][col] = name
|
|
|
|
def asDict(self):
|
|
record = {
|
|
"name": self.name,
|
|
"tileSize": tileSize,
|
|
"cols": self.cols,
|
|
"rows": self.rows,
|
|
"layers": {"collision": self.collision},
|
|
"playerSpawn": list(self.playerSpawn),
|
|
"enemies": [spawn.asDict() for spawn in self.enemies],
|
|
"items": [spawn.asDict() for spawn in self.items],
|
|
"checkpoints": [list(point) for point in self.checkpoints],
|
|
"platforms": [platform.asDict() for platform in self.platforms],
|
|
"exit": list(self.exit) if self.exit else None,
|
|
}
|
|
if self.background is not None:
|
|
record["layers"]["background"] = self.background
|
|
if self.foreground is not None:
|
|
record["layers"]["foreground"] = self.foreground
|
|
return record
|
|
|
|
|
|
def parseAscii(name, rows):
|
|
width = max((len(row) for row in rows), default=0)
|
|
padded = [row.ljust(width) for row in rows]
|
|
collision = [[AIR for _ in range(width)] for _ in padded]
|
|
level = Level(name, collision)
|
|
for rowIndex, line in enumerate(padded):
|
|
for colIndex, char in enumerate(line):
|
|
if char in asciiToTile:
|
|
collision[rowIndex][colIndex] = asciiToTile[char]
|
|
elif char in asciiEntities:
|
|
kind = asciiEntities[char]
|
|
px = colIndex * tileSize
|
|
py = rowIndex * tileSize
|
|
if kind == "playerSpawn":
|
|
level.playerSpawn = (px, py)
|
|
elif kind == "checkpoint":
|
|
level.checkpoints.append((px, py))
|
|
elif kind == "exit":
|
|
level.exit = (px, py)
|
|
elif kind in ("coin", "gem", "key", "heart"):
|
|
level.items.append(EntitySpawn(kind, px, py))
|
|
else:
|
|
level.enemies.append(EntitySpawn(kind, px, py))
|
|
if level.exit is None and level.checkpoints:
|
|
level.exit = level.checkpoints[-1]
|
|
return level
|
|
|
|
|
|
def levelToAscii(level):
|
|
lines = []
|
|
entityAt = {}
|
|
for spawn in level.enemies + level.items:
|
|
col = int(round(spawn.x / tileSize))
|
|
row = int(round(spawn.y / tileSize))
|
|
entityAt[(col, row)] = spawn.kind
|
|
spx, spy = level.playerSpawn
|
|
entityAt[(int(round(spx / tileSize)), int(round(spy / tileSize)))] = "playerSpawn"
|
|
for point in level.checkpoints:
|
|
entityAt[(int(round(point[0] / tileSize)), int(round(point[1] / tileSize)))] = "checkpoint"
|
|
if level.exit:
|
|
entityAt[(int(round(level.exit[0] / tileSize)), int(round(level.exit[1] / tileSize)))] = "exit"
|
|
kindToChar = {value: key for key, value in asciiEntities.items()}
|
|
for rowIndex in range(level.rows):
|
|
chars = []
|
|
for colIndex in range(level.cols):
|
|
key = (colIndex, rowIndex)
|
|
name = level.collision[rowIndex][colIndex]
|
|
if name != AIR:
|
|
chars.append(tileToAscii.get(name, "?"))
|
|
elif key in entityAt:
|
|
chars.append(kindToChar[entityAt[key]])
|
|
else:
|
|
chars.append(" ")
|
|
lines.append("".join(chars).rstrip())
|
|
return lines
|
|
|
|
|
|
def levelFromDict(record):
|
|
collision = record["layers"]["collision"]
|
|
background = record["layers"].get("background")
|
|
foreground = record["layers"].get("foreground")
|
|
level = Level(record.get("name", "unnamed"), collision, background, foreground)
|
|
level.playerSpawn = tuple(record.get("playerSpawn", (tileSize, tileSize)))
|
|
for enemy in record.get("enemies", []):
|
|
level.enemies.append(EntitySpawn(enemy["kind"], enemy["x"], enemy["y"], enemy.get("meta")))
|
|
for item in record.get("items", []):
|
|
level.items.append(EntitySpawn(item["kind"], item["x"], item["y"], item.get("meta")))
|
|
level.checkpoints = [tuple(point) for point in record.get("checkpoints", [])]
|
|
for platform in record.get("platforms", []):
|
|
level.platforms.append(
|
|
PlatformSpawn(
|
|
platform["x"],
|
|
platform["y"],
|
|
platform["w"],
|
|
platform["h"],
|
|
platform["toX"],
|
|
platform["toY"],
|
|
platform["speed"],
|
|
platform.get("oneWay", False),
|
|
)
|
|
)
|
|
exitPoint = record.get("exit")
|
|
level.exit = tuple(exitPoint) if exitPoint else None
|
|
return level
|
|
|
|
|
|
def saveLevelJson(level, path):
|
|
with open(path, "w", encoding="ascii") as handle:
|
|
json.dump(level.asDict(), handle, indent=2)
|
|
|
|
|
|
def loadLevelJson(path):
|
|
with open(path, "r", encoding="ascii") as handle:
|
|
record = json.load(handle)
|
|
return levelFromDict(record)
|
|
|
|
|
|
def loadLevel(source):
|
|
if isinstance(source, Level):
|
|
return source
|
|
if isinstance(source, dict):
|
|
return levelFromDict(source)
|
|
if isinstance(source, (list, tuple)):
|
|
return parseAscii("ascii", list(source))
|
|
if isinstance(source, str) and os.path.isfile(source):
|
|
return loadLevelJson(source)
|
|
raise ValueError("unsupported level source: %r" % (source,))
|