| name | beat-em-up |
| description | Generates side-scrolling beat-em-up games (Double Dragon / Final Fight style) with pseudo-3D Y-depth movement, punch/kick combat, enemy AI, score HUD, and layered parallax backgrounds. Provides codesmith patterns for the genre. Use when the description involves street brawling, beat-em-up, fighting through waves of enemies. |
Beat-Em-Up
Classic side-scrolling brawler. Characters occupy a pseudo-3D "lane" — X is left/right, Y is depth into the screen. Y-position determines render depth (y-sort) and what counts as the same "row" for melee contact.
When to use
- "Make a beat-em-up / brawler / street fighter"
- "Game where I fight through waves of enemies"
- "Double Dragon / Final Fight / River City Ransom style game"
Genre settings
{
"genre": "beat-em-up",
"controls": {
"movement": "4-direction",
"actions": [
{ "key": "SPACE", "name": "attack", "description": "Punch nearby enemies" },
{ "key": "Z", "name": "jump", "description": "Jump over obstacles" }
]
}
}
Tileset palette
[
{ "id": "GROUND", "color": "#8b5e3c", "passable": true },
{ "id": "WALL", "color": "#4a3020", "passable": false },
{ "id": "SHADOW", "color": "#6b4a2a", "passable": true },
{ "id": "PROP", "color": "#7a6040", "passable": false }
]
Level layout: wide horizontal strip (30-40 tiles wide, 12-14 tiles tall). Ground fills rows 6-12. Wall tiles border top and bottom. Camera scrolls rightward only as player advances. Player cannot scroll camera back left.
Entity design
[
{ "id": "BRAWLER", "kind": "player", "color": "teal-blue", "desc": "Muscular hero in leather jacket and jeans", "states": ["idle","walk"], "speed": 90, "hp": 5 },
{ "id": "THUG", "kind": "enemy", "color": "dark-grey", "desc": "Street thug in torn shirt and sunglasses", "states": ["idle","walk"]
Background (REQUIRED for this genre)
Always invoke bg-artist with --theme forest-park or --theme city-street or --theme warehouse depending on the level theme. The layered background is what makes the genre look correct.
scrollFactor: 0.4 for mid-layer (trunks/buildings)
scrollFactor: 0.2 for far layer (sky/canopy)
Codesmith — Game.js patterns
Core constants
const FLOOR_Y_MIN = 160;
const FLOOR_Y_MAX = 280;
const ATTACK_RANGE_X = 48;
const ATTACK_RANGE_Y = 20;
const ATTACK_DURATION = 220;
const SCROLL_LOCK_X = 100;
Pseudo-3D movement
const speed = 90;
b.setVelocity(0, 0);
if (left) b.setVelocityX(-speed);
if (right) b.setVelocityX(+speed);
if (up) b.setVelocityY(-speed * 0.6);
if (down) b.setVelocityY(+speed * 0.6);
this.player.y = Phaser.Math.Clamp(this.player.y, FLOOR_Y_MIN, FLOOR_Y_MAX);
this.player.setDepth(this.player.y);
for (const e of this.enemies) e.setDepth(e.y);
const camX = Math.max(this.cameras.main., .. - .. * );
...(camX, );
Attack system
this.attacking = false;
this.attackBox = this.add.rectangle(0, 0, ATTACK_RANGE_X, ATTACK_RANGE_Y * 2, 0xffff00, 0).setDepth(999);
_onAttack() {
if (this.attacking || this.gameOver) return;
this.attacking = true;
this.player.setTint(0xffddaa);
this.time.delayedCall(ATTACK_DURATION, () => {
this.attacking = false;
this.player.clearTint();
});
}
if (this.attacking) {
const px = .. + (. ? * : - * );
( enemy .) {
(enemy. <= ) ;
(.(enemy. - px) < &&
.(enemy. - ..) < ) {
.(enemy);
}
}
}
() {
(enemy.) ;
enemy.--;
enemy. = ;
enemy.();
...(, );
. += ;
..(, {
enemy. = ;
enemy.();
});
(enemy. <= ) .(enemy);
}
() {
.++;
..({ : enemy, : , : enemy. - , : ,
: { enemy.?.(); enemy.(); }});
. = ..( e !== enemy);
.();
(. >= .) .();
}
Enemy AI
this.time.addEvent({
delay: 2200,
loop: true,
callback: () => {
if (this.enemies.length < 4 && !this.gameOver) this._spawnEnemy();
}
});
_spawnEnemy() {
const side = Math.random() < 0.5 ? -1 : 1;
const spawnX = this.player.x + side * (this.scale.width * 0.6 + 40);
const spawnY = Phaser.Math.Between(FLOOR_Y_MIN + 20, FLOOR_Y_MAX - 20);
const sh = this.findSheet('THUG');
if (!sh) return;
const enemy = this.add.(spawnX, spawnY, sh., sh. * sh.);
enemy. = ;
enemy. = ;
enemy.(, );
enemy.(spawnY);
enemy.();
..(enemy);
}
( enemy .) {
(enemy. <= ) ;
dx = .. - enemy.;
dy = .. - enemy.;
dist = .(dx, dy);
(dist > ) {
enemy. += (dx / dist) * * (delta / );
enemy. += (dy / dist) * * (delta / );
enemy. = ..(enemy., , );
enemy.(dx < );
enemy.(, );
enemy.(enemy.);
} {
enemy.(, );
(!enemy.) {
enemy. = ;
..(, {
enemy. = ;
(.(.. - enemy., .. - enemy.) < )
.();
});
}
}
}
HUD — health bar + score
_buildHud() {
const W = this.scale.width;
this.hudBg = this.add.graphics().setScrollFactor(0).setDepth(300);
this.hudBg.fillStyle(0x000000, 0.8);
this.hudBg.fillRect(0, 0, W, 36);
this.hudBg.lineStyle(1, 0x444444, 1);
this.hudBg.lineBetween(0, 36, W, 36);
this.hpTrack = this.add.graphics().setScrollFactor(0).setDepth(301);
this.hpTrack.fillStyle(, );
..(, , , );
. = ..().().();
. = ..(W - , , , {
: , : , : ,
: , : ,
}).().().(, );
..(, , , {
: , : , : ,
}).().();
.();
}
() {
pct = .(, . / .);
..();
..(pct > ? : pct > ? : , );
..(, , .( * pct), );
..( + (.).(, ));
}
win / lose / updateState
updateState() {
window.__gameState = {
phase: this.gameOver ? (this.won ? 'won' : 'lost') : 'playing',
playerX: this.player?.x ?? 0,
playerY: this.player?.y ?? 0,
playerHp: this.playerHp,
score: this.score,
enemiesDefeated: this.enemiesDefeated,
};
this._redrawHud();
}
Visual notes
- No tile collision for floor — the floor is visual only. Physics world gravity = 0 (no gravity). The Y floor band is enforced by clamping, not physics.
- Shadow under each character — draw a small dark ellipse at
(sprite.x, FLOOR_Y_MAX + 4) that scales with distance from back-Y: closer to front = larger shadow. Adds huge depth for minimal code.
- Punch flash — set
setTint(0xffddaa) for 220ms on the attacking character, clear after. No separate attack sprite frame needed.
- Enemy variety — if multiple enemy IDs exist, alternate spawning. Boss spawns after
enemiesDefeated >= WIN_ENEMIES - 2.