Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

<!-- markdownlint-disable no-duplicate-heading blanks-around-fences single-h1 -->

All notable changes to this project will be documented in this file.

The format is (mostly) based on
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions examples/multicharLevel.js
Original file line number Diff line number Diff line change
@@ -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")],
},
});
44 changes: 36 additions & 8 deletions src/ecs/components/level/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,31 @@ export interface LevelCompOpt {
sym: string,
pos: Vec2,
) => CompList<Comp> | 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;
}

/**
Expand Down Expand Up @@ -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<LevelComp>) => {
const size = level.numRows() * level.numColumns();
const traverse = (i: number, index: number) => {
Expand Down Expand Up @@ -344,13 +369,15 @@ export function level(map: string[], opt: LevelCompOpt): LevelComp {
id: "level",

add(this: GameObj<LevelComp>) {
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() {
Expand All @@ -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(
Expand Down