Attract mode: 6-page cab loop using the idle attract + villain art
The title screen now cycles every 7s like a real cabinet:
0 SELECT — logo + portrait class cards (playable page, maze visible behind)
1 STORY — the closing-down shop painting + the last-night premise
2 BESTIARY — 8 grunts with names ("they are not here for the music")
3 BOSSES — the 5 unkillables ("the airhorn does not work on everything")
4 ARCHIVE — the descending-staircase painting + the White Label
5 SCORES — hall of fame, or "NO NAMES ON THE WALL YET"
Any key or tap snaps back to page 0; keys 1-4 start the game from any page, so
the attract loop never costs you a coin. Info pages get a dimmed backdrop and
label backgrounds so art reads instead of fighting the maze behind it.
Wires up attract_shop + attract_archive (generated in batch 3, never used).
Bug caught while verifying: the backdrop was pushed into 5 page arrays, but the
per-page visibility loop sets each object true for its page then false again for
every later page — last write wins, so it never appeared. Shared objects can't
live in the page arrays; it's toggled explicitly in showAttract now.
Verified: full 0-1-2-3-4-5-0 cycle runs unattended, any-key returns to select,
1-4 starts from any page with the right class sprite, no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2266e9feeb
commit
e11a201d75
@ -101,6 +101,8 @@ TABLE = {
|
||||
# full-art images: downscale + copy, never chroma-keyed (they aren't sprites)
|
||||
ART = {
|
||||
"logo": ("logo_marquee_b.png", 800),
|
||||
"attract_shop": ("attract_shop.png", 720),
|
||||
"attract_archive": ("attract_archive.png", 720),
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
public/sprites/attract_archive.png
Normal file
BIN
public/sprites/attract_archive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 613 KiB |
BIN
public/sprites/attract_shop.png
Normal file
BIN
public/sprites/attract_shop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 595 KiB |
156
src/GameScene.js
156
src/GameScene.js
@ -42,7 +42,9 @@ const TEXTURE_KEYS = [
|
||||
'decal_cables', 'decal_cups', 'decal_flyers', 'decal_confetti',
|
||||
'decal_tape', 'decal_sleeve', 'decal_dust', 'decal_puddle',
|
||||
'port_digger', 'port_selector', 'port_promoter', 'port_soundtech', 'logo',
|
||||
'attract_shop', 'attract_archive',
|
||||
];
|
||||
const ATTRACT_MS = 7000; // how long each attract page holds before cycling
|
||||
const BOSS_SCORE = { conductor: 5000, rpm78: 10000 };
|
||||
const SHAZAM_SHOT_SPEED = 170;
|
||||
const SHAZAM_SHOT_DAMAGE = 12;
|
||||
@ -170,6 +172,10 @@ export default class GameScene extends Phaser.Scene {
|
||||
if (this.state !== 'TITLE') return;
|
||||
const i = ['1', '2', '3', '4'].indexOf(ev.key);
|
||||
if (i >= 0) this.startGame(Object.keys(CLASSES)[i]);
|
||||
else if (this.attractIndex !== 0) this.showAttract(0); // any key = back to the coin slot
|
||||
});
|
||||
this.input.on('pointerdown', () => {
|
||||
if (this.state === 'TITLE' && this.attractIndex !== 0) this.showAttract(0);
|
||||
});
|
||||
this.setupTouch();
|
||||
|
||||
@ -287,6 +293,7 @@ export default class GameScene extends Phaser.Scene {
|
||||
|
||||
this.resetStats();
|
||||
this.buildLevel(0);
|
||||
this.buildAttract();
|
||||
this.showTitle();
|
||||
}
|
||||
|
||||
@ -1402,6 +1409,146 @@ export default class GameScene extends Phaser.Scene {
|
||||
this.sub.setText(`score ${this.score}\n\npress R`).setVisible(true);
|
||||
}
|
||||
|
||||
// The cab's attract loop: select -> story -> bestiary -> bosses -> archive -> scores.
|
||||
// Page 0 is the playable one; any key or tap on another page snaps back to it.
|
||||
buildAttract() {
|
||||
const T = (y, txt, size, color, x = 480) =>
|
||||
this.add
|
||||
.text(x, y, txt, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${size}px`,
|
||||
color,
|
||||
align: 'center',
|
||||
backgroundColor: '#000000c0',
|
||||
padding: { x: 10, y: 6 },
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(101)
|
||||
.setVisible(false);
|
||||
const IMG = (key, y, w, h) =>
|
||||
this.add
|
||||
.image(480, y, key)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(100)
|
||||
.setDisplaySize(w, h)
|
||||
.setVisible(false);
|
||||
const CREATURE = (key, x, y, label, scale = 1.6) => {
|
||||
const objs = [];
|
||||
if (this.textures.exists(key)) {
|
||||
objs.push(this.add.image(x, y, key).setScrollFactor(0).setDepth(101).setScale(scale).setVisible(false));
|
||||
}
|
||||
objs.push(
|
||||
this.add
|
||||
.text(x, y + 54, label, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '11px',
|
||||
color: '#00e5ff',
|
||||
align: 'center',
|
||||
backgroundColor: '#000000d0',
|
||||
padding: { x: 5, y: 3 },
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(101)
|
||||
.setVisible(false),
|
||||
);
|
||||
return objs;
|
||||
};
|
||||
|
||||
const story = [
|
||||
IMG('attract_shop', 260, 620, 465),
|
||||
T(60, 'THE LAST NIGHT', 30, '#ff00ff'),
|
||||
T(560,
|
||||
"The city's oldest record shop shuts at dawn.\n" +
|
||||
'The aisles rearrange. The bargain bins boil over.\n' +
|
||||
'The Sound Guy walks through the walls like he owns them.\n\n' +
|
||||
'DIG DEEP. NAME EVERY TUNE. REACH THE BOTTOM.',
|
||||
15, '#ffffff'),
|
||||
];
|
||||
|
||||
const bestiary = [
|
||||
T(60, 'KNOW YOUR CROWD', 28, '#ff00ff'),
|
||||
...CREATURE('poser', 170, 210, 'DRINK SPILLER'),
|
||||
...CREATURE('nerd', 370, 210, 'GEAR NERD'),
|
||||
...CREATURE('pirate', 570, 210, 'RECORD PIRATE'),
|
||||
...CREATURE('ghoul', 770, 210, 'CRATE GHOUL'),
|
||||
...CREATURE('shazam', 170, 380, 'SHAZAM ZOMBIE'),
|
||||
...CREATURE('gatekeeper', 370, 380, 'GATEKEEPER'),
|
||||
...CREATURE('scalper', 570, 380, 'VINYL SCALPER'),
|
||||
...CREATURE('warped', 770, 380, 'WARPED RECORD'),
|
||||
T(520, 'they are not here for the music', 15, '#aaaaaa'),
|
||||
];
|
||||
|
||||
const bosses = [
|
||||
T(60, 'THE UNKILLABLE', 28, '#ff00ff'),
|
||||
...CREATURE('soundguy', 190, 230, 'THE SOUND GUY', 2),
|
||||
...CREATURE('firemarshal', 480, 230, 'THE FIRE MARSHAL', 2),
|
||||
...CREATURE('queen', 770, 230, 'THE MITE QUEEN', 1.4),
|
||||
...CREATURE('conductor', 300, 430, 'THE SPECTRAL CONDUCTOR', 1.5),
|
||||
...CREATURE('rpm78', 640, 430, 'THE MONSTROUS 78 RPM', 1.3),
|
||||
T(560, 'the airhorn does not work on everything', 15, '#aaaaaa'),
|
||||
];
|
||||
|
||||
const archive = [
|
||||
IMG('attract_archive', 260, 620, 465),
|
||||
T(60, 'THE WHITE LABEL', 30, '#ff00ff'),
|
||||
T(560,
|
||||
'No artist. No title. No year.\n' +
|
||||
'The only tune that has never been trainspotted.\n\n' +
|
||||
'It is down there. It knows you are coming.',
|
||||
15, '#ffffff'),
|
||||
];
|
||||
|
||||
this.scoreBoard = T(300, '', 22, '#ffd11a');
|
||||
const scores = [T(120, 'HALL OF FAME', 30, '#ff00ff'), this.scoreBoard];
|
||||
|
||||
// info pages get a dimmed backdrop so art and labels aren't fighting the maze
|
||||
this.attractBackdrop = this.add
|
||||
.rectangle(480, 360, 960, 720, 0x000000, 0.8)
|
||||
.setScrollFactor(0)
|
||||
.setDepth(98)
|
||||
.setVisible(false);
|
||||
// NOT added to the page arrays: it's shared, and the per-page visibility loop
|
||||
// would set it true for its page then false again for every later one.
|
||||
|
||||
this.attractPages = [
|
||||
[this.titleLogo, ...this.classButtons, this.sub],
|
||||
story,
|
||||
bestiary,
|
||||
bosses,
|
||||
archive,
|
||||
scores,
|
||||
];
|
||||
this.attractIndex = 0;
|
||||
this.time.addEvent({
|
||||
delay: ATTRACT_MS,
|
||||
loop: true,
|
||||
callback: () => {
|
||||
if (this.state === 'TITLE') this.showAttract((this.attractIndex + 1) % this.attractPages.length);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showAttract(i) {
|
||||
this.attractIndex = i;
|
||||
this.attractPages.forEach((page, n) => page.forEach((o) => o.setVisible(n === i)));
|
||||
this.attractBackdrop.setVisible(i !== 0); // page 0 keeps the maze visible behind it
|
||||
if (i === 5) {
|
||||
const hs = this.loadScores();
|
||||
this.scoreBoard.setText(
|
||||
hs.length
|
||||
? hs.map((s, n) => `${n + 1}. ${s.initials} ${s.score}`).join('\n')
|
||||
: 'NO NAMES ON THE WALL YET',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
hideAttract() {
|
||||
if (this.attractPages) this.attractPages.forEach((page) => page.forEach((o) => o.setVisible(false)));
|
||||
if (this.attractBackdrop) this.attractBackdrop.setVisible(false);
|
||||
}
|
||||
|
||||
showTitle() {
|
||||
this.state = 'TITLE';
|
||||
this.physics.pause();
|
||||
@ -1414,10 +1561,8 @@ export default class GameScene extends Phaser.Scene {
|
||||
this.sub
|
||||
.setText((hs ? `HIGH SCORES ${hs.replace(/\n/g, ' ')}` : 'dig deep. name every tune.') + nbLine)
|
||||
.setPosition(480, 630)
|
||||
.setFontSize(15)
|
||||
.setVisible(true);
|
||||
this.titleLogo.setVisible(true);
|
||||
this.classButtons.forEach((b) => b.setVisible(true));
|
||||
.setFontSize(15);
|
||||
this.showAttract(0);
|
||||
}
|
||||
|
||||
startGame(clsKey) {
|
||||
@ -1427,8 +1572,7 @@ export default class GameScene extends Phaser.Scene {
|
||||
this.physics.resume();
|
||||
this.resetStats();
|
||||
this.hideOverlay();
|
||||
this.titleLogo.setVisible(false);
|
||||
this.classButtons.forEach((b) => b.setVisible(false));
|
||||
this.hideAttract();
|
||||
this.state = 'PLAYING';
|
||||
this.buildLevel(0);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user