oden/game/level.ts
2023-08-23 20:10:22 -07:00

295 lines
6.6 KiB
TypeScript

// NOTE: It's super not clear how much of this should be in rust vs
// javascript. Dealing with the level code is really nice in javascript
// but there's a bunch of weird stuff that really feels lower level.
import { load_texture } from "./assets";
import { Texture, print, spr, use_texture } from "./graphics";
import { load_string } from "./io";
// TODO: Use io-ts? YIKES.
export interface Tile {
px: [number, number];
src: [number, number];
f: number;
t: number;
a: number;
}
export interface TileSet {
id: number;
texture: Texture;
tags: Map<string, number[]>;
}
export interface TileLayer {
type: "tile";
tileset: TileSet;
grid_size: number;
offset: [number, number];
tiles: Tile[];
}
export interface Entity {
type: string;
id: string;
px: [number, number];
bounds: [number, number];
// TODO: More props here.
}
export interface EntityLayer {
type: "entity";
entities: Entity[];
}
export interface Level {
world_x: number;
world_y: number;
width: number;
height: number;
cw: number;
ch: number;
tile_layers: TileLayer[];
entities: Entity[];
values: number[];
}
export interface World {
levels: Level[];
tilesets: Map<number, TileSet>;
}
interface LDTKTilesetDef {
uid: number;
relPath: string;
enumTags: { enumValueId: string; tileIds: number[] }[];
}
async function load_tileset(def: LDTKTilesetDef): Promise<TileSet> {
let relPath = def.relPath as string;
if (relPath.endsWith(".aseprite")) {
// Whoops let's load the export instead?
relPath = relPath.substring(0, relPath.length - 8) + "png";
}
let texture = await load_texture(relPath);
let tags = new Map();
for (const et of def.enumTags) {
tags.set(et.enumValueId, et.tileIds);
}
print("Loaded tileset", def.uid, "from", relPath, "as ID", texture.id());
return { id: def.uid, texture, tags };
}
interface LDTKTileLayerInstance {
__type: "Tiles";
__gridSize: number;
__pxTotalOffsetX: number;
__pxTotalOffsetY: number;
__tilesetDefUid: number;
gridTiles: {
px: [number, number];
src: [number, number];
f: number;
t: number;
a: number;
}[];
}
function load_tile_layer_instance(
tile_sets: Map<number, TileSet>,
li: LDTKTileLayerInstance
): TileLayer {
const tileset = tile_sets.get(li.__tilesetDefUid);
if (!tileset) {
throw new Error("Unable to find texture!!! " + li.__tilesetDefUid);
}
return {
type: "tile",
tileset,
grid_size: li.__gridSize,
offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY],
tiles: li.gridTiles,
};
}
interface LDTKEntityLayerInstance {
__type: "Entities";
entityInstances: {
__identifier: string;
iid: string;
width: number;
height: number;
px: [number, number];
}[];
}
function load_entity_layer_instance(li: LDTKEntityLayerInstance): Entity[] {
return li.entityInstances.map((ei) => {
return {
type: ei.__identifier,
id: ei.iid,
px: ei.px,
bounds: [ei.width, ei.height],
};
});
}
interface LDTKIntGridLayerInstance {
__type: "IntGrid";
__cWid: number;
__cHei: number;
intGridCsv: number[];
}
type LDTKLayerInstance =
| LDTKTileLayerInstance
| LDTKEntityLayerInstance
| LDTKIntGridLayerInstance;
function is_tile_layer_instance(
x: LDTKLayerInstance
): x is LDTKTileLayerInstance {
return x.__type == "Tiles";
}
function is_entity_layer_instance(
x: LDTKLayerInstance
): x is LDTKEntityLayerInstance {
return x.__type == "Entities";
}
function is_intgrid_layer_instance(
x: LDTKLayerInstance
): x is LDTKIntGridLayerInstance {
return x.__type == "IntGrid";
}
type LDTKLevel = {
worldX: number;
worldY: number;
pxWid: number;
pxHei: number;
layerInstances: LDTKLayerInstance[];
};
function load_level(tile_sets: Map<number, TileSet>, def: LDTKLevel): Level {
const result: Level = {
world_x: def.worldX,
world_y: def.worldY,
width: def.pxWid,
height: def.pxHei,
cw: 0,
ch: 0,
tile_layers: [],
entities: [],
values: [],
};
for (const li of def.layerInstances) {
if (is_tile_layer_instance(li)) {
const tli = load_tile_layer_instance(tile_sets, li);
result.tile_layers.push(tli);
} else if (is_entity_layer_instance(li)) {
// TODO: Why would I support multiple entity layers?
result.entities = load_entity_layer_instance(li);
} else if (is_intgrid_layer_instance(li)) {
result.cw = li.__cWid;
result.ch = li.__cHei;
result.values = li.intGridCsv; // ?
} else {
print("WARNING: Unknown layer type");
}
}
return result;
}
interface LDTKMap {
__header__: {
fileType: "LDtk Project JSON";
app: "LDtk";
doc: "https://ldtk.io/json";
schema: "https://ldtk.io/files/JSON_SCHEMA.json";
appAuthor: "Sebastien 'deepnight' Benard";
appVersion: "1.3.3";
url: "https://ldtk.io";
};
defs: {
tilesets: LDTKTilesetDef[];
};
levels: LDTKLevel[];
}
function is_ldtk_map(map: unknown): map is LDTKMap {
if (
map instanceof Object &&
"__header__" in map &&
map.__header__ instanceof Object
) {
const header = map.__header__;
if ("fileType" in header && header.fileType == "LDtk Project JSON") {
return true;
}
}
return false;
}
export async function load_world(path: string): Promise<World> {
print("Loading map:", path);
const blob = await load_string(path);
const map = JSON.parse(blob);
if (!is_ldtk_map(map)) {
throw new Error("Map does not appear to be an LDTK level");
}
const tilesets = new Map<number, TileSet>();
let loaded_tilesets = await Promise.all(
map.defs.tilesets
.filter((def) => def.relPath != null)
.map((def) => load_tileset(def))
);
for (const ts of loaded_tilesets) {
tilesets.set(ts.id, ts);
}
const levels = map.levels.map((l: any) => load_level(tilesets, l));
return { levels, tilesets };
}
export function has_collision(level: Level, cx: number, cy: number): boolean {
if (!level) return true;
if (cx < 0 || cx >= level.cw) return false;
if (cy < 0 || cy >= level.ch) return false;
return level.values[cy * level.cw + cx] == 1; // TODO: MAGIC NUMBER?
}
export function draw_level(
level: Level,
offset_x: number = 0,
offset_y: number = 0
) {
for (const layer of level.tile_layers) {
use_texture(layer.tileset.texture);
let [ofx, ofy] = layer.offset;
ofx += offset_x;
ofy += offset_y;
for (const tile of layer.tiles) {
// TODO: Flip and whatnot.
spr(
tile.px[0] + ofx,
tile.px[1] + ofy,
layer.grid_size,
layer.grid_size,
tile.src[0],
tile.src[1]
);
}
}
}