diff --git a/assets/sprites/iso_grass.png b/assets/sprites/iso_grass.png new file mode 100644 index 000000000..70e49ff28 Binary files /dev/null and b/assets/sprites/iso_grass.png differ diff --git a/assets/sprites/iso_grass_darker.png b/assets/sprites/iso_grass_darker.png new file mode 100644 index 000000000..596fa6e4f Binary files /dev/null and b/assets/sprites/iso_grass_darker.png differ diff --git a/demo/isometricLevel.js b/demo/isometricLevel.js new file mode 100644 index 000000000..858c70304 --- /dev/null +++ b/demo/isometricLevel.js @@ -0,0 +1,69 @@ +// Build levels with addIsometricLevel() + +// Start game +kaboom() + +// Load assets +loadSprite("grass", "/sprites/iso_grass.png") +loadSprite("darker_grass", "/sprites/iso_grass_darker.png") + +gravity(0) +camScale(0.33) +camPos(vec2(100, 1000)) + +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([ + // 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.