Files

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,))