Mario-style platformer · qwen3.8-2.4t-a95b
Using the pre-provided phaser.min.js (global Phaser), build a playable side-scrolling platformer in the classic Mario style: run and jump across scrolling platforms, stomp enemies, collect coins, and reach a goal flag. Use ONLY procedurally generated graphics, no external image or audio assets.
transcript · 52 messages
Using the pre-provided phaser.min.js (global Phaser), build a playable side-scrolling platformer in the classic Mario style: run and jump across scrolling platforms, stomp enemies, collect coins, and reach a goal flag. Use ONLY procedurally generated graphics, no external image or audio assets.
thinking
Build a Mario-style platformer with Phaser 3 and procedurally-generated textures (Graphics → generateTexture). Single index.html. Let's write a complete game: player physics, platforms, enemies (Goombas), coins, goal flag, camera scrolling, score/lives HUD, restart.
First check whether phaser.min.js exists, then write the file.
Design:
- Create textures with Graphics: player (simple character), enemy, coin, ground tile, brick, flag, cloud, hill backgrounds.
- Level defined as tilemap strings or arrays.
- Phaser arcade physics: static bodies for platforms, enemies patrol, stomping detection, overlap for coin collection, flag for victory.
- Controls: arrows/WASD + space to jump.
- Camera follows the player.
- HUD: coins, score, lives, timer (maybe).
- Death → lose a life and respawn; game over screen; victory screen; R key to restart.
Write everything in one file. Keep it robust. Alright, let's write it.
Level layout: use a tilemap string with characters: '#' ground, 'B' brick, '?' coin block (maybe) — simpler: 'C' coin, 'E' enemy, 'P' player start position, 'F' flag, '=' platform. Build the level from the string grid, TILE size 32.
Physics: gravity ~2000. Player speed 220, jump velocity -700. Enemies walk, reverse direction at walls/edges (edge detection via tile check, or just reverse at walls; use body.blocked).
Stomping: player overlap with enemy; if player body is falling (velocity.y>0) and player's bottom is above enemy's center → destroy enemy, bounce.
Coins: overlap → collect, +100.
Flag: overlap → victory state.
Death: fall off the bottom of the world, or hit by enemy from the side → lose life, respawn.
Add particles? Simple: score popups can probably be skipped. Background: parallax clouds/hills via tileSprite or scrollFactor.
Phaser version: global Phaser from phaser.min.js. Use Phaser.Game with scene.
Let's carefully write the code.
Texture generation:
- 'player': 24x30 red cap, blue overalls, skin-colored face.
- 'enemy': 28x24 brown mushroom-like blob with eyes.
- 'coin': 20x20 yellow circle with shine, maybe use animation frames — generate 4 frames with different widths for rotation.
- 'ground': 32x32 brown with grass on top.
- 'brick': 32x32 brick pattern.
- 'block': question block? Maybe add '?' blocks that spit out coins when hit from below — nice but optional. Keep it: '?' block gives coin once when hit from below. Adds fun. Implement: block is static; in physics world overlap, or check for player's head collision: use body.blocked.up and check the tile above. Simpler: when player is blocked.up, find qblocks overlapping above player's head. Loop through qblock group and check distance.
- 'flag': pole + flag: make pole tile and flag sprite.
- 'cloud', 'hill', 'bush' for background.
Coin animation: 4 textures coin0..coin3 with different ellipse widths.
Scene structure: Boot scene generates textures, Game scene.
HUD: score text, coins, lives, fixed to camera.
Camera: startFollow, setBounds. World bounds from level width.
Timer probably skipped; keep it simple with score/lives/coins.
Let's write the level map as an array of strings, each row same length. Height ~15 rows, width ~200 columns.
Let's design the level (15 rows, index 0 at top). Tile 32px, game 960x540. Rows: 540/32 ≈ 16.9, use 17 rows; ground is rows 14-16.
Legend:
- 'X' ground block (grass top on row 14, dirt below)
- 'B' brick
- '?' qblock
- 'C' coin
- 'E' enemy spawn
- 'P' player start
- 'F' flag base position (pole is from there upward)
- 'G' goal flag (place pole tiles)
- '.' empty
Let me write ~220 columns of rows. Write it programmatically but hand-design it. Actually easier: build the rows via code as strings; hand-craft it. To avoid errors, make a helper: pad rows to equal length.
Let me write rows (17 rows). Write with gaps: ground with gaps (holes), bricks/coin platform rows, floating coin arcs, enemies on ground.
Craft it carefully but not too long (~200 chars per row). Write the string.
Row indices 0..16. Ground rows: 14, 15, 16 = 'X'. Holes: omit X at some columns.
Plan column ranges:
- 0-15: flat start, P at column 2 row 13.
- 16-19: '?B?B?' structure at row 10, coins above.
- Enemy around col 20.
- 24-27: hole (gap in ground).
- 28-35: ground, brick platform at row 10 col 30-33 with coins.
- Enemy at 32.
- 36-38: hole, with floating platform 'B' at row 12 col 37? Let's have 'B' bricks at row 12 spanning 36-38.
- 40-55 ground; stairs up: X step pyramid at 46-49 (heights 1,2,3,4); enemy at 43, 52. Coins.
- 56-58 hole.
- 59-75: ground; qblock at row 10 col 62; bricks at row 7 col 65-67, coins above; enemies 63, 70.
- 76-78 hole with platform at row 11.
- 80-100: ground; arc of coins; enemies at 85, 88, 92.
- Stairs at 95-99.
- 100-102 hole.
- 103-120: ground, brick platforms at row 9 and row 6 with coins, enemies 108, 114.
- 121-124 hole with stepping platforms at row 12, row 10.
- 125-145 ground; enemies 128,132,136; pyramid stairs 140-143.
- 146-148 hole.
- 150-200: flat home stretch, enemies 155, 160; flag at col 170: pole rows 4-13 col 170 ('F' marker to place pole), flag sprite. Castle? Skip, or a simple castle decoration maybe.
- Level width 210, with wall at end.
Hand-writing the exact strings for 17 rows × 210 columns is error-prone but doable if we build it by segments. Alternative: build the level in code: a function addGround(c0,c1), addBrick(c,r), etc. Cleaner and less error-prone. Let's go with programmatic level construction.
Implementation:
```js
const T=32, ROWS=17, LEVEL_W=210;
const solids = []; // {c,r,type}
function ground(c0,c1){ for(let c=c0;c<=c1;c++){ for(let r=14;r<=16;r++) setTile(c,r,'X'); } }
function tile(c,r,t){ map[r][c]=t; }
```
Use a 2D array of characters, then iterate to create static sprites.
Enemy group: spawn at specified columns on ground row 13 (y = 13*32 + 16).
Code plan:
Game scene create():
- Generate textures in init/boot (either call createTextures() in preload, or with graphics inside create before adding; do it in a Boot scene's create, then start Game).
- Build map, create static layer via individual images? For performance, use one static group with 32px images — a few hundred sprites; fine. Use Phaser.Physics.Arcade.StaticGroup with 'ground'/'brick'/'qblock' keys.
- Coins: physics group with bodies setAllowGravity false, static? Use static group for coins with overlap.
- Enemies: physics group, each velocity.x = -60; collides with solids; reverse direction on blocked.
- Player: sprite, body 20x28, maxVelocity, drag.
- Camera bounds, world bounds.
- Background: tileSprite sky gradient as camera bg? Add clouds with scrollFactor 0.3, hills with scrollFactor 0.6, generated textures, placed repeatedly.
- Input cursors + WASD + space.
- HUD text with setScrollFactor(0).
update():
- Player controls; flip sprite; jump with coyote/buffer optional — keep simple but allow a bit of jump buffering.
- Enemy patrol: if body.blocked.left/right reverse; also if touching world edge.
- Colliders: player vs solids, enemies vs solids, player vs enemies (stomp check), player vs coins, player vs qblocks handled via blocked.up: when player.body.blocked.up, loop qblocks for blocks whose x within player x± and bottom near block top... Actually blocked.up occurs on collision; easier: in update, if player.body.blocked.up, loop qblock group and find ones whose x is within player x ± and bottom near block top. Hit: change texture to 'usedblock', add 200 score, spawn floating coin text.
- Fall death: player.y > ROWS*32 + 100.
- Flag overlap → victory.
- Camera follow with look-ahead? Just follow.
Death handling: lives--, if >0 respawn (reset position, reset enemies? just reset player pos and velocity), else game over overlay. Restart via R key or click button → scene.restart().
Victory overlay with score, R to restart.
Extra juice: squash scale on player on landing, spinning coin animation, enemy stomp → flatten then destroy, score popup text.
Sound: no audio assets; could use WebAudio oscillator beeps — allowed (procedural, no assets). Add a small helper with AudioContext for jump/coin/stomp sounds. Wrap in try/catch. Good.
Let's write it.
Coin animation frames: generate 'coin0'..'coin3': ellipse widths 20, 14, 6, 14.
Textures via this.make.graphics({add:false}) then generateTexture.
Player texture draw (26x30):
- Red cap: top rectangle
- Skin face
- Blue overalls body
- Boots.
Enemy (28x26): brown dome, darker feet, white eyes.
Ground tile: fill #8a5a2b, top strip grass #3faa43, dark speckles, border.
Brick: #b5651d with dark lines.
qblock: yellow with '?' text? Graphics text not easy; draw '?' as shapes, or use fillText? Graphics doesn't do text. Use this.add.text separately? For texture, draw '?' via lines/arc — approximated with a circle + tail rectangle + dot. Or simpler: draw a rounded square with 4 dots rivets and a white square in middle? Draw a '?' style: arc (partial circle) + vertical stub + dot. graphics.arc exists. Use graphics.arc(x,y,r,start,end) then strokePath. Good enough.
Used block: brown, dull.
Flag: pole = thin grey tile of height 10; flag texture: green triangle. Build pole as multiple small images, or one tall generated texture (12 wide, 320 tall) — generateTexture supports any size.
Cloud: white blob, ellipse.
Hill: green triangle-ish, use fillTriangle with big size.
Bush: few green circles.
Sky: gradient background via graphics 2x540 generated as 'sky' texture with gradient fill? Graphics supports gradientFill? fillGradientStyle exists. Use that: g.fillGradientStyle(top, top, bottom, bottom). Generate a 4x540 texture, add image at fixed position with scrollFactor 0, displayWidth = width. Or set scene backgroundColor and skip gradient; but gradient is nicer. Use camera.setBackgroundColor? Simpler: add sky image with scrollFactor 0, sized to game, fixed. Camera moves but scrollFactor 0 keeps it fixed. Good.
Parallax clouds: create images at random positions in world with scrollFactor 0.4; but fixed with scrollFactor <1 so positions scale; place them covering range 0..worldWidth*0.4. Fine.
OK now let's write the whole thing. Also handle pause? Skip.
Death respawn: also respawn enemies? Classic: reset level. Reposition enemies to initial positions and coins stay collected? Simpler: save enemies initial positions and reset. Keep it: store spawn data and reset function that repositions enemies (revives destroyed ones? enemies removed from group via destroy). Alternative: recreate group on respawn. Structure Game scene with buildLevel() that clears groups and rebuilds, called on start and respawn (death resets coins too? Mario keeps coins). Simplify: on death, rebuild everything but keep score/coins/lives. On full restart (R) reset score.
So scene.create: reset score/coins/lives, then buildWorld(). die() → lives--; if lives>0 rebuildWorld() else showGameOver.
buildWorld(): destroy existing groups if any, rebuild tiles/coins/enemies/flag/player position.
HUD updated via function.
Let's now write out code fully, watching Phaser API:
- this.physics.add.staticGroup? Use this.physics.add.staticGroup({classType: Phaser.Physics.Arcade.Sprite}) — easier: create group `this.solids = this.physics.add.staticGroup();` then `this.solids.create(x,y,'ground')`. Static body size = texture size. Good.
- Coins: staticGroup too, with animation 'spin'.
- Enemies: this.physics.add.group({allowGravity:true}) with create, body.setSize probably not needed.
- Player: this.physics.add.sprite.
Collisions:
```
this.physics.add.collider(this.player, this.solids);
this.physics.add.collider(this.enemies, this.solids);
this.physics.add.collider(this.enemies, this.enemies); // optional
this.physics.add.overlap(this.player, this.coins, collectCoin);
this.physics.add.overlap(this.player, this.enemies, hitEnemy);
this.physics.add.overlap(this.player, this.flagZone, winGame);
```
Flag zone: invisible sprite with body.
hitEnemy(player, enemy):
```
if (player.body.velocity.y > 100 && player.body.bottom - enemy.body.top < 20) stomp
else die();
```
Better: player.body.touching.down? Standard check: player.body.velocity.y > 0 && player.body.y + player.body.height - enemy.body.y < 16.
Stomp: disable enemy body, setScale y to 0.4, tween alpha, destroy after 300ms; player.body.velocity.y = -350 bounce; score += 100; popup.
qblock hit: in update after physics step:
```
if (this.player.body.blocked.up) {
for (const b of this.qblocks.getChildren()) {
if (!b.getData('used') && Math.abs(b.x - player.x) < 24 && (player.body.top - b.body.bottom) < 6 && player.body.top - b.body.bottom > -10) hit
}
}
```
Hmm, blocked.up means player's top touches block bottom. Condition: b.body.bottom ≈ player.body.top, and horizontal overlap. b.y - block is above player so player.body.top <= b.body.bottom. Check: Math.abs((b.body.bottom) - player.body.top) < 8 and horizontal overlap. Then set used, change texture to 'usedblock', spawn coin pop animation + score 200, coins++.
Also allow hitting brick from below? Skip, or small bump animation. Skip.
Enemy edge turn: just turn at walls; place enemies carefully so they stay on platforms; if falling edge, let them fall into hole and die (destroy if y > world). Also reverse randomly? Fine.
Timer: add elapsed time? Skip.
Camera: this.cameras.main.setBounds(0,0,W,H*...); startFollow(player, true, 0.1, 0.1); setDeadzone(200,100)?
Player physics numbers: gravity 1800, jump -650, move speed 240, accel 1200, drag 1000, maxVel y 1000.
Variable jump: release jump → if velocity.y < -200, set to -200. Nice feel.
Input: cursors, keys WASD, space/up/W = jump.
Jump only when on ground (body.blocked.down or touching.down).
Death: falling y > 17*32+64 → die (no stomp).
Flag: pole at column FC; flag zone around pole. On win: freeze controls, player walks? Simple: show overlay text "COURSE CLEAR!", score bonus +1000, R to restart.
HUD: top-left "SCORE 000000", "COINS ×NN", "LIVES ×N". Retro font via CSS font family monospace.
Add title/instructions overlay at start: "Press any key / arrow to start"? Just show instruction text at bottom: "←→/AD move SPACE/↑ jump R restart".
Sound: simple beep function:
```js
let actx;
function beep(freq, dur=0.08, type='square', vol=0.04, slide=0){
try{
actx = actx || new (window.AudioContext||window.webkitAudioContext)();
const o=actx.createOscillator(), g=actx.createGain();
o.type=type;o.frequency.value=freq;
if(slide) o.frequency.linearRampToValueAtTime(freq+slide, actx.currentTime+dur);
g.gain.value=vol; g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+dur);
o.connect(g).connect(actx.destination);
o.start(); o.stop(actx.currentTime+dur);
}catch(e){}
}
```
AudioContext needs user gesture; create on first keydown. Fine.
Now write the whole file. Let's also make the level build list:
```js
const T=32, ROWS=17, GROUND=14, LEVEL_W=214;
let grid;
function setT(c,r,ch){ if(c>=0&&c<LEVEL_W&&r>=0&&r<ROWS) grid[r][c]=ch; }
function fillRow(c0,c1,r,ch){for(let c=c0;c<=c1;c++)setT(c,r,ch);}
function ground(c0,c1){fillRow(c0,c1,14,'X');fillRow(c0,c1,15,'X');fillRow(c0,c1,16,'X');}
function stairs(c,h,dir){ // pyramid steps of dir asc
}
```
Level plan (columns):
- ground(0,25)
- P at (2,12)
- qblock row: '?' at (16,10), 'B' at (17,10), '?' at (18,10), 'B' at (19,10), '?' at (20,10); coins above at row 8? maybe no coins above. Add coin at (18,7).
- Enemy 22.
- ground(29,55) → hole 26-28 (3-wide).
- Bricks row 10 cols 32-35 'BBBB', coins row 7 cols 32-35? Coins at (33,7), (34,7).
- '?' at (37,10).
- Enemies 34, 40.
- Pyramid stairs up at 46-49: heights 1..4: set X above ground: col 46 row 13; col 47 rows 12-13; col 48 rows 11-13; col 49 rows 10-13.
- Hole 56-58. ground(59,84).
- '?' at (61,10); bricks at row 7 (64-67): 'BBBB'; coins above at row 5 (64-67)? Coins at row 4? Reachable from bricks (row 7, standing on top of row 6). Jump height ~3-4 tiles. Coin at (65,4), (66,4)? Hmm, let's place at row 4 above bricks: coin y row 4. ok coins (64,4), (65,4), (66,4), (67,4).
- Enemies 63, 69, 74.
- Platform: 'B' at (77-79, row 11)? Hole 85-88. Let's do: ground(59,84); hole 85-88, 4-wide; floating bricks row 12 at cols 86-87. ground(89,118).
- Coin arc over hole: (85,10), (86,9), (87,9), (88,10).
- Enemies 93, 97, 101.
- qblocks at (95,10), (97,10)? Add '?' at 95 r10, 'B' at 96 r10, '?' at 97 r10.
- Pyramid at 106-109 heights 4..1 (descending)? Stairs up then gap: col 106 rows 10-13, 107 11-13, ... Let's do descending: heights 4,3,2,1.
- Hole 119-122; platforms at row 11 col 120, row 12? Use 'B': (120,11), (121,12)? Just single brick at (120,12) and (121,10)? Keep one platform at (120,12), (121,12).
- ground(123,150).
- Brick platforms: row 10 126-128, row 7 131-133, coins above each.
- Enemies 127, 135, 140, 143 (a bunch).
- Big stairs at 145-148: heights 1..4 ascending; then hole 149-151; ground(152,213).
- After hole, descending stairs 152-155 heights 4..1? Classic. ok.
- Enemies 160, 164.
- Coin row at (166-170, r10)?? Coins at row 10, 166-170? Place 5 coins in a row at row 11.
- Flag at col 180: pole rows 4..13, base 'X' at (180,13)? Ground is from row 14; put a block base at row 13: setT(180,13,'B')? Flag marker 'F' = pole. Set pole at col 180 rows 4-12 with 'F', base at (180,13) with 'X'. Flag sprite attached at row 5.
- End wall: col 190-194 castle decoration (drawn as bricks?): build a small castle with 'B' at rows 11-13 cols 190-194 and top 12-13? Just make a simple brick structure with a decorative image; simpler: brick tiles at rows 12-13 cols 190-194, rows 10-11 cols 191-193, row 9 cols 192. And 'X' wall at end col 208-213? Just level bound.
Level width 214 → world 6848 px.
Coins placed: define list as we go via setT(c,r,'C').
Coin list: (18,7); (33,7), (34,7); (32,10)? no, bricks at 32-35 r10, coins above at row 7: (32,7),(33,7),(34,7),(35,7)? standing on bricks (top r10, stand on r9) jump to reach r7 coin ok.
(64,4)..(67,4).
Arc (85,10), (86,9), (87,9), (88,10).
(95,7), (97,7)? above qblocks.
(126,7), (127,7), (128,7)? above bricks row 10: coins at r7. Hmm, stand on bricks at r10 (feet at r9, head at r8) so coin at r7 reachable.
(131,4), (132,4), (133,4) above bricks r7.
Row (166..170, 11).
(146,8)? coins above staircase tops: (148,8).
Total ~30 coins.
Enemy spawn list: cols on ground: y = 13*T+? Enemy is 26 tall; spawn at y = GROUND*T - 13 (center), so feet on ground: y = 14*32 - 13 = 435. Just spawn at (c*T+16, 13*T+16) and let gravity settle. Fine.
Flag zone at (180*T, rows 4..13): create invisible sprite at x=180*T+16, y from 4*T to 14*T, width 24, height 10*T.
Now write everything. Watch out: static group's create returns sprite; set body immovable (statics are immovable by default).
Enemies collide with solids; enemies also collide with each other so they reverse: on collision both reverse. Use collider enemies-enemies and enemies-solids, check blocked each frame in update.
Enemy update:
```
enemies.children.each(e => {
if(e.getData('dead'))return;
if(e.body.blocked.left || e.body.blocked.right) e.setVelocityX(-Math.sign(e.body.velocity.x)*60 || 60);
else if(!e.body.velocity.x) e.setVelocityX(e.getData('dir')*60);
if(e.y > ROWS*T+100) e.destroy();
});
```
Simpler: keep dir data; on blocked, dir*=-1, save, setVelocityX(dir*60).
Also initial direction -1 (walk left toward player).
Player animation: just flipX and slight rotation on jump? Add squash/stretch via scale tween on jump/land.
Coin collect: coin.disableBody, popup "+100", score, coins++, beep.
Respawn on death: implement resetWorld() that rebuilds everything (also recreate player). Structure Game scene with buildLevel() that clears groups and rebuilds, called on start and respawn.
buildWorld(): destroy existing groups if any, rebuild.
HUD updated via function.
Text style: fontFamily 'Courier New', bold, with shadow.
Overlay states: use this.state = 'play' | 'dead' | 'over' | 'win'. When not play, ignore inputs (maybe except R).
Restart key R: full scene restart via this.scene.restart().
Now let's write the file. Watch for template literals inside — fine.
One concern: Phaser staticGroup with generateTexture keys — ok.
Cloud placement: create N clouds with scrollFactor 0.35: x in 0..LEVEL_W*T*0.35+960, y 40..180. Hills scrollFactor 0.6, on ground line: y = 14*T, origin bottom. Bush scrollFactor 0.8.
Sky image scrollFactor 0, 960x540.
Draw order: sky, hills, clouds, bushes, then solids etc.
Now let's write the code.
Also handle window blur pause? Skip.
Let's write index.html.
Edge case: on flag, winGame sets state 'win', show overlay text, bonus score, play. Player stops moving (set velocity 0), slide down pole maybe? Keep simple: freeze.
Game over: overlay + "Press R".
Also display coin count and lives on HUD with icons? Text is fine.
Time bonus? Skip.
Code:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Super Pixel Bros</title>
<style>
html,body{margin:0;padding:0;background:#111;height:100%;overflow:hidden}
#game{display:flex;align-items:center;justify-content:center;height:100%}
canvas{image-rendering:pixelated}
</style>
</head>
<body>
<div id="game"></div>
<script src="phaser.min.js"></script>
<script>
...
</script>
</body>
</html>
```
Scale: Phaser.Scale.FIT, autoCenter, width 960 height 540.
Write the full JS. Physics config: arcade gravity y 1800? Set per-scene with this.physics.world.gravity.y=1800.
Texture generation details:
```js
function makeTextures(scene){
let g = scene.make.graphics({add:false});
// ground
g.clear();
g.fillStyle(0x9c5a24); g.fillRect(0,0,32,32);
g.fillStyle(0x7a4418); g.fillRect(2,10,8,6); g.fillRect(18,18,10,6); g.fillRect(10,24,6,5);
g.fillStyle(0x46c94d); g.fillRect(0,0,32,7);
g.fillStyle(0x2f9e3a); g.fillRect(0,5,32,3); // edge
g.lineStyle(1,0x5e3413); g.strokeRect(0,0,32,32);
g.generateTexture('ground',32,32);
...
}
```
Bricks:
```
g.fillStyle(0xc9622f); fillRect(0,0,32,32);
g.lineStyle(2,0x7e3410);
horizontal lines at y=8,16,24; alternating vertical lines.
strokeRect border.
```
qblock:
```
fillStyle 0xf8b800 fillRect; border 0x8a5a00 2px; rivets at corners 4 dots;
'?' via arc: g.lineStyle(3,0xffffff); g.strokeCircle? draw arc from -60° to 200°? then stem down + dot.
```
Arc: g.beginPath(); g.arc(16,13,5, -2.2, 1.6); g.strokePath(); then vertical line from (16+?, ) hmm approximation: line from (18,17) to (17,20)? and dot at (17,24). Let's do: arc centered (16,12) radius 5 from angle 5.5 to 2.6 (clockwise?) — Phaser's arc angles are standard math but y is down. Fine — it'll look like a partial circle and a stem + dot reads as '?'. Good enough; or draw '?' with two rects and a dot: top bar rect(12,8,8,4)? not great. Use arc approach.
Actually simpler: overlay a Phaser text? generateTexture from graphics only. Alternative: draw texture to canvas via scene.add.text? Overkill. Arc is fine.
Used block: 0x8a6a3a fill, dark border, 4 rivets.
Coin frames coin0-3:
```
for(i=0..3){ widths=[20,14,6,14]; w=widths[i];
g.clear(); g.fillStyle(0xffd800); g.fillEllipse(16,16,w,22);
g.lineStyle(2,0xc79a00); g.strokeEllipse(16,16,w,22);
if(w>10){g.fillStyle(0xfff3a0); g.fillEllipse(16- w*0.15,12,w*0.3,8);}
g.generateTexture('coin'+i,32,32);
}
```
Hmm, frame texture size 32 but display at 32? Body is smaller. ok, set coin sprite body setSize(16,20) offset. Actually static body from texture is 32x32; overlap only so fine.
Player 26x30:
```
Cap: fillStyle 0xe52521 fillRect(5,0,16,6); fillRect(3,4,22,4)? brim: fillRect(3,5,20,3)
Face: 0xffc9a3 fillRect(6,7,14,8)
Eye: 0x222 fillRect(15,9,3,4) (facing right)
Mustache? skip
Body overalls: 0x2a6df4 fillRect(5,15,16,10)
Arms red: fillRect(2,15,4,7) right fillRect(20,15,4,7)
Hands skin: fillRect(2,21,4,3) etc
Boots: 0x6b3b12 fillRect(3,25,9,5) fillRect(14,25,9,5)
Shirt between arms: red fillRect(5,13,16,4)?
```
Order: torso red shirt rows 14-17, blue overall 16-25, straps. Fine — simple blocky hero.
Enemy 28x26:
```
Dome: 0xa05a20 fillEllipse(14,12,26,20)? Phaser fillEllipse(x,y,w,h).
Feet: 0x5a2d0c fillEllipse(7,23,10,6), fillEllipse(21,23,10,6)
Eyes white: fillEllipse(9,10,6,8), (19,10,6,8); pupils black fillRect(8,11,2,4), (18,11,2,4)
Angry brow lines? skip
```
Flattened enemy texture 'enemyFlat': dome squashed.
Flag pole texture: width 8? Let's draw width 12, height T*10=320:
```
Pole: fillStyle 0x9adf6a? classic green pole: 0x2fae3a? Actually SMB flagpole is greenish grey with a ball on top. Use 0xbfbfbf? Green: 0x3aae4a vertical rect x=4 w=4, ball on top yellow circle at (6,4) r 5.
```
Flag cloth texture 'flagcloth': green triangle pointing left 24x20: fillTriangle(0,0, 0,20, 24,10)? attached on left side of pole, apex on pole. Draw triangle (24,0)-(24,20)-(0,10).
Cloud: white: fillEllipse combos, size 120x60.
Hill: 0x2e9e41 fillTriangle(0,120, 160,120, 80,0) with darker spots. Size 160x120.
Bush: 3 green ellipses, size 100x40, color 0x37b24a.
Sky: gradient from 0x63a5ff to 0xbfe3ff? fillGradientStyle(0x5aa0ff,0x5aa0ff,0xcfeaff,0xcfeaff); fillRect(0,0,4,540); generateTexture('sky',4,540), image stretched to 960.
Now scene code.
```js
class GameScene extends Phaser.Scene {
constructor(){super('game');}
create(){
this.physics.world.gravity.y=1900;
makeTextures(this);
this.anims.create({key:'spin',frames:[{key:'coin0'},{key:'coin1'},{key:'coin2'},{key:'coin3'}],frameRate:8,repeat:-1});
this.score=0; this.coinCount=0; this.lives=3; this.state='play';
this.cameras.main.setBounds(0,0,LEVEL_W*T,ROWS*T+200);
this.physics.world.setBounds(0,0,LEVEL_W*T,ROWS*T+400);
buildWorld();
hud();
input keys...
}
}
```
Keys: this.input.keyboard.addKeys('LEFT,RIGHT,UP,SPACE,A,D,W,R,ENTER').
buildWorld destroys previous groups: if this.solids exists destroy etc. Player too. Then build.
Grid construction function buildGrid() as above.
Create tiles: loop grid:
- 'X': texture 'ground'
- 'B': 'brick'
- '?': 'qblock' (track in qblocks array)
- 'C': coin sprite (group coins), not solid.
- 'F': pole texture segment? Instead draw whole pole as single image: find flag col, place pole image at (c*T+6, topR*T) with height covering rows 4-13. Simpler: when we encounter 'F' in top row of flag col, place pole. Compute FLAG_COL constant and just add pole and flag cloth directly, don't use grid 'F'.
Base block under pole: setT(FLAG_COL,13,'X').
Flag sprite: pole image (origin 0), flag cloth image at (FLAG_COL*T+6-24, 5*T).
Flag zone: invisible: this.flagZone = this.physics.add.staticImage? Use this.add.zone with arcade body:
```
const z=this.add.zone(FLAG_COL*T+4, 4*T, 40, 10*T);
this.physics.add.existing(z,true);
```
Zone origin 0.5 — watch centering. Use this.add.rectangle? Use zone with setOrigin(0).
Win overlap: this.physics.add.overlap(this.player, this.flagZone, ...) — overlap with zone works if it has body.
After building groups, setup colliders.
Player creation: this.player=this.physics.add.sprite(2*T+16, 12*T, 'player'); body setSize(20,28) offset(3,1); setCollideWorldBounds(true).
Camera follow.
HUD text fixed.
Update loop:
```js
update(time, delta){
if(this.state==='play'){ handleInput(); }
else { player.setVelocityX(0); }
updateEnemies();
checkQblocks();
if(this.player.y > ROWS*T+120 && this.state==='play') this.die();
if(this.cursors...R pressed) restart;
}
```
handleInput:
```
const left=k.LEFT.isDown||k.A.isDown; right=...;
if(left){p.setVelocityX(-RUN); p.setFlipX(true);} else if(right){...} else p.setVelocityX(0);
const jumpPressed = Phaser.Input.Keyboard.JustDown(k.SPACE)||JustDown(k.UP)||JustDown(k.W);
if(jumpPressed && (p.body.blocked.down||p.body.touching.down)){ p.setVelocityY(-680); squash; beep; }
// variable jump
if(!(k.SPACE.isDown||k.UP.isDown||k.W.isDown) && p.body.velocity.y < -240) p.setVelocityY(-240);
```
Land squash: track wasFalling.
updateEnemies:
```
this.enemies.children.each(e=>{
if(e.getData('squashed')) return;
if(e.body.blocked.left){e.setData('dir',1);} else if(e.body.blocked.right){e.setData('dir',-1);}
e.setVelocityX(e.getData('dir')*SPEED);
e.setFlipX(e.getData('dir')>0);
if(e.y>ROWS*T+100) e.destroy();
});
```
Note: children.each during destroy — destroy inside each may modify iteration; use getChildren().slice().
Stomp:
```
hitEnemy(p,e){
if(this.state!=='play'||e.getData('squashed'))return;
if(p.body.velocity.y>0 && p.body.bottom < e.body.top+e.body.height*0.6){
squashed: e.setData('squashed',true); e.setVelocity(0,0); e.body.enable=false? keep body off. e.setTexture('enemyFlat')? generate flat texture; tween scale y; destroy after 400ms via time.delayedCall.
p.setVelocityY(-420); score+=100; popup; beep stomp.
} else this.die();
}
```
die(): state='dead'; beep; player death anim: body.enable=false, tween y up then down, then delayedCall 1000 → lives--; if lives<=0 gameOver else rebuildWorld().
Simplify: set state='dead', disable player body, tween scaleY? Mario's spin. Then respawn.
gameOver(): state='over'; overlay text "GAME OVER — press R".
win(): state='win'; bonus 1000; overlay "COURSE CLEAR! press R"; stop player.
R handler: if state is over/win or anytime: this.scene.restart().
popup(x,y,txt): floating text tween.
HUD update function refreshes text contents.
OK — also add this.time.delayedCall; fine.
One more: enemy group with gravity; enemies spawn on platforms and fall to ground on spawn. Fine.
qblocks check: qblocks stored in array of static sprites. When player.body.blocked.up:
```
const pb=this.player.body;
for(const b of this.qblocks){
if(b.getData('used'))continue;
const bb=b.body;
if(Math.abs(pb.top - bb.bottom) < 6 && pb.right > bb.left+4 && pb.left < bb.right-4){
b.setData('used',true); b.setTexture('usedblock');
score+=200; coinCount++; popup; coin pop anim: little coin sprite tweens up from block then destroyed; beep coin.
bump anim: tween b y down/up? static body move: use setY to tween? Just visual skip, or small: b.y -=4 then tween back — but body won't follow statics unless updateFromGameObject... Actually for static body, body position syncs with game object each frame? For static bodies, Phaser syncs body with game object each frame? Static bodies update when object moves via updateFromGameObject — yeah arcade static bodies auto-sync in preUpdate (static bodies check for position change). Yeah safe.
}
}
```
Edge case: hitting block with head also bumps player down — fine.
Also bricks 'B' could bounce, skip.
Also enemies shouldn't be pushed weirdly by player — no player vs enemy collider, just overlap. Fine.
Let me write it all now. Double-check Phaser API details:
- this.make.graphics({add:false}) ok (or {x:0,y:0,add:false}).
- g.fillGradientStyle(c1,c2,c3,c4,alpha) then fillRect.
- g.strokeEllipse(x,y,w,h), g.fillEllipse.
- g.fillTriangle(x1,y1,x2,y2,x3,y3), strokeTriangle.
- g.arc(x,y,r,startAngle,endAngle,anticlockwise) then strokePath. Angles in radians, 0 at positive x, clockwise on screen coords... fine.
- generateTexture(key,w,h).
- staticGroup: this.physics.add.staticGroup(); g.create(x,y,'key').
- sprite.body.setSize(w,h,true) centers. Use setSize(20,28).setOffset(3,2).
- scene.restart via this.scene.restart().
- Keyboard: this.input.keyboard.addKeys(...) returns object with Key objects; JustDown(key).
- Zone: this.add.zone(x,y,w,h).setOrigin(0,0); this.physics.add.existing(zone,true) → static body.
Camera: this.cameras.main.startFollow(this.player,true,0.12,0.12); setDeadzone maybe not since follow with lerp is fine.
Parallax: create bgGroup before solids.
HUD depth: setDepth(100), setScrollFactor(0).
Also add ground edge texture variation: 'ground' used for all; fine.
Now let me write it.
Enemy count ~14. Lives 3.
Death tween:
```
this.player.body.enable=false;
this.tweens.add({targets:this.player,y:this.player.y-120,duration:250,ease:'Quad.easeOut',yoyo:true,onComplete:()=>{...respawn}});
```
But yoyo brings it down; then delay and respawn anyway. Simpler: tween up then delayedCall 800 → finishDeath().
Also prevent double die: guard with state.
Now let me write the final file. Careful with grid build — write the functions:
```js
function buildGrid(){
grid=Array.from({length:ROWS},()=>Array(LEVEL_W).fill('.'));
const T_= (c,r,ch)=>setT(c,r,ch);
const ground=(a,b)=>{for(let c=a;c<=b;c++)for(let r=14;r<ROWS;r++)setT(c,r,'X');};
const row=(a,b,r,ch)=>{for(let c=a;c<=b;c++)setT(c,r,ch);};
ground(0,25);
row(16,20,10,'?'); // wait, alternate
...
}
```
qblocks 16,18,20 and bricks 17,19: setT individually: setT(16,10,'?'); setT(17,10,'B'); setT(18,10,'?'); setT(19,10,'B'); setT(20,10,'?');
Coin at (18,7).
Enemies: this.enemySpawns=[22,34,40,63,69,74,93,97,101,127,135,140,143,160,164].
Wait, enemy at 34 is under bricks row 10 cols 32-35 — walking under bricks, fine (bricks are at row 10, enemy is at ground 13, space is rows 11-12 which is open). ok.
ground(29,55); row(32,35,10,'B'); coins (32..35,7)? Let's do coins (33,7), (34,7). setT(37,10,'?').
Pyramid: c=46..49 h=c-45: for h 1..4 rows 14-h ..13: setT(46,13,'X'); setT(47,12..13); setT(48,11..13); setT(49,10..13).
Wait, ground(0,25) then hole 26-28, ground(29,55) then hole 56-58, ground(59,84), hole 85-88, ground(89,118), hole 119-122, ground(123,148)? Then stairs at 145-148 ascending height 4 at 148, then hole 149-151, ground(152,213), stairs descending 152-155? Overlap: ground(152,213) plus stairs at 152-155 heights 4..1: setT(152, 10..13), 153 11..13, 154 12..13, 155 13.
ground(59,84):
setT(61,10,'?'); row(64,67,7,'B'); coins (64..67,4).
Enemies 63, 69, 74.
Hole 85-88: platform bricks row(86,87,12,'B'); arc coins setT(85,10,'C'), (86,9,'C'), (87,9,'C'), (88,10,'C').
ground(89,118):
setT(95,10,'?'); setT(96,10,'B'); setT(97,10,'?'); coins (95,7), (97,7).
Enemies 93, 97 (under qblock? move to 98), 101. Use 93, 99, 103.
Pyramid descending at 106-109: heights 4,3,2,1.
Hole 119-122: bricks (120,12), (121,11)? step-up: setT(120,12,'B'); setT(121,11,'B')? Jump from ground r14 standing r13 up to r12 top ok, then r11 ok. Coins above hole (120,9), (121,8)?
ground(123,148):
row(126,128,10,'B'); coins (126..128,7).
row(131,133,7,'B'); coins (131..133,4).
Enemies 127? under bricks — fine (rows 11-12 open). Enemies 127, 136, 140, 143.
Ascending stairs 145-148 heights 1..4. Coin at (148,8)? on top.
Hole 149-151.
ground(152,213); descending stairs 152-155.
Enemies 160, 164.
Coin row row(166,170,11,'C').
FLAG_COL=180: setT(180,13,'X') base; pole rows 4-12 (image).
Castle: col 190-194: rows 12-13: row(190,194,12,'B'); row(190,194,13,'B'); row(191,193,10,'B'); row(191,193,11,'B'); setT(192,9,'B'); door: leave (192,12), (192,13) as '.'? Then hole — no, keep solid. Fine.
```
End wall: col LEVEL_W-1, setT 213 rows 10-13 'X'? World bounds stop anyway.
Wait, row 13 at FLAG_COL: base block at row 13 sits above ground row 14. Pole from row 4 to 13 (ball on top). Pole image height: (13-4+1)*32=320. Place at x=180*T+12, y=4*T.
Flag cloth at (180*T+12-22, 5*T+6).
Flag zone: x from 180*T-8 width 40, y=4*T height 10*T.
OK. Also add some coins on pyramid tops etc. Enough.
Now let's write final code. Keep the functions clean. Estimated ~600 lines. Go.
Also HUD styling with fontFamily 'monospace' and stroke via textStyle stroke.
Phaser text: this.add.text(x,y,str,{fontFamily:'"Press Start 2P", monospace'...}) — no external font; use 'bold 18px monospace' and stroke.
Now write the file.
{"command":"ls -la /tmp/ws && head -c 200 /tmp/ws/phaser.min.js"}total 1156 drwxr-sr-x. 2 node node 27 Aug 13 01:31 . drwxrwsrwx. 3 root node 16 Aug 13 01:31 .. -rw-r--r--. 1 node node 1181917 Aug 13 01:31 phaser.min.js !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Phaser",[],e):"object"==typeof exports?exports.Phaser=e():t.Phaser=e()
thinking
Phaser is already in place. Next, I'll create index.html.
Phaser is present. Now I'll write the complete game into a single self-contained `index.html`.
/tmp/ws/index.html (24114 bytes)Successfully wrote 24114 bytes to /tmp/ws/index.html
thinking
Next, let's do a sanity check of the JS syntax. I'll extract the script and run node --check. Also, I'll verify whether the Phaser global loads in the headless environment. Alright, let's extract it and lint it.
Now let me verify the JavaScript syntax and check for common issues:
{"command":"cd /tmp/ws && python3 -c \"\nimport re\nhtml = open('index.html').read()\nm = re.findall(r'<script>(.*?)</script>', html, re.S)\/bin/bash: line 1: python3: command not found Command exited with code 127
thinking
No python3. Use node for extraction.
{"command":"cd /tmp/ws && node -e \"\nconst fs=require('fs');\nconst html=fs.readFileSync('index.html','utf8');\nconst m=html.match(/<scriptextracted 23619 SYNTAX OK
thinking
The syntax is fine. Now, can we run a smoke test using Phaser in Node? Phaser requires the DOM. If jsdom is available, maybe we could simulate a headless run, but... it's probably not installed. Alternative: check the Phaser version, and check for obvious API misuse.
Let's verify the Phaser version string.
Also, let me review a few potential bugs:
1. There's a paused tween `this.tweens.add({ targets: cloth, y: 11 * T, duration: 0, paused: true })` — pointless leftover cruft, but harmless? A paused tween with duration 0 — no problem, but should we delete it to keep things clean? No harm. Actually, let's delete it.
2. In `hitEnemy`, we check `pb.velocity.y > 60`. Arcade body's `velocity` — `body.velocity` is valid.
3. Zone overlap: `this.physics.add.overlap(this.player, this.flagZone, ...)` — flagZone has a static body via `this.physics.add.existing(zone, true)`. overlap works with zone bodies. OK.
4. `this.solids.destroy(true)` destroys children including bodies. buildWorld is called from create — first call: this.solids is undefined so it's skipped. OK. But fxLayer group — created first in buildWorld. However, the overlay text in showOverlay is also added to fxLayer, and... fxLayer gets destroyed on restart via buildWorld — but buildWorld is only called on respawn (buildWorld inside die()), and the overlay only shows on gameOver (no buildWorld). scene.restart() calls create again and everything is reset. OK.
Wait — the die() flow: after the tween's yoyo complete, delayedCall 500 → buildWorld. But if the scene restarts during that time (R key), the delayedCall could fire after the restart. scene.restart() recreates the scene — will old timers on the previous scene instance be cleared? On restart the scene's systems reset, and the Time clock is cleared. Should be fine.
5. Fall-into-pit path: state set to 'falling' — after respawn, state='play'. Good. But if during 'falling' lives<=0 → gameOver() sets state to 'over'. Good.
6. update() when state==='dead': player body is disabled; p.body.enable check is fine.
7. Enemy body.setSize(26,24) — the texture is 28x26 so OK.
8. `this.cameras.main.setFollowOffset(140, 60)` — this makes the camera target ahead of the player. When moving left, the camera stays offset to the right and the look-ahead feels off, but it's within acceptable range.
9. qblock bump tween: tweening y on a static body — arcade's static body is updated via `updateFromGameObject` when the game object moves. Yes, static bodies sync position every frame in ArcadeWorld.update? Static bodies are updated via `updateFromGameObject` when the game object moves. Yes, Phaser's Arcade static body has updateFromGameObject called during the static tree's update. No problem.
10. The ground row uses rows 14, 15, 16 — ROWS=17 so rows go up to index 16. Good.
11. Pit at 26-28: player starts on ground(0,25) at column 2. Enemy at column 22 walks left, reaches the edge at column 25, and walks off into the pit? Walks left from 22 toward 0, hits the world boundary on the left, turns around. Walks right and eventually falls into the pit at column 26 — no problem, destroyed when y > WORLD_H+80. OK.
12. Enemy at column 173 — final stretch, no problem. Flag at column 180. Zone x = 180*32-10, width 44, y from 4*32 with height 320 — covers up to row 14. Player touches it and wins. Good.
13. The castle's bricks are solid at rows 10-13, columns 190-194 — the player can jump on top. No problem.
14. Coins at (166..170, row 11) — floating at row 11 above the ground; reachable with a jump. Good.
15. `this.add.zone(...).setOrigin(0,0)` — zones have setOrigin. OK.
16. HUD coin text uses '\u25CF x' — the filled-circle character as a coin icon. OK.
17. Phaser version check: staticGroup().create returns a sprite; setting data works. `q.setData('used', false)` no problem.
18. `this.make.graphics({add:false})` — Phaser's GameObjectFactory.make signature: make(options) with options.add. OK.
19. Potential issue: `this.enemies.getChildren().slice()` and then calling `e.destroy()` inside the loop — no problem since it's a copy.
20. In the enemies-enemies collider callback, setData is called on both — could trigger repeatedly every frame while they're touching; the direction flickers while overlapping, but each collision flips both, so they stay pushed apart. Minor jitter is within acceptable range.
21. hitQblocks runs every frame while blocked.up; the used flag prevents repeats.
22. Death on touching an enemy while the player is invincible? No invincibility frames — die() guards with state.
One more check: does Phaser's `zone` work with physics overlap? A zone is a GameObject without rendering, but with an arcade body; overlap works between bodies. Yes.
Also, `this.flagZone.destroy()` inside buildWorld — zones are destroyed without issue.
Sky image display size is 960x540, scrollFactor 0, depth -100. Parallax hills at depth -80 etc. Solids default depth 0. Player depth 10. Good.
Camera bounds height is WORLD_H+200, world physics bounds y starts at -100, height WORLD_H+300. Player can fall to WORLD_H+90 before dying — inside the bounds. Good.
Let me check the Phaser version to confirm API compatibility (e.g., fillGradientStyle exists in v3.12+, zones in 3.12+). Let's grep the version.
Also, maybe I can run a headless smoke test: install jsdom? Probably no network. Let me check whether jsdom exists in node_modules. Probably not. At minimum I can verify that the Phaser file exposes a version.
Syntax is valid. Let me check the Phaser version and try a headless smoke test:
{"command":"cd /tmp/ws && grep -o '\"3\\.[0-9]*\\.[0-9]*\"' phaser.min.js | head -3; node -e \"console.log(require.resolve.paths('jsdom') ? "3.80.1" x @earendil-works corepack npm
thinking
Phaser 3.80.1 — all the APIs being used are compatible. No jsdom available for a headless DOM test. Should I try a minimal smoke test with a fake DOM? Phaser 3.80 demands quite a lot of DOM (document.createElement, canvas). If there's no network to install jsdom, it's likely to fail. Let me quickly check whether jsdom is anywhere: `node -e "require('jsdom')"` — will probably fail. Skip that, and instead do a careful static review of the risky spots.
Actually, I could try a smoke test with a lightweight DOM shim — Phaser's HEADLESS renderer (type: Phaser.HEADLESS) avoids the canvas, but Phaser still needs `document`/`window` at boot time for scale management etc. Building a shim is risky, and even if it errors out that doesn't necessarily mean it would fail in a browser. Skip the heavy emulation.
One real bug to fix: a leftover paused tween for cloth. Let's delete it. Also double-check the `this.anims.create` frame keys — no problem.
Another consideration: inside `buildWorld`, before creating, if it exists, `this.fxLayer.destroy(true)`; in create, buildWorld is called before the HUD is created — the HUD isn't included in fxLayer. Good.
Also: gameOver is called from a delayedCall inside die's onComplete — buildWorld is not called; overlay is shown. Restart with R works.
Edge case: `winGame` → flagZone overlap is triggered; the player may still be moving; set velocity to 0, but after that update() sets velocityX to 0 when state!=play — but gravity still applies, so the player might land/slide; no problem.
Note: in update, when state==='win', p.body.enable is true so it enters the `else if (p.body.enable)` branch → setVelocityX(0). OK.
Delete the extraneous tween and do a final listing.
Phaser 3.80.1 — all the APIs being used are compatible. Let me clean up one unnecessary leftover tween and run a final sanity check:
{"path":"/tmp/ws/index.html","edits":[{"oldText":" const cloth = this.add.image(FLAG_COL * T + 12 - 24, 5 * T + 8, 'flagcloth').setDepth(Successfully replaced 1 block(s) in /tmp/ws/index.html.
thinking
Now, should I try a headless smoke test with a minimal DOM shim? Let's give it a shot — create a minimal window/document shim and run Phaser in HEADLESS mode. Phaser 3.80's HEADLESS renderer avoids canvas rendering, but DOM APIs are still needed: document.createElement('canvas') is used for texture generation (CanvasRenderer), but... in HEADLESS mode does Phaser use CanvasRenderer? Phaser.HEADLESS uses a HeadlessRenderer that doesn't draw, but texture generation via Graphics.generateTexture may still require a canvas context. Since my game procedurally generates textures via Graphics → generateTexture, the canvas 2D context is needed. It's possible to shim a fake canvas with a 2D context stubbed out (make all methods no-ops, getImageData returns a buffer) — considerable effort, but it might let me catch runtime errors in my own game logic (level construction, scene creation, update loop).
Let's give it a try: create a shim equipped with a Proxy-based 2D context that returns sensible default values (measureText, getImageData). Phaser also needs window.addEventListener, requestAnimationFrame, performance, navigator, devicePixelRatio, innerWidth/innerHeight, document.body.appendChild, document.documentElement, and so on. It's risky, but worth a shot. If errors related to Phaser internals come up, I can evaluate whether they're shim-related or game-related.
Let me write /tmp/shim-test.js:
```js
// Minimal DOM shim
const noop = () => {};
function makeCtx() {
return new Proxy({}, {
get(t, prop) {
if (prop === 'getImageData') return () => ({ data: new Uint8ClampedArray(4) });
if (prop === 'measureText') return () => ({ width: 0 });
if (prop === 'createLinearGradient' || prop==='createRadialGradient' || prop==='createPattern') return () => ({ addColorStop: noop });
if (prop === 'canvas') return null;
return typeof prop === 'string' ? noop : undefined;
},
set() { return true; }
});
}
```
Hmm, the Proxy's get returns a function for everything, but property reads like ctx.globalAlpha return a function — if Phaser reads and writes, that's fine, but if it does arithmetic it becomes NaN. Phaser does reads like `ctx.fillStyle`... it's mostly set-only. There's risk, but let's give it a shot.
Canvas element stub:
```js
function makeCanvas() {
const c = {
width: 0, height: 0, style: {},
getContext: (t) => makeCtx(),
addEventListener: noop, removeEventListener: noop,
getBoundingClientRect: () => ({ left:0, top:0, width:960, height:540, right:960, bottom:540, x:0,y:0 }),
setAttribute: noop, toDataURL: () => '',
};
return c;
}
global.window = global;
global.document = {
createElement: (t) => t === 'canvas' ? makeCanvas() : ({ style: {}, appendChild: noop, addEventListener: noop, setAttribute: noop }),
createElementNS: (ns,t) => makeCanvas(),
body: { appendChild: noop, removeChild: noop },
documentElement: { style: {} },
getElementById: () => ({ appendChild: noop, style:{} }),
addEventListener: noop, removeEventListener: noop,
hidden: false, visibilityState: 'visible',
readyState: 'complete',
};
global.navigator = { userAgent: 'node', maxTouchPoints: 0, getGamepads: () => [] };
global.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 16);
global.cancelAnimationFrame = clearTimeout;
global.addEventListener = noop; global.removeEventListener = noop;
global.performance = require('perf_hooks').performance;
global.devicePixelRatio = 1;
global.innerWidth = 960; global.innerHeight = 540;
global.Image = class { set src(v){ setTimeout(()=>this.onerror&&this.onerror(),0);} };
global.HTMLCanvasElement = function(){};
global.AudioContext = undefined; // game guards this
```
After that, load phaser.min.js with require (since it's UMD, in node it should set module.exports via the "object"==typeof exports branch). Then extract the game script and eval it with Phaser in scope. However, my game script calls `new Phaser.Game(config)` — config's type: Phaser.AUTO → in headless environments AUTO picks... it checks device support, but with the shim it will probably pick Canvas. I could temporarily patch it to Phaser.HEADLESS for the test. I'll eval the script but override the config type after extraction — easier: eval the script in a context where Phaser is defined, and let it run. Then pump frames: setTimeout loop, run for about 3 seconds to simulate key input? Simulate input via game.scene.keys... maybe I can call scene methods directly: after boot, verify that scene.create has run, that solids/coins/enemies counts are nonzero, simulate update ticks, call collectCoin, stomp, etc.
This could turn into a quagmire. Time-boxed: give it a try, and if Phaser's boot fails with shim-related errors, log it and fall back to static review.
Actually, Phaser 3.80's HEADLESS renderer: Phaser.Game with type HEADLESS completely skips canvas creation for rendering, but the CanvasTexture/graphics generateTexture path uses CanvasRenderer... even in HEADLESS, textures are generated via CanvasTexture which creates a canvas element and 2d context. My shim provides those as no-ops — generateTexture should "work" (pixel content is empty). Physics doesn't need canvas. ScaleManager uses window listeners, document, getBoundingClientRect — shimmed.
Let's give it a try.
Now let me attempt a headless smoke test with a minimal DOM shim to catch runtime errors in the game logic:
/tmp/smoke.js (6244 bytes)Successfully wrote 6244 bytes to /tmp/smoke.js
thinking
Note: eval(src) uses `game` and `FLAG_COL` from the script — eval within the same scope, so variables should be accessible afterward (eval in non-strict scope creates bindings in the enclosing function scope… `const config` and `new Phaser.Game` — `game` is not stored! My game script doesn't assign the Phaser.Game instance to anything: `new Phaser.Game(config);` — need a reference. Let me modify the smoke test to capture it: replace `new Phaser.Game(config);` with `global.game = new Phaser.Game(config);` and `FLAG_COL` is a const within eval scope — is a top-level const accessible within the same eval'd code? In non-strict eval, const/let are confined to the eval's own scope block and are not exposed. But `grid` etc. are also const/let. However, the GameScene class is also within that scope; the smoke test needs scene access — this is possible via the game instance. FLAG_COL can just be hardcoded as 180*32. Let me patch the smoke test: replace 'new Phaser.Game(config);' with 'global.game = new Phaser.Game(config);' and use 180*32.
{"command":"cd /tmp && sed -i \"s/src = src.replace('type: Phaser.AUTO', 'type: Phaser.HEADLESS');/src = src.replace('type: Phaser.AUTO', 'ts(e)||this.curves.push(new a(e,t)),this},cubicBezierTo:function(t,e,i,s,r,o){var a,h,l,u=this.getEndPoint();return t instanceof d?(a=t,h=e,l=i):(a=new d(i,s),h=new d(r,o),l=new d(t,e)),this.add(new n(u,a,h,l))},quadraticBezierTo:function(t,e,i,s){var n,r,o=this.getEndPoint();return t instanceof d?(n=t,r=e):(n=new d(i,s),r=new d(t,e)),this.add(new l(o,n,r))},draw:function(t,e){for(var i=0;i<this.curves.length;i++){var s=this.curves[i];s.active&&s.draw(t,e)}return t},ellipseTo:function(t,e,i,s,n,o){var a=new r(0,0,t,e,i,s,n,o),h=this.getEndPoint(this._tmpVec2A),l=a.getStartPoint(this._tmpVec2B);return h.subtract(l),a.x=h.x,a.y=h.y,this.add(a)},fromJSON:function(t){this.curves=[],this.cacheLengths=[],this.startPoint.set(t.x,t.y),this.autoClose=t.autoClose;for(var e=0;e<t.curves.length;e++){var i=t.curves[e];switch(i.type){case"LineCurve":this.add(a.fromJSON(i));break;case"EllipseCurve":this.add(r.fromJSON(i));break;case"SplineCurve":this.add(c.fromJSON(i));break;case"CubicBezierCurve":this.add(n.fromJSON(i));break;case"QuadraticBezierCurve":this.add(l.fromJSON(i))}}return this},getBounds:function(t,e){void 0===t&&(t=new u),void 0===e&&(e=16),t.x=Number.MAX_VALUE,t.y=Number.MAX_VALUE;for(var i=new u,s=f.MIN_SAFE_INTEGER,n=f.MIN_SAFE_INTEGER,r=0;r<this.curves.length;r++){var o=this.curves[r];o.active&&(o.getBounds(i,e),t.x=Math.min(t.x,i.x),t.y=Math.min(t.y,i.y),s=Math.max(s,i.right),n=Math.max(n,i.bottom))}return t.right=s,t.bottom=n,t},getCurveLengths:function(){if(this.cacheLengths.length===this.curves.length)return this.cacheLengths;for(var t=[],e=0,i=0;i<this.curves.length;i++)e+=this.curves[i].getLength(),t.push(e);return this.cacheLengths=t,t},getCurveAt:function(t){for(var e=t*this.getLength(),i=this.getCurveLengths(),s=0;s<i.length;){if(i[s]>=e)return this.curves[s];s++}return null},getEndPoint:function(t){return void 0===t&&(t=new d),this.curves.length>0?this.curves[this.curves.length-1].getPoint(1,t):t.copy(this.startPoint),t},getLength:function(){var t=this.getCurveLengths();return t[t.length-1]},getPoint:function(t,e){void 0===e&&(e=new d);for(var i=t*this.getLength(),s=this.getCurveLengths(),n=0;n<s.length;){if(s[n]>=i){var r=s[n]-i,o=this.curves[n],a=o.getLength(),h=0===a?0:1-r/a;return o.getPointAt(h,e)}n++}return null},getPoints:function(t,e){t||e||(t=this.defaultDivisions);for(var i,s=[],n=0;n<this.curves.length;n++){var r=this.curves[n];if(r.active)for(var o=r.getResolution(t),a=r.getPoints(o,e),h=0;h<a.length;h++){var l=a[h];i&&i.equals(l)||(s.push(l),i=l)}}return this.autoClose&&s.length>1&&!s[s.length-1].equals(s[0])&&s.push(s[0]),s},getRandomPoint:function(t){return void 0===t&&(t=new d),this.getPoint(Math.random(),t)},getSpacedPoints:function(t){void 0===t&&(t=40);for(var e=[],i=0;i<=t;i++)e.push(this.getPoint(i/t));return this.autoClose&&e.push(e[0]),e},getStartPoint:function(t){return void 0===t&&(t=new d),t.copy(this.startPoint)},getTangent:function(t,e){void 0===e&&(e=new d);for(var i=t*this.getLength(),s=this.getCurveLengths(),n=0;n<s.length;){if(s[n]>=i){var r=s[n]-i,o=this.curves[n],a=o.getLength(),h=0===a?0:1-r/a;return o.getTangentAt(h,e)}n++}return null},lineTo:function(t,e){t instanceof d?this._tmpVec2B.copy(t):"object"==typeof t?this._tmpVec2B.setFromObject(t):this._tmpVec2B.set(t,e);var i=this.getEndPoint(this._tmpVec2A);return this.add(new a([i.x,i.y,this._tmpVec2B.x,this._tmpVec2B.y]))},splineTo:function(t){return t.unshift(this.getEndPoint()),this.add(new c(t))},moveTo:function(t,e){return t instanceof d?this.add(new h(t.x,t.y)):this.add(new h(t,e))},toJSON:function(){for(var t=[],e=0;e<this.curves.length;e++)t.push(this.curves[e].toJSON());return{type:"Path",x:this.startPoint.x,y:this.startPoint.y,autoClose:this.autoClose,curves:t}},updateArcLengths:function(){this.cacheLengths=[],this.getCurveLengths()},destroy:function(){this.curves.length=0,this.cacheLengths.length=0,this.startPoint=void 0}});o.register("path",(function(t,e){return new p(t,e)})),t.exports=p},45893:(t,e,i)=>{var s=i(83419),n=i(24882),r=new s({initialize:function(t,e){this.parent=t,this.events=e,e||(this.events=t.events?t.events:t),this.list={},this.values={},this._frozen=!1,!t.hasOwnProperty("sys")&&this.events&&this.events.once(n.DESTROY,this.destroy,this)},get:function(t){var e=this.list;if(Array.isArray(t)){for(var i=[],s=0;s<t.length;s++)i.push(e[t[s]]);return i}return e[t]},getAll:function(){var t={};for(var e in this.list)this.list.hasOwnProperty(e)&&(t[e]=this.list[e]);return t},query:function(t){var e={};for(var i in this.list)this.list.hasOwnProperty(i)&&i.match(t)&&(e[i]=this.list[i]);return e},set:function(t,e){if(this._frozen)return this;if("string"==typeof t)return this.setValue(t,e);for(var i in t)this.setValue(i,t[i]);return this},inc:function(t,e){if(this._frozen)return this;void 0===e&&(e=1);var i=this.get(t);return void 0===i&&(i=0),this.set(t,i+e),this},toggle:function(t){return this._frozen||this.set(t,!this.get(t)),this},setValue:function(t,e){if(this._frozen)return this;if(this.has(t))this.values[t]=e;else{var i=this,s=this.list,r=this.events,o=this.parent;Object.defineProperty(this.values,t,{enumerable:!0,configurable:!0,get:function(){return s[t]},set:function(e){if(!i._frozen){var a=s[t];s[t]=e,r.emit(n.CHANGE_DATA,o,t,e,a),r.emit(n.CHANGE_DATA_KEY+t,o,e,a)}}}),s[t]=e,r.emit(n.SET_DATA,o,t,e)}return this},each:function(t,e){for(var i=[this.parent,null,void 0],s=1;s<arguments.length;s++)i.push(arguments[s]);for(var n in this.list)i[1]=n,i[2]=this.list[n],t.apply(e,i);return this},merge:function(t,e){for(var i in void 0===e&&(e=!0),t)t.hasOwnProperty(i)&&(e||!e&&!this.has(i))&&this.setValue(i,t[i]);return this},remove:function(t){if(this._frozen)return this;if(!Array.isArray(t))return this.removeValue(t);for(var e=0;e<t.length;e++)this.removeValue(t[e]);return this},removeValue:function(t){if(this.has(t)){var e=this.list[t];delete this.list[t],delete this.values[t],this.events.emit(n.REMOVE_DATA,this.parent,t,e)}return this},pop:function(t){var e=void 0;return!this._frozen&&this.has(t)&&(e=this.list[t],delete this.list[t],delete this.values[t],this.events.emit(n.REMOVE_DATA,this.parent,t,e)),e},has:function(t){return this.list.hasOwnProperty(t)},setFreeze:function(t){return this._frozen=t,this},reset:function(){for(var t in this.list)delete this.list[t],delete this.values[t];return this._frozen=!1,this},destroy:function(){this.reset(),this.events.off(n.CHANGE_DATA),this.events.off(n.SET_DATA),this.events.off(n.REMOVE_DATA),this.parent=null},freeze:{get:function(){return this._frozen},set:function(t){this._frozen=!!t}},count:{get:function(){var t=0;for(var e in this.list)void 0!==this.list[e]&&t++;return t}}});t.exports=r},63646:(t,e,i)=>{var s=i(83419),n=i(45893),r=i(37277),o=i(44594),a=new s({Extends:n,initialize:function(t){n.call(this,t,t.sys.events),this.scene=t,this.systems=t.sys,t.sys.events.once(o.BOOT,this.boot,this),t.sys.events.on(o.START,this.start,this)},boot:function(){this.events=this.systems.events,this.events.once(o.DESTROY,this.destroy,this)},start:function(){this.events.once(o.SHUTDOWN,this.shutdown,this)},shutdown:function(){this.systems.events.off(o.SHUTDOWN,this.shutdown,this)},destroy:function(){n.prototype.destroy.call(this),this.events.off(o.START,this.start,this),this.scene=null,this.systems=null}});r.register("DataManagerPlugin",a,"data"),t.exports=a},10700:t=>{t.exports="changedata"},93608:t=>{t.exports="changedata-"},60883:t=>{t.exports="destroy"},69780:t=>{t.exports="removedata"},22166:t=>{t.exports="setdata"},24882:(t,e,i)=>{t.exports={CHANGE_DATA:i(10700),CHANGE_DATA_KEY:i(93608),DESTROY:i(60883),REMOVE_DATA:i(69780),SET_DATA:i(22166)}},44965:(t,e,i)=>{t.exports={DataManager:i(45893),DataManagerPlugin:i(63646),Events:i(24882)}},7098:(t,e,i)=>{var s=i(84148),n={flac:!1,aac:!1,audioData:!1,dolby:!1,m4a:!1,mp3:!1,ogg:!1,opus:!1,wav:!1,webAudio:!1,webm:!1};t.exports=function(){if("function"==typeof importScripts)return n;n.audioData=!!window.Audio,n.webAudio=!(!window.AudioContext&&!window.webkitAudioContext);var t=document.createElement("audio"),e=!!t.canPlayType;try{if(e){var i=function(e,i){var s=t.canPlayType("audio/"+e).replace(/^no$/,"");return i?Boolean(s||t.canPlayType("audio/"+i).replace(/^no$/,"")):Boolean(s)};if(n.ogg=i('ogg; codecs="vorbis"'),n.opus=i('ogg; codecs="opus"',"opus"),n.mp3=i("mpeg"),n.wav=i("wav"),n.m4a=i("x-m4a"),n.aac=i("aac"),n.flac=i("flac","x-flac"),n.webm=i('webm; codecs="vorbis"'),""!==t.canPlayType('audio/mp4; codecs="ec-3"'))if(s.edge)n.dolby=!0;else if(s.safari&&s.safariVersion>=9&&/Mac OS X (\d+)_(\d+)/.test(navigator.userAgent)){var r=parseInt(RegExp.$1,10),o=parseInt(RegExp.$2,10);(10===r&&o>=11||r>10)&&(n.dolby=!0)}}}catch(t){}return n}()},84148:(t,e,i)=>{var s,n=i(25892),r={chrome:!1,chromeVersion:0,edge:!1,firefox:!1,firefoxVersion:0,ie:!1,ieVersion:0,mobileSafari:!1,opera:!1,safari:!1,safariVersion:0,silk:!1,trident:!1,tridentVersion:0,es2019:!1};t.exports=(s=navigator.userAgent,/Edg\/\d+/.test(s)?(r.edge=!0,r.es2019=!0):/OPR/.test(s)?(r.opera=!0,r.es2019=!0):/Chrome\/(\d+)/.test(s)&&!n.windowsPhone?(r.chrome=!0,r.chromeVersion=parseInt(RegExp.$1,10),r.es2019=r.chromeVersion>69):/Firefox\D+(\d+)/.test(s)?(r.firefox=!0,r.firefoxVersion=parseInt(RegExp.$1,10),r.es2019=r.firefoxVersion>10):/AppleWebKit\/(?!.*CriOS)/.test(s)&&n.iOS?(r.mobileSafari=!0,r.es2019=!0):/MSIE (\d+\.\d+);/.test(s)?(r.ie=!0,r.ieVersion=parseInt(RegExp.$1,10)):/Version\/(\d+\.\d+(\.\d+)?) Safari/.test(s)&&!n.windowsPhone?(r.safari=!0,r.safariVersion=parseInt(RegExp.$1,10),r.es2019=r.safariVersion>10):/Trident\/(\d+\.\d+)(.*)rv:(\d+\.\d+)/.test(s)&&(r.ie=!0,r.trident=!0,r.tridentVersion=parseInt(RegExp.$1,10),r.ieVersion=parseInt(RegExp.$3,10)),/Silk/.test(s)&&(r.silk=!0),r)},89289:(t,e,i)=>{var s,n,r,o=i(27919),a={supportInverseAlpha:!1,supportNewBlendModes:!1};t.exports=("function"!=typeof importScripts&&void 0!==document&&(a.supportNewBlendModes=(s="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABAQMAAADD8p2OAAAAA1BMVEX/",n="AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==",(r=new Image).onload=function(){var t=new Image;t.onload=function(){var e=o.create2D(t,6).getContext("2d",{willReadFrequently:!0});if(e.globalCompositeOperation="multiply",e.drawImage(r,0,0),e.drawImage(t,2,0),!e.getImageData(2,0,1,1))return!1;var i=e.getImageData(2,0,1,1).data;o.remove(t),a.supportNewBlendModes=255===i[0]&&0===i[1]&&0===i[2]},t.src=s+"/wCKxvRF"+n},r.src=s+"AP804Oa6"+n,!1),a.supportInverseAlpha=function(){var t=o.create2D(this,2).getContext("2d",{willReadFrequently:!0});t.fillStyle="rgba(10, 20, 30, 0.5)",t.fillRect(0,0,1,1);var e=t.getImageData(0,0,1,1);if(null===e)return!1;t.putImageData(e,1,0);var i=t.getImageData(1,0,1,1),s=i.data[0]===e.data[0]&&i.data[1]===e.data[1]&&i.data[2]===e.data[2]&&i.data[3]===e.data[3];return o.remove(this),s}()),a)},89357:(t,e,i)=>{var s=i(25892),n=i(84148),r=i(27919),o={canvas:!1,canvasBitBltShift:null,file:!1,fileSystem:!1,getUserMedia:!0,littleEndian:!1,localStorage:!1,pointerLock:!1,stableSort:!1,support32bit:!1,vibration:!1,webGL:!1,worker:!1};t.exports=function(){if("function"==typeof importScripts)return o;o.canvas=!!window.CanvasRenderingContext2D;try{o.localStorage=!!localStorage.getItem}catch(t){o.localStorage=!1}o.file=!!(window.File&&window.FileReader&&window.FileList&&window.Blob),o.fileSystem=!!window.requestFileSystem;var t,e,i,a=!1;return o.webGL=function(){if(window.WebGLRenderingContext)try{var t=r.createWebGL(this),e=t.getContext("webgl")||t.getContext("experimental-webgl"),i=r.create2D(this),s=i.getContext("2d",{willReadFrequently:!0}).createImageData(1,1);return a=s.data instanceof Uint8ClampedArray,r.remove(t),r.remove(i),!!e}catch(t){return!1}return!1}(),o.worker=!!window.Worker,o.pointerLock="pointerLockElement"in document||"mozPointerLockElement"in document||"webkitPointerLockElement"in document,navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia||navigator.oGetUserMedia,window.URL=window.URL||window.webkitURL||window.mozURL||window.msURL,o.getUserMedia=o.getUserMedia&&!!navigator.getUserMedia&&!!window.URL,n.firefox&&n.firefoxVersion<21&&(o.getUserMedia=!1),!s.iOS&&(n.ie||n.firefox||n.chrome)&&(o.canvasBitBltShift=!0),(n.safari||n.mobileSafari)&&(o.canvasBitBltShift=!1),navigator.vibrate=navigator.vibrate||navigator.webkitVibrate||navigator.mozVibrate||navigator.msVibrate,navigator.vibrate&&(o.vibration=!0),"undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint32Array&&(o.littleEndian=(t=new ArrayBuffer(4),e=new Uint8Array(t),i=new Uint32Array(t),e[0]=161,e[1]=178,e[2]=195,e[3]=212,3569595041===i[0]||2712847316!==i[0]&&null)),o.support32bit="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof Int32Array&&null!==o.littleEndian&&a,o}()},91639:t=>{var e={available:!1,cancel:"",keyboard:!1,request:""};t.exports=function(){if("function"==typeof importScripts)return e;var t,i="Fullscreen",s="FullScreen",n=["request"+i,"request"+s,"webkitRequest"+i,"webkitRequest"+s,"msRequest"+i,"msRequest"+s,"mozRequest"+s,"mozRequest"+i];for(t=0;t<n.length;t++)if(document.documentElement[n[t]]){e.available=!0,e.request=n[t];break}var r=["cancel"+s,"exit"+i,"webkitCancel"+s,"webkitExit"+i,"msCancel"+s,"msExit"+i,"mozCancel"+s,"mozExit"+i];if(e.available)for(t=0;t<r.length;t++)if(document[r[t]]){e.cancel=r[t];break}return window.Element&&Element.ALLOW_KEYBOARD_INPUT&&!/ Version\/5\.1(?:\.\d+)? Safari\//.test(navigator.userAgent)&&(e.keyboard=!0),Object.defineProperty(e,"active",{get:function(){return!!(document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement||document.msFullscreenElement)}}),e}()},31784:(t,e,i)=>{var s=i(84148),n={gamepads:!1,mspointer:!1,touch:!1,wheelEvent:null};t.exports=("function"==typeof importScripts||(("ontouchstart"in document.documentElement||navigator.maxTouchPoints&&navigator.maxTouchPoints>=1)&&(n.touch=!0),(navigator.msPointerEnabled||navigator.pointerEnabled)&&(n.mspointer=!0),navigator.getGamepads&&(n.gamepads=!0),"onwheel"in window||s.ie&&"WheelEvent"in window?n.wheelEvent="wheel":"onmousewheel"in window?n.wheelEvent="mousewheel":s.firefox&&"MouseScrollEvent"in window&&(n.wheelEvent="DOMMouseScroll")),n)},25892:t=>{var e={android:!1,chromeOS:!1,cordova:!1,crosswalk:!1,desktop:!1,ejecta:!1,electron:!1,iOS:!1,iOSVersion:0,iPad:!1,iPhone:!1,kindle:!1,linux:!1,macOS:!1,node:!1,nodeWebkit:!1,pixelRatio:1,webApp:!1,windows:!1,windowsPhone:!1};t.exports=function(){if("function"==typeof importScripts)return e;var t=navigator.userAgent;/Windows/.test(t)?e.windows=!0:/Mac OS/.test(t)&&!/like Mac OS/.test(t)?navigator.maxTouchPoints&&navigator.maxTouchPoints>2?(e.iOS=!0,e.iPad=!0,navigator.appVersion.match(/Version\/(\d+)/),e.iOSVersion=parseInt(RegExp.$1,10)):e.macOS=!0:/Android/.test(t)?e.android=!0:/Linux/.test(t)?e.linux=!0:/iP[ao]d|iPhone/i.test(t)?(e.iOS=!0,navigator.appVersion.match(/OS (\d+)/),e.iOSVersion=parseInt(RegExp.$1,10),e.iPhone=-1!==t.toLowerCase().indexOf("iphone"),e.iPad=-1!==t.toLowerCase().indexOf("ipad")):/Kindle/.test(t)||/\bKF[A-Z][A-Z]+/.test(t)||/Silk.*Mobile Safari/.test(t)?e.kindle=!0:/CrOS/.test(t)&&(e.chromeOS=!0),(/Windows Phone/i.test(t)||/IEMobile/i.test(t))&&(e.android=!1,e.iOS=!1,e.macOS=!1,e.windows=!0,e.windowsPhone=!0);var i=/Silk/.test(t);return(e.windows||e.macOS||e.linux&&!i||e.chromeOS)&&(e.desktop=!0),(e.windowsPhone||/Windows NT/i.test(t)&&/Touch/i.test(t))&&(e.desktop=!1),navigator.standalone&&(e.webApp=!0),"function"!=typeof importScripts&&(void 0!==window.cordova&&(e.cordova=!0),void 0!==window.ejecta&&(e.ejecta=!0)),"undefined"!=typeof process&&process.versions&&process.versions.node&&(e.node=!0),e.node&&"object"==typeof process.versions&&(e.nodeWebkit=!!process.versions["node-webkit"],e.electron=!!process.versions.electron),/Crosswalk/.test(t)&&(e.crosswalk=!0),e.pixelRatio=window.devicePixelRatio||1,e}()},43267:(t,e,i)=>{var s=i(95540),n={h264:!1,hls:!1,mp4:!1,m4v:!1,ogg:!1,vp9:!1,webm:!1,hasRequestVideoFrame:!1};t.exports=function(){if("function"==typeof importScripts)return n;var t=document.createElement("video"),e=!!t.canPlayType,i=/^no$/;try{e&&(t.canPlayType('video/ogg; codecs="theora"').replace(i,"")&&(n.ogg=!0),t.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(i,"")&&(n.h264=!0,n.mp4=!0),t.canPlayType("video/x-m4v").replace(i,"")&&(n.m4v=!0),t.canPlayType('video/webm; codecs="vp8, vorbis"').replace(i,"")&&(n.webm=!0),t.canPlayType('video/webm; codecs="vp9"').replace(i,"")&&(n.vp9=!0),t.canPlayType('application/x-mpegURL; codecs="avc1.42E01E"').replace(i,"")&&(n.hls=!0))}catch(t){}return t.parentNode&&t.parentNode.removeChild(t),n.getVideoURL=function(t){Array.isArray(t)||(t=[t]);for(var e=0;e<t.length;e++){var i,r=s(t[e],"url",t[e]);if(0===r.indexOf("blob:"))return{url:r,type:""};if(i=0===r.indexOf("data:")?r.split(",")[0].match(/\/(.*?);/):r.match(/\.([a-zA-Z0-9]+)($|\?)/),i=s(t[e],"type",i?i[1]:"").toLowerCase(),n[i])return{url:r,type:i}}return null},n}()},82264:(t,e,i)=>{t.exports={os:i(25892),browser:i(84148),features:i(89357),input:i(31784),audio:i(7098),video:i(43267),fullscreen:i(91639),canvasFeatures:i(89289)}},89422:(t,e,i)=>{var s=i(83419),n=new Float32Array(20),r=new s({initialize:function(){this._matrix=new Float32Array(20),this.alpha=1,this._dirty=!0,this._data=new Float32Array(20),this.reset()},set:function(t){return this._matrix.set(t),this._dirty=!0,this},reset:function(){var t=this._matrix;return t.fill(0),t[0]=1,t[6]=1,t[12]=1,t[18]=1,this.alpha=1,this._dirty=!0,this},getData:function(){var t=this._data;return this._dirty&&(t.set(this._matrix),t[4]/=255,t[9]/=255,t[14]/=255,t[19]/=255,this._dirty=!1),t},brightness:function(t,e){void 0===t&&(t=0),void 0===e&&(e=!1);var i=t;return this.multiply([i,0,0,0,0,0,i,0,0,0,0,0,i,0,0,0,0,0,1,0],e)},saturate:function(t,e){void 0===t&&(t=0),void 0===e&&(e=!1);var i=2*t/3+1,s=-.5*(i-1);return this.multiply([i,s,s,0,0,s,i,s,0,0,s,s,i,0,0,0,0,0,1,0],e)},desaturate:function(t){return void 0===t&&(t=!1),this.saturate(-1,t)},hue:function(t,e){void 0===t&&(t=0),void 0===e&&(e=!1),t=t/180*Math.PI;var i=Math.cos(t),s=Math.sin(t),n=.213,r=.715,o=.072;return this.multiply([n+.787*i+s*-n,r+i*-r+s*-r,o+i*-o+.928*s,0,0,n+i*-n+.143*s,r+i*(1-r)+.14*s,o+i*-o+-.283*s,0,0,n+i*-n+-.787*s,r+i*-r+s*r,o+.928*i+s*o,0,0,0,0,0,1,0],e)},grayscale:function(t,e){return void 0===t&&(t=1),void 0===e&&(e=!1),this.saturate(-t,e)},blackWhite:function(t){return void 0===t&&(t=!1),this.multiply(r.BLACK_WHITE,t)},contrast:function(t,e){void 0===t&&(t=0),void 0===e&&(e=!1);var i=t+1,s=-.5*(i-1);return this.multiply([i,0,0,0,s,0,i,0,0,s,0,0,i,0,s,0,0,0,1,0],e)},negative:function(t){return void 0===t&&(t=!1),this.multiply(r.NEGATIVE,t)},desaturateLuminance:function(t){return void 0===t&&(t=!1),this.multiply(r.DESATURATE_LUMINANCE,t)},sepia:function(t){return void 0===t&&(t=!1),this.multiply(r.SEPIA,t)},night:function(t,e){return void 0===t&&(t=.1),void 0===e&&(e=!1),this.multiply([-2*t,-t,0,0,0,-t,0,t,0,0,0,t,2*t,0,0,0,0,0,1,0],e)},lsd:function(t){return void 0===t&&(t=!1),this.multiply(r.LSD,t)},brown:function(t){return void 0===t&&(t=!1),this.multiply(r.BROWN,t)},vintagePinhole:function(t){return void 0===t&&(t=!1),this.multiply(r.VINTAGE,t)},kodachrome:function(t){return void 0===t&&(t=!1),this.multiply(r.KODACHROME,t)},technicolor:function(t){return void 0===t&&(t=!1),this.multiply(r.TECHNICOLOR,t)},polaroid:function(t){return void 0===t&&(t=!1),this.multiply(r.POLAROID,t)},shiftToBGR:function(t){return void 0===t&&(t=!1),this.multiply(r.SHIFT_BGR,t)},multiply:function(t,e){void 0===e&&(e=!1),e||this.reset();var i=this._matrix,s=n;return s.set(i),i.set([s[0]*t[0]+s[1]*t[5]+s[2]*t[10]+s[3]*t[15],s[0]*t[1]+s[1]*t[6]+s[2]*t[11]+s[3]*t[16],s[0]*t[2]+s[1]*t[7]+s[2]*t[12]+s[3]*t[17],s[0]*t[3]+s[1]*t[8]+s[2]*t[13]+s[3]*t[18],s[0]*t[4]+s[1]*t[9]+s[2]*t[14]+s[3]*t[19]+s[4],s[5]*t[0]+s[6]*t[5]+s[7]*t[10]+s[8]*t[15],s[5]*t[1]+s[6]*t[6]+s[7]*t[11]+s[8]*t[16],s[5]*t[2]+s[6]*t[7]+s[7]*t[12]+s[8]*t[17],s[5]*t[3]+s[6]*t[8]+s[7]*t[13]+s[8]*t[18],s[5]*t[4]+s[6]*t[9]+s[7]*t[14]+s[8]*t[19]+s[9],s[10]*t[0]+s[11]*t[5]+s[12]*t[10]+s[13]*t[15],s[10]*t[1]+s[11]*t[6]+s[12]*t[11]+s[13]*t[16],s[10]*t[2]+s[11]*t[7]+s[12]*t[12]+s[13]*t[17],s[10]*t[3]+s[11]*t[8]+s[12]*t[13]+s[13]*t[18],s[10]*t[4]+s[11]*t[9]+s[12]*t[14]+s[13]*t[19]+s[14],s[15]*t[0]+s[16]*t[5]+s[17]*t[10]+s[18]*t[15],s[15]*t[1]+s[16]*t[6]+s[17]*t[11]+s[18]*t[16],s[15]*t[2]+s[16]*t[7]+s[17]*t[12]+s[18]*t[17],s[15]*t[3]+s[16]*t[8]+s[17]*t[13]+s[18]*t[18],s[15]*t[4]+s[16]*t[9]+s[17]*t[14]+s[18]*t[19]+s[19]]),this._dirty=!0,this}});r.BLACK_WHITE=[.3,.6,.1,0,0,.3,.6,.1,0,0,.3,.6,.1,0,0,0,0,0,1,0],r.NEGATIVE=[-1,0,0,1,0,0,-1,0,1,0,0,0,-1,1,0,0,0,0,1,0],r.DESATURATE_LUMINANCE=[.2764723,.929708,.0938197,0,-37.1,.2764723,.929708,.0938197,0,-37.1,.2764723,.929708,.0938197,0,-37.1,0,0,0,1,0],r.SEPIA=[.393,.7689999,.18899999,0,0,.349,.6859999,.16799999,0,0,.272,.5339999,.13099999,0,0,0,0,0,1,0],r.LSD=[2,-.4,.5,0,0,-.5,2,-.4,0,0,-.4,-.5,3,0,0,0,0,0,1,0],r.BROWN=[.5997023498159715,.34553243048391263,-.2708298674538042,0,47.43192855600873,-.037703249837783157,.8609577587992641,.15059552388459913,0,-36.96841498319127,.24113635128153335,-.07441037908422492,.44972182064877153,0,-7.562075277591283,0,0,0,1,0],r.VINTAGE=[.6279345635605994,.3202183420819367,-.03965408211312453,0,9.651285835294123,.02578397704808868,.6441188644374771,.03259127616149294,0,7.462829176470591,.0466055556782719,-.0851232987247891,.5241648018700465,0,5.159190588235296,0,0,0,1,0],r.KODACHROME=[1.1285582396593525,-.3967382283601348,-.03992559172921793,0,63.72958762196502,-.16404339962244616,1.0835251566291304,-.05498805115633132,0,24.732407896706203,-.16786010706155763,-.5603416277695248,1.6014850761964943,0,35.62982807460946,0,0,0,1,0],r.TECHNICOLOR=[1.9125277891456083,-.8545344976951645,-.09155508482755585,0,11.793603434377337,-.3087833385928097,1.7658908555458428,-.10601743074722245,0,-70.35205161461398,-.231103377548616,-.7501899197440212,1.847597816108189,0,30.950940869491138,0,0,0,1,0],r.POLAROID=[1.438,-.062,-.062,0,0,-.122,1.378,-.122,0,0,-.016,-.016,1.483,0,0,0,0,0,1,0],r.SHIFT_BGR=[0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0],t.exports=r},51767:(t,e,i)=>{var s=i(83419),n=i(29747),r=new s({initialize:function(t,e,i){this._rgb=[0,0,0],this.onChangeCallback=n,this.dirty=!1,this.set(t,e,i)},set:function(t,e,i){return void 0===t&&(t=0),void 0===e&&(e=0),void 0===i&&(i=0),this._rgb=[t,e,i],this.onChange(),this},equals:function(t,e,i){var s=this._rgb;return s[0]===t&&s[1]===e&&s[2]===i},onChange:function(){this.dirty=!0;var t=this._rgb;this.onChangeCallback.call(this,t[0],t[1],t[2])},r:{get:function(){return this._rgb[0]},set:function(t){this._rgb[0]=t,this.onChange()}},g:{get:function(){return this._rgb[1]},set:function(t){this._rgb[1]=t,this.onChange()}},b:{get:function(){return this._rgb[2]},set:function(t){this._rgb[2]=t,this.onChange()}},destroy:function(){this.onChangeCallback=null}});t.exports=r},60461:t=>{t.exports={TOP_LEFT:0,TOP_CENTER:1,TOP_RIGHT:2,LEFT_TOP:3,LEFT_CENTER:4,LEFT_BOTTOM:5,CENTER:6,RIGHT_TOP:7,RIGHT_CENTER:8,RIGHT_BOTTOM:9,BOTTOM_LEFT:10,BOTTOM_CENTER:11,BOTTOM_RIGHT:12}},54312:(t,e,i)=>{var s=i(62235),n=i(35893),r=i(86327),o=i(88417);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)+i),r(t,s(e)+a),t}},46768:(t,e,i)=>{var s=i(62235),n=i(26541),r=i(86327),o=i(385);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)-i),r(t,s(e)+a),t}},35827:(t,e,i)=>{var s=i(62235),n=i(54380),r=i(86327),o=i(40136);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)+i),r(t,s(e)+a),t}},46871:(t,e,i)=>{var s=i(66786),n=i(35893),r=i(7702);t.exports=function(t,e,i,o){return void 0===i&&(i=0),void 0===o&&(o=0),s(t,n(e)+i,r(e)+o),t}},5198:(t,e,i)=>{var s=i(7702),n=i(26541),r=i(20786),o=i(385);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)-i),r(t,s(e)+a),t}},11879:(t,e,i)=>{var s=i(60461),n=[];n[s.BOTTOM_CENTER]=i(54312),n[s.BOTTOM_LEFT]=i(46768),n[s.BOTTOM_RIGHT]=i(35827),n[s.CENTER]=i(46871),n[s.LEFT_CENTER]=i(5198),n[s.RIGHT_CENTER]=i(80503),n[s.TOP_CENTER]=i(89698),n[s.TOP_LEFT]=i(922),n[s.TOP_RIGHT]=i(21373),n[s.LEFT_BOTTOM]=n[s.BOTTOM_LEFT],n[s.LEFT_TOP]=n[s.TOP_LEFT],n[s.RIGHT_BOTTOM]=n[s.BOTTOM_RIGHT],n[s.RIGHT_TOP]=n[s.TOP_RIGHT];t.exports=function(t,e,i,s,r){return n[i](t,e,s,r)}},80503:(t,e,i)=>{var s=i(7702),n=i(54380),r=i(20786),o=i(40136);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)+i),r(t,s(e)+a),t}},89698:(t,e,i)=>{var s=i(35893),n=i(17717),r=i(88417),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,s(e)+i),o(t,n(e)-a),t}},922:(t,e,i)=>{var s=i(26541),n=i(17717),r=i(385),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,s(e)-i),o(t,n(e)-a),t}},21373:(t,e,i)=>{var s=i(54380),n=i(17717),r=i(40136),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,s(e)+i),o(t,n(e)-a),t}},91660:(t,e,i)=>{t.exports={BottomCenter:i(54312),BottomLeft:i(46768),BottomRight:i(35827),Center:i(46871),LeftCenter:i(5198),QuickSet:i(11879),RightCenter:i(80503),TopCenter:i(89698),TopLeft:i(922),TopRight:i(21373)}},71926:(t,e,i)=>{var s=i(60461),n=i(79291),r={In:i(91660),To:i(16694)};r=n(!1,r,s),t.exports=r},21578:(t,e,i)=>{var s=i(62235),n=i(35893),r=i(88417),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,n(e)+i),o(t,s(e)+a),t}},10210:(t,e,i)=>{var s=i(62235),n=i(26541),r=i(385),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,n(e)-i),o(t,s(e)+a),t}},82341:(t,e,i)=>{var s=i(62235),n=i(54380),r=i(40136),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,n(e)+i),o(t,s(e)+a),t}},87958:(t,e,i)=>{var s=i(62235),n=i(26541),r=i(86327),o=i(40136);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)-i),r(t,s(e)+a),t}},40080:(t,e,i)=>{var s=i(7702),n=i(26541),r=i(20786),o=i(40136);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)-i),r(t,s(e)+a),t}},88466:(t,e,i)=>{var s=i(26541),n=i(17717),r=i(40136),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,s(e)-i),o(t,n(e)-a),t}},38829:(t,e,i)=>{var s=i(60461),n=[];n[s.BOTTOM_CENTER]=i(21578),n[s.BOTTOM_LEFT]=i(10210),n[s.BOTTOM_RIGHT]=i(82341),n[s.LEFT_BOTTOM]=i(87958),n[s.LEFT_CENTER]=i(40080),n[s.LEFT_TOP]=i(88466),n[s.RIGHT_BOTTOM]=i(19211),n[s.RIGHT_CENTER]=i(34609),n[s.RIGHT_TOP]=i(48741),n[s.TOP_CENTER]=i(49440),n[s.TOP_LEFT]=i(81288),n[s.TOP_RIGHT]=i(61323);t.exports=function(t,e,i,s,r){return n[i](t,e,s,r)}},19211:(t,e,i)=>{var s=i(62235),n=i(54380),r=i(86327),o=i(385);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)+i),r(t,s(e)+a),t}},34609:(t,e,i)=>{var s=i(7702),n=i(54380),r=i(20786),o=i(385);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,n(e)+i),r(t,s(e)+a),t}},48741:(t,e,i)=>{var s=i(54380),n=i(17717),r=i(385),o=i(66737);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),r(t,s(e)+i),o(t,n(e)-a),t}},49440:(t,e,i)=>{var s=i(35893),n=i(17717),r=i(86327),o=i(88417);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,s(e)+i),r(t,n(e)-a),t}},81288:(t,e,i)=>{var s=i(26541),n=i(17717),r=i(86327),o=i(385);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,s(e)-i),r(t,n(e)-a),t}},61323:(t,e,i)=>{var s=i(54380),n=i(17717),r=i(86327),o=i(40136);t.exports=function(t,e,i,a){return void 0===i&&(i=0),void 0===a&&(a=0),o(t,s(e)+i),r(t,n(e)-a),t}},16694:(t,e,i)=>{t.exports={BottomCenter:i(21578),BottomLeft:i(10210),BottomRight:i(82341),LeftBottom:i(87958),LeftCenter:i(40080),LeftTop:i(88466),QuickSet:i(38829),RightBottom:i(19211),RightCenter:i(34609),RightTop:i(48741),TopCenter:i(49440),TopLeft:i(81288),TopRight:i(61323)}},66786:(t,e,i)=>{var s=i(88417),n=i(20786);t.exports=function(t,e,i){return s(t,e),n(t,i)}},62235:t=>{t.exports=function(t){return t.y+t.height-t.height*t.originY}},72873:(t,e,i)=>{var s=i(62235),n=i(26541),r=i(54380),o=i(17717),a=i(87841);t.exports=function(t,e){void 0===e&&(e=new a);var i=n(t),h=o(t);return e.x=i,e.y=h,e.width=r(t)-i,e.height=s(t)-h,e}},35893:t=>{t.exports=function(t){return t.x-t.width*t.originX+.5*t.width}},7702:t=>{t.exports=function(t){return t.y-t.height*t.originY+.5*t.height}},26541:t=>{t.exports=function(t){return t.x-t.width*t.originX}},87431:t=>{t.exports=function(t){return t.width*t.originX}},46928:t=>{t.exports=function(t){return t.height*t.originY}},54380:t=>{t.exports=function(t){return t.x+t.width-t.width*t.originX}},17717:t=>{t.exports=function(t){return t.y-t.height*t.originY}},86327:t=>{t.exports=function(t,e){return t.y=e-t.height+t.height*t.originY,t}},88417:t=>{t.exports=function(t,e){var i=t.width*t.originX;return t.x=e+i-.5*t.width,t}},20786:t=>{t.exports=function(t,e){var i=t.height*t.originY;return t.y=e+i-.5*t.height,t}},385:t=>{t.exports=function(t,e){return t.x=e+t.width*t.originX,t}},40136:t=>{t.exports=function(t,e){return t.x=e-t.width+t.width*t.originX,t}},66737:t=>{t.exports=function(t,e){return t.y=e+t.height*t.originY,t}},58724:(t,e,i)=>{t.exports={CenterOn:i(66786),GetBottom:i(62235),GetBounds:i(72873),GetCenterX:i(35893),GetCenterY:i(7702),GetLeft:i(26541),GetOffsetX:i(87431),GetOffsetY:i(46928),GetRight:i(54380),GetTop:i(17717),SetBottom:i(86327),SetCenterX:i(88417),SetCenterY:i(20786),SetLeft:i(385),SetRight:i(40136),SetTop:i(66737)}},20623:t=>{t.exports={setCrisp:function(t){return["optimizeSpeed","-moz-crisp-edges","-o-crisp-edges","-webkit-optimize-contrast","optimize-contrast","crisp-edges","pixelated"].forEach((function(e){t.style["image-rendering"]=e})),t.style.msInterpolationMode="nearest-neighbor",t},setBicubic:function(t){return t.style["image-rendering"]="auto",t.style.msInterpolationMode="bicubic",t}}},27919:(t,e,i)=>{var s,n,r,o=i(8054),a=i(68703),h=[],l=!1;t.exports=(r=function(){var t=0;return h.forEach((function(e){e.parent&&t++})),t},{create2D:function(t,e,i){return s(t,e,i,o.CANVAS)},create:s=function(t,e,i,s,r){var u;void 0===e&&(e=1),void 0===i&&(i=1),void 0===s&&(s=o.CANVAS),void 0===r&&(r=!1);var c=n(s);return null===c?(c={parent:t,canvas:document.createElement("canvas"),type:s},s===o.CANVAS&&h.push(c),u=c.canvas):(c.parent=t,u=c.canvas),r&&(c.parent=u),u.width=e,u.height=i,l&&s===o.CANVAS&&a.disable(u.getContext("2d",{willReadFrequently:!1})),u},createWebGL:function(t,e,i){return s(t,e,i,o.WEBGL)},disableSmoothing:function(){l=!0},enableSmoothing:function(){l=!1},first:n=function(t){if(void 0===t&&(t=o.CANVAS),t===o.WEBGL)return null;for(var e=0;e<h.length;e++){var i=h[e];if(!i.parent&&i.type===t)return i}return null},free:function(){return h.length-r()},pool:h,remove:function(t){var e=t instanceof HTMLCanvasElement;h.forEach((function(i){(e&&i.canvas===t||!e&&i.parent===t)&&(i.parent=null,i.canvas.width=1,i.canvas.height=1)}))},total:r})},68703:t=>{var e,i="";t.exports={disable:function(t){return""===i&&(i=e(t)),i&&(t[i]=!1),t},enable:function(t){return""===i&&(i=e(t)),i&&(t[i]=!0),t},getPrefix:e=function(t){for(var e=["i","webkitI","msI","mozI","oI"],i=0;i<e.length;i++){var s=e[i]+"mageSmoothingEnabled";if(s in t)return s}return null},isEnabled:function(t){return null!==i?t[i]:null}}},65208:t=>{t.exports=function(t,e){return void 0===e&&(e="none"),t.style.msTouchAction=e,t.style["ms-touch-action"]=e,t.style["touch-action"]=e,t}},91610:t=>{t.exports=function(t,e){void 0===e&&(e="none");return["-webkit-","-khtml-","-moz-","-ms-",""].forEach((function(i){t.style[i+"user-select"]=e})),t.style["-webkit-touch-callout"]=e,t.style["-webkit-tap-highlight-color"]="rgba(0, 0, 0, 0)",t}},26253:(t,e,i)=>{t.exports={CanvasInterpolation:i(20623),CanvasPool:i(27919),Smoothing:i(68703),TouchAction:i(65208),UserSelect:i(91610)}},40987:(t,e,i)=>{var s=i(83419),n=i(37589),r=i(1e3),o=i(7537),a=i(87837),h=new s({initialize:function(t,e,i,s){void 0===t&&(t=0),void 0===e&&(e=0),void 0===i&&(i=0),void 0===s&&(s=255),this.r=0,this.g=0,this.b=0,this.a=255,this._h=0,this._s=0,this._v=0,this._locked=!1,this.gl=[0,0,0,1],this._color=0,this._color32=0,this._rgba="",this.setTo(t,e,i,s)},transparent:function(){return this._locked=!0,this.red=0,this.green=0,this.blue=0,this.alpha=0,this._locked=!1,this.update(!0)},setTo:function(t,e,i,s,n){return void 0===s&&(s=255),void 0===n&&(n=!0),this._locked=!0,this.red=t,this.green=e,this.blue=i,this.alpha=s,this._locked=!1,this.update(n)},setGLTo:function(t,e,i,s){return void 0===s&&(s=1),this._locked=!0,this.redGL=t,this.greenGL=e,this.blueGL=i,this.alphaGL=s,this._locked=!1,this.update(!0)},setFromRGB:function(t){return this._locked=!0,this.red=t.r,this.green=t.g,this.blue=t.b,t.hasOwnProperty("a")&&(this.alpha=t.a),this._locked=!1,this.update(!0)},setFromHSV:function(t,e,i){return o(t,e,i,this)},update:function(t){if(void 0===t&&(t=!1),this._locked)return this;var e=this.r,i=this.g,s=this.b,o=this.a;return this._color=n(e,i,s),this._color32=r(e,i,s,o),this._rgba="rgba("+e+","+i+","+s+","+o/255+")",t&&a(e,i,s,this),this},updateHSV:function(){var t=this.r,e=this.g,i=this.b;return a(t,e,i,this),this},clone:function(){return new h(this.r,this.g,this.b,this.a)},gray:function(t){return this.setTo(t,t,t)},random:function(t,e){void 0===t&&(t=0),void 0===e&&(e=255);var i=Math.floor(t+Math.random()*(e-t)),s=Math.floor(t+Math.random()*(e-t)),n=Math.floor(t+Math.random()*(e-t));return this.setTo(i,s,n)},randomGray:function(t,e){void 0===t&&(t=0),void 0===e&&(e=255);var i=Math.floor(t+Math.random()*(e-t));return this.setTo(i,i,i)},saturate:function(t){return this.s+=t/100,this},desaturate:function(t){return this.s-=t/100,this},lighten:function(t){return this.v+=t/100,this},darken:function(t){return this.v-=t/100,this},brighten:function(t){var e=this.r,i=this.g,s=this.b;return e=Math.max(0,Math.min(255,e-Math.round(-t/100*255))),i=Math.max(0,Math.min(255,i-Math.round(-t/100*255))),s=Math.max(0,Math.min(255,s-Math.round(-t/100*255))),this.setTo(e,i,s)},color:{get:function(){return this._color}},color32:{get:function(){return this._color32}},rgba:{get:function(){return this._rgba}},redGL:{get:function(){return this.gl[0]},set:function(t){this.gl[0]=Math.min(Math.abs(t),1),this.r=Math.floor(255*this.gl[0]),this.update(!0)}},greenGL:{get:function(){return this.gl[1]},set:function(t){this.gl[1]=Math.min(Math.abs(t),1),this.g=Math.floor(255*this.gl[1]),this.update(!0)}},blueGL:{get:function(){return this.gl[2]},set:function(t){this.gl[2]=Math.min(Math.abs(t),1),this.b=Math.floor(255*this.gl[2]),this.update(!0)}},alphaGL:{get:function(){return this.gl[3]},set:function(t){this.gl[3]=Math.min(Math.abs(t),1),this.a=Math.floor(255*this.gl[3]),this.update()}},red:{get:function(){return this.r},set:function(t){t=Math.floor(Math.abs(t)),this.r=Math.min(t,255),this.gl[0]=t/255,this.update(!0)}},green:{get:function(){return this.g},set:function(t){t=Math.floor(Math.abs(t)),this.g=Math.min(t,255),this.gl[1]=t/255,this.update(!0)}},blue:{get:function(){return this.b},set:function(t){t=Math.floor(Math.abs(t)),this.b=Math.min(t,255),this.gl[2]=t/255,this.update(!0)}},alpha:{get:function(){return this.a},set:function(t){t=Math.floor(Math.abs(t)),this.a=Math.min(t,255),this.gl[3]=t/255,this.update()}},h:{get:function(){return this._h},set:function(t){this._h=t,o(t,this._s,this._v,this)}},s:{get:function(){return this._s},set:function(t){this._s=t,o(this._h,t,this._v,this)}},v:{get:function(){return this._v},set:function(t){this._v=t,o(this._h,this._s,t,this)}}});t.exports=h},92728:(t,e,i)=>{var s=i(37589);t.exports=function(t){void 0===t&&(t=1024);var e,i=[],n=255,r=255,o=0,a=0;for(e=0;e<=n;e++)i.push({r:r,g:e,b:a,color:s(r,e,a)});for(o=255,e=n;e>=0;e--)i.push({r:e,g:o,b:a,color:s(e,o,a)});for(r=0,e=0;e<=n;e++,o--)i.push({r:r,g:o,b:e,color:s(r,o,e)});for(o=0,a=255,e=0;e<=n;e++,a--,r++)i.push({r:r,g:o,b:a,color:s(r,o,a)});if(1024===t)return i;var h=[],l=0,u=1024/t;for(e=0;e<t;e++)h.push(i[Math.floor(l)]),l+=u;return h}},91588:t=>{t.exports=function(t){var e={r:t>>16&255,g:t>>8&255,b:255&t,a:255};return t>16777215&&(e.a=t>>>24),e}},62957:t=>{t.exports=function(t){var e=t.toString(16);return 1===e.length?"0"+e:e}},37589:t=>{t.exports=function(t,e,i){return t<<16|e<<8|i}},1e3:t=>{t.exports=function(t,e,i,s){return s<<24|t<<16|e<<8|i}},62183:(t,e,i)=>{var s=i(40987),n=i(89528);t.exports=function(t,e,i){var r=i,o=i,a=i;if(0!==e){var h=i<.5?i*(1+e):i+e-i*e,l=2*i-h;r=n(l,h,t+1/3),o=n(l,h,t),a=n(l,h,t-1/3)}return(new s).setGLTo(r,o,a,1)}},27939:(t,e,i)=>{var s=i(7537);t.exports=function(t,e){void 0===t&&(t=1),void 0===e&&(e=1);for(var i=[],n=0;n<=359;n++)i.push(s(n/359,t,e));return i}},7537:(t,e,i)=>{var s=i(37589);function n(t,e,i,s){var n=(t+6*e)%6,r=Math.min(n,4-n,1);return Math.round(255*(s-s*i*Math.max(0,r)))}t.exports=function(t,e,i,r){void 0===e&&(e=1),void 0===i&&(i=1);var o=n(5,t,e,i),a=n(3,t,e,i),h=n(1,t,e,i);return r?r.setTo?r.setTo(o,a,h,r.alpha,!0):(r.r=o,r.g=a,r.b=h,r.color=s(o,a,h),r):{r:o,g:a,b:h,color:s(o,a,h)}}},70238:(t,e,i)=>{var s=i(40987);t.exports=function(t){var e=new s;t=t.replace(/^(?:#|0x)?([a-f\d])([a-f\d])([a-f\d])$/i,(function(t,e,i,s){return e+e+i+i+s+s}));var i=/^(?:#|0x)?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);if(i){var n=parseInt(i[1],16),r=parseInt(i[2],16),o=parseInt(i[3],16);e.setTo(n,r,o)}return e}},89528:t=>{t.exports=function(t,e,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}},30100:(t,e,i)=>{var s=i(40987),n=i(90664);t.exports=function(t){var e=n(t);return new s(e.r,e.g,e.b,e.a)}},90664:t=>{t.exports=function(t){return t>16777215?{a:t>>>24,r:t>>16&255,g:t>>8&255,b:255&t}:{a:255,r:t>>16&255,g:t>>8&255,b:255&t}}},13699:(t,e,i)=>{var s=i(28915),n=function(t,e,i,n,r,o,a,h){void 0===a&&(a=100),void 0===h&&(h=0);var l=h/a;return{r:s(t,n,l),g:s(e,r,l),b:s(i,o,l)}};t.exports={RGBWithRGB:n,ColorWithRGB:function(t,e,i,s,r,o){return void 0===r&&(r=100),void 0===o&&(o=0),n(t.r,t.g,t.b,e,i,s,r,o)},ColorWithColor:function(t,e,i,s){return void 0===i&&(i=100),void 0===s&&(s=0),n(t.r,t.g,t.b,e.r,e.g,e.b,i,s)}}},68957:(t,e,i)=>{var s=i(40987);t.exports=function(t){return new s(t.r,t.g,t.b,t.a)}},87388:(t,e,i)=>{var s=i(40987);t.exports=function(t){var e=new s,i=/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+(?:\.\d+)?))?\s*\)$/.exec(t.toLowerCase());if(i){var n=parseInt(i[1],10),r=parseInt(i[2],10),o=parseInt(i[3],10),a=void 0!==i[4]?parseFloat(i[4]):1;e.setTo(n,r,o,255*a)}return e}},87837:t=>{t.exports=function(t,e,i,s){void 0===s&&(s={h:0,s:0,v:0}),t/=255,e/=255,i/=255;var n=Math.min(t,e,i),r=Math.max(t,e,i),o=r-n,a=0,h=0===r?0:o/r,l=r;return r!==n&&(r===t?a=(e-i)/o+(e<i?6:0):r===e?a=(i-t)/o+2:r===i&&(a=(t-e)/o+4),a/=6),s.hasOwnProperty("_h")?(s._h=a,s._s=h,s._v=l):(s.h=a,s.s=h,s.v=l),s}},75723:(t,e,i)=>{var s=i(62957);t.exports=function(t,e,i,n,r){return void 0===n&&(n=255),void 0===r&&(r="#"),"#"===r?"#"+((1<<24)+(t<<16)+(e<<8)+i).toString(16).slice(1,7):"0x"+s(n)+s(t)+s(e)+s(i)}},85386:(t,e,i)=>{var s=i(30976),n=i(40987);t.exports=function(t,e){return void 0===t&&(t=0),void 0===e&&(e=255),new n(s(t,e),s(t,e),s(t,e))}},80333:(t,e,i)=>{var s=i(70238),n=i(30100),r=i(68957),o=i(87388);t.exports=function(t){switch(typeof t){case"string":return"rgb"===t.substr(0,3).toLowerCase()?o(t):s(t);case"number":return n(t);case"object":return r(t)}}},3956:(t,e,i)=>{var s=i(40987);s.ColorSpectrum=i(92728),s.ColorToRGBA=i(91588),s.ComponentToHex=i(62957),s.GetColor=i(37589),s.GetColor32=i(1e3),s.HexStringToColor=i(70238),s.HSLToColor=i(62183),s.HSVColorWheel=i(27939),s.HSVToRGB=i(7537),s.HueToComponent=i(89528),s.IntegerToColor=i(30100),s.IntegerToRGB=i(90664),s.Interpolate=i(13699),s.ObjectToColor=i(68957),s.RandomRGB=i(85386),s.RGBStringToColor=i(87388),s.RGBToHSV=i(87837),s.RGBToString=i(75723),s.ValueToColor=i(80333),t.exports=s},27460:(t,e,i)=>{t.exports={Align:i(71926),BaseShader:i(73894),Bounds:i(58724),Canvas:i(26253),Color:i(3956),ColorMatrix:i(89422),Masks:i(69781),RGB:i(51767)}},6858:(t,e,i)=>{var s=i(83419),n=i(39429),r=new s({initialize:function(t,e,i,s,n,r){e||(e=t.sys.make.image({x:i,y:s,key:n,frame:r,add:!1})),this.bitmapMask=e,this.invertAlpha=!1,this.isStencil=!1},setBitmap:function(t){this.bitmapMask=t},preRenderWebGL:function(t,e,i){t.pipelines.BITMAPMASK_PIPELINE.beginMask(this,e,i)},postRenderWebGL:function(t,e,i){t.pipelines.BITMAPMASK_PIPELINE.endMask(this,e,i)},preRenderCanvas:function(){},postRenderCanvas:function(){},destroy:function(){this.bitmapMask=null}});n.register("bitmapMask",(function(t,e,i,s,n){return new r(this.scene,t,e,i,s,n)})),t.exports=r},80661:(t,e,i)=>{var s=new(i(83419))({initialize:function(t,e){this.geometryMask=e,this.invertAlpha=!1,this.isStencil=!0,this.level=0},setShape:function(t){return this.geometryMask=t,this},setInvertAlpha:function(t){return void 0===t&&(t=!0),this.invertAlpha=t,this},preRenderWebGL:function(t,e,i){var s=t.gl;t.flush(),0===t.maskStack.length&&(s.enable(s.STENCIL_TEST),s.clear(s.STENCIL_BUFFER_BIT),t.maskCount=0),t.currentCameraMask.mask!==this&&(t.currentMask.mask=this),t.maskStack.push({mask:this,camera:i}),this.applyStencil(t,i,!0),t.maskCount++},applyStencil:function(t,e,i){var s=t.gl,n=this.geometryMask,r=t.maskCount,o=255;s.colorMask(!1,!1,!1,!1),i?(s.stencilFunc(s.EQUAL,r,o),s.stencilOp(s.KEEP,s.KEEP,s.INCR),r++):(s.stencilFunc(s.EQUAL,r+1,o),s.stencilOp(s.KEEP,s.KEEP,s.DECR)),this.level=r,n.renderWebGL(t,n,e),t.flush(),s.colorMask(!0,!0,!0,!0),s.stencilOp(s.KEEP,s.KEEP,s.KEEP),this.invertAlpha?s.stencilFunc(s.NOTEQUAL,r,o):s.stencilFunc(s.EQUAL,r,o)},postRenderWebGL:function(t){var e=t.gl;t.maskStack.pop(),t.maskCount--,t.flush();var i=t.currentMask;if(0===t.maskStack.length)i.mask=null,e.disable(e.STENCIL_TEST);else{var s=t.maskStack[t.maskStack.length-1];s.mask.applyStencil(t,s.camera,!1),t.currentCameraMask.mask!==s.mask?(i.mask=s.mask,i.camera=s.camera):i.mask=null}},preRenderCanvas:function(t,e,i){var s=this.geometryMask;t.currentContext.save(),s.renderCanvas(t,s,i,null,null,!0),t.currentContext.clip()},postRenderCanvas:function(t){t.currentContext.restore()},destroy:function(){this.geometryMask=null}});t.exports=s},69781:(t,e,i)=>{t.exports={BitmapMask:i(6858),GeometryMask:i(80661)}},73894:(t,e,i)=>{var s=new(i(83419))({initialize:function(t,e,i,s){e&&""!==e||(e=["precision mediump float;","uniform vec2 resolution;","varying vec2 fragCoord;","void main () {"," vec2 uv = fragCoord / resolution.xy;"," gl_FragColor = vec4(uv.xyx, 1.0);","}"].join("\n")),i&&""!==i||(i=["precision mediump float;","uniform mat4 uProjectionMatrix;","uniform mat4 uViewMatrix;","uniform vec2 uResolution;","attribute vec2 inPosition;","varying vec2 fragCoord;","varying vec2 outTexCoord;","void main () {"," gl_Position = uProjectionMatrix * uViewMatrix * vec4(inPosition, 1.0, 1.0);"," fragCoord = vec2(inPosition.x, uResolution.y - inPosition.y);"," outTexCoord = vec2(inPosition.x / uResolution.x, fragCoord.y / uResolution.y);","}"].join("\n")),void 0===s&&(s=null),this.key=t,this.fragmentSrc=e,this.vertexSrc=i,this.uniforms=s}});t.exports=s},40366:t=>{t.exports=function(t,e){var i;if(e)"string"==typeof e?i=document.getElementById(e):"object"==typeof e&&1===e.nodeType&&(i=e);else if(t.parentElement||null===e)return t;return i||(i=document.body),i.appendChild(t),t}},83719:(t,e,i)=>{var s=i(40366);t.exports=function(t){var e=t.config;if(e.parent&&e.domCreateContainer){var i=document.createElement("div");i.style.cssText=["display: block;","width: "+t.scale.width+"px;","height: "+t.scale.height+"px;","padding: 0; margin: 0;","position: absolute;","overflow: hidden;","pointer-events: "+e.domPointerEvents+";","transform: scale(1);","transform-origin: left top;"].join(" "),t.domContainer=i,s(i,e.parent)}}},57264:(t,e,i)=>{var s=i(25892);t.exports=function(t){if("complete"!==document.readyState&&"interactive"!==document.readyState){var e=function(){document.removeEventListener("deviceready",e,!0),document.removeEventListener("DOMContentLoaded",e,!0),window.removeEventListener("load",e,!0),t()};document.body?s.cordova?document.addEventListener("deviceready",e,!1):(document.addEventListener("DOMContentLoaded",e,!0),window.addEventListener("load",e,!0)):window.setTimeout(e,20)}else t()}},57811:t=>{t.exports=function(t){if(!t)return window.innerHeight;var e=Math.abs(window.orientation),i={w:0,h:0},s=document.createElement("div");return s.setAttribute("style","position: fixed; height: 100vh; width: 0; top: 0"),document.documentElement.appendChild(s),i.w=90===e?s.offsetHeight:window.innerWidth,i.h=90===e?window.innerWidth:s.offsetHeight,document.documentElement.removeChild(s),s=null,90!==Math.abs(window.orientation)?i.h:i.w}},45818:(t,e,i)=>{var s=i(13560);t.exports=function(t,e){var i=window.screen,n=!!i&&(i.orientation||i.mozOrientation||i.msOrientation);return n&&"string"==typeof n.type?n.type:"string"==typeof n?n:"number"==typeof window.orientation?0===window.orientation||180===window.orientation?s.ORIENTATION.PORTRAIT:s.ORIENTATION.LANDSCAPE:window.matchMedia?window.matchMedia("(orientation: portrait)").matches?s.ORIENTATION.PORTRAIT:window.matchMedia("(orientation: landscape)").matches?s.ORIENTATION.LANDSCAPE:void 0:e>t?s.ORIENTATION.PORTRAIT:s.ORIENTATION.LANDSCAPE}},74403:t=>{t.exports=function(t){var e;return""!==t&&("string"==typeof t?e=document.getElementById(t):t&&1===t.nodeType&&(e=t)),e||(e=document.body),e}},56836:t=>{t.exports=function(t){var e="";try{if(window.DOMParser)e=(new DOMParser).parseFromString(t,"text/xml");else(e=new ActiveXObject("Microsoft.XMLDOM")).loadXML(t)}catch(t){e=null}return e&&e.documentElement&&!e.getElementsByTagName("parsererror").length?e:null}},35846:t=>{t.exports=function(t){t.parentNode&&t.parentNode.removeChild(t)}},43092:(t,e,i)=>{var s=i(83419),n=i(29747),r=new s({initialize:function(){this.isRunning=!1,this.callback=n,this.isSetTimeOut=!1,this.timeOutID=null,this.delay=0;var t=this;this.step=function e(i){t.callback(i),t.isRunning&&(t.timeOutID=window.requestAnimationFrame(e))},this.stepTimeout=function e(){t.isRunning&&(t.timeOutID=window.setTimeout(e,t.delay)),t.callback(window.performance.now())}},start:function(t,e,i){this.isRunning||(this.callback=t,this.isSetTimeOut=e,this.delay=i,this.isRunning=!0,this.timeOutID=e?window.setTimeout(this.stepTimeout,0):window.requestAnimationFrame(this.step))},stop:function(){this.isRunning=!1,this.isSetTimeOut?clearTimeout(this.timeOutID):window.cancelAnimationFrame(this.timeOutID)},destroy:function(){this.stop(),this.callback=n}});t.exports=r},84902:(t,e,i)=>{var s={AddToDOM:i(40366),DOMContentLoaded:i(57264),GetInnerHeight:i(57811),GetScreenOrientation:i(45818),GetTarget:i(74403),ParseXML:i(56836),RemoveFromDOM:i(35846),RequestAnimationFrame:i(43092)};t.exports=s},47565:(t,e,i)=>{var s=i(83419),n=i(50792),r=i(37277),o=new s({Extends:n,initialize:function(){n.call(this)},shutdown:function(){this.removeAllListeners()},destroy:function(){this.removeAllListeners()}});r.register("EventEmitter",o,"events"),t.exports=o},93055:(t,e,i)=>{t.exports={EventEmitter:i(47565)}},20122:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e){void 0===e&&(e=1),n.call(this,r.BARREL,t),this.amount=e}});t.exports=o},32251:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s,o,a,h){void 0===i&&(i=1),void 0===s&&(s=1),void 0===o&&(o=1),void 0===a&&(a=1),void 0===h&&(h=4),n.call(this,r.BLOOM,t),this.steps=h,this.offsetX=i,this.offsetY=s,this.blurStrength=o,this.strength=a,this.glcolor=[1,1,1],null!=e&&(this.color=e)},color:{get:function(){var t=this.glcolor;return(255*t[0]<<16)+(255*t[1]<<8)+(255*t[2]|0)},set:function(t){var e=this.glcolor;e[0]=(t>>16&255)/255,e[1]=(t>>8&255)/255,e[2]=(255&t)/255}}});t.exports=o},9047:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s,o,a,h){void 0===e&&(e=0),void 0===i&&(i=2),void 0===s&&(s=2),void 0===o&&(o=1),void 0===h&&(h=4),n.call(this,r.BLUR,t),this.quality=e,this.x=i,this.y=s,this.steps=h,this.strength=o,this.glcolor=[1,1,1],null!=a&&(this.color=a)},color:{get:function(){var t=this.glcolor;return(255*t[0]<<16)+(255*t[1]<<8)+(255*t[2]|0)},set:function(t){var e=this.glcolor;e[0]=(t>>16&255)/255,e[1]=(t>>8&255)/255,e[2]=(255&t)/255}}});t.exports=o},27885:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s,o,a,h,l){void 0===e&&(e=.5),void 0===i&&(i=1),void 0===s&&(s=.2),void 0===o&&(o=!1),void 0===a&&(a=1),void 0===h&&(h=1),void 0===l&&(l=1),n.call(this,r.BOKEH,t),this.radius=e,this.amount=i,this.contrast=s,this.isTiltShift=o,this.strength=l,this.blurX=a,this.blurY=h}});t.exports=o},12578:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s,o,a){void 0===e&&(e=8),void 0===o&&(o=1),void 0===a&&(a=.005),n.call(this,r.CIRCLE,t),this.scale=o,this.feather=a,this.thickness=e,this.glcolor=[1,.2,.7],this.glcolor2=[1,0,0,.4],null!=i&&(this.color=i),null!=s&&(this.backgroundColor=s)},color:{get:function(){var t=this.glcolor;return(255*t[0]<<16)+(255*t[1]<<8)+(255*t[2]|0)},set:function(t){var e=this.glcolor;e[0]=(t>>16&255)/255,e[1]=(t>>8&255)/255,e[2]=(255&t)/255}},backgroundColor:{get:function(){var t=this.glcolor2;return(255*t[0]<<16)+(255*t[1]<<8)+(255*t[2]|0)},set:function(t){var e=this.glcolor2;e[0]=(t>>16&255)/255,e[1]=(t>>8&255)/255,e[2]=(255&t)/255}},backgroundAlpha:{get:function(){return this.glcolor2[3]},set:function(t){this.glcolor2[3]=t}}});t.exports=o},15802:(t,e,i)=>{var s=i(83419),n=i(89422),r=i(14811),o=new s({Extends:n,initialize:function(t){n.call(this),this.type=r.COLOR_MATRIX,this.gameObject=t,this.active=!0},destroy:function(){this.gameObject=null,this._matrix=null,this._data=null}});t.exports=o},72898:(t,e,i)=>{var s=new(i(83419))({initialize:function(t,e){this.type=t,this.gameObject=e,this.active=!0},setActive:function(t){return this.active=t,this},destroy:function(){this.gameObject=null,this.active=!1}});t.exports=s},44553:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s){void 0===e&&(e="__WHITE"),void 0===i&&(i=.005),void 0===s&&(s=.005),n.call(this,r.DISPLACEMENT,t),this.x=i,this.y=s,this.glTexture,this.setTexture(e)},setTexture:function(t){var e=this.gameObject.scene.sys.textures.getFrame(t);return e&&(this.glTexture=e.glTexture),this}});t.exports=o},68531:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s,o){void 0===i&&(i=4),void 0===s&&(s=0),void 0===o&&(o=!1),n.call(this,r.GLOW,t),this.outerStrength=i,this.innerStrength=s,this.knockout=o,this.glcolor=[1,1,1,1],void 0!==e&&(this.color=e)},color:{get:function(){var t=this.glcolor;return(255*t[0]<<16)+(255*t[1]<<8)+(255*t[2]|0)},set:function(t){var e=this.glcolor;e[0]=(t>>16&255)/255,e[1]=(t>>8&255)/255,e[2]=(255&t)/255}}});t.exports=o},37102:(t,e,i)=>{var s=i(83419),n=i(72898),r=i(14811),o=new s({Extends:n,initialize:function(t,e,i,s,o,a,h,l,u){void 0===s&&(s=.2),void 0===o&&(o=0),void 0===a&&(aEXIT: 1 [Showing last 50.0KB of line 2 (line is 0B). Full output: /tmp/pi-bash-19f6a103fd047c3d.log]
thinking
The output is flooded with source text from phaser.min.js — probably a stack trace containing minified sources, meaning an error was thrown at require/boot time. Exit code 1. The output got cut off midway. The error message itself might be in the part that was truncated earlier. Let me rerun while saving the output to a file, and check the top portion of the output.
The output got swamped by the minified stack trace. Let me capture the actual error message:
{"command":"cd /tmp && timeout 20 node smoke.js > /tmp/out.log 2>&1; echo \"EXIT $?\"; grep -a -E \"UNCAUGHT|ERR|TypeError|ReferenceError|noEXIT 1 !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Phaser",[],e):"object"==typeof exports?exports.Phaser=e():t.Phaser=e()}(this,(()=>(()=>{var t={50792:t=>{"use strict";var e=Object.prototype.hasOwnProperty,i="~";function ReferenceError: HTMLVideoElement is not defined
thinking
The error is `HTMLVideoElement is not defined` — it's a shim problem, not a game problem. Add global HTMLVideoElement, and there's probably also HTMLImageElement, HTMLAudioElement, and so on. Let's add the classes and retry.
That's a shim gap, not a game bug. Adding the missing globals:
{"command":"cd /tmp && sed -i \"s|global.HTMLCanvasElement = function () {};|global.HTMLCanvasElement = function () {};\\nglobal.HTMLVideoElEXIT 1 Phaser version: 3.80.1 UNCAUGHT: Cannot read properties of null (reading 'blendModes') TypeError: Cannot read properties of null (reading 'blendModes') scene active: false
thinking
Progress: Phaser starts up, but the renderer fails with a null reading of 'blendModes'. Probably because of HEADLESS... hmm, "Cannot read properties of null (reading 'blendModes')" — this is probably the CanvasRenderer referencing something. Might be related to my Proxy 2D context stub returning functions where Phaser expects objects, or maybe the texture manager's __DEFAULT / __MISSING texture creation is failing. Let me grab the stack trace to pinpoint the location. The stack is minified, but I can search for function context. Let me output more lines of the UNCAUGHT stack from out.log.
{"command":"grep -a -A6 \"UNCAUGHT\" /tmp/out.log | cut -c1-400 | head -12"}UNCAUGHT: Cannot read properties of null (reading 'blendModes') TypeError: Cannot read properties of null (reading 'blendModes') at t.exports (/tmp/ws/phaser.min.js:1:802949) at t.exports [as renderCanvas] (/tmp/ws/phaser.min.js:1:233034) at initialize.generateTexture (/tmp/ws/phaser.min.js:1:232746) --- after boot --- scene active: false
thinking
The error occurs in Graphics.renderCanvas → generateTexture. Is the canvas context being used null? Is "Cannot read properties of null (reading 'blendModes')" reading `.blendModes` on null? Is it actually `renderer.blendModes`, with renderer being null? Inside renderCanvas: `pipelineUtils...`? Let me look at the minified code at position 802949 and check what's being read.
{"command":"cd /tmp/ws && node -e \"\nconst s = require('fs').readFileSync('phaser.min.js','utf8');\nconsole.log(s.slice(802800, 803200));\n,e,i)=>{var s=i(91296);t.exports=function(t,e,i,n,r){var o=n.alpha*i.alpha;if(o<=0)return!1;var a=s(i,n,r).calc;return e.globalCompositeOperation=t.blendModes[i.blendMode],e.globalAlpha=o,e.save(),a.setToContext(e),e.imageSmoothingEnabled=i.frame?!i.frame.source.scaleMode:t.antialias,!0}},63899:t=>{t.exports="losewebgl"},6119:t=>{t.exports="postrender"},48070:t=>{t.exports="prerender"},15640:t=>{t
thinking
`t.blendModes` — t is renderer... `e.globalCompositeOperation = t.blendModes[i.blendMode]` where t is null → renderer is null. In Graphics.renderCanvas(src, renderer/camera...)? When called from generateTexture of CanvasTexture in the HEADLESS renderer, the renderer passed is possibly the game's renderer. In HEADLESS mode, is game.renderer ... HeadlessRenderer with canvas null? The error indicates the first arg `t` is null. In Graphics.renderCanvas: `var renderer = src.scene.sys.renderer`? In headless, game.renderer exists (HeadlessRenderer) but... maybe it's calling with `this.scene.sys.game.renderer`? Hmm, `t` is null → maybe the scene's sys isn't fully up because create was being called during the make? No — I'm calling makeTextures inside create, and the scene's sys should be fine.
Wait: this is `t.exports=function(t,e,i,n,r)` — SetTransformStyle or similar, called from renderCanvas of a shape with args (renderer, ctx, ...). renderer being null is probably because the Graphics object was created with `scene.make.graphics({add:false})` and the scene's `sys.renderer` in HEADLESS is... does HeadlessRenderer set `this.canvas = null; this.type = HEADLESS`? Maybe HeadlessRenderer doesn't have blendModes. But the error says reading 'blendModes' of null — so t is null, not that t.blendModes is undefined. So the renderer argument was null.
In Graphics.generateTexture: uses CanvasTexture + renderCanvas(this, ...). Actually the generateTexture code around position 232746: `initialize.generateTexture` is probably Graphics.generateTexture → calls this.renderCanvas? Let me check that snippet. Actually the stack is: generateTexture → renderCanvas → SetTransform(t=null). Graphics.renderCanvas signature is (src, renderer, camera, calcMatrix) and is it called as `this.renderCanvas(this, renderer...)`? Hmm, Graphics.renderCanvas is `renderCanvas(src, renderer, camera, calcMatrix)`; does generateTexture call `GraphicsPipeline...`? Let me look at the code around 232746.
{"command":"cd /tmp/ws && node -e \"\nconst s = require('fs').readFileSync('phaser.min.js','utf8');\nconsole.log(s.slice(232400, 233400));\nlY=this.y;var a={willReadFrequently:!0};if("string"==typeof t)if(r.textures.exists(t)){var h=(s=r.textures.get(t)).getSourceImage();h instanceof HTMLCanvasElement&&(n=h.getContext("2d",a))}else n=(s=r.textures.createCanvas(t,e,i)).getSourceImage().getContext("2d",a);else t instanceof HTMLCanvasElement&&(n=t.getContext("2d",a));return n&&(this.renderCanvas(o,this,f.TargetCamera,null,n,!1),s&&s.refresh()),this},preDestroy:function(){this.commandBuffer=[]}});f.TargetCamera=new s,t.exports=f},32768:(t,e,i)=>{var s=i(85592),n=i(20926);t.exports=function(t,e,i,r,o,a){var h=e.commandBuffer,l=h.length,u=o||t.currentContext;if(0!==l&&n(t,u,e,i,r)){i.addToRenderList(e);var c=1,d=1,f=0,p=0,v=1,g=0,m=0,y=0;u.beginPath();for(var x=0;x<l;++x){switch(h[x]){case s.ARC:u.arc(h[x+1],h[x+2],h[x+3],h[x+4],h[x+5],h[x+6]),x+=7;break;case s.LINE_STYLE:v=h[x+1],f=h[x+2],c=h[x+3],g=(16711680&f)>>>16,m=(65280&f)>>>8,y=255&f,u.strokeStyle="rgba("+g+","+m+","+y+","+c+")",u.lineWidth=v,x+=3;break;case s.FILL_STYLE
thinking
generateTexture is calling `this.renderCanvas(o, this, f.TargetCamera, null, n, !1)`. Here `o` is probably `this.scene.sys.renderer` or `this.renderer`. `renderCanvas(src, renderer, camera, ...)`: the first argument `o` = null → renderer is null. In a scene under the HEADLESS renderer, does `scene.sys.renderer` become null? In Phaser, is System.renderer a reference to game.renderer? Actually it's `sys.renderer = game.renderer`. HeadlessRenderer… probably exists. But `o` was null — maybe I defined `o` earlier in generateTexture: probably `var o = this.scene.sys.renderer` — under HEADLESS, is sys.renderer null? Hmm. Actually, is HeadlessRenderer inheriting from CanvasRenderer? Let me check: Phaser.HEADLESS uses HeadlessRenderer, which does almost nothing. There's a possibility that game.renderer exists. But in generateTexture, `o` is probably defined like `var o=this.systems.renderer`, maybe… the scene here is `r` (scene systems). The code: `var r=this.scene.sys`, and then afterwards `this.renderCanvas(o,this,f.TargetCamera,null,n,!1)` — o was defined earlier (cut off part). Probably `var o=r.renderer`. In HEADLESS, is game.renderer created after the scene boots? It should be created though. Hmm, but maybe under HEADLESS, `sys.renderer` becomes null. This is a limitation of Phaser's HEADLESS — Graphics.generateTexture requires the Canvas/WebGL renderer. Workaround: force `type: Phaser.CANVAS` in the test. The CanvasRenderer will work with my shimmed 2D context (all methods are no-ops). CanvasTexture generation uses canvas 2D — no problem. Rendering: the CanvasRenderer draws images to the main canvas via drawImage — no-op. Also CanvasRenderer calls ctx.getImageData in some places for snapshot only. Should be OK. Change the replacement to Phaser.CANVAS.
`Graphics.generateTexture` requires an actual Canvas/WebGL renderer — it doesn't work with HEADLESS. I'll try forcing `Phaser.CANVAS` in the tests:
{"command":"cd /tmp && sed -i \"s/'type: Phaser.HEADLESS'/'type: Phaser.CANVAS'/\" smoke.js && timeout 25 node smoke.js > /tmp/out.log 2>&1;EXIT 0 Phaser version: 3.80.1 UNCAUGHT: Cannot create Canvas context, aborting.
thinking
The CanvasRenderer boot is failing: "Cannot create Canvas context, aborting." Phaser's CanvasRenderer.createCanvas calls CanvasPool.create, then does getContext('2d') to check the context. My shim is returning a Proxy, so that should be truthy... The check might be `if (!this.context)`. Let's look for where this message is being thrown — it's probably inside the CanvasRenderer constructor, and maybe it's checking `document.createElement('canvas').getContext('2d', options)`. My makeCanvas's getContext returns a Proxy. Hmm.
Maybe CanvasPool.create is using `document.createElement('canvas')` — in my shim that returns makeCanvas(). Then `canvas.getContext('2d', {willReadFrequently:false})` returns a Proxy. It should be fine but...
Could the issue be that `getContext` is being called on the same canvas with '2d', and then Phaser is checking `context instanceof CanvasRenderingContext2D`? That seems unlikely.
Let me search for the string "Cannot create Canvas context" within phaser and check the surrounding logic.
{"command":"cd /tmp/ws && node -e \"\nconst s = require('fs').readFileSync('phaser.min.js','utf8');\nconst i = s.indexOf('Cannot create CanvCC"),window&&(window.FORCE_WEBGL?this.renderType=n.WEBGL:window.FORCE_CANVAS&&(this.renderType=n.CANVAS))}});t.exports=p},86054:(t,e,i)=>{var s=i(20623),n=i(27919),r=i(8054),o=i(89357);t.exports=function(t){var e=t.config;if((e.customEnvironment||e.canvas)&&e.renderType===r.AUTO)throw new Error("Must set explicit renderType in custom environment");if(!e.customEnvironment&&!e.canvas&&e.renderType!==r.HEADLESS)if(e.renderType===r.AUTO&&(e.renderType=o.webGL?r.WEBGL:r.CANVAS),e.renderType===r.WEBGL){if(!o.webGL)throw new Error("Cannot create WebGL context, aborting.")}else{if(e.renderType!==r.CANVAS)throw new Error("Unknown value for renderer type: "+e.renderType);if(!o.canvas)throw new Error("Cannot create Canvas context, aborting.")}e.antialias||n.disableSmoothing();var a,h,l=t.scale.baseSize,u=l.width,c=l.height;(e.canvas?(t.canvas=e.canvas,t.canvas.width=u,t.canvas.height=c):t.canvas=n.
thinking
`o.canvas` — feature detection: `o.canvas=!!window.CanvasRenderingContext2D`. I had set `global.HTMLCanvasElement` but hadn't set `CanvasRenderingContext2D`. Add `global.CanvasRenderingContext2D = function(){}`.
Feature detection checks `window.CanvasRenderingContext2D`. Adding it:
{"command":"cd /tmp && sed -i \"s|global.HTMLCanvasElement = function () {};|global.HTMLCanvasElement = function () {};\\nglobal.CanvasRendeEXIT 0 Phaser version: 3.80.1 scene active: true solids: 677 coins: 28 enemies: 16 qblocks: 7 player: true state: play player pos: 80 433 state: dead pos: 512 376 lives: 3 score: 0 coins collected: 0 player died during auto-run (may be fine); lives: 3
thinking
The scene boots up, and the level builds: 677 solid objects, 28 coins, 16 enemies, 7 Q-blocks. The player spawns at the correct position. But during auto-run, state became 'dead' at x=512 (col 16, the '?' block zone) — pos 512,376. Hmm, state 'dead' but lives is still 3? So did die() get called but lives only gets decremented after a delayed call (which is 380*2+500ms later)? Actually, the tween is 380 with yoyo followed by a 500 delay → about 1260ms; we sampled at 2500ms so respawn should have occurred… but state is 'dead' and lives is 3. Wait — did respawn happen? state 'dead' at the 2.5s mark. Hmm, the death tween's onComplete yoyo: the tween with yoyo has duration 380, total 760, then delayedCall 500 → respawn at about 1260ms after death. If death occurred at about 1.2s, respawn should be around 2.5s — borderline. Or maybe the 'dead' state is lingering because… let me think about why the player died at x=512. x=512 = col 16. That's exactly where the '?' block at row 10 starts. Player runs along the ground and… what kills them there? Enemies at col 22 (x=704+). Player at 512, y=376 — ground level (ground top surface is 14*32=448; player center when standing is 448-15=433). y=376 is above the ground — maybe mid-death-tween (tween moves y -130 then yoyos back: 433→303→433; 376 is mid-tween). Cause of death: the auto-jump every 420ms sets SPACE.isDown=true for 30ms. But JustDown detection requires the key event machinery — manually setting isDown might make JustDown always return true. Since I'm not resetting the internal state… I set `k.SPACE._justDown = false` — but Phaser's JustDown uses keyCode state on `Key.justDown` that's set on the keydown event; manually setting isDown may not trigger JustDown (justDown is only set within the OnKeyDown handler). So the player probably never jumped, just ran straight ahead… into an enemy? An enemy at col 22 walks left toward the player. Player runs right, meets the enemy around cols 10–20 → side collision → die. That's plausible: enemy walks from 704 leftward to meet player around col 14. Player position at death is 512 (col 16). Makes sense! The player never jumped (JustDown didn't fire), got hit by an enemy from the side and correctly died. So the game logic is behaving correctly — this is actually good validation of enemy AI + die(). But then why didn't respawn occur? Death at some time t, respawn t+1260ms. The auto-run script sampled at 2500ms after boot. When did the player die? Enemy at col 22 (x≈720) walks left at 62px/s from boot, player runs right at 240px/s from x=80. Meeting time ≈ (720-80)/(240+62) ≈ 2.1s. Hmm, that's close to the 2.5s sampling. So death was around 2.1s, respawn around 3.4s — we just sampled before respawn. Also, 'lives: 3' is consistent: lives get decremented on respawn. So everything is working. Let me improve the test: simulate jump via a dispatchKeyEvent-style call to make JustDown work? Simpler: call scene's methods directly to verify stomp/win. The test exited at the 'player died' branch. Let me change the flow: after 2.5s, if state is dead, wait for respawn (poll until state is play), then do the teleport stomp test and flag test. Let me patch the tail of smoke.js: replace the else branch with a wait-until-play, then continue testing. Actually, more simply: modify the test to wait for respawn first: poll every 200ms, when state==='play' run the stomp test. Let me edit smoke.js accordingly — replace the else-branch block.
Excellent! The level construction is correct (677 solids, 28 coins, 16 enemies, 7 ? blocks). And the automatic execution player *correctly* died after making contact with a walking enemy from the side (since a simulated key press doesn't fire `JustDown`, they never jumped once). Let's finish the test after respawn, and verify stomp, ? blocks, coins, and the flag:
{"command":"cd /tmp && node -e \"\nconst fs=require('fs');\nlet s=fs.readFileSync('smoke.js','utf8');\nconst oldBlock = s.indexOf('} else {'patched EXIT 1 scene active: true solids: 677 coins: 28 enemies: 16 qblocks: 7 player: true state: play state: dead pos: 512 381 lives: 3 score: 0 coins collected: 0 UNCAUGHT: Cannot read properties of undefined (reading 'entries') respawned; lives now: 2 score: 0 state: play enemy squashed? false --- after qblock bump --- qblock used? false score: 0 coins: 0 coins: 0 score: 0 state: play score: 0 ERRORS: 1
thinking
Interesting results: 1. Respawn works (lives 3→2). ✓ 2. However, UNCAUGHT: "Cannot read properties of undefined (reading 'entries')" — need to track down the cause. This occurred around stomp (score 0, not stomped). Probably caused by my teleports: setPosition onto an enemy then setVelocity — or maybe the enemy was already destroyed (squashed in the meantime?). Actually 'enemy squashed? false', score 0 — stomp did not occur. Maybe the selected enemy was far off-screen and… no, physics runs everywhere. Player is teleported on top of the enemy, falling at velocity 300 — overlap should trigger hitEnemy → stomp. But nothing happened. Perhaps that enemy fell into a hole and got destroyed and `e` was stale? find returned an active enemy; setPosition onto it; 400ms later state play, no stomp. Hmm — perhaps the enemy moved (walking at 62px/s, ~25px in 400ms) and the player fell next to it, triggering… no, no death, score 0. Wait — the 'entries' error: "Cannot read properties of undefined (reading 'entries')" — could be Phaser's group add? fxLayer = this.add.group(); and then this.fxLayer.add(t) (t is a Text) — no problem. Or `Phaser.Utils.Array.GetRandom` in winGame — no, I didn't reach win. Maybe the error comes from a tween targeting a destroyed object? Where is 'entries' used? Map? `body.blocked`? Let me grab the stack. 3. qblock bump didn't trigger — I teleported the player under the qblock and gave it an upward velocity of -900; but I set position to q.y+64 — block is at row 10 y=336, +64 → 400, player in the air… rises and hits the block? Player's top needs to touch the block's bottom edge. From y=400 (center), rising at -900 with gravity 1900… reaches block in about 0.15s, blocked.up should trigger. Then hitQblocks checks Math.abs(pb.top - bb.bottom) < 8. Should hit. Unless the player's position got pushed out. Or the qblock found was far away (the first unused one = the one at col 16) but the player had already… hmm 'qblock used? false'. Also coins are still 0 — I teleported to the coin's position, and overlap should trigger collectCoin. Didn't happen. And the flag: teleported to 180*32, 12*32 — should overlap with flagZone… didn't win. Suspicious pattern: after respawn, teleport/overlap events are not triggering. Maybe… after buildWorld, new colliders/overlaps were added but old ones were destroyed along with the groups. Hmm, but physics.add.overlap using this.player — new player. Should work. But wait — maybe the issue is: after respawn via buildWorld() inside a delayedCall, the scene's physics world is… no, no. Another suspicion: the 'entries' UNCAUGHT error is breaking something. "reading 'entries'" — could it be from `this.anims`? Or inside ArcadeWorld after destroying a group with bodies? Let me get the full stack from the log.
Respawn works (lives 3→2), but something broke overlaps after respawn — let me see the stack of that `entries` error:
{"command":"grep -a -A8 \"UNCAUGHT\" /tmp/out.log | cut -c1-300 | head -14"}UNCAUGHT: Cannot read properties of undefined (reading 'entries') TypeError: Cannot read properties of undefined (reading 'entries') at initialize.getChildren (/tmp/ws/phaser.min.js:1:242335) at initialize.collideGroupVsGroup (/tmp/ws/phaser.min.js:1:671825) at initialize.collideHandler (/tmp/ws/phaser.min.js:1:668932) respawned; lives now: 2 --- after stomp attempt --- score: 0 state: play enemy squashed? false --- after qblock bump ---
thinking
`getChildren` failed inside collideGroupVsGroup — one of the groups was destroyed, but the collider referencing it was left behind. This is because buildWorld() destroys the groups (solids/coins/enemies), but the old colliders/overlaps from the previous buildWorld are still registered with the physics world and reference the destroyed groups. Destroying a Phaser Group by default doesn't remove colliders... Actually, destroying a GameObject with a body removes the body and the Collider should also be automatically removed (when a body is destroyed, World removes colliders that involve that body?). Colliders are tied to bodies; ArcadeWorld automatically removes colliders referencing a destroyed body (inside removeBody... via `this.colliders` check?). Apparently it's not fully removed here — collideGroupVsGroup calls object2.getChildren(), and object2 is a destroyed group (its children property has become undefined? Group.destroy sets children to undefined). Fix: track and destroy colliders/overlaps in buildWorld. Save them into an array `this.colliders = []`, push each one, and call `this.colliders.forEach(c => c.destroy())` before rebuilding. This is the real bug in my game — after death/respawn, all collisions stop working (player falls through the floor? no wait — player vs solids collider was also destroyed... but in the test, the player was standing normally after respawn, qblock check etc. Hmm, but if the collider is dead, the player should fall through the floor. In the test, after respawn, the player was in play state — but teleporting hides the physics. Actually, if the collider is broken, the player would immediately fall into the ground and die again. In the test, state remained 'play' because I immediately teleported. Either way — the fix is essential. Does Phaser remove the Collider when the body is destroyed? ArcadeWorld.removeCollider exists; is a Collider destroyed when either object is destroyed? For groups, group.destroy(true) destroys the children and their bodies; World's `removeBody` is... inside World.update, colliders check `collider.active`. Hmm, Collider has both a `destroy` method and a `shouldBeRemoved` logic. Apparently group-level colliders survive — a known Phaser behavior: colliders must be destroyed manually. Fix in index.html: add this.colliders array; wrap each physics.add.collider/overlap: `this.colliders.push(this.physics.add.collider(...))`. In buildWorld, destroy them first.
Found the actual bug: on respawn, `buildWorld()` destroys the group, but the old colliders/overlaps are still registered in the physics world and reference the destroyed group. Fix: track and destroy the colliders before rebuilding.
{"edits":[{"newText":" if (this.solids) this.solids.destroy(true);\n if (this.coins) this.coins.destroy(true);\n if (this.enemies) Successfully replaced 2 block(s) in /tmp/ws/index.html.
thinking
Wait — one detail: in `buildWorld`, `this.colliders` is destroyed after the group. It's better to destroy the collider before the group, but since we're destroying everything before reconstruction, the order doesn't matter that much. In fact, the collider's destroy only removes it from the world — safe even after the group's destroy. No problem. Alright, let's rerun the smoke test.
{"command":"cd /tmp && timeout 30 node smoke.js > /tmp/out.log 2>&1; echo \"EXIT $?\"; grep -a -E \"UNCAUGHT|respawned|squashed|qblock|coinsEXIT 0 solids: 677 coins: 28 enemies: 16 qblocks: 7 player: true state: play state: dead pos: 512 380 lives: 3 score: 0 coins collected: 0 respawned; lives now: 2 score: 200 state: play enemy squashed? true --- after qblock bump --- qblock used? false score: 200 coins: 0 coins: 1 score: 300 state: win score: 1300 NO RUNTIME ERRORS
thinking
Currently, everything passes with zero runtime errors: - Level construction ✓ - Side contact with enemy → death + respawn, remaining lives decrement ✓ - Stomp → enemy crushed, +200 score, bounce ✓ - Coin overlap → collection, +100, coin count ✓ - Flag → victory, +1000 bonus ✓ - No runtime errors ✓ The qblock bump test didn't trigger (`used? false`) — probably my synthetic teleport velocity didn't make actual head contact under headless physics timing (blocked.up requires a collision resolution; with a 500ms wait after -900 velocity, it should have hit, but... maybe the player hit the side of the block, or the head check window of <8px was missed at that exact sample moment — blocked.up is only true on the frame of collision). The hitQblocks logic only fires while `pb.blocked.up` is true, which lasts only 1–2 frames; the teleport test can't guarantee timing. The logic itself is standard and correct; in actual gameplay, when the head hits the block, blocked.up is true on the same frame. Hmm, but there's a subtle issue: `blocked.up` is set during the collision step, and update() runs after physics (in Phaser's arcade, the scene's update is... actually physics runs during the scene update cycle: the Physics world updates on the PRE_UPDATE event, and the scene update comes after). So blocked.up from this frame's collision is readable in the scene's update. ✓ Let me do a slightly more direct verification of the qblock path: directly simulate the conditions — manually set pb.blocked.up to true and place the player right below the block. Actually even simpler: call scene.hitQblocks with the player positioned to align, and manually set player.body.blocked.up = true. Patch a small test... can be done quickly with a node one-liner after boot. Add to the smoke test before the win test: align the player to the qblock, set blocked.up = true, call hitQblocks. But it's a hassle to re-edit the smoke file again; the logic is deterministic (the conditions are pure computation), so a targeted check is enough. Let me quickly write a small dedicated test that reuses the shim. Actually even simpler: modify smoke.js with sed? The helper function area... I'll just write a second small test file that requires the same shim. Let me create smoke2.js that imports the shim part... it's easier to copy smoke.js's shim by extracting from smoke.js up to the eval. Or just sed-copy: reuse the whole file with a different inspection. Quick approach: write smoke2.js that reads smoke.js's source up to "let it boot" to reuse the shim... this is getting silly. Alternative: run a modified smoke that, after respawn, places the player with their head exactly at the block's bottom edge and forces blocked.up. Let me just do that with node -e against a copy.
All core mechanics verified with zero runtime errors. One last check — the `?`-block head-bump logic (my teleport couldn't hit the exact collision frame). Let me verify it deterministically:
{"command":"cd /tmp && node -e \"\nconst fs=require('fs');\nlet s=fs.readFileSync('smoke.js','utf8');\nconst marker = \\\" // tesEXIT 0 --- qblock direct check --- qblock used? false score: 200 coins: 0 qblock used? false score: 200 coins: 0 state: win score: 1300 NO RUNTIME ERRORS
thinking
Direct check also fails. That means my alignment calculation is wrong. Player body: setSize(20,28) offset(3,2), sprite origin 0.5, texture 26x30. Body top = sprite.y - 30*0.5 + 2 = y - 13. Block bottom = q.y + 16. The condition in hitQblocks: |pb.top - bb.bottom| < 8 and horizontally: pb.right > bb.left+4 && pb.left < bb.right-4. I set p.setPosition(q2.x, q2.y + 16 + p.displayHeight/2 + 0.01) → y = q.y+16+15 = q.y+31 → body top = q.y+31-13 = q.y+18, block bottom = q.y+16 → diff 2 < 8. Horizontally they're aligned. Should pass... Unless pb.top uses body position, and after setPosition, it's... body.position only updates during the physics step! After setPosition, body.top is still stale until preUpdate sync. In actual gameplay, this is fine (blocked.up implies collision in the same frame → body position is fresh). But in my direct test, the body position was stale (the player had been teleported but the body hadn't synced). So the test's false negative is due to stale body sync, not a game bug. Fix the test: call p.body.update(p.gameObject...) or set p.body.position manually? Simplest is: after setPosition, manually set `p.body.position.set(p.x - p.body.halfWidth... )` or call `p.body.reset(p.x, p.y)` — Arcade Body has reset(x,y), which syncs position. Use p.body.reset(q2.x, y). Update smoke2 to use body.reset.
The failed check is a test artifact — `setPosition` doesn't sync the physics body until the next step, so `body.top` was stale. Using `body.reset()` in the test:
{"command":"cd /tmp && sed -i \"s|p.setPosition(q2.x, q2.y + 16 + p.displayHeight/2 + 0.01); // head flush under block|p.body.reset(q2.x, q2EXIT 124 UNCAUGHT: this.add.image(...).play is not a function
thinking
Two things:
1. This run timed out (124) and a new error occurred: `this.add.image(...).play is not a function` — this is the coin pop-out from the qblock inside hitQblocks: `const c = this.add.image(b.x, b.y - 30, 'coin0').play('spin')` — Image doesn't have .play; you need a Sprite. This is an actual bug I introduced (in the case of Image, `.play` only exists on Sprite). Fix: use this.add.sprite(...).
2. The qblock's 'used?' result wasn't output because the error occurred first during hitQblocks (after setting used, it crashed on coin pop → so used? was actually true, but the error swallowed the output... actually an uncaught error inside the hitQblocks call within the test → caught by uncaughtException, the script continues but subsequent console.log inside that block? The error was thrown on the `.play` line → the rest of the block was aborted; the subsequent lines of the test were aborted → timeout).
Fix for index.html: change the coin pop-out image to a sprite.
Caught a real bug: the coin that pops out of the `?`-block uses `this.add.image(...).play('spin')` — `play` only exists on Sprites. Fixing:
{"edits":[{"newText":" const c = this.add.sprite(b.x, b.y - 30, 'coin0').play('spin').setDepth(20);","oldText":" const c = thiSuccessfully replaced 1 block(s) in /tmp/ws/index.html.
thinking
Coins inside the coins group use this.coins.create(...) — for a staticGroup, what create() returns is... in the case of physics' staticGroup, create returns a Phaser.Physics.Arcade.Sprite (the default classType of a physics group is Sprite). I've already called coin.play('spin') there — staticGroup's default classType: does Phaser.Physics.Arcade.StaticGroup inherit from Group with classType = ArcadeSprite? Let me check: PhysicsGroup sets classType to ArcadeSprite (both dynamic and static). Yes — the default classType for `PhysicsGroup` is ArcadeSprite, and Sprite has play(). The boot test passed with coins being created and animated (no errors), so no problem there.
Re-run smoke2.
{"command":"cd /tmp && timeout 30 node smoke2.js > /tmp/out2.log 2>&1; echo \"EXIT $?\"; grep -a -E \"qblock direct|used\\?|UNCAUGHT|NO RUNTEXIT 0 solids: 677 coins: 28 enemies: 16 --- qblock direct check --- qblock used? true score: 400 coins: 1 qblock used? false score: 400 coins: 1 coins: 2 score: 500 state: win score: 1500 NO RUNTIME ERRORS
thinking
All checks passed: - ?block bump → used=true, +200, coin +1 ✓ - Stomp ✓ (score 400 = stomp 200 + block 200) - Coin pickup ✓ - Flag victory + bonus ✓ - No runtime errors ✓ Finally, run the original full smoke test (with the auto-run death + respawn path) one more time to verify everything end-to-end, and verify the final syntax of index.html. After that, wrap up.
All mechanics verified. Final end-to-end run plus syntax check:
{"command":"cd /tmp && timeout 30 node smoke.js > /tmp/out3.log 2>&1; echo \"SMOKE EXIT $?\"; grep -a -cE \"UNCAUGHT\" /tmp/out3.log; grep -SMOKE EXIT 0 0 NO RUNTIME ERRORS external refs: NONE loads local phaser: true SYNTAX OK total 1180 drwxr-sr-x. 2 node node 45 Aug 13 01:36 . drwxrwsrwx. 3 root node 163 Aug 13 01:42 .. -rw-r--r--. 1 node node 24267 Aug 13 01:42 index.html -rw-r--r--. 1 node node 1181917 Aug 13 01:31 phaser.min.js
thinking
Everything's done. Passed all the tests with zero runtime errors. Let me do a quick summary.
Done. **`/tmp/ws/index.html`** is a complete, self-contained Mario-style platformer — verified via a headless Phaser boot + gameplay simulation with **zero runtime errors**. ## What's in the game **Gameplay** - 🏃 Run (←→/AD, SHIFT to sprint) and jump (SPACE/↑/W) with variable jump height, squash & stretch animation - 🍄 16 patrolling enemies — stomp them for +200 (bounce off), touch them from the side and you die - 🪙 28 spinning coins (+100 each) plus 7 `?`-blocks that pop coins when head-bumped (+200) - 🚩 Goal flag with +1000 bonus, confetti, and a "COURSE CLEAR!" screen - ❤️ 3 lives, death animation, respawn, GAME OVER screen, R to restart - 📜 ~6800px scrolling level: pits, brick platforms, pyramids/staircases, coin arcs, a castle at the end - Parallax sky gradient, clouds, hills, and bushes (3 scroll layers) **Fully procedural — no assets** - Every texture (player, enemies, coins, ground, bricks, `?`-blocks, flag, pole, clouds, hills, bushes, sky) is drawn with Phaser `Graphics` + `generateTexture` - Sound effects are synthesized live with WebAudio oscillators (jump slide, coin chime, stomp, fanfare) — no audio files - Only dependency is the local `phaser.min.js`, loaded via `<script src="phaser.min.js">`; no CDNs, fonts, or network requests **Bugs caught & fixed during headless testing** 1. Stale physics colliders after respawn caused `getChildren` crashes and disabled all collisions → colliders are now tracked and destroyed on rebuild 2. `?`-block coin pop used `Image.play()` (Sprite-only API) → switched to `Sprite` **Verified by test:** level builds (677 solids), side-touch death → respawn with life decrement, stomp (+200), coin collect (+100), `?`-block bump (+200, coin +1), and flag win (+1000) all confirmed.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Procedurally drawn Mario-style platformer with movement, jumping, enemies, and game-over states triggered by input.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.