From dcba23cee31e354b901f1a1ea64ae98d7de70e2c Mon Sep 17 00:00:00 2001 From: Gustavo Giserman Date: Wed, 10 Nov 2021 03:15:52 -0300 Subject: [PATCH] Implement isometric level support Refactor isometric grid rendering Fix isometric level demo Improve isometric level support and add more demo examples Lint addIsometricLevel: Mirror width and height property names from addLevel Remove unnecessary from2dTo1dIndex method of isometricLevel Rename and lint isometricLevel interface to match levels --- assets/sprites/iso_grass.png | Bin 0 -> 2009 bytes assets/sprites/iso_grass_darker.png | Bin 0 -> 2437 bytes demo/isometricLevel.js | 69 +++++++++++++++++ src/kaboom.ts | 116 ++++++++++++++++++++++++++++ src/types.ts | 73 +++++++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 assets/sprites/iso_grass.png create mode 100644 assets/sprites/iso_grass_darker.png create mode 100644 demo/isometricLevel.js diff --git a/assets/sprites/iso_grass.png b/assets/sprites/iso_grass.png new file mode 100644 index 0000000000000000000000000000000000000000..70e49ff28298301cf70667536fc541b6025a2931 GIT binary patch literal 2009 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 xT4cU&v9 [ + sprite("grass"), + ], + "!": () => [ + sprite("darker_grass"), + ], +}) + +addIsometricLevel([ + // 15x9 grid + "@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@", + "@@@@@@@!@@@@@@@", + "@@@@@@@!@@@@@@@", + "@@@@@@@!@@@@@@@", + "@@@@@@@!@@@@@@@", + "@@@@@@@!@@@@@@@", + "@@@@@@@@@@@@@@@", + "@@@@@@@!@@@@@@@", + "@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@", +], { + width: 144, + height: 71, + "@": () => [ + sprite("grass"), + ], + "!": () => [ + sprite("darker_grass"), + ], + // The position of the top left block + pos: vec2(0, 1200), +}) diff --git a/src/kaboom.ts b/src/kaboom.ts index 30d543163..ea36a3e54 100644 --- a/src/kaboom.ts +++ b/src/kaboom.ts @@ -142,6 +142,8 @@ import { BoomOpt, PeditFile, Shape, + IsometricLevelOpt, + IsometricLevel, } from "./types" import FPSCounter from "./fps" @@ -5094,6 +5096,18 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } + function isometricGrid(isometricLevel: IsometricLevel, position: Vec2) { + + return { + + id: "isometricGrid", + gridPos: position.clone(), + level: isometricLevel, + + } + + } + function addLevel(map: string[], opt: LevelOpt): GameObj { if (!opt.width || !opt.height) { @@ -5193,6 +5207,106 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } + function addIsometricLevel(map: string[], options: IsometricLevelOpt): IsometricLevel { + if (!options.width || !options.height) { + throw new Error("Must provide isometric level tile width & height.") + } + + const objects: GameObj[] = [] + const offset = vec2(options.pos || vec2(0)) + + const halfTileWidth = Math.floor(options.width / 2) + const halfTileHeight = Math.floor(options.height / 2) + + const maxWidthInTiles = map.reduce((width, row): number => Math.max(width, row.length), 0) + const heightInTiles = map.length + + const isometricLevel = { + offset() { + return offset.clone() + }, + + gridWidth() { + return options.width + }, + + gridHeight() { + return options.height + }, + + getPos: (...args): Vec2 => { + const p = vec2(...args) + return vec2((p.x - p.y) * halfTileWidth, (p.x + p.y) * halfTileHeight) + }, + + spawn: (position: Vec2, symbol: string): GameObj => { + const comps = (() => { + if (options[symbol]) { + if (typeof options[symbol] !== "function") { + throw new Error("isometric level symbol def must be a function returning a component list") + } + return options[symbol](position) + } else if (options.any) { + return options.any(symbol, position) + } + })() + + if (!comps) { + return + } + + const posComp = vec2( + offset.x + position.x, + offset.y + position.y, + ) + + for (const comp of comps) { + if (comp.id === "pos") { + posComp.x += comp.pos.x + posComp.y += comp.pos.y + + break + } + } + + comps.push(pos(posComp)) + comps.push(isometricGrid(this, position)) + + const object = add(comps) + objects.push(object) + + return object + }, + + width() { + return maxWidthInTiles * options.width + }, + + height() { + return heightInTiles * options.height + }, + + destroy() { + for (const someObject of objects) { + destroy(someObject) + } + }, + } + + for (let row = 0; row < heightInTiles; row++) { + for (let col = 0; col < maxWidthInTiles; col++) { + const position = isometricLevel.getPos(col, row) + const rowContent: string = map[row] + const symbols: string[] = rowContent.split("") + const symbol = symbols[col] + + isometricLevel.spawn(position, symbol) + } + } + + return isometricLevel + } + function record(frameRate?): Recording { const stream = app.canvas.captureStream(frameRate) @@ -6180,6 +6294,8 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { go, // level addLevel, + // isometric level + addIsometricLevel, // storage getData, setData, diff --git a/src/types.ts b/src/types.ts index d98f9d18f..d585a254b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1650,6 +1650,46 @@ export interface KaboomCtx { * ``` */ addLevel(map: string[], options: LevelOpt): GameObj, + /** + * Construct a isometric level based on symbols. + * + * @section IsometricLevel + * + * @example + * ```js + * addIsometricLevel([ + * // Design the isometric level layout with symbols + * // 15x15 grid in this case so we have a perfect tiled square diamond shaped map + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@!@@@@!@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@!@@@!@@@@@", + * "@@@@@@!!!@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * "@@@@@@@@@@@@@@@", + * ], { + * // The size of each grid + * width: 144, + * height: 71, + * // Define what each symbol means (in components) + * "@": () => [ + * sprite("grass"), + * ], + * "!": () => [ + * sprite("darker_grass"), + * ], + * }) + * ``` + */ + addIsometricLevel(map: string[], options: IsometricLevelOpt): IsometricLevel, /** * Get data from local storage, if not present can set to a default value. * @@ -4313,6 +4353,39 @@ export interface LevelComp extends Comp { levelHeight(): number, } +export interface IsometricLevelOpt { + /** + * Width of each block. + */ + width: number, + /** + * Height of each block. + */ + height: number, + /** + * Position of the first block. + */ + pos?: Vec2, + /** + * Called when encountered an undefined symbol. + */ + any?: (s: string, pos: Vec2) => CompList | undefined, + // TODO: should return CompList + [sym: string]: any, +} + +export interface IsometricLevel { + spawn(position: Vec2, symbol: string): GameObj, + gridWidth(): number, + gridHeight(): number, + offset(): Vec2, + getPos(p: Vec2): Vec2, + getPos(x: number, y: number): Vec2, + width(): number, + height(): number, + destroy(): void, +} + export interface BoomOpt { /** * Animation speed.