Compare commits

..

No commits in common. "61d49eab57d3180eba825820b8c0e9bb6dd3fc95" and "e71dab9c4561d2e4111b0126f60a5ff9dd6f2df0" have entirely different histories.

76 changed files with 36 additions and 760 deletions

View File

@ -1,13 +0,0 @@
#!/bin/bash
# Rename all .png files in this directory to lowercase with underscores replacing spaces.
cd "$(dirname "$0")" || exit 1
for f in *.png; do
[ -f "$f" ] || continue
newname=$(echo "$f" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
if [ "$f" != "$newname" ]; then
mv "$f" "$newname"
echo "Renamed: $f -> $newname"
fi
done

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -1,75 +0,0 @@
#!/bin/bash
# Regenerates assets/puzzles.json and assets/thumbnails.json from the
# images in assets/images/puzzles/.
#
# Conventions:
# Full image: assets/images/puzzles/<name>.png
# Thumbnail: assets/images/puzzles/thumb_<name>.png
# Puzzle key: puzzle_<name> (hyphens replaced with underscores)
# Thumb key: thumb_<name> (hyphens replaced with underscores)
# Label: title-cased from <name>, underscores/hyphens → spaces
#
# Usage: cd <repo-root> && bash assets/images/puzzles/sync_json.sh
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
PUZZLE_DIR="assets/images/puzzles"
PUZZLES_JSON="assets/puzzles.json"
THUMBS_JSON="assets/thumbnails.json"
puzzles="["
thumbs="["
first=true
for img in "$PUZZLE_DIR"/*.png; do
base=$(basename "$img" .png)
# Skip thumbnails, scripts
[[ "$base" == thumb_* ]] && continue
[[ "$base" == *.sh ]] && continue
# Derive keys (replace hyphens with underscores for keys)
safe="${base//-/_}"
puzzle_key="puzzle_${safe}"
thumb_key="thumb_${safe}"
thumb_file="$PUZZLE_DIR/thumb_${base}.png"
# Skip if no matching thumbnail
if [ ! -f "$thumb_file" ]; then
echo "WARNING: No thumbnail for $base — skipping"
continue
fi
# Build label: replace underscores/hyphens with spaces, then title-case
label=$(echo "$base" | sed 's/[_-]/ /g' | sed 's/\b\(.\)/\u\1/g')
sep=""
$first || sep=","
first=false
puzzles+="${sep}
{
\"key\": \"${puzzle_key}\",
\"path\": \"${PUZZLE_DIR}/${base}.png\",
\"label\": \"${label}\"
}"
thumbs+="${sep}
{
\"key\": \"${thumb_key}\",
\"path\": \"${PUZZLE_DIR}/thumb_${base}.png\",
\"puzzleKey\": \"${puzzle_key}\"
}"
done
puzzles+="
]"
thumbs+="
]"
echo "$puzzles" > "$PUZZLES_JSON"
echo "$thumbs" > "$THUMBS_JSON"
echo "Updated $PUZZLES_JSON ($(grep -c '"key"' "$PUZZLES_JSON") entries)"
echo "Updated $THUMBS_JSON ($(grep -c '"key"' "$THUMBS_JSON") entries)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

View File

@ -1,146 +1,46 @@
[
{
"key": "puzzle_above_the_city",
"path": "assets/images/puzzles/above_the_city.png",
"label": "Above The City"
},
{
"key": "puzzle_abstract_highway",
"path": "assets/images/puzzles/abstract_highway.png",
"label": "Abstract Highway"
},
{
"key": "puzzle_alpine_mountains",
"path": "assets/images/puzzles/alpine_mountains.png",
"label": "Alpine Mountains"
},
{
"key": "puzzle_america",
"path": "assets/images/puzzles/america.png",
"label": "America"
},
{
"key": "puzzle_arctic_fox",
"path": "assets/images/puzzles/arctic_fox.png",
"label": "Arctic Fox"
},
{
"key": "puzzle_cartoon_land",
"path": "assets/images/puzzles/cartoon_land.png",
"label": "Cartoon Land"
},
{
"key": "puzzle_christmas_in_the_park",
"path": "assets/images/puzzles/christmas_in_the_park.png",
"label": "Christmas In The Park"
},
{
"key": "puzzle_combat_mech",
"path": "assets/images/puzzles/combat_mech.png",
"label": "Combat Mech"
},
{
"key": "puzzle_cottages_on_the_street",
"path": "assets/images/puzzles/cottages_on_the_street.png",
"label": "Cottages On The Street"
},
{
"key": "puzzle_dwarven_blacksmith",
"path": "assets/images/puzzles/dwarven_blacksmith.png",
"label": "Dwarven Blacksmith"
},
{
"key": "puzzle_dwarven_mining_village",
"path": "assets/images/puzzles/dwarven_mining_village.png",
"label": "Dwarven Mining Village"
},
{
"key": "puzzle_ferarri_in_the_driveway",
"path": "assets/images/puzzles/ferarri_in_the_driveway.png",
"label": "Ferarri In The Driveway"
},
{
"key": "puzzle_floating_castles",
"path": "assets/images/puzzles/floating_castles.png",
"label": "Floating Castles"
},
{
"key": "puzzle_floating_wizards_village",
"path": "assets/images/puzzles/floating_wizards_village.png",
"label": "Floating Wizards Village"
},
{
"key": "puzzle_four_seasons_2",
"path": "assets/images/puzzles/four_seasons_2.png",
"label": "Four Seasons 2"
},
{
"key": "puzzle_fruit_bowl",
"path": "assets/images/puzzles/fruit_bowl.png",
"label": "Fruit Bowl"
},
{
"key": "puzzle_fuzzy_halloween",
"path": "assets/images/puzzles/fuzzy_halloween.png",
"label": "Fuzzy Halloween"
},
{
"key": "puzzle_golden_in_the_mountains",
"path": "assets/images/puzzles/golden_in_the_mountains.png",
"label": "Golden In The Mountains"
"label": "Golden in the Mountains"
},
{
"key": "puzzle_good_vs_evil",
"path": "assets/images/puzzles/good_vs_evil.png",
"label": "Good Vs Evil"
"label": "Good vs Evil"
},
{
"key": "puzzle_growing_ea",
"path": "assets/images/puzzles/growing_ea.png",
"label": "Growing Ea"
"label": "Growing Computer Parts"
},
{
"key": "puzzle_guilin_landscape",
"path": "assets/images/puzzles/guilin_landscape.png",
"label": "Guilin Landscape"
},
{
"key": "puzzle_heavy_storm_in_the_city",
"path": "assets/images/puzzles/heavy_storm_in_the_city.png",
"label": "Heavy Storm In The City"
},
{
"key": "puzzle_home_sweet_home",
"path": "assets/images/puzzles/home_sweet_home.png",
"label": "Home Sweet Home"
},
{
"key": "puzzle_jeweled_frogs",
"path": "assets/images/puzzles/jeweled_frogs.png",
"label": "Jeweled Frogs"
},
{
"key": "puzzle_jungle_macaw",
"path": "assets/images/puzzles/jungle_macaw.png",
"label": "Jungle Macaw"
},
{
"key": "puzzle_lake_treehouse",
"path": "assets/images/puzzles/lake_treehouse.png",
"label": "Lake Treehouse"
},
{
"key": "puzzle_lone_ship_in_the_seas",
"key": "puzzle_lone_ship",
"path": "assets/images/puzzles/lone_ship_in_the_seas.png",
"label": "Lone Ship In The Seas"
"label": "Lone Ship on the Ocean"
},
{
"key": "puzzle_mini_dennys",
"path": "assets/images/puzzles/mini-dennys.png",
"label": "Mini Dennys"
},
{
"key": "puzzle_misty_exploration",
"key": "puzzle_misty",
"path": "assets/images/puzzles/misty_exploration.png",
"label": "Misty Exploration"
},
@ -149,104 +49,29 @@
"path": "assets/images/puzzles/model_village.png",
"label": "Model Village"
},
{
"key": "puzzle_moroccan_lounge",
"path": "assets/images/puzzles/moroccan_lounge.png",
"label": "Moroccan Lounge"
},
{
"key": "puzzle_mountain_blanket",
"path": "assets/images/puzzles/mountain_blanket.png",
"label": "Mountain Blanket"
},
{
"key": "puzzle_opulant_flowers",
"path": "assets/images/puzzles/opulant_flowers.png",
"label": "Opulant Flowers"
},
{
"key": "puzzle_orc_and_the_undead",
"path": "assets/images/puzzles/orc_and_the_undead.png",
"label": "Orc And The Undead"
},
{
"key": "puzzle_red_panda_in_the_bamboo_forest",
"path": "assets/images/puzzles/red_panda_in_the_bamboo_forest.png",
"label": "Red Panda In The Bamboo Forest"
},
{
"key": "puzzle_robot_in_a_flavela",
"path": "assets/images/puzzles/robot_in_a_flavela.png",
"label": "Robot In A Flavela"
"label": "Robot in a Favela"
},
{
"key": "puzzle_shanty_stack",
"path": "assets/images/puzzles/shanty_stack.png",
"label": "Shanty Stack"
},
{
"key": "puzzle_sleepy_golden",
"path": "assets/images/puzzles/sleepy_golden.png",
"label": "Sleepy Golden"
},
{
"key": "puzzle_technorhino",
"path": "assets/images/puzzles/technorhino.png",
"label": "Technorhino"
},
{
"key": "puzzle_the_beige_tree",
"path": "assets/images/puzzles/the_beige_tree.png",
"label": "The Beige Tree"
},
{
"key": "puzzle_the_disassembled_android",
"key": "puzzle_disassembled_android",
"path": "assets/images/puzzles/the_disassembled_android.png",
"label": "The Disassembled Android"
},
{
"key": "puzzle_the_old_w0rm",
"path": "assets/images/puzzles/the_old_w0rm.png",
"label": "The Old W0rm"
},
{
"key": "puzzle_the_swirl",
"path": "assets/images/puzzles/the_swirl.png",
"label": "The Swirl"
},
{
"key": "puzzle_thoughtful_mandrill",
"key": "puzzle_mandrill",
"path": "assets/images/puzzles/thoughtful_mandrill.png",
"label": "Thoughtful Mandrill"
},
{
"key": "puzzle_tiger_in_bamboo_forest",
"path": "assets/images/puzzles/tiger_in_bamboo_forest.png",
"label": "Tiger In Bamboo Forest"
},
{
"key": "puzzle_tokyo_mech_2",
"path": "assets/images/puzzles/tokyo_mech_2.png",
"label": "Tokyo Mech 2"
},
{
"key": "puzzle_tune_the_news",
"path": "assets/images/puzzles/tune_the_news.png",
"label": "Tune The News"
},
{
"key": "puzzle_turtle_metropolis",
"path": "assets/images/puzzles/turtle_metropolis.png",
"label": "Turtle Metropolis"
},
{
"key": "puzzle_ultimate_tree_fort",
"path": "assets/images/puzzles/ultimate_tree_fort.png",
"label": "Ultimate Tree Fort"
},
{
"key": "puzzle_victorian_steampunk_rooftops",
"path": "assets/images/puzzles/victorian_steampunk_rooftops.png",
"label": "Victorian Steampunk Rooftops"
}
]
]

View File

@ -1,89 +1,14 @@
[
{
"key": "thumb_above_the_city",
"path": "assets/images/puzzles/thumb_above_the_city.png",
"puzzleKey": "puzzle_above_the_city"
},
{
"key": "thumb_abstract_highway",
"path": "assets/images/puzzles/thumb_abstract_highway.png",
"puzzleKey": "puzzle_abstract_highway"
},
{
"key": "thumb_alpine_mountains",
"path": "assets/images/puzzles/thumb_alpine_mountains.png",
"puzzleKey": "puzzle_alpine_mountains"
},
{
"key": "thumb_america",
"path": "assets/images/puzzles/thumb_america.png",
"puzzleKey": "puzzle_america"
},
{
"key": "thumb_arctic_fox",
"path": "assets/images/puzzles/thumb_arctic_fox.png",
"puzzleKey": "puzzle_arctic_fox"
},
{
"key": "thumb_cartoon_land",
"path": "assets/images/puzzles/thumb_cartoon_land.png",
"puzzleKey": "puzzle_cartoon_land"
},
{
"key": "thumb_christmas_in_the_park",
"path": "assets/images/puzzles/thumb_christmas_in_the_park.png",
"puzzleKey": "puzzle_christmas_in_the_park"
},
{
"key": "thumb_combat_mech",
"path": "assets/images/puzzles/thumb_combat_mech.png",
"puzzleKey": "puzzle_combat_mech"
},
{
"key": "thumb_cottages_on_the_street",
"path": "assets/images/puzzles/thumb_cottages_on_the_street.png",
"puzzleKey": "puzzle_cottages_on_the_street"
},
{
"key": "thumb_dwarven_blacksmith",
"path": "assets/images/puzzles/thumb_dwarven_blacksmith.png",
"puzzleKey": "puzzle_dwarven_blacksmith"
},
{
"key": "thumb_dwarven_mining_village",
"path": "assets/images/puzzles/thumb_dwarven_mining_village.png",
"puzzleKey": "puzzle_dwarven_mining_village"
},
{
"key": "thumb_ferarri_in_the_driveway",
"path": "assets/images/puzzles/thumb_ferarri_in_the_driveway.png",
"puzzleKey": "puzzle_ferarri_in_the_driveway"
},
{
"key": "thumb_floating_castles",
"path": "assets/images/puzzles/thumb_floating_castles.png",
"puzzleKey": "puzzle_floating_castles"
},
{
"key": "thumb_floating_wizards_village",
"path": "assets/images/puzzles/thumb_floating_wizards_village.png",
"puzzleKey": "puzzle_floating_wizards_village"
},
{
"key": "thumb_four_seasons_2",
"path": "assets/images/puzzles/thumb_four_seasons_2.png",
"puzzleKey": "puzzle_four_seasons_2"
},
{
"key": "thumb_fruit_bowl",
"path": "assets/images/puzzles/thumb_fruit_bowl.png",
"puzzleKey": "puzzle_fruit_bowl"
},
{
"key": "thumb_fuzzy_halloween",
"path": "assets/images/puzzles/thumb_fuzzy_halloween.png",
"puzzleKey": "puzzle_fuzzy_halloween"
},
{
"key": "thumb_golden_in_the_mountains",
"path": "assets/images/puzzles/thumb_golden_in_the_mountains.png",
@ -104,149 +29,49 @@
"path": "assets/images/puzzles/thumb_guilin_landscape.png",
"puzzleKey": "puzzle_guilin_landscape"
},
{
"key": "thumb_heavy_storm_in_the_city",
"path": "assets/images/puzzles/thumb_heavy_storm_in_the_city.png",
"puzzleKey": "puzzle_heavy_storm_in_the_city"
},
{
"key": "thumb_home_sweet_home",
"path": "assets/images/puzzles/thumb_home_sweet_home.png",
"puzzleKey": "puzzle_home_sweet_home"
},
{
"key": "thumb_jeweled_frogs",
"path": "assets/images/puzzles/thumb_jeweled_frogs.png",
"puzzleKey": "puzzle_jeweled_frogs"
},
{
"key": "thumb_jungle_macaw",
"path": "assets/images/puzzles/thumb_jungle_macaw.png",
"puzzleKey": "puzzle_jungle_macaw"
},
{
"key": "thumb_lake_treehouse",
"path": "assets/images/puzzles/thumb_lake_treehouse.png",
"puzzleKey": "puzzle_lake_treehouse"
},
{
"key": "thumb_lone_ship_in_the_seas",
"key": "thumb_lone_ship",
"path": "assets/images/puzzles/thumb_lone_ship_in_the_seas.png",
"puzzleKey": "puzzle_lone_ship_in_the_seas"
"puzzleKey": "puzzle_lone_ship"
},
{
"key": "thumb_mini_dennys",
"path": "assets/images/puzzles/thumb_mini-dennys.png",
"puzzleKey": "puzzle_mini_dennys"
},
{
"key": "thumb_misty_exploration",
"key": "thumb_misty",
"path": "assets/images/puzzles/thumb_misty_exploration.png",
"puzzleKey": "puzzle_misty_exploration"
"puzzleKey": "puzzle_misty"
},
{
"key": "thumb_model_village",
"path": "assets/images/puzzles/thumb_model_village.png",
"puzzleKey": "puzzle_model_village"
},
{
"key": "thumb_moroccan_lounge",
"path": "assets/images/puzzles/thumb_moroccan_lounge.png",
"puzzleKey": "puzzle_moroccan_lounge"
},
{
"key": "thumb_mountain_blanket",
"path": "assets/images/puzzles/thumb_mountain_blanket.png",
"puzzleKey": "puzzle_mountain_blanket"
},
{
"key": "thumb_opulant_flowers",
"path": "assets/images/puzzles/thumb_opulant_flowers.png",
"puzzleKey": "puzzle_opulant_flowers"
},
{
"key": "thumb_orc_and_the_undead",
"path": "assets/images/puzzles/thumb_orc_and_the_undead.png",
"puzzleKey": "puzzle_orc_and_the_undead"
},
{
"key": "thumb_red_panda_in_the_bamboo_forest",
"path": "assets/images/puzzles/thumb_red_panda_in_the_bamboo_forest.png",
"puzzleKey": "puzzle_red_panda_in_the_bamboo_forest"
},
{
"key": "thumb_robot_in_a_flavela",
"path": "assets/images/puzzles/thumb_robot_in_a_flavela.png",
"puzzleKey": "puzzle_robot_in_a_flavela"
},
{
"key": "thumb_shanty_stack",
"path": "assets/images/puzzles/thumb_shanty_stack.png",
"puzzleKey": "puzzle_shanty_stack"
},
{
"key": "thumb_sleepy_golden",
"path": "assets/images/puzzles/thumb_sleepy_golden.png",
"puzzleKey": "puzzle_sleepy_golden"
},
{
"key": "thumb_technorhino",
"path": "assets/images/puzzles/thumb_technorhino.png",
"puzzleKey": "puzzle_technorhino"
},
{
"key": "thumb_the_beige_tree",
"path": "assets/images/puzzles/thumb_the_beige_tree.png",
"puzzleKey": "puzzle_the_beige_tree"
},
{
"key": "thumb_the_disassembled_android",
"key": "thumb_disassembled_android",
"path": "assets/images/puzzles/thumb_the_disassembled_android.png",
"puzzleKey": "puzzle_the_disassembled_android"
"puzzleKey": "puzzle_disassembled_android"
},
{
"key": "thumb_the_old_w0rm",
"path": "assets/images/puzzles/thumb_the_old_w0rm.png",
"puzzleKey": "puzzle_the_old_w0rm"
},
{
"key": "thumb_the_swirl",
"path": "assets/images/puzzles/thumb_the_swirl.png",
"puzzleKey": "puzzle_the_swirl"
},
{
"key": "thumb_thoughtful_mandrill",
"key": "thumb_mandrill",
"path": "assets/images/puzzles/thumb_thoughtful_mandrill.png",
"puzzleKey": "puzzle_thoughtful_mandrill"
},
{
"key": "thumb_tiger_in_bamboo_forest",
"path": "assets/images/puzzles/thumb_tiger_in_bamboo_forest.png",
"puzzleKey": "puzzle_tiger_in_bamboo_forest"
},
{
"key": "thumb_tokyo_mech_2",
"path": "assets/images/puzzles/thumb_tokyo_mech_2.png",
"puzzleKey": "puzzle_tokyo_mech_2"
},
{
"key": "thumb_tune_the_news",
"path": "assets/images/puzzles/thumb_tune_the_news.png",
"puzzleKey": "puzzle_tune_the_news"
},
{
"key": "thumb_turtle_metropolis",
"path": "assets/images/puzzles/thumb_turtle_metropolis.png",
"puzzleKey": "puzzle_turtle_metropolis"
"puzzleKey": "puzzle_mandrill"
},
{
"key": "thumb_ultimate_tree_fort",
"path": "assets/images/puzzles/thumb_ultimate_tree_fort.png",
"puzzleKey": "puzzle_ultimate_tree_fort"
},
{
"key": "thumb_victorian_steampunk_rooftops",
"path": "assets/images/puzzles/thumb_victorian_steampunk_rooftops.png",
"puzzleKey": "puzzle_victorian_steampunk_rooftops"
}
]
]

View File

@ -46,25 +46,6 @@ class PuzzleScene extends Phaser.Scene {
this._playerNames = new Map(); // playerId -> name
this._remoteLabels = new Map(); // playerId -> { bg: Graphics, text: Text }
// Reset state from any previous puzzle
this._completed = false;
this._completionTime = null;
this._ready = false;
this._heldPiece = null;
this._isPanning = false;
this._isBoxSelecting = false;
this._boxStartWorld = null;
this._boxSelectedIds = null;
this._startTime = null;
this._currentMusic = null;
this._musicStarted = false;
this._timerEl = null;
this._playersListEl = null;
this._connStatusEl = null;
this._trackInfoEl = null;
this._muteBtn = null;
this._minZoom = null;
// Network join from MainMenuScene
if (this.cfg._networkJoin) {
this._isNetworked = true;
@ -197,9 +178,6 @@ class PuzzleScene extends Phaser.Scene {
update() {
if (!this._ready) return;
// Update puzzle timer once per second
this._updateTimer();
// Fallback: detect when music track has finished (Phaser 3.9 'complete' event can be unreliable)
if (this._currentMusic && this._musicStarted && !this._currentMusic.isPlaying && !this._currentMusic.isPaused) {
this._playNextTrack();
@ -332,9 +310,6 @@ class PuzzleScene extends Phaser.Scene {
this._panLastY = 0;
this._ready = true;
// Puzzle timer — restore from saved state or start fresh
this._startTime = (saved && saved.startTime) ? saved.startTime : Date.now();
// Box selection state (CTRL+drag)
this._isBoxSelecting = false;
this._boxStartWorld = null; // { x, y } in world coords
@ -421,7 +396,6 @@ class PuzzleScene extends Phaser.Scene {
}
this._setupNetworkListeners();
this._updatePlayersList();
} else if (NetworkManager.connected) {
// Host — create the room on the server
this._isNetworked = true;
@ -438,7 +412,6 @@ class PuzzleScene extends Phaser.Scene {
completed: false,
bgKey: this._bgKey,
bgPath: this._bgPath,
startTime: this._startTime,
});
NetworkManager.createRoom(this.cfg.roomCode, state.serialize(), this.cfg.playerName);
this._setupNetworkListeners();
@ -575,7 +548,6 @@ class PuzzleScene extends Phaser.Scene {
this._playerNames.set(msg.playerId, msg.playerName);
}
console.log(`${msg.playerName || 'Player ' + msg.playerId} joined the room`);
this._updatePlayersList();
}
_onNetworkPlayerLeft(msg) {
@ -592,7 +564,6 @@ class PuzzleScene extends Phaser.Scene {
}
this._playerNames.delete(msg.playerId);
console.log(`Player ${msg.playerId} left the room`);
this._updatePlayersList();
}
_onNetworkCompleted() {
@ -1108,8 +1079,6 @@ class PuzzleScene extends Phaser.Scene {
const gm = this._groupManager;
if (gm.groupCount === 1 && gm.pieceCount === this._pieces.length) {
this._completed = true;
// Freeze the timer
this._completionTime = Date.now() - this._startTime;
StorageManager.clearCurrent();
this._showCompletion(true);
}
@ -1169,46 +1138,6 @@ class PuzzleScene extends Phaser.Scene {
this.time.delayedCall(animate ? 1300 : 0, () => this._showDomCompletion());
}
// ─── Room Stats helpers ──────────────────────────────────────────────
_updateTimer() {
if (!this._timerEl || !this._startTime || this._completed) return;
const elapsed = Math.floor((Date.now() - this._startTime) / 1000);
this._timerEl.textContent = this._formatTime(elapsed);
}
_formatTime(totalSeconds) {
const h = Math.floor(totalSeconds / 3600);
const m = Math.floor((totalSeconds % 3600) / 60);
const s = totalSeconds % 60;
return String(h).padStart(2, '0') + ':' +
String(m).padStart(2, '0') + ':' +
String(s).padStart(2, '0');
}
_updatePlayersList() {
if (!this._playersListEl) return;
const names = [];
// Add local player first
if (this.cfg.playerName) {
names.push(this.cfg.playerName + ' (you)');
}
// Add remote players
this._playerNames.forEach((name) => {
names.push(name);
});
if (names.length === 0) {
this._playersListEl.textContent = '\u2014';
} else {
this._playersListEl.innerHTML = '';
names.forEach(n => {
const row = document.createElement('div');
row.textContent = n;
this._playersListEl.appendChild(row);
});
}
}
// ─── DOM UI ──────────────────────────────────────────────────────────
// ─── Music ──────────────────────────────────────────────────────────
@ -1455,219 +1384,9 @@ class PuzzleScene extends Phaser.Scene {
this._saveState();
this.scene.start('MainMenuScene');
});
Object.assign(backBtn.style, { top: '10px', left: '10px' });
Object.assign(backBtn.style, { top: '1%', left: '0.5%' });
this._uiLayer.appendChild(backBtn);
// Room Stats panel — below menu button
// States: 'expanded' → 'folded' (content hidden, title visible) → 'collapsed' (icon only)
const statsPanel = document.createElement('div');
Object.assign(statsPanel.style, {
position: 'absolute',
top: '50px',
left: '10px',
background: 'rgba(0, 0, 0, 0.55)',
padding: '10px 16px',
borderRadius: '4px',
fontFamily: 'Arial, sans-serif',
color: '#ddeeff',
fontSize: '14px',
lineHeight: '1.6',
overflow: 'hidden',
transition: 'width 0.3s ease, padding 0.3s ease',
width: 'auto',
});
// Title row with collapse toggle
const statsHeader = document.createElement('div');
Object.assign(statsHeader.style, {
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
pointerEvents: 'auto',
userSelect: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
});
const toggleIcon = document.createElement('span');
Object.assign(toggleIcon.style, {
fontSize: '14px',
color: '#8899bb',
display: 'inline-block',
flexShrink: '0',
});
toggleIcon.textContent = '\u25B2'; // ▲
const statsTitle = document.createElement('span');
Object.assign(statsTitle.style, {
fontWeight: 'bold',
fontSize: '15px',
color: '#ddeeff',
letterSpacing: '0.05em',
marginLeft: '8px',
overflow: 'hidden',
transition: 'max-width 0.3s ease, opacity 0.25s ease, margin 0.3s ease',
display: 'inline-block',
maxWidth: '200px',
opacity: '1',
});
statsTitle.textContent = 'Room Stats';
statsHeader.appendChild(toggleIcon);
statsHeader.appendChild(statsTitle);
statsPanel.appendChild(statsHeader);
// Collapsible content wrapper
const statsContent = document.createElement('div');
Object.assign(statsContent.style, {
overflow: 'hidden',
transition: 'max-height 0.3s ease, opacity 0.25s ease',
opacity: '1',
});
// Puzzle Time
const timeLabel = document.createElement('div');
Object.assign(timeLabel.style, { color: '#8899bb', fontSize: '12px', marginTop: '6px' });
timeLabel.textContent = 'Puzzle Time';
statsContent.appendChild(timeLabel);
this._timerEl = document.createElement('div');
Object.assign(this._timerEl.style, {
color: '#ddeeff',
fontSize: '16px',
fontFamily: 'monospace',
fontWeight: 'bold',
});
this._timerEl.textContent = '00:00:00';
statsContent.appendChild(this._timerEl);
// Players
const playersLabel = document.createElement('div');
Object.assign(playersLabel.style, { color: '#8899bb', fontSize: '12px', marginTop: '8px' });
playersLabel.textContent = 'Players';
statsContent.appendChild(playersLabel);
this._playersListEl = document.createElement('div');
Object.assign(this._playersListEl.style, {
color: '#ddeeff',
fontSize: '13px',
});
statsContent.appendChild(this._playersListEl);
statsPanel.appendChild(statsContent);
// Measure natural height after first render
requestAnimationFrame(() => {
statsContent.style.maxHeight = statsContent.scrollHeight + 'px';
});
// Collapse / expand state machine
let statsState = 'expanded'; // 'expanded' | 'folded' | 'collapsed'
let animating = false;
const collapseContent = () => {
// Phase 1: fold content up
animating = true;
statsContent.style.maxHeight = statsContent.scrollHeight + 'px';
statsContent.offsetHeight; // eslint-disable-line no-unused-expressions
statsContent.style.maxHeight = '0px';
statsContent.style.opacity = '0';
toggleIcon.textContent = '\u25BC'; // ▼
statsContent.addEventListener('transitionend', function onFolded(ev) {
if (ev.propertyName !== 'max-height') return;
statsContent.removeEventListener('transitionend', onFolded);
statsState = 'folded';
// Phase 2: collapse title horizontally
collapseTitle();
});
};
const collapseTitle = () => {
// Snapshot current width so we can animate from it
const currentW = statsPanel.offsetWidth;
statsPanel.style.width = currentW + 'px';
statsPanel.offsetHeight; // eslint-disable-line no-unused-expressions
statsTitle.style.maxWidth = '0px';
statsTitle.style.opacity = '0';
statsTitle.style.marginLeft = '0px';
// Icon is ~14px + 8px padding each side = 30px
statsPanel.style.width = '30px';
statsPanel.style.padding = '8px';
statsTitle.addEventListener('transitionend', function onTitleDone(ev) {
if (ev.propertyName !== 'max-width') return;
statsTitle.removeEventListener('transitionend', onTitleDone);
toggleIcon.textContent = '\u25B6'; // ▶
statsState = 'collapsed';
animating = false;
});
};
const expandTitle = () => {
// Phase 1: expand title horizontally
animating = true;
toggleIcon.textContent = '\u25BC'; // ▼
statsPanel.style.padding = '10px 16px';
statsPanel.style.width = 'auto';
// Measure the full expanded width, then animate to it
const fullW = statsPanel.offsetWidth;
statsPanel.style.width = '30px';
statsPanel.offsetHeight; // eslint-disable-line no-unused-expressions
statsPanel.style.width = fullW + 'px';
statsTitle.style.maxWidth = '200px';
statsTitle.style.opacity = '1';
statsTitle.style.marginLeft = '8px';
statsTitle.addEventListener('transitionend', function onTitleBack(ev) {
if (ev.propertyName !== 'max-width') return;
statsTitle.removeEventListener('transitionend', onTitleBack);
statsPanel.style.width = 'auto';
statsState = 'folded';
// Phase 2: expand content
expandContent();
});
};
const expandContent = () => {
// Measure natural height
statsContent.style.transition = 'none';
statsContent.style.maxHeight = 'none';
const fullHeight = statsContent.scrollHeight;
statsContent.style.maxHeight = '0px';
statsContent.offsetHeight; // eslint-disable-line no-unused-expressions
statsContent.style.transition = 'max-height 0.3s ease, opacity 0.25s ease';
statsContent.style.maxHeight = fullHeight + 'px';
statsContent.style.opacity = '1';
toggleIcon.textContent = '\u25B2'; // ▲
statsContent.addEventListener('transitionend', function onExpanded(ev) {
if (ev.propertyName !== 'max-height') return;
statsContent.removeEventListener('transitionend', onExpanded);
statsContent.style.maxHeight = 'none';
statsState = 'expanded';
animating = false;
});
};
statsHeader.addEventListener('click', (e) => {
e.stopPropagation();
if (animating) return;
if (statsState === 'expanded') {
collapseContent();
} else if (statsState === 'collapsed') {
expandTitle();
}
});
// Hover feedback on toggle icon
statsHeader.addEventListener('mouseenter', () => { toggleIcon.style.color = '#ddeeff'; });
statsHeader.addEventListener('mouseleave', () => { toggleIcon.style.color = '#8899bb'; });
this._uiLayer.appendChild(statsPanel);
this._updatePlayersList();
// Music controls — top-right
const musicRow = document.createElement('div');
Object.assign(musicRow.style, {
@ -1680,9 +1399,9 @@ class PuzzleScene extends Phaser.Scene {
});
const btnStyle = {
background: '#1565c0',
background: 'rgba(0, 0, 0, 0.55)',
color: '#ddeeff',
border: '1px solid #64b5f6',
border: '1px solid #4477bb',
borderRadius: '4px',
fontSize: '1.8vmin',
fontFamily: 'Arial, sans-serif',
@ -1696,16 +1415,16 @@ class PuzzleScene extends Phaser.Scene {
skipBtn.textContent = '\u23ED'; // next track symbol
skipBtn.title = 'Next Track';
Object.assign(skipBtn.style, btnStyle);
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#1e88e5'; });
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = '#1565c0'; });
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#223355'; });
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = 'rgba(0, 0, 0, 0.55)'; });
skipBtn.addEventListener('click', () => this._skipTrack());
// Mute/unmute button
this._muteBtn = document.createElement('button');
this._muteBtn.title = 'Mute / Unmute';
Object.assign(this._muteBtn.style, btnStyle);
this._muteBtn.addEventListener('mouseenter', () => { this._muteBtn.style.background = '#1e88e5'; });
this._muteBtn.addEventListener('mouseleave', () => { this._muteBtn.style.background = '#1565c0'; });
this._muteBtn.addEventListener('mouseenter', () => { this._muteBtn.style.background = '#223355'; });
this._muteBtn.addEventListener('mouseleave', () => { this._muteBtn.style.background = 'rgba(0, 0, 0, 0.55)'; });
this._muteBtn.addEventListener('click', () => {
this._toggleMute();
this._muteBtn.textContent = this._musicMuted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
@ -1764,14 +1483,12 @@ class PuzzleScene extends Phaser.Scene {
});
title.textContent = 'Puzzle Complete!';
const timeLabel = document.createElement('div');
Object.assign(timeLabel.style, {
const roomLabel = document.createElement('div');
Object.assign(roomLabel.style, {
color: '#aaccff',
fontSize: '2.5vmin',
fontFamily: 'Arial, sans-serif',
fontSize: '2.2vmin',
});
const totalSec = Math.floor((this._completionTime || 0) / 1000);
timeLabel.textContent = `Total Puzzle Time: ${this._formatTime(totalSec)}`;
roomLabel.textContent = `Room: ${this.cfg.roomCode}`;
const btnRow = document.createElement('div');
Object.assign(btnRow.style, {
@ -1789,7 +1506,7 @@ class PuzzleScene extends Phaser.Scene {
btnRow.appendChild(menuBtn);
overlay.appendChild(title);
overlay.appendChild(timeLabel);
overlay.appendChild(roomLabel);
overlay.appendChild(btnRow);
this._uiLayer.appendChild(overlay);
}
@ -1800,9 +1517,9 @@ class PuzzleScene extends Phaser.Scene {
Object.assign(btn.style, {
position: 'absolute',
padding: '0.8vmin 2vmin',
background: '#1565c0',
background: '#1a2a4a',
color: '#ddeeff',
border: '1px solid #64b5f6',
border: '1px solid #4477bb',
borderRadius: '4px',
fontSize: '1.6vmin',
fontFamily: 'Arial, sans-serif',
@ -1811,8 +1528,8 @@ class PuzzleScene extends Phaser.Scene {
whiteSpace: 'nowrap',
});
// Hover effect
btn.addEventListener('mouseenter', () => { btn.style.background = '#1e88e5'; btn.style.borderColor = '#64b5f6'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#1565c0'; btn.style.borderColor = '#64b5f6'; });
btn.addEventListener('mouseenter', () => { btn.style.background = '#223355'; btn.style.borderColor = '#66aaff'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#1a2a4a'; btn.style.borderColor = '#4477bb'; });
btn.addEventListener('click', onClick);
return btn;
}
@ -1878,7 +1595,6 @@ class PuzzleScene extends Phaser.Scene {
completed: this._completed || false,
bgKey: this._bgKey,
bgPath: this._bgPath,
startTime: this._startTime,
});
StorageManager.save(state);
StorageManager.saveCurrent(this.cfg.roomCode);

View File

@ -1,5 +1,5 @@
class PuzzleState {
constructor({ imageKey, imagePath, pieceCount, roomCode, cols, rows, pieces, groups, completed, bgKey, bgPath, startTime }) {
constructor({ imageKey, imagePath, pieceCount, roomCode, cols, rows, pieces, groups, completed, bgKey, bgPath }) {
this.imageKey = imageKey;
this.imagePath = imagePath;
this.pieceCount = pieceCount;
@ -11,7 +11,6 @@ class PuzzleState {
this.completed = completed || false;
this.bgKey = bgKey || 'bg_dark_wood';
this.bgPath = bgPath || 'assets/images/ui/dark_wood.jpg';
this.startTime = startTime || Date.now();
}
serialize() {
@ -34,7 +33,6 @@ class PuzzleState {
completed: this.completed,
bgKey: this.bgKey,
bgPath: this.bgPath,
startTime: this.startTime,
};
}