diff --git a/CHANGELOG.md b/CHANGELOG.md index 4daff334b..dce16df84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog + + All notable changes to this project will be documented in this file. The format is (mostly) based on @@ -44,6 +46,9 @@ So your change should look like: @mflerackers - Added xorshift32 as random generator (#1057) - @mflerackers - **(examples)** Added a new `gacha` example! (#1057) - @imaginarny +- Added the `LevelCompOpt.charsPerTile` option for the `level()` component and + `addLevel` which lets you use more than one character per tile, that way you + can make clearer maps without having to resort to Unicode - @dragoncoder047 - Added Alea as random generator (#1097) - @Stanko ### Changed @@ -95,6 +100,12 @@ So your change should look like: - Fixed broadphase objects cleared on scene switch including those with `stay()` (#1077) - @imaginarny, @mflerackers +### Added + +- Added a `repack: false` option to `loadSpite()` and a repack parameter to + `loadSpriteAtlas()`, for faster loading if you're packing stuff at build-time + (#1063) - @dragoncoder047 + ## [4000.0.0-alpha.27] - 2026-03-19 ### Added diff --git a/examples/multicharLevel.js b/examples/multicharLevel.js new file mode 100644 index 000000000..777a7465d --- /dev/null +++ b/examples/multicharLevel.js @@ -0,0 +1,31 @@ +/** + * @file Multi-Character Level + * @description Build levels from ASCII + * @difficulty 0 + * @tags basics, game + * @minver 4000.0 + * @category basics + */ + +kaplay(); + +loadBean(); +loadSprite("bobo", "/sprites/bobo.png"); +loadSprite("dino", "/sprites/dino.png"); +loadSprite("ghosty", "/sprites/ghosty.png"); + +addLevel([ + "bean dino", + " ghos bobo", +], { + pos: vec2(100, 100), + tileWidth: 50, + tileHeight: 50, + charsPerTile: 4, + tiles: { + bean: () => [sprite("bean")], + bobo: () => [sprite("bobo")], + dino: () => [sprite("dino")], + ghos: () => [sprite("ghosty")], + }, +}); diff --git a/src/ecs/components/level/level.ts b/src/ecs/components/level/level.ts index a239a5260..510ed0b6e 100644 --- a/src/ecs/components/level/level.ts +++ b/src/ecs/components/level/level.ts @@ -118,6 +118,31 @@ export interface LevelCompOpt { sym: string, pos: Vec2, ) => CompList | null | undefined; + /** + * The number of characters that specify one tile. + * + * If the line length isn't a multiple of this, the last tile will be padded + * with spaces to be the same length before things get looked up. + * + * @example + * ```js + * addLevel([ + * "BABA IS YOU", + * ], { + * tileWidth: 24, + * tileHeight: 24, + * charsPerTile: 4, + * tiles: { + * "BABA": () => [sprite("text_baba")], + * " IS ": () => [sprite("text_is")], + * "YOU ": () => [sprite("text_you")], + * } + * }); + * ``` + * + * @default 1 + */ + charsPerTile?: number; } /** @@ -242,7 +267,7 @@ export function level(map: string[], opt: LevelCompOpt): LevelComp { }; // The connectivity map is used to see whether two locations are connected - // -1: inaccesible n: connectivity group + // -1: inaccessible n: connectivity group const createConnectivityMap = (level: GameObj) => { const size = level.numRows() * level.numColumns(); const traverse = (i: number, index: number) => { @@ -344,13 +369,15 @@ export function level(map: string[], opt: LevelCompOpt): LevelComp { id: "level", add(this: GameObj) { - map.forEach((row, i) => { - const keys = row.split(""); - numColumns = Math.max(keys.length, numColumns); - keys.forEach((key, j) => { - this.spawn(key, vec2(j, i)); - }); - }); + const charsPerTile = opt.charsPerTile ?? 1; + for (let y = 0; y < map.length; y++) { + const row = map[y]!; + for (let j = 0, x = 0; j < row.length; j += charsPerTile) { + this.spawn(row.slice(j, j + charsPerTile), vec2(x, y)); + x++; + if (numColumns < x) numColumns = x; + } + } }, tileWidth() { @@ -370,6 +397,7 @@ export function level(map: string[], opt: LevelCompOpt): LevelComp { const comps = (() => { if (typeof key === "string") { + key = key.padEnd(opt.charsPerTile ?? 1); if (opt.tiles[key]) { if (typeof opt.tiles[key] !== "function") { throw new Error(