Add new puzzle assets (12 images + thumbnails), background selection UI, and persistent background state

- Added 12 new puzzle images with corresponding 576×324 thumbnails
- Extended `puzzles.json` to include all new puzzles
- Implemented background selection in `NewPuzzleScene.js` (Dark Wood / Green Felt)
- Updated thumbnail display logic to use `thumb_` prefixed versions
- Added background key/path state propagation to `PuzzleState`, network join, and room creation
- Tiled background texture across the playfield in `PuzzleScene.js`
- Enhanced room code UI styling for better visibility
This commit is contained in:
Brian Fertig 2026-04-04 20:44:43 -06:00
parent 6900416050
commit 28d237f75d
33 changed files with 231 additions and 13 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@ -0,0 +1,55 @@
#!/bin/bash
#
# Generate thumbnail versions of puzzle images.
# Run from the assets/images/puzzles/ directory.
#
# Requires ImageMagick (convert command).
# Ubuntu/Debian: sudo apt install imagemagick
# macOS: brew install imagemagick
#
# Thumbnails are 576x324 (16:9) and named thumb_<original_filename>.
THUMB_WIDTH=576
THUMB_HEIGHT=324
cd "$(dirname "$0")" || exit 1
if ! command -v convert &> /dev/null; then
echo "Error: ImageMagick is not installed."
echo " Ubuntu/Debian: sudo apt install imagemagick"
echo " macOS: brew install imagemagick"
exit 1
fi
count=0
skipped=0
for file in *; do
# Skip non-files, thumbnails, and this script
[ -f "$file" ] || continue
case "$file" in
thumb_*|*.sh) continue ;;
esac
# Check it's actually an image
mime=$(file --mime-type -b "$file")
case "$mime" in
image/*) ;;
*) continue ;;
esac
thumb="thumb_${file}"
if [ -f "$thumb" ]; then
echo "SKIP $thumb (already exists)"
skipped=$((skipped + 1))
continue
fi
convert "$file" -resize "${THUMB_WIDTH}x${THUMB_HEIGHT}!" -strip "$thumb"
echo "MADE $thumb"
count=$((count + 1))
done
echo ""
echo "Done. Created: $count, Skipped: $skipped"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

View File

@ -1,9 +1,39 @@
[
{
"key": "puzzle_america",
"path": "assets/images/puzzles/america.png",
"label": "America"
},
{
"key": "puzzle_dwarven_mining_village",
"path": "assets/images/puzzles/dwarven_mining_village.png",
"label": "Dwarven Mining Village"
},
{
"key": "puzzle_golden_in_the_mountains",
"path": "assets/images/puzzles/golden_in_the_mountains.png",
"label": "Golden in the Mountains"
},
{
"key": "puzzle_good_vs_evil",
"path": "assets/images/puzzles/good_vs_evil.png",
"label": "Good vs Evil"
},
{
"key": "puzzle_growing_ea",
"path": "assets/images/puzzles/growing_ea.png",
"label": "Growing Computer Parts"
},
{
"key": "puzzle_guilin_landscape",
"path": "assets/images/puzzles/guilin_landscape.png",
"label": "Guilin Landscape"
},
{
"key": "puzzle_home_sweet_home",
"path": "assets/images/puzzles/home_sweet_home.png",
"label": "Home Sweet Home"
},
{
"key": "puzzle_lone_ship",
"path": "assets/images/puzzles/lone_ship_in_the_seas.png",
@ -14,9 +44,34 @@
"path": "assets/images/puzzles/misty_exploration",
"label": "Misty Exploration"
},
{
"key": "puzzle_model_village",
"path": "assets/images/puzzles/model_village.png",
"label": "Model Village"
},
{
"key": "puzzle_opulant_flowers",
"path": "assets/images/puzzles/opulant_flowers.png",
"label": "Opulant Flowers"
},
{
"key": "puzzle_robot_in_a_flavela",
"path": "assets/images/puzzles/robot_in_a_flavela.png",
"label": "Robot in a Favela"
},
{
"key": "puzzle_disassembled_android",
"path": "assets/images/puzzles/the_disassembled_android.png",
"label": "The Disassembled Android"
},
{
"key": "puzzle_mandrill",
"path": "assets/images/puzzles/thoughtful_mandrill",
"label": "Thoughtful Mandrill"
},
{
"key": "puzzle_ultimate_tree_fort",
"path": "assets/images/puzzles/ultimate_tree_fort",
"label": "Ultimate Tree Fort"
}
]
]

View File

@ -7,6 +7,11 @@ const DIFFICULTIES = [
{ pieces: 100, label: '100 Pieces' }
];
const BACKGROUNDS = [
{ key: 'bg_dark_wood', path: 'assets/images/ui/dark_wood.jpg', label: 'Dark Wood' },
{ key: 'bg_green_felt', path: 'assets/images/ui/green_felt.jpg', label: 'Green Felt' },
];
class NewPuzzleScene extends Phaser.Scene {
constructor() {
super({ key: 'NewPuzzleScene' });
@ -20,6 +25,7 @@ class NewPuzzleScene extends Phaser.Scene {
create() {
this.selectedImageIdx = null;
this.selectedPieces = null;
this.selectedBg = BACKGROUNDS[0]; // default to dark wood
const W = this.sys.game.config.width;
const H = this.sys.game.config.height;
@ -124,7 +130,9 @@ class NewPuzzleScene extends Phaser.Scene {
});
const thumb = document.createElement('img');
thumb.src = img.path;
// Use thumbnail version: insert "thumb_" before the filename
const lastSlash = img.path.lastIndexOf('/');
thumb.src = img.path.substring(0, lastSlash + 1) + 'thumb_' + img.path.substring(lastSlash + 1);
Object.assign(thumb.style, {
display: 'block',
width: '100%',
@ -225,6 +233,48 @@ class NewPuzzleScene extends Phaser.Scene {
return { el: btn, pieces: diff.pieces };
});
// "Background" label
const bgLabel = document.createElement('div');
Object.assign(bgLabel.style, {
color: '#8899cc',
fontSize: '1.8vmin',
fontFamily: 'Arial, sans-serif',
});
bgLabel.textContent = 'Background';
// Background buttons row
const bgRow = document.createElement('div');
Object.assign(bgRow.style, {
display: 'flex',
gap: '2vmin',
});
this._bgBtnEls = BACKGROUNDS.map((bg, i) => {
const btn = document.createElement('button');
btn.textContent = bg.label;
const isDefault = bg.key === this.selectedBg.key;
Object.assign(btn.style, {
padding: '0.7vmin 2.5vmin',
background: isDefault ? '#224488' : '#111133',
color: '#ccddff',
border: '1px solid ' + (isDefault ? '#44aaff' : '#222255'),
borderRadius: '4px',
fontSize: '1.6vmin',
fontFamily: 'Arial, sans-serif',
cursor: 'pointer',
transition: 'background 0.1s, border-color 0.1s',
});
btn.addEventListener('mouseenter', () => {
if (this.selectedBg.key !== bg.key) btn.style.background = '#1a3366';
});
btn.addEventListener('mouseleave', () => {
if (this.selectedBg.key !== bg.key) btn.style.background = '#111133';
});
btn.addEventListener('click', () => this._selectBackground(i));
bgRow.appendChild(btn);
return { el: btn, bg };
});
// Action buttons row (Back left, Start right)
const actionRow = document.createElement('div');
Object.assign(actionRow.style, {
@ -255,6 +305,8 @@ class NewPuzzleScene extends Phaser.Scene {
bar.appendChild(diffLabel);
bar.appendChild(diffRow);
bar.appendChild(bgLabel);
bar.appendChild(bgRow);
bar.appendChild(actionRow);
uiLayer.appendChild(bar);
}
@ -281,6 +333,15 @@ class NewPuzzleScene extends Phaser.Scene {
this._refreshStartButton();
}
_selectBackground(idx) {
this.selectedBg = BACKGROUNDS[idx];
this._bgBtnEls.forEach(({ el, bg }) => {
const sel = bg.key === this.selectedBg.key;
el.style.background = sel ? '#224488' : '#111133';
el.style.borderColor = sel ? '#44aaff' : '#222255';
});
}
_refreshStartButton() {
if (!this._startBtnEl) return;
const ready = this.selectedImageIdx !== null && this.selectedPieces !== null;
@ -335,7 +396,9 @@ class NewPuzzleScene extends Phaser.Scene {
imageKey: img.key,
imagePath: img.path,
pieceCount: this.selectedPieces,
roomCode: generateRoomCode()
roomCode: generateRoomCode(),
bgKey: this.selectedBg.key,
bgPath: this.selectedBg.path,
});
}
}

View File

@ -34,6 +34,13 @@ class PuzzleScene extends Phaser.Scene {
}
preload() {
// Playfield background texture (default to dark wood)
this._bgKey = this.cfg.bgKey || 'bg_dark_wood';
this._bgPath = this.cfg.bgPath || 'assets/images/ui/dark_wood.jpg';
if (!this.textures.exists(this._bgKey)) {
this.load.image(this._bgKey, this._bgPath);
}
// Network join: load the image from the state received from server
if (this.cfg._networkJoin && this.cfg._networkState) {
const ns = this.cfg._networkState;
@ -42,9 +49,17 @@ class PuzzleScene extends Phaser.Scene {
imagePath: ns.imagePath,
pieceCount: ns.pieceCount,
roomCode: ns.roomCode,
bgKey: ns.bgKey,
bgPath: ns.bgPath,
_networkJoin: true,
_networkState: ns,
});
// Re-derive background from network state
this._bgKey = ns.bgKey || 'bg_dark_wood';
this._bgPath = ns.bgPath || 'assets/images/ui/dark_wood.jpg';
if (!this.textures.exists(this._bgKey)) {
this.load.image(this._bgKey, this._bgPath);
}
if (!this.textures.exists(ns.imageKey)) {
this.load.image(ns.imageKey, ns.imagePath);
}
@ -141,6 +156,24 @@ class PuzzleScene extends Phaser.Scene {
_buildScene(saved) {
const { width, height } = this.sys.game.config;
// Tile the background texture across the playfield, centered on the world
const worldBounds = this._calcWorldBounds();
const bgSrc = this.textures.get(this._bgKey).getSourceImage();
const bgW = bgSrc.naturalWidth || bgSrc.width;
const bgH = bgSrc.naturalHeight || bgSrc.height;
const cx = worldBounds.w / 2;
const cy = worldBounds.h / 2;
// Tile outward from centre far enough to cover panning beyond the world
const pad = Math.max(worldBounds.w, worldBounds.h);
const halfTilesX = Math.ceil((worldBounds.w / 2 + pad) / bgW);
const halfTilesY = Math.ceil((worldBounds.h / 2 + pad) / bgH);
for (let ty = -halfTilesY; ty <= halfTilesY; ty++) {
for (let tx = -halfTilesX; tx <= halfTilesX; tx++) {
this.add.image(cx + tx * bgW, cy + ty * bgH, this._bgKey)
.setDepth(-1);
}
}
// Glow graphics (above pieces, below UI)
this._glowGraphics = this.add.graphics().setDepth(500);
@ -163,7 +196,6 @@ class PuzzleScene extends Phaser.Scene {
}
// Calculate world bounds and set up camera
const worldBounds = this._calcWorldBounds();
this._worldW = worldBounds.w;
this._worldH = worldBounds.h;
@ -267,6 +299,8 @@ class PuzzleScene extends Phaser.Scene {
pieces: this._pieces,
groups: this._groupManager.serialize(),
completed: false,
bgKey: this._bgKey,
bgPath: this._bgPath,
});
NetworkManager.createRoom(this.cfg.roomCode, state.serialize());
this._setupNetworkListeners();
@ -753,12 +787,17 @@ class PuzzleScene extends Phaser.Scene {
// Room code — bottom-right
const roomEl = document.createElement('div');
Object.assign(roomEl.style, {
position: 'absolute',
bottom: '1%',
right: '0.8%',
color: '#445577',
fontSize: '1.3vmin',
fontFamily: 'monospace',
position: 'absolute',
bottom: '1.5%',
right: '1%',
color: '#ddeeff',
fontSize: '2.4vmin',
fontFamily: 'monospace',
fontWeight: 'bold',
letterSpacing:'0.15em',
background: 'rgba(0, 0, 0, 0.55)',
padding: '0.6vmin 1.2vmin',
borderRadius: '4px',
});
roomEl.textContent = `Room: ${this.cfg.roomCode}`;
this._uiLayer.appendChild(roomEl);
@ -933,7 +972,9 @@ class PuzzleScene extends Phaser.Scene {
rows: this._rows,
pieces: this._pieces,
groups: this._groupManager.serialize(),
completed: this._completed || false
completed: this._completed || false,
bgKey: this._bgKey,
bgPath: this._bgPath,
});
StorageManager.save(state);
}

View File

@ -1,5 +1,5 @@
class PuzzleState {
constructor({ imageKey, imagePath, pieceCount, roomCode, cols, rows, pieces, groups, completed }) {
constructor({ imageKey, imagePath, pieceCount, roomCode, cols, rows, pieces, groups, completed, bgKey, bgPath }) {
this.imageKey = imageKey;
this.imagePath = imagePath;
this.pieceCount = pieceCount;
@ -9,6 +9,8 @@ class PuzzleState {
this.pieces = pieces; // PieceData[]
this.groups = groups; // number[][] — each inner array is one group's piece IDs
this.completed = completed || false;
this.bgKey = bgKey || 'bg_dark_wood';
this.bgPath = bgPath || 'assets/images/ui/dark_wood.jpg';
}
serialize() {
@ -28,7 +30,9 @@ class PuzzleState {
y: p.y
})),
groups: this.groups,
completed: this.completed
completed: this.completed,
bgKey: this.bgKey,
bgPath: this.bgPath,
};
}