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
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
|
@ -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"
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 9.7 MiB |
|
After Width: | Height: | Size: 7.7 MiB |
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||