// 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, spr, use_texture } from "./graphics"; import { load_string } from "./io"; import { log } from "./log"; // 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; } 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 { iid: string; world_x: number; world_y: number; width: number; height: number; cw: number; ch: number; tile_layers: TileLayer[]; entities: Entity[]; values: { [key: string]: number[] }; neighbors: { n: string | null; s: string | null; e: string | null; w: string | null; }; } export interface World { levels: Level[]; level_map: Map; tilesets: Map; current_level: Level; } interface LDTKTilesetDef { uid: number; relPath: string; enumTags: { enumValueId: string; tileIds: number[] }[]; } async function load_tileset(def: LDTKTilesetDef): Promise { 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); } log( "map_load", "Loaded tileset", def.uid, "from", relPath, "as ID", texture.id() ); return { id: def.uid, texture, tags }; } interface LDTKTile { px: [number, number]; src: [number, number]; f: number; t: number; a: number; } interface LDTKTileLayerInstance { __type: "Tiles" | "IntGrid"; __identifier: string; __gridSize: number; __pxTotalOffsetX: number; __pxTotalOffsetY: number; __tilesetDefUid: number; autoLayerTiles: LDTKTile[]; gridTiles: LDTKTile[]; } function load_tile_layer_instance( tile_sets: Map, li: LDTKTileLayerInstance ): TileLayer | undefined { const tiles = li.autoLayerTiles.concat(li.gridTiles); if (tiles.length == 0) { return; } 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, }; } 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"; __identifier: string; __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" || x.__type == "IntGrid"; } 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 = { iid: string; worldX: number; worldY: number; pxWid: number; pxHei: number; layerInstances: LDTKLayerInstance[]; __neighbours: { levelIid: string; dir: "n" | "s" | "e" | "w" }[]; }; function load_level(tile_sets: Map, def: LDTKLevel): Level { const result: Level = { iid: def.iid, world_x: def.worldX, world_y: def.worldY, width: def.pxWid, height: def.pxHei, cw: 0, ch: 0, tile_layers: [], entities: [], values: {}, neighbors: { n: null, s: null, e: null, w: null }, }; for (const li of def.layerInstances) { if (is_tile_layer_instance(li)) { const tli = load_tile_layer_instance(tile_sets, li); if (tli) { result.tile_layers.push(tli); } } if (is_entity_layer_instance(li)) { // TODO: Why would I support multiple entity layers? result.entities = load_entity_layer_instance(li); } if (is_intgrid_layer_instance(li)) { result.cw = li.__cWid; result.ch = li.__cHei; result.values[li.__identifier] = li.intGridCsv; } } result.tile_layers = result.tile_layers.reverse(); const neighbors = result.neighbors; for (const n of def.__neighbours) { if (n.dir == "n") { neighbors.n = n.levelIid; } else if (n.dir == "s") { neighbors.s = n.levelIid; } else if (n.dir == "e") { neighbors.e = n.levelIid; } else if (n.dir == "w") { neighbors.w = n.levelIid; } } 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 { log("map_load", "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(); 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)); const level_map = new Map(); for (const level of levels) { level_map.set(level.iid, level); } const current_level = levels.find((l) => l.world_x == 0 && l.world_y == 0); if (!current_level) { throw new Error("UNABLE TO FIND LEVEL AT 0,0: CANNOT START"); } return { levels, level_map, tilesets, current_level }; } 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; const values = level.values.collisions; return values[cy * level.cw + cx] != 0; // 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] ); } } }