Zuma Level Refactor
This commit is contained in:
parent
eabeef3165
commit
dbc1c64d51
|
|
@ -0,0 +1,160 @@
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"name": "Temple Gate",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
580
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"seed": 16838,
|
||||||
|
"starScores": [
|
||||||
|
820,
|
||||||
|
1230,
|
||||||
|
1640
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "horseshoe",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1191,
|
||||||
|
154
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1398,
|
||||||
|
210
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1565,
|
||||||
|
294
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1680,
|
||||||
|
399
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1734,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1727,
|
||||||
|
632
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1659,
|
||||||
|
742
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1538,
|
||||||
|
836
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1375,
|
||||||
|
908
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1185,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
983,
|
||||||
|
964
|
||||||
|
],
|
||||||
|
[
|
||||||
|
785,
|
||||||
|
946
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
462,
|
||||||
|
831
|
||||||
|
],
|
||||||
|
[
|
||||||
|
361,
|
||||||
|
743
|
||||||
|
],
|
||||||
|
[
|
||||||
|
311,
|
||||||
|
646
|
||||||
|
],
|
||||||
|
[
|
||||||
|
313,
|
||||||
|
547
|
||||||
|
],
|
||||||
|
[
|
||||||
|
366,
|
||||||
|
454
|
||||||
|
],
|
||||||
|
[
|
||||||
|
464,
|
||||||
|
374
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
313
|
||||||
|
],
|
||||||
|
[
|
||||||
|
755,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
[
|
||||||
|
923,
|
||||||
|
263
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1087,
|
||||||
|
276
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1235,
|
||||||
|
312
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1356,
|
||||||
|
368
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1441,
|
||||||
|
438
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1485,
|
||||||
|
517
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1485,
|
||||||
|
598
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1445,
|
||||||
|
673
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1368,
|
||||||
|
739
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 26,
|
||||||
|
"introBalls": 8,
|
||||||
|
"pushSpeed": 76,
|
||||||
|
"powerUpRate": 0.07
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": "zuma-background-2"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"name": "Temple River",
|
||||||
|
"frog": [
|
||||||
|
1626,
|
||||||
|
805
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"seed": 12345,
|
||||||
|
"starScores": [
|
||||||
|
640,
|
||||||
|
960,
|
||||||
|
1280
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "custom",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
650,
|
||||||
|
65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
931,
|
||||||
|
83
|
||||||
|
],
|
||||||
|
[
|
||||||
|
993,
|
||||||
|
327
|
||||||
|
],
|
||||||
|
[
|
||||||
|
441,
|
||||||
|
559
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
821
|
||||||
|
],
|
||||||
|
[
|
||||||
|
767,
|
||||||
|
838
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1292,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1566,
|
||||||
|
73
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1624,
|
||||||
|
288
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1431,
|
||||||
|
544
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1247,
|
||||||
|
907
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 24,
|
||||||
|
"introBalls": 8,
|
||||||
|
"pushSpeed": 45,
|
||||||
|
"powerUpRate": 0.06
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,400 @@
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"name": "Twin Pools",
|
||||||
|
"frog": [
|
||||||
|
620,
|
||||||
|
600
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 24757,
|
||||||
|
"starScores": [
|
||||||
|
970,
|
||||||
|
1460,
|
||||||
|
1940
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "doubleLoop",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-10,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
270,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
340,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
410,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
480,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
550,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
689,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
755,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
813,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
861,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
895,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
915,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
919,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
907,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
880,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
785,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
723,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
655,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
585,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
517,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
455,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
402,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
360,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
321,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
325,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
345,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
379,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
427,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
551,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
691,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
762,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
833,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
904,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
975,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1045,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1116,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1187,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1258,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1329,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1400,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1469,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1535,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1593,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1641,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1675,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1695,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1699,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1687,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1660,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1618,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1565,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1503,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1297,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1235,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1182,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1140,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1113,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1101,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1105,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1125,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1159,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1207,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1265,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1331,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1400,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1473,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1547,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1691,
|
||||||
|
311
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1755,
|
||||||
|
344
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1806,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1839,
|
||||||
|
459
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1850,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1839,
|
||||||
|
601
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1806,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1755,
|
||||||
|
716
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1691,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1620,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1555,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1490,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1425,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1360,
|
||||||
|
760
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 30,
|
||||||
|
"introBalls": 9,
|
||||||
|
"pushSpeed": 84,
|
||||||
|
"powerUpRate": 0.065
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"name": "Switchbacks",
|
||||||
|
"frog": [
|
||||||
|
1620,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 32676,
|
||||||
|
"starScores": [
|
||||||
|
1040,
|
||||||
|
1550,
|
||||||
|
2070
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "zigzag",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
126,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
195,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
264,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
401,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
470,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
539,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
608,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
676,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
745,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
882,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
951,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1020,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1089,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1158,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1226,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
149
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
340
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
391
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
429
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
[
|
||||||
|
110,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
671
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
709
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
970
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 32,
|
||||||
|
"introBalls": 10,
|
||||||
|
"pushSpeed": 84,
|
||||||
|
"powerUpRate": 0.065
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"name": "Serpent Coil",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
575
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 40595,
|
||||||
|
"starScores": [
|
||||||
|
1250,
|
||||||
|
1870,
|
||||||
|
2490
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "spiral",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1195,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1406,
|
||||||
|
201
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1577,
|
||||||
|
287
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1694,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1749,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1739,
|
||||||
|
635
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1667,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1541,
|
||||||
|
846
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1370,
|
||||||
|
919
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1171,
|
||||||
|
962
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
973
|
||||||
|
],
|
||||||
|
[
|
||||||
|
754,
|
||||||
|
952
|
||||||
|
],
|
||||||
|
[
|
||||||
|
571,
|
||||||
|
901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
322,
|
||||||
|
732
|
||||||
|
],
|
||||||
|
[
|
||||||
|
276,
|
||||||
|
628
|
||||||
|
],
|
||||||
|
[
|
||||||
|
285,
|
||||||
|
523
|
||||||
|
],
|
||||||
|
[
|
||||||
|
349,
|
||||||
|
425
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
341
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
279
|
||||||
|
],
|
||||||
|
[
|
||||||
|
779,
|
||||||
|
243
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1136,
|
||||||
|
253
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1292,
|
||||||
|
297
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1417,
|
||||||
|
362
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1502,
|
||||||
|
442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1540,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1530,
|
||||||
|
619
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1475,
|
||||||
|
702
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1256,
|
||||||
|
823
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
853
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
859
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
843
|
||||||
|
],
|
||||||
|
[
|
||||||
|
685,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
583,
|
||||||
|
751
|
||||||
|
],
|
||||||
|
[
|
||||||
|
515,
|
||||||
|
685
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
494,
|
||||||
|
539
|
||||||
|
],
|
||||||
|
[
|
||||||
|
541,
|
||||||
|
472
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
721,
|
||||||
|
375
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
352
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
348
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [
|
||||||
|
[
|
||||||
|
2633,
|
||||||
|
3291
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quota": 38,
|
||||||
|
"introBalls": 11,
|
||||||
|
"pushSpeed": 88,
|
||||||
|
"powerUpRate": 0.06
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"name": "Crossroads",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
990
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 48514,
|
||||||
|
"starScores": [
|
||||||
|
1120,
|
||||||
|
1670,
|
||||||
|
2230
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "figureEight",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
505
|
||||||
|
],
|
||||||
|
[
|
||||||
|
136,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
154,
|
||||||
|
333
|
||||||
|
],
|
||||||
|
[
|
||||||
|
183,
|
||||||
|
259
|
||||||
|
],
|
||||||
|
[
|
||||||
|
223,
|
||||||
|
199
|
||||||
|
],
|
||||||
|
[
|
||||||
|
274,
|
||||||
|
156
|
||||||
|
],
|
||||||
|
[
|
||||||
|
334,
|
||||||
|
134
|
||||||
|
],
|
||||||
|
[
|
||||||
|
404,
|
||||||
|
132
|
||||||
|
],
|
||||||
|
[
|
||||||
|
481,
|
||||||
|
152
|
||||||
|
],
|
||||||
|
[
|
||||||
|
565,
|
||||||
|
191
|
||||||
|
],
|
||||||
|
[
|
||||||
|
655,
|
||||||
|
249
|
||||||
|
],
|
||||||
|
[
|
||||||
|
749,
|
||||||
|
321
|
||||||
|
],
|
||||||
|
[
|
||||||
|
846,
|
||||||
|
403
|
||||||
|
],
|
||||||
|
[
|
||||||
|
945,
|
||||||
|
491
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1044,
|
||||||
|
581
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1142,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1237,
|
||||||
|
741
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1328,
|
||||||
|
803
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1414,
|
||||||
|
849
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1494,
|
||||||
|
874
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1566,
|
||||||
|
879
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1629,
|
||||||
|
863
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1683,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1726,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1759,
|
||||||
|
701
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1780,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1789,
|
||||||
|
532
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1787,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1773,
|
||||||
|
357
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1747,
|
||||||
|
280
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1710,
|
||||||
|
215
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1663,
|
||||||
|
167
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1605,
|
||||||
|
138
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1538,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1463,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
177
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1293,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1200,
|
||||||
|
298
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1103,
|
||||||
|
377
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1005,
|
||||||
|
464
|
||||||
|
],
|
||||||
|
[
|
||||||
|
906,
|
||||||
|
554
|
||||||
|
],
|
||||||
|
[
|
||||||
|
808,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
[
|
||||||
|
711,
|
||||||
|
719
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
786
|
||||||
|
],
|
||||||
|
[
|
||||||
|
531,
|
||||||
|
837
|
||||||
|
],
|
||||||
|
[
|
||||||
|
449,
|
||||||
|
869
|
||||||
|
],
|
||||||
|
[
|
||||||
|
375,
|
||||||
|
880
|
||||||
|
],
|
||||||
|
[
|
||||||
|
309,
|
||||||
|
870
|
||||||
|
],
|
||||||
|
[
|
||||||
|
252,
|
||||||
|
839
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 34,
|
||||||
|
"introBalls": 10,
|
||||||
|
"pushSpeed": 88,
|
||||||
|
"powerUpRate": 0.06
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,384 @@
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"name": "Rapids",
|
||||||
|
"frog": [
|
||||||
|
880,
|
||||||
|
770
|
||||||
|
],
|
||||||
|
"colors": 4,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 56433,
|
||||||
|
"starScores": [
|
||||||
|
910,
|
||||||
|
1360,
|
||||||
|
1810
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "sCurve",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
126,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
195,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
263,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
332,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
401,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
470,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
538,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
676,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
744,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
813,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
882,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
950,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1019,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1088,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1157,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1294,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1363,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1431,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1500,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1572,
|
||||||
|
153
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1635,
|
||||||
|
189
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1682,
|
||||||
|
245
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1707,
|
||||||
|
314
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1707,
|
||||||
|
386
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1682,
|
||||||
|
455
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1635,
|
||||||
|
511
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1572,
|
||||||
|
547
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1500,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1431,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1362,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1293,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1224,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1087,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1018,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
949,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
880,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
811,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
742,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
673,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
604,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
536,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
467,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
398,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
329,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
260,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
188,
|
||||||
|
573
|
||||||
|
],
|
||||||
|
[
|
||||||
|
125,
|
||||||
|
609
|
||||||
|
],
|
||||||
|
[
|
||||||
|
78,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
53,
|
||||||
|
734
|
||||||
|
],
|
||||||
|
[
|
||||||
|
53,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
78,
|
||||||
|
875
|
||||||
|
],
|
||||||
|
[
|
||||||
|
125,
|
||||||
|
931
|
||||||
|
],
|
||||||
|
[
|
||||||
|
188,
|
||||||
|
967
|
||||||
|
],
|
||||||
|
[
|
||||||
|
260,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
329,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
398,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
467,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
536,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
604,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
673,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
742,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
811,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
880,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
949,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1018,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1087,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1224,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1293,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1362,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1431,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1500,
|
||||||
|
980
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1573,
|
||||||
|
966
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1634,
|
||||||
|
924
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1676,
|
||||||
|
863
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1690,
|
||||||
|
790
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1690,
|
||||||
|
717
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1690,
|
||||||
|
643
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1690,
|
||||||
|
570
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1678,
|
||||||
|
509
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1643,
|
||||||
|
457
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1591,
|
||||||
|
422
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1530,
|
||||||
|
410
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 26,
|
||||||
|
"introBalls": 8,
|
||||||
|
"pushSpeed": 108,
|
||||||
|
"powerUpRate": 0.06
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"name": "Sun Court",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
580
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 64352,
|
||||||
|
"starScores": [
|
||||||
|
1010,
|
||||||
|
1520,
|
||||||
|
2020
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "horseshoe",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1191,
|
||||||
|
154
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1398,
|
||||||
|
210
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1565,
|
||||||
|
294
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1680,
|
||||||
|
399
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1734,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1727,
|
||||||
|
632
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1659,
|
||||||
|
742
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1538,
|
||||||
|
836
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1375,
|
||||||
|
908
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1185,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
983,
|
||||||
|
964
|
||||||
|
],
|
||||||
|
[
|
||||||
|
785,
|
||||||
|
946
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
462,
|
||||||
|
831
|
||||||
|
],
|
||||||
|
[
|
||||||
|
361,
|
||||||
|
743
|
||||||
|
],
|
||||||
|
[
|
||||||
|
311,
|
||||||
|
646
|
||||||
|
],
|
||||||
|
[
|
||||||
|
313,
|
||||||
|
547
|
||||||
|
],
|
||||||
|
[
|
||||||
|
366,
|
||||||
|
454
|
||||||
|
],
|
||||||
|
[
|
||||||
|
464,
|
||||||
|
374
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
313
|
||||||
|
],
|
||||||
|
[
|
||||||
|
755,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
[
|
||||||
|
923,
|
||||||
|
263
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1087,
|
||||||
|
276
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1235,
|
||||||
|
312
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1356,
|
||||||
|
368
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1441,
|
||||||
|
438
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1485,
|
||||||
|
517
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1485,
|
||||||
|
598
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1445,
|
||||||
|
673
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1368,
|
||||||
|
739
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [
|
||||||
|
[
|
||||||
|
1857,
|
||||||
|
2403
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quota": 30,
|
||||||
|
"introBalls": 9,
|
||||||
|
"pushSpeed": 96,
|
||||||
|
"powerUpRate": 0.055
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"name": "Thunder Steps",
|
||||||
|
"frog": [
|
||||||
|
1620,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 72271,
|
||||||
|
"starScores": [
|
||||||
|
1210,
|
||||||
|
1820,
|
||||||
|
2420
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "zigzag",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
126,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
195,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
264,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
401,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
470,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
539,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
608,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
676,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
745,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
882,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
951,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1020,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1089,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1158,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1226,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
149
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
340
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
391
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
429
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
[
|
||||||
|
110,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
671
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
709
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
970
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 36,
|
||||||
|
"introBalls": 11,
|
||||||
|
"pushSpeed": 96,
|
||||||
|
"powerUpRate": 0.055
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,405 @@
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"name": "Twin Serpents",
|
||||||
|
"frog": [
|
||||||
|
620,
|
||||||
|
600
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 80190,
|
||||||
|
"starScores": [
|
||||||
|
1170,
|
||||||
|
1760,
|
||||||
|
2340
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "doubleLoop",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-10,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
270,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
340,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
410,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
480,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
550,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
689,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
755,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
813,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
861,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
895,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
915,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
919,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
907,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
880,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
785,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
723,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
655,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
585,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
517,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
455,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
402,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
360,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
321,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
325,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
345,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
379,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
427,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
551,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
691,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
762,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
833,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
904,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
975,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1045,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1116,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1187,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1258,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1329,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1400,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1469,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1535,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1593,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1641,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1675,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1695,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1699,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1687,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1660,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1618,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1565,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1503,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1297,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1235,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1182,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1140,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1113,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1101,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1105,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1125,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1159,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1207,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1265,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1331,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1400,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1473,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1547,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1691,
|
||||||
|
311
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1755,
|
||||||
|
344
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1806,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1839,
|
||||||
|
459
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1850,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1839,
|
||||||
|
601
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1806,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1755,
|
||||||
|
716
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1691,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1620,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1555,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1490,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1425,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1360,
|
||||||
|
760
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [
|
||||||
|
[
|
||||||
|
2581,
|
||||||
|
3226
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quota": 34,
|
||||||
|
"introBalls": 10,
|
||||||
|
"pushSpeed": 104,
|
||||||
|
"powerUpRate": 0.055
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"name": "Deep Coil",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
575
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 88109,
|
||||||
|
"starScores": [
|
||||||
|
1450,
|
||||||
|
2170,
|
||||||
|
2890
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "spiral",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1195,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1406,
|
||||||
|
201
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1577,
|
||||||
|
287
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1694,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1749,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1739,
|
||||||
|
635
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1667,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1541,
|
||||||
|
846
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1370,
|
||||||
|
919
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1171,
|
||||||
|
962
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
973
|
||||||
|
],
|
||||||
|
[
|
||||||
|
754,
|
||||||
|
952
|
||||||
|
],
|
||||||
|
[
|
||||||
|
571,
|
||||||
|
901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
322,
|
||||||
|
732
|
||||||
|
],
|
||||||
|
[
|
||||||
|
276,
|
||||||
|
628
|
||||||
|
],
|
||||||
|
[
|
||||||
|
285,
|
||||||
|
523
|
||||||
|
],
|
||||||
|
[
|
||||||
|
349,
|
||||||
|
425
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
341
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
279
|
||||||
|
],
|
||||||
|
[
|
||||||
|
779,
|
||||||
|
243
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1136,
|
||||||
|
253
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1292,
|
||||||
|
297
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1417,
|
||||||
|
362
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1502,
|
||||||
|
442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1540,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1530,
|
||||||
|
619
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1475,
|
||||||
|
702
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1256,
|
||||||
|
823
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
853
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
859
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
843
|
||||||
|
],
|
||||||
|
[
|
||||||
|
685,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
583,
|
||||||
|
751
|
||||||
|
],
|
||||||
|
[
|
||||||
|
515,
|
||||||
|
685
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
494,
|
||||||
|
539
|
||||||
|
],
|
||||||
|
[
|
||||||
|
541,
|
||||||
|
472
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
721,
|
||||||
|
375
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
352
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
348
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [
|
||||||
|
[
|
||||||
|
2194,
|
||||||
|
2925
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quota": 42,
|
||||||
|
"introBalls": 12,
|
||||||
|
"pushSpeed": 104,
|
||||||
|
"powerUpRate": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"name": "Tangled Path",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
990
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 96028,
|
||||||
|
"starScores": [
|
||||||
|
1320,
|
||||||
|
1980,
|
||||||
|
2640
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "figureEight",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
505
|
||||||
|
],
|
||||||
|
[
|
||||||
|
136,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
154,
|
||||||
|
333
|
||||||
|
],
|
||||||
|
[
|
||||||
|
183,
|
||||||
|
259
|
||||||
|
],
|
||||||
|
[
|
||||||
|
223,
|
||||||
|
199
|
||||||
|
],
|
||||||
|
[
|
||||||
|
274,
|
||||||
|
156
|
||||||
|
],
|
||||||
|
[
|
||||||
|
334,
|
||||||
|
134
|
||||||
|
],
|
||||||
|
[
|
||||||
|
404,
|
||||||
|
132
|
||||||
|
],
|
||||||
|
[
|
||||||
|
481,
|
||||||
|
152
|
||||||
|
],
|
||||||
|
[
|
||||||
|
565,
|
||||||
|
191
|
||||||
|
],
|
||||||
|
[
|
||||||
|
655,
|
||||||
|
249
|
||||||
|
],
|
||||||
|
[
|
||||||
|
749,
|
||||||
|
321
|
||||||
|
],
|
||||||
|
[
|
||||||
|
846,
|
||||||
|
403
|
||||||
|
],
|
||||||
|
[
|
||||||
|
945,
|
||||||
|
491
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1044,
|
||||||
|
581
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1142,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1237,
|
||||||
|
741
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1328,
|
||||||
|
803
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1414,
|
||||||
|
849
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1494,
|
||||||
|
874
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1566,
|
||||||
|
879
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1629,
|
||||||
|
863
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1683,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1726,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1759,
|
||||||
|
701
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1780,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1789,
|
||||||
|
532
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1787,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1773,
|
||||||
|
357
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1747,
|
||||||
|
280
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1710,
|
||||||
|
215
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1663,
|
||||||
|
167
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1605,
|
||||||
|
138
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1538,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1463,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
177
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1293,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1200,
|
||||||
|
298
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1103,
|
||||||
|
377
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1005,
|
||||||
|
464
|
||||||
|
],
|
||||||
|
[
|
||||||
|
906,
|
||||||
|
554
|
||||||
|
],
|
||||||
|
[
|
||||||
|
808,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
[
|
||||||
|
711,
|
||||||
|
719
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
786
|
||||||
|
],
|
||||||
|
[
|
||||||
|
531,
|
||||||
|
837
|
||||||
|
],
|
||||||
|
[
|
||||||
|
449,
|
||||||
|
869
|
||||||
|
],
|
||||||
|
[
|
||||||
|
375,
|
||||||
|
880
|
||||||
|
],
|
||||||
|
[
|
||||||
|
309,
|
||||||
|
870
|
||||||
|
],
|
||||||
|
[
|
||||||
|
252,
|
||||||
|
839
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 38,
|
||||||
|
"introBalls": 11,
|
||||||
|
"pushSpeed": 108,
|
||||||
|
"powerUpRate": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"name": "Lightning Run",
|
||||||
|
"frog": [
|
||||||
|
1620,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 103947,
|
||||||
|
"starScores": [
|
||||||
|
1210,
|
||||||
|
1820,
|
||||||
|
2420
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "zigzag",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
126,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
195,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
264,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
401,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
470,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
539,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
608,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
676,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
745,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
882,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
951,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1020,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1089,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1158,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1226,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
149
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
340
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
391
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
429
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
[
|
||||||
|
110,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
671
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
709
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
970
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 34,
|
||||||
|
"introBalls": 10,
|
||||||
|
"pushSpeed": 116,
|
||||||
|
"powerUpRate": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"name": "Whirlpool",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
575
|
||||||
|
],
|
||||||
|
"colors": 5,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 111866,
|
||||||
|
"starScores": [
|
||||||
|
1640,
|
||||||
|
2460,
|
||||||
|
3280
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "spiral",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1195,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1406,
|
||||||
|
201
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1577,
|
||||||
|
287
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1694,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1749,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1739,
|
||||||
|
635
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1667,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1541,
|
||||||
|
846
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1370,
|
||||||
|
919
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1171,
|
||||||
|
962
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
973
|
||||||
|
],
|
||||||
|
[
|
||||||
|
754,
|
||||||
|
952
|
||||||
|
],
|
||||||
|
[
|
||||||
|
571,
|
||||||
|
901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
322,
|
||||||
|
732
|
||||||
|
],
|
||||||
|
[
|
||||||
|
276,
|
||||||
|
628
|
||||||
|
],
|
||||||
|
[
|
||||||
|
285,
|
||||||
|
523
|
||||||
|
],
|
||||||
|
[
|
||||||
|
349,
|
||||||
|
425
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
341
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
279
|
||||||
|
],
|
||||||
|
[
|
||||||
|
779,
|
||||||
|
243
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1136,
|
||||||
|
253
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1292,
|
||||||
|
297
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1417,
|
||||||
|
362
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1502,
|
||||||
|
442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1540,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1530,
|
||||||
|
619
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1475,
|
||||||
|
702
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1256,
|
||||||
|
823
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
853
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
859
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
843
|
||||||
|
],
|
||||||
|
[
|
||||||
|
685,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
583,
|
||||||
|
751
|
||||||
|
],
|
||||||
|
[
|
||||||
|
515,
|
||||||
|
685
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
494,
|
||||||
|
539
|
||||||
|
],
|
||||||
|
[
|
||||||
|
541,
|
||||||
|
472
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
721,
|
||||||
|
375
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
352
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
348
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [
|
||||||
|
[
|
||||||
|
1901,
|
||||||
|
2633
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4388,
|
||||||
|
5119
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quota": 46,
|
||||||
|
"introBalls": 13,
|
||||||
|
"pushSpeed": 116,
|
||||||
|
"powerUpRate": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"name": "Obsidian Gate",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
580
|
||||||
|
],
|
||||||
|
"colors": 6,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 119785,
|
||||||
|
"starScores": [
|
||||||
|
1170,
|
||||||
|
1750,
|
||||||
|
2330
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "horseshoe",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1191,
|
||||||
|
154
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1398,
|
||||||
|
210
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1565,
|
||||||
|
294
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1680,
|
||||||
|
399
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1734,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1727,
|
||||||
|
632
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1659,
|
||||||
|
742
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1538,
|
||||||
|
836
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1375,
|
||||||
|
908
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1185,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
983,
|
||||||
|
964
|
||||||
|
],
|
||||||
|
[
|
||||||
|
785,
|
||||||
|
946
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
462,
|
||||||
|
831
|
||||||
|
],
|
||||||
|
[
|
||||||
|
361,
|
||||||
|
743
|
||||||
|
],
|
||||||
|
[
|
||||||
|
311,
|
||||||
|
646
|
||||||
|
],
|
||||||
|
[
|
||||||
|
313,
|
||||||
|
547
|
||||||
|
],
|
||||||
|
[
|
||||||
|
366,
|
||||||
|
454
|
||||||
|
],
|
||||||
|
[
|
||||||
|
464,
|
||||||
|
374
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
313
|
||||||
|
],
|
||||||
|
[
|
||||||
|
755,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
[
|
||||||
|
923,
|
||||||
|
263
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1087,
|
||||||
|
276
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1235,
|
||||||
|
312
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1356,
|
||||||
|
368
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1441,
|
||||||
|
438
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1485,
|
||||||
|
517
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1485,
|
||||||
|
598
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1445,
|
||||||
|
673
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1368,
|
||||||
|
739
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 32,
|
||||||
|
"introBalls": 9,
|
||||||
|
"pushSpeed": 124,
|
||||||
|
"powerUpRate": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,405 @@
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"name": "Twin Tempests",
|
||||||
|
"frog": [
|
||||||
|
620,
|
||||||
|
600
|
||||||
|
],
|
||||||
|
"colors": 6,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 127704,
|
||||||
|
"starScores": [
|
||||||
|
1400,
|
||||||
|
2100,
|
||||||
|
2800
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "doubleLoop",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-10,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
270,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
340,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
410,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
480,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
550,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
689,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
755,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
813,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
861,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
895,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
915,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
919,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
907,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
880,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
785,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
723,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
655,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
585,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
517,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
455,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
402,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
360,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
321,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
325,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
345,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
379,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
427,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
551,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
691,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
762,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
833,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
904,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
975,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1045,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1116,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1187,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1258,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1329,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1400,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1469,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1535,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1593,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1641,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1675,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1695,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1699,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1687,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1660,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1618,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1565,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1503,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
898
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1297,
|
||||||
|
882
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1235,
|
||||||
|
851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1182,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1140,
|
||||||
|
750
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1113,
|
||||||
|
686
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1101,
|
||||||
|
617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1105,
|
||||||
|
548
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1125,
|
||||||
|
481
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1159,
|
||||||
|
421
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1207,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1265,
|
||||||
|
332
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1331,
|
||||||
|
308
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1400,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1473,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1547,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1620,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1691,
|
||||||
|
311
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1755,
|
||||||
|
344
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1806,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1839,
|
||||||
|
459
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1850,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1839,
|
||||||
|
601
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1806,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1755,
|
||||||
|
716
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1691,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1620,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1555,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1490,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1425,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1360,
|
||||||
|
760
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [
|
||||||
|
[
|
||||||
|
2452,
|
||||||
|
2968
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"quota": 38,
|
||||||
|
"introBalls": 11,
|
||||||
|
"pushSpeed": 128,
|
||||||
|
"powerUpRate": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"name": "Stormsteps",
|
||||||
|
"frog": [
|
||||||
|
1620,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
"colors": 6,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 135623,
|
||||||
|
"starScores": [
|
||||||
|
1230,
|
||||||
|
1840,
|
||||||
|
2450
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "zigzag",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
126,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
195,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
264,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
333,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
401,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
470,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
539,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
608,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
676,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
745,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
882,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
951,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1020,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1089,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1158,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1226,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
149
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
340
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
391
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
429
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
[
|
||||||
|
110,
|
||||||
|
550
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
671
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
709
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1435,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1416,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1365,
|
||||||
|
951
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1295,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1225,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1156,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1086,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1016,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
947,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
877,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
807,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
598,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
529,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
389,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
320,
|
||||||
|
970
|
||||||
|
],
|
||||||
|
[
|
||||||
|
250,
|
||||||
|
970
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 34,
|
||||||
|
"introBalls": 10,
|
||||||
|
"pushSpeed": 120,
|
||||||
|
"powerUpRate": 0.045
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"name": "Maelstrom Cross",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
990
|
||||||
|
],
|
||||||
|
"colors": 6,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 143542,
|
||||||
|
"starScores": [
|
||||||
|
1440,
|
||||||
|
2160,
|
||||||
|
2880
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "figureEight",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
830
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
505
|
||||||
|
],
|
||||||
|
[
|
||||||
|
136,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
154,
|
||||||
|
333
|
||||||
|
],
|
||||||
|
[
|
||||||
|
183,
|
||||||
|
259
|
||||||
|
],
|
||||||
|
[
|
||||||
|
223,
|
||||||
|
199
|
||||||
|
],
|
||||||
|
[
|
||||||
|
274,
|
||||||
|
156
|
||||||
|
],
|
||||||
|
[
|
||||||
|
334,
|
||||||
|
134
|
||||||
|
],
|
||||||
|
[
|
||||||
|
404,
|
||||||
|
132
|
||||||
|
],
|
||||||
|
[
|
||||||
|
481,
|
||||||
|
152
|
||||||
|
],
|
||||||
|
[
|
||||||
|
565,
|
||||||
|
191
|
||||||
|
],
|
||||||
|
[
|
||||||
|
655,
|
||||||
|
249
|
||||||
|
],
|
||||||
|
[
|
||||||
|
749,
|
||||||
|
321
|
||||||
|
],
|
||||||
|
[
|
||||||
|
846,
|
||||||
|
403
|
||||||
|
],
|
||||||
|
[
|
||||||
|
945,
|
||||||
|
491
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1044,
|
||||||
|
581
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1142,
|
||||||
|
665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1237,
|
||||||
|
741
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1328,
|
||||||
|
803
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1414,
|
||||||
|
849
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1494,
|
||||||
|
874
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1566,
|
||||||
|
879
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1629,
|
||||||
|
863
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1683,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1726,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1759,
|
||||||
|
701
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1780,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1789,
|
||||||
|
532
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1787,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1773,
|
||||||
|
357
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1747,
|
||||||
|
280
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1710,
|
||||||
|
215
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1663,
|
||||||
|
167
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1605,
|
||||||
|
138
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1538,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1463,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
177
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1293,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1200,
|
||||||
|
298
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1103,
|
||||||
|
377
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1005,
|
||||||
|
464
|
||||||
|
],
|
||||||
|
[
|
||||||
|
906,
|
||||||
|
554
|
||||||
|
],
|
||||||
|
[
|
||||||
|
808,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
[
|
||||||
|
711,
|
||||||
|
719
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
786
|
||||||
|
],
|
||||||
|
[
|
||||||
|
531,
|
||||||
|
837
|
||||||
|
],
|
||||||
|
[
|
||||||
|
449,
|
||||||
|
869
|
||||||
|
],
|
||||||
|
[
|
||||||
|
375,
|
||||||
|
880
|
||||||
|
],
|
||||||
|
[
|
||||||
|
309,
|
||||||
|
870
|
||||||
|
],
|
||||||
|
[
|
||||||
|
252,
|
||||||
|
839
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 40,
|
||||||
|
"introBalls": 11,
|
||||||
|
"pushSpeed": 120,
|
||||||
|
"powerUpRate": 0.045
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"name": "Abyss Coil",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
575
|
||||||
|
],
|
||||||
|
"colors": 6,
|
||||||
|
"background": "zuma-background-1",
|
||||||
|
"seed": 151461,
|
||||||
|
"starScores": [
|
||||||
|
1940,
|
||||||
|
2910,
|
||||||
|
3880
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "spiral",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1195,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1406,
|
||||||
|
201
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1577,
|
||||||
|
287
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1694,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1749,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1739,
|
||||||
|
635
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1667,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1541,
|
||||||
|
846
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1370,
|
||||||
|
919
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1171,
|
||||||
|
962
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
973
|
||||||
|
],
|
||||||
|
[
|
||||||
|
754,
|
||||||
|
952
|
||||||
|
],
|
||||||
|
[
|
||||||
|
571,
|
||||||
|
901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
322,
|
||||||
|
732
|
||||||
|
],
|
||||||
|
[
|
||||||
|
276,
|
||||||
|
628
|
||||||
|
],
|
||||||
|
[
|
||||||
|
285,
|
||||||
|
523
|
||||||
|
],
|
||||||
|
[
|
||||||
|
349,
|
||||||
|
425
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
341
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
279
|
||||||
|
],
|
||||||
|
[
|
||||||
|
779,
|
||||||
|
243
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1136,
|
||||||
|
253
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1292,
|
||||||
|
297
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1417,
|
||||||
|
362
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1502,
|
||||||
|
442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1540,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1530,
|
||||||
|
619
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1475,
|
||||||
|
702
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1256,
|
||||||
|
823
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
853
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
859
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
843
|
||||||
|
],
|
||||||
|
[
|
||||||
|
685,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
583,
|
||||||
|
751
|
||||||
|
],
|
||||||
|
[
|
||||||
|
515,
|
||||||
|
685
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
494,
|
||||||
|
539
|
||||||
|
],
|
||||||
|
[
|
||||||
|
541,
|
||||||
|
472
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
721,
|
||||||
|
375
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
352
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
348
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 50,
|
||||||
|
"introBalls": 14,
|
||||||
|
"pushSpeed": 148,
|
||||||
|
"powerUpRate": 0.045
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"name": "The Final Coil",
|
||||||
|
"frog": [
|
||||||
|
960,
|
||||||
|
575
|
||||||
|
],
|
||||||
|
"colors": 6,
|
||||||
|
"background": "zuma-background-2",
|
||||||
|
"seed": 159380,
|
||||||
|
"starScores": [
|
||||||
|
2040,
|
||||||
|
3060,
|
||||||
|
4080
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"shape": "spiral",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
-80,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1195,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1406,
|
||||||
|
201
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1577,
|
||||||
|
287
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1694,
|
||||||
|
395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1749,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1739,
|
||||||
|
635
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1667,
|
||||||
|
749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1541,
|
||||||
|
846
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1370,
|
||||||
|
919
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1171,
|
||||||
|
962
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
973
|
||||||
|
],
|
||||||
|
[
|
||||||
|
754,
|
||||||
|
952
|
||||||
|
],
|
||||||
|
[
|
||||||
|
571,
|
||||||
|
901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
826
|
||||||
|
],
|
||||||
|
[
|
||||||
|
322,
|
||||||
|
732
|
||||||
|
],
|
||||||
|
[
|
||||||
|
276,
|
||||||
|
628
|
||||||
|
],
|
||||||
|
[
|
||||||
|
285,
|
||||||
|
523
|
||||||
|
],
|
||||||
|
[
|
||||||
|
349,
|
||||||
|
425
|
||||||
|
],
|
||||||
|
[
|
||||||
|
459,
|
||||||
|
341
|
||||||
|
],
|
||||||
|
[
|
||||||
|
607,
|
||||||
|
279
|
||||||
|
],
|
||||||
|
[
|
||||||
|
779,
|
||||||
|
243
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1136,
|
||||||
|
253
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1292,
|
||||||
|
297
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1417,
|
||||||
|
362
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1502,
|
||||||
|
442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1540,
|
||||||
|
530
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1530,
|
||||||
|
619
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1475,
|
||||||
|
702
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1381,
|
||||||
|
771
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1256,
|
||||||
|
823
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
853
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
859
|
||||||
|
],
|
||||||
|
[
|
||||||
|
814,
|
||||||
|
843
|
||||||
|
],
|
||||||
|
[
|
||||||
|
685,
|
||||||
|
806
|
||||||
|
],
|
||||||
|
[
|
||||||
|
583,
|
||||||
|
751
|
||||||
|
],
|
||||||
|
[
|
||||||
|
515,
|
||||||
|
685
|
||||||
|
],
|
||||||
|
[
|
||||||
|
485,
|
||||||
|
612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
494,
|
||||||
|
539
|
||||||
|
],
|
||||||
|
[
|
||||||
|
541,
|
||||||
|
472
|
||||||
|
],
|
||||||
|
[
|
||||||
|
619,
|
||||||
|
416
|
||||||
|
],
|
||||||
|
[
|
||||||
|
721,
|
||||||
|
375
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
352
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
348
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"tunnels": [],
|
||||||
|
"quota": 52,
|
||||||
|
"introBalls": 14,
|
||||||
|
"pushSpeed": 152,
|
||||||
|
"powerUpRate": 0.045
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"name": "Riverbend",
|
||||||
|
"file": "level-001.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"name": "Temple Gate",
|
||||||
|
"file": "level-002.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"name": "Twin Pools",
|
||||||
|
"file": "level-003.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"name": "Switchbacks",
|
||||||
|
"file": "level-004.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"name": "Serpent Coil",
|
||||||
|
"file": "level-005.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"name": "Crossroads",
|
||||||
|
"file": "level-006.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"name": "Rapids",
|
||||||
|
"file": "level-007.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"name": "Sun Court",
|
||||||
|
"file": "level-008.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"name": "Thunder Steps",
|
||||||
|
"file": "level-009.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"name": "Twin Serpents",
|
||||||
|
"file": "level-010.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"name": "Deep Coil",
|
||||||
|
"file": "level-011.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"name": "Tangled Path",
|
||||||
|
"file": "level-012.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"name": "Lightning Run",
|
||||||
|
"file": "level-013.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"name": "Whirlpool",
|
||||||
|
"file": "level-014.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"name": "Obsidian Gate",
|
||||||
|
"file": "level-015.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"name": "Twin Tempests",
|
||||||
|
"file": "level-016.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"name": "Stormsteps",
|
||||||
|
"file": "level-017.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"name": "Maelstrom Cross",
|
||||||
|
"file": "level-018.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"name": "Abyss Coil",
|
||||||
|
"file": "level-019.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"name": "The Final Coil",
|
||||||
|
"file": "level-020.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5640
data/zuma.json
5640
data/zuma.json
File diff suppressed because it is too large
Load Diff
|
|
@ -1,11 +1,12 @@
|
||||||
// Zuma level editor. Secret entrance: index.html?zuma-editor=1 (see
|
// Zuma level editor. Secret entrance: index.html?zuma-editor=1 (see
|
||||||
// PreloadScene). Mirrors the Peggle / Super Kart / Goo Tower editors: load a
|
// PreloadScene). Mirrors the Peggle / Super Kart / Goo Tower editors exactly:
|
||||||
// level (from the banked data/zuma.json or any local .json) → edit → Test Play
|
// one file per level (assets/gamedata/zuma/level-NNN.json) plus a lightweight
|
||||||
// → ⬇ Export Bank, then hand-drop the file into data/.
|
// levels.json manifest ({level, name, file}), eager-loaded under the cache
|
||||||
//
|
// key 'zuma-levels' while the per-level payloads are fetched on demand. Load
|
||||||
// Unlike those games Zuma has no per-level files — the whole 20-level bank is
|
// a level (from the manifest dropdown or any local .json) → edit → Test Play
|
||||||
// one JSON preloaded under the cache key 'zuma' — so "export" upserts this
|
// → ⬇ Export Level (the level file itself — drop it straight into
|
||||||
// level into a copy of that bank and downloads the lot.
|
// assets/gamedata/zuma/) and ⬇ Export Manifest (an updated levels.json with
|
||||||
|
// this level's entry upserted, same folder).
|
||||||
//
|
//
|
||||||
// A level is just a Catmull-Rom control polyline plus a frog position and a
|
// A level is just a Catmull-Rom control polyline plus a frog position and a
|
||||||
// handful of numbers, so editing is: drag/insert/delete points, drag the frog,
|
// handful of numbers, so editing is: drag/insert/delete points, drag the frog,
|
||||||
|
|
@ -79,12 +80,15 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
const raw = this.cache.json.get('zuma');
|
const raw = this.cache.json.get('zuma-levels');
|
||||||
this.bank = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
this.manifest = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
|
// Always starts fresh unless resuming an in-progress edit — matching
|
||||||
|
// Peggle/Super Kart/Goo Tower's editors, none of which auto-load the
|
||||||
|
// first banked level either. Pick one from "LOAD FROM BANK" instead.
|
||||||
this.level = this.resume
|
this.level = this.resume
|
||||||
? (this.registry.get('zuma-editor-state') ?? this.defaultLevel())
|
? (this.registry.get('zuma-editor-state') ?? this.defaultLevel())
|
||||||
: (this.bank[0] ? clone(this.bank[0]) : this.defaultLevel());
|
: this.defaultLevel();
|
||||||
|
|
||||||
this.tool = 'move';
|
this.tool = 'move';
|
||||||
this.selected = -1;
|
this.selected = -1;
|
||||||
|
|
@ -95,6 +99,8 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
this.portalImgs = [];
|
this.portalImgs = [];
|
||||||
this.undoStack = [];
|
this.undoStack = [];
|
||||||
this.errs = [];
|
this.errs = [];
|
||||||
|
this.activePathIdx = 0; // which of this.level.paths the tools below edit
|
||||||
|
this.pathTabButtons = [];
|
||||||
|
|
||||||
buildPortalTextures(this);
|
buildPortalTextures(this);
|
||||||
buildPitTextures(this);
|
buildPitTextures(this);
|
||||||
|
|
@ -120,22 +126,36 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
|
|
||||||
defaultLevel() {
|
defaultLevel() {
|
||||||
return {
|
return {
|
||||||
level: (this.bank?.length ?? 0) + 1,
|
level: (this.manifest?.length ?? 0) + 1,
|
||||||
name: 'New Path',
|
name: 'New Path',
|
||||||
shape: 'custom',
|
|
||||||
points: [[-80, 300], [300, 300], [800, 380], [1300, 520], [1600, 760]],
|
|
||||||
frog: [960, 900],
|
frog: [960, 900],
|
||||||
tunnels: [],
|
|
||||||
colors: 4,
|
colors: 4,
|
||||||
|
seed: 12345,
|
||||||
|
starScores: [640, 960, 1280],
|
||||||
|
paths: [this.defaultPath()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second (or third...) path starts shifted down-screen from the first so
|
||||||
|
// it isn't drawn directly on top of it — the author drags it into its real
|
||||||
|
// layout (opposite side of the screen, its own tunnels) from there.
|
||||||
|
defaultPath(offsetY = 0) {
|
||||||
|
return {
|
||||||
|
shape: 'custom',
|
||||||
|
points: [[-80, 300 + offsetY], [300, 300 + offsetY], [800, 380 + offsetY],
|
||||||
|
[1300, 520 + offsetY], [1600, 760 + offsetY]],
|
||||||
|
tunnels: [],
|
||||||
quota: 24,
|
quota: 24,
|
||||||
introBalls: 8,
|
introBalls: 8,
|
||||||
pushSpeed: 30,
|
pushSpeed: 30,
|
||||||
powerUpRate: 0.06,
|
powerUpRate: 0.06,
|
||||||
seed: 12345,
|
|
||||||
starScores: [640, 960, 1280],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activePath() {
|
||||||
|
return this.level.paths[this.activePathIdx];
|
||||||
|
}
|
||||||
|
|
||||||
// ── Coordinate transforms ──────────────────────────────────────────────────
|
// ── Coordinate transforms ──────────────────────────────────────────────────
|
||||||
|
|
||||||
toBoard(wx, wy) {
|
toBoard(wx, wy) {
|
||||||
|
|
@ -159,8 +179,14 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
fontFamily: FONT, fontSize: '30px', color: COLORS.goldHex,
|
fontFamily: FONT, fontSize: '30px', color: COLORS.goldHex,
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.buildPathTabs();
|
||||||
|
new Button(this, cx - 70, 118, '+ Add Path', () => this.addPath(),
|
||||||
|
{ width: 132, height: 34, fontSize: 14, variant: 'ghost' });
|
||||||
|
new Button(this, cx + 70, 118, 'Remove Path', () => this.removePath(),
|
||||||
|
{ width: 132, height: 34, fontSize: 14, variant: 'ghost' });
|
||||||
|
|
||||||
this.toolButtons = {};
|
this.toolButtons = {};
|
||||||
let y = 92;
|
let y = 172;
|
||||||
for (const [id, label] of TOOLS) {
|
for (const [id, label] of TOOLS) {
|
||||||
this.toolButtons[id] = new Button(this, cx, y, label, () => this.setTool(id),
|
this.toolButtons[id] = new Button(this, cx, y, label, () => this.setTool(id),
|
||||||
{ width: bw, height: 50, fontSize: 19 });
|
{ width: bw, height: 50, fontSize: 19 });
|
||||||
|
|
@ -170,10 +196,10 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
y += 16;
|
y += 16;
|
||||||
new Button(this, cx, y, '▶ Test Play', () => this.testPlay(), { width: bw, height: 56, fontSize: 22 });
|
new Button(this, cx, y, '▶ Test Play', () => this.testPlay(), { width: bw, height: 56, fontSize: 22 });
|
||||||
y += 66;
|
y += 66;
|
||||||
new Button(this, cx, y, '⬇ Export Bank', () => this.exportBank(), { width: bw, height: 50, fontSize: 19 });
|
|
||||||
y += 58;
|
|
||||||
new Button(this, cx, y, '⬇ Export Level', () => this.exportLevel(), { width: bw, height: 50, fontSize: 19 });
|
new Button(this, cx, y, '⬇ Export Level', () => this.exportLevel(), { width: bw, height: 50, fontSize: 19 });
|
||||||
y += 58;
|
y += 58;
|
||||||
|
new Button(this, cx, y, '⬇ Export Manifest', () => this.exportManifest(), { width: bw, height: 50, fontSize: 19 });
|
||||||
|
y += 58;
|
||||||
|
|
||||||
const half = bw / 2 - 6;
|
const half = bw / 2 - 6;
|
||||||
new Button(this, cx - bw / 4 - 3, y, 'Undo (Z)', () => this.undo(),
|
new Button(this, cx - bw / 4 - 3, y, 'Undo (Z)', () => this.undo(),
|
||||||
|
|
@ -181,6 +207,8 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
new Button(this, cx + bw / 4 + 3, y, 'New Level', () => {
|
new Button(this, cx + bw / 4 + 3, y, 'New Level', () => {
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
this.level = this.defaultLevel();
|
this.level = this.defaultLevel();
|
||||||
|
this.activePathIdx = 0;
|
||||||
|
this.buildPathTabs();
|
||||||
this.selected = -1;
|
this.selected = -1;
|
||||||
this.pendingEnter = null;
|
this.pendingEnter = null;
|
||||||
this.syncForm();
|
this.syncForm();
|
||||||
|
|
@ -216,6 +244,71 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Path tabs ────────────────────────────────────────────────────────────
|
||||||
|
// Every point tool (move/insert/append/delete/tunnel) and the per-path form
|
||||||
|
// fields (quota/introBalls/pushSpeed/powerUpRate) act on this.activePath();
|
||||||
|
// switching tabs is the only way to reach a different path's own points.
|
||||||
|
|
||||||
|
// Rebuilt whenever the path count changes (add/remove) — unlike the rest of
|
||||||
|
// buildToolbar(), which only ever runs once.
|
||||||
|
buildPathTabs() {
|
||||||
|
for (const b of this.pathTabButtons) b.destroy();
|
||||||
|
this.pathTabButtons = [];
|
||||||
|
|
||||||
|
const cx = BOARD_X / 2;
|
||||||
|
const bw = 280;
|
||||||
|
const gap = 6;
|
||||||
|
const n = this.level.paths.length;
|
||||||
|
const tabW = Math.min(90, (bw - (n - 1) * gap) / n);
|
||||||
|
const totalW = n * tabW + (n - 1) * gap;
|
||||||
|
let x = cx - totalW / 2 + tabW / 2;
|
||||||
|
const y = 76;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const idx = i;
|
||||||
|
const btn = new Button(this, x, y, `Path ${i + 1}`, () => this.setActivePath(idx),
|
||||||
|
{ width: tabW, height: 36, fontSize: 14 });
|
||||||
|
btn.setActive(idx === this.activePathIdx);
|
||||||
|
this.pathTabButtons.push(btn);
|
||||||
|
x += tabW + gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActivePath(idx) {
|
||||||
|
this.activePathIdx = idx;
|
||||||
|
this.pathTabButtons.forEach((b, i) => b.setActive(i === idx));
|
||||||
|
this.selected = -1;
|
||||||
|
this.pendingEnter = null;
|
||||||
|
this.syncForm();
|
||||||
|
this.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// New paths start offset from path 1 (see defaultPath) rather than stacked
|
||||||
|
// on top of it — the whole point of the tab strip is comparing paths
|
||||||
|
// against each other while editing one at a time.
|
||||||
|
addPath() {
|
||||||
|
this.pushUndo();
|
||||||
|
const n = this.level.paths.length;
|
||||||
|
this.level.paths.push(this.defaultPath(200 * n));
|
||||||
|
this.activePathIdx = this.level.paths.length - 1;
|
||||||
|
this.buildPathTabs();
|
||||||
|
this.selected = -1;
|
||||||
|
this.pendingEnter = null;
|
||||||
|
this.syncForm();
|
||||||
|
this.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
removePath() {
|
||||||
|
if (this.level.paths.length <= 1) return;
|
||||||
|
this.pushUndo();
|
||||||
|
this.level.paths.splice(this.activePathIdx, 1);
|
||||||
|
this.activePathIdx = Math.min(this.activePathIdx, this.level.paths.length - 1);
|
||||||
|
this.buildPathTabs();
|
||||||
|
this.selected = -1;
|
||||||
|
this.pendingEnter = null;
|
||||||
|
this.syncForm();
|
||||||
|
this.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
// ── DOM settings strip ─────────────────────────────────────────────────────
|
// ── DOM settings strip ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
buildForm() {
|
buildForm() {
|
||||||
|
|
@ -268,12 +361,17 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
|
|
||||||
this.loadSelect = q('#zu-load');
|
this.loadSelect = q('#zu-load');
|
||||||
this.loadSelect.innerHTML = ['<option value="">— pick a banked level —</option>',
|
this.loadSelect.innerHTML = ['<option value="">— pick a banked level —</option>',
|
||||||
...this.bank.map((l) => `<option value="${l.level}">L${l.level} · ${l.name}</option>`)].join('');
|
...this.manifest.map((m) => `<option value="${m.file}">L${m.level} · ${m.name}</option>`)].join('');
|
||||||
this.loadSelect.addEventListener('change', (ev) => {
|
this.loadSelect.addEventListener('change', async (ev) => {
|
||||||
const n = Number(ev.target.value);
|
const file = ev.target.value;
|
||||||
const def = this.bank.find((l) => l.level === n);
|
|
||||||
if (def) this.loadLevel(clone(def), `bank L${n}`);
|
|
||||||
ev.target.value = '';
|
ev.target.value = '';
|
||||||
|
if (!file) return;
|
||||||
|
const entry = this.manifest.find((m) => m.file === file);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`assets/gamedata/zuma/${file}`);
|
||||||
|
const def = await res.json();
|
||||||
|
this.loadLevel(def, entry ? `L${entry.level} · ${entry.name}` : file);
|
||||||
|
} catch (_) { this.flashWarn(`Could not load ${file}`); }
|
||||||
});
|
});
|
||||||
|
|
||||||
q('#zu-file').addEventListener('change', (ev) => {
|
q('#zu-file').addEventListener('change', (ev) => {
|
||||||
|
|
@ -298,28 +396,35 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
syncForm() {
|
syncForm() {
|
||||||
if (!this.fields) return;
|
if (!this.fields) return;
|
||||||
const l = this.level;
|
const l = this.level;
|
||||||
|
const p = this.activePath();
|
||||||
this.fields.level.value = l.level;
|
this.fields.level.value = l.level;
|
||||||
this.fields.name.value = l.name;
|
this.fields.name.value = l.name;
|
||||||
this.fields.colors.value = l.colors;
|
this.fields.colors.value = l.colors;
|
||||||
this.fields.quota.value = l.quota;
|
this.fields.quota.value = p.quota;
|
||||||
this.fields.introBalls.value = l.introBalls;
|
this.fields.introBalls.value = p.introBalls;
|
||||||
this.fields.pushSpeed.value = l.pushSpeed;
|
this.fields.pushSpeed.value = p.pushSpeed;
|
||||||
this.fields.powerUpRate.value = l.powerUpRate;
|
this.fields.powerUpRate.value = p.powerUpRate;
|
||||||
this.fields.seed.value = l.seed;
|
this.fields.seed.value = l.seed;
|
||||||
this.fields.star1.value = l.starScores[0];
|
this.fields.star1.value = l.starScores[0];
|
||||||
this.fields.star2.value = l.starScores[1];
|
this.fields.star2.value = l.starScores[1];
|
||||||
this.fields.star3.value = l.starScores[2];
|
this.fields.star3.value = l.starScores[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// colors/seed/starScores are level-wide; quota/introBalls/pushSpeed/
|
||||||
|
// powerUpRate belong to whichever path the tab strip has active.
|
||||||
readForm(key) {
|
readForm(key) {
|
||||||
const v = this.fields[key].value;
|
const v = this.fields[key].value;
|
||||||
const num = Number(v);
|
const num = Number(v);
|
||||||
|
const PATH_FIELDS = new Set(['quota', 'introBalls', 'pushSpeed']);
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'name': this.level.name = String(v || 'Untitled'); break;
|
case 'name': this.level.name = String(v || 'Untitled'); break;
|
||||||
case 'star1': case 'star2': case 'star3':
|
case 'star1': case 'star2': case 'star3':
|
||||||
this.level.starScores[Number(key.slice(4)) - 1] = Math.round(num) || 0; break;
|
this.level.starScores[Number(key.slice(4)) - 1] = Math.round(num) || 0; break;
|
||||||
case 'powerUpRate': this.level.powerUpRate = num; break;
|
case 'powerUpRate': this.activePath().powerUpRate = num; break;
|
||||||
default: this.level[key] = Math.round(num) || 0; break;
|
default:
|
||||||
|
if (PATH_FIELDS.has(key)) this.activePath()[key] = Math.round(num) || 0;
|
||||||
|
else this.level[key] = Math.round(num) || 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
@ -331,18 +436,20 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLevel(data, label) {
|
loadLevel(data, label) {
|
||||||
if (!data || !Array.isArray(data.points) || data.points.length < 3 || !Array.isArray(data.frog)) {
|
const migrated = this.migrateLevelShape(data);
|
||||||
|
if (!migrated) {
|
||||||
this.flashWarn(`${label}: not a valid Zuma level`);
|
this.flashWarn(`${label}: not a valid Zuma level`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
const d = this.defaultLevel();
|
const d = this.defaultLevel();
|
||||||
this.level = {
|
this.level = {
|
||||||
...d, ...clone(data),
|
...d, ...migrated,
|
||||||
starScores: Array.isArray(data.starScores) && data.starScores.length === 3
|
starScores: Array.isArray(migrated.starScores) && migrated.starScores.length === 3
|
||||||
? clone(data.starScores) : d.starScores,
|
? clone(migrated.starScores) : d.starScores,
|
||||||
tunnels: Array.isArray(data.tunnels) ? clone(data.tunnels) : [],
|
|
||||||
};
|
};
|
||||||
|
this.activePathIdx = 0;
|
||||||
|
this.buildPathTabs();
|
||||||
this.selected = -1;
|
this.selected = -1;
|
||||||
this.pendingEnter = null;
|
this.pendingEnter = null;
|
||||||
this.syncForm();
|
this.syncForm();
|
||||||
|
|
@ -351,6 +458,23 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
this.flash(`Loaded ${label}`);
|
this.flash(`Loaded ${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accepts the current { paths: [...] } schema, or an old flat single-path
|
||||||
|
// export (points/tunnels/quota/introBalls/pushSpeed/powerUpRate at the top
|
||||||
|
// level, from before multi-path existed) — wrapping the latter into a
|
||||||
|
// one-element paths array so previously-exported level files keep loading.
|
||||||
|
// Returns null if `data` isn't recognizable as either shape.
|
||||||
|
migrateLevelShape(data) {
|
||||||
|
if (!data || !Array.isArray(data.frog)) return null;
|
||||||
|
if (Array.isArray(data.paths) && data.paths.length) {
|
||||||
|
return clone(data);
|
||||||
|
}
|
||||||
|
if (Array.isArray(data.points) && data.points.length >= 3) {
|
||||||
|
const { points, tunnels, quota, introBalls, pushSpeed, powerUpRate, shape, ...rest } = clone(data);
|
||||||
|
return { ...rest, paths: [{ shape, points, tunnels: tunnels ?? [], quota, introBalls, pushSpeed, powerUpRate }] };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Input ──────────────────────────────────────────────────────────────────
|
// ── Input ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
bindPointer() {
|
bindPointer() {
|
||||||
|
|
@ -371,7 +495,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
const x = Math.round(Phaser.Math.Clamp(wx, WORLD.x0, WORLD.x0 + WORLD.w));
|
const x = Math.round(Phaser.Math.Clamp(wx, WORLD.x0, WORLD.x0 + WORLD.w));
|
||||||
const y = Math.round(Phaser.Math.Clamp(wy, WORLD.y0, WORLD.y0 + WORLD.h));
|
const y = Math.round(Phaser.Math.Clamp(wy, WORLD.y0, WORLD.y0 + WORLD.h));
|
||||||
if (this.draggingFrog) this.level.frog = [x, y];
|
if (this.draggingFrog) this.level.frog = [x, y];
|
||||||
else this.level.points[this.dragging] = [x, y];
|
else this.activePath().points[this.dragging] = [x, y];
|
||||||
this.draw();
|
this.draw();
|
||||||
});
|
});
|
||||||
this.input.on('pointerup', () => {
|
this.input.on('pointerup', () => {
|
||||||
|
|
@ -389,8 +513,9 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
// degenerate ones so a mis-click cannot make an un-undoable mess.
|
// degenerate ones so a mis-click cannot make an un-undoable mess.
|
||||||
|
|
||||||
tunnelsOf() {
|
tunnelsOf() {
|
||||||
if (!Array.isArray(this.level.tunnels)) this.level.tunnels = [];
|
const p = this.activePath();
|
||||||
return this.level.tunnels;
|
if (!Array.isArray(p.tunnels)) p.tunnels = [];
|
||||||
|
return p.tunnels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nearest mouth to a world point, as { idx, end, s }, or null.
|
// Nearest mouth to a world point, as { idx, end, s }, or null.
|
||||||
|
|
@ -435,8 +560,9 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
this.tunnelsOf().push([a, b]);
|
const tunnels = this.tunnelsOf();
|
||||||
this.level.tunnels.sort((p, q) => p[0] - q[0]);
|
tunnels.push([a, b]);
|
||||||
|
tunnels.sort((p, q) => p[0] - q[0]);
|
||||||
playSound(this, SFX.UI_CHIME);
|
playSound(this, SFX.UI_CHIME);
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +587,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
clearTunnels() {
|
clearTunnels() {
|
||||||
if (!this.tunnelsOf().length) return;
|
if (!this.tunnelsOf().length) return;
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
this.level.tunnels = [];
|
this.activePath().tunnels = [];
|
||||||
this.pendingEnter = null;
|
this.pendingEnter = null;
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
@ -481,7 +607,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
const max = maxPx / K;
|
const max = maxPx / K;
|
||||||
let best = -1;
|
let best = -1;
|
||||||
let bd = max;
|
let bd = max;
|
||||||
this.level.points.forEach(([x, y], i) => {
|
this.activePath().points.forEach(([x, y], i) => {
|
||||||
const d = Math.hypot(x - wx, y - wy);
|
const d = Math.hypot(x - wx, y - wy);
|
||||||
if (d < bd) { bd = d; best = i; }
|
if (d < bd) { bd = d; best = i; }
|
||||||
});
|
});
|
||||||
|
|
@ -489,7 +615,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(wx, wy) {
|
handleClick(wx, wy) {
|
||||||
const pts = this.level.points;
|
const pts = this.activePath().points;
|
||||||
switch (this.tool) {
|
switch (this.tool) {
|
||||||
case 'move': {
|
case 'move': {
|
||||||
const i = this.nearestPoint(wx, wy);
|
const i = this.nearestPoint(wx, wy);
|
||||||
|
|
@ -536,17 +662,17 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
|
|
||||||
deletePointAt(wx, wy) {
|
deletePointAt(wx, wy) {
|
||||||
const i = this.nearestPoint(wx, wy);
|
const i = this.nearestPoint(wx, wy);
|
||||||
if (i < 0 || this.level.points.length <= 3) return;
|
if (i < 0 || this.activePath().points.length <= 3) return;
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
this.level.points.splice(i, 1);
|
this.activePath().points.splice(i, 1);
|
||||||
this.selected = -1;
|
this.selected = -1;
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSelected() {
|
deleteSelected() {
|
||||||
if (this.selected < 0 || this.level.points.length <= 3) return;
|
if (this.selected < 0 || this.activePath().points.length <= 3) return;
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
this.level.points.splice(this.selected, 1);
|
this.activePath().points.splice(this.selected, 1);
|
||||||
this.selected = -1;
|
this.selected = -1;
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
@ -568,19 +694,28 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidate() {
|
revalidate() {
|
||||||
let geo = { errs: ['path invalid'], length: 0, minFrog: 0, minRadius: 0 };
|
let geo = { errs: ['a path is invalid'], paths: [] };
|
||||||
try { geo = validateLevel(this.level); } catch (_) { /* keep the placeholder */ }
|
try { geo = validateLevel(this.level); } catch (_) { /* keep the placeholder */ }
|
||||||
this.errs = [...validateLevelParams(this.level), ...geo.errs];
|
this.errs = [...validateLevelParams(this.level), ...geo.errs];
|
||||||
this.geo = geo;
|
this.geo = geo;
|
||||||
this.issueText?.setText(this.errs.length ? `⚠ ${this.errs.join(' • ')}` : '');
|
this.issueText?.setText(this.errs.length ? `⚠ ${this.errs.join(' • ')}` : '');
|
||||||
const cap = Math.floor(geo.length / (TUNING.BALL_SPACING * 1.6));
|
|
||||||
|
const active = geo.paths[this.activePathIdx] ?? { length: 0, minFrog: 0, minRadius: 0, hiddenFrac: 0 };
|
||||||
|
const cap = Math.floor(active.length / (TUNING.BALL_SPACING * 1.6));
|
||||||
const nTun = this.tunnelsOf().length;
|
const nTun = this.tunnelsOf().length;
|
||||||
this.statText?.setText(
|
const ap = this.activePath();
|
||||||
`length ${geo.length.toFixed(0)}px • quota ${this.level.quota}/${cap}`
|
let line = `path ${this.activePathIdx + 1}/${this.level.paths.length}`
|
||||||
+ ` • min radius ${Number.isFinite(geo.minRadius) ? geo.minRadius.toFixed(0) : '—'}px`
|
+ ` • length ${active.length.toFixed(0)}px • quota ${ap.quota}/${cap}`
|
||||||
+ ` • frog clear ${geo.minFrog.toFixed(0)}px • ${this.level.points.length} points`
|
+ ` • min radius ${Number.isFinite(active.minRadius) ? active.minRadius.toFixed(0) : '—'}px`
|
||||||
+ ` • ${nTun} tunnel${nTun === 1 ? '' : 's'} (${((geo.hiddenFrac ?? 0) * 100).toFixed(0)}% buried)`
|
+ ` • frog clear ${active.minFrog.toFixed(0)}px • ${ap.points.length} points`
|
||||||
);
|
+ ` • ${nTun} tunnel${nTun === 1 ? '' : 's'} (${((active.hiddenFrac ?? 0) * 100).toFixed(0)}% buried)`;
|
||||||
|
if (this.level.paths.length > 1) {
|
||||||
|
const others = geo.paths
|
||||||
|
.map((r, i) => (i === this.activePathIdx ? null : `path ${i + 1} ${r.errs.length ? '⚠' : '✓'}`))
|
||||||
|
.filter(Boolean).join(' ');
|
||||||
|
line += ` • ${others}`;
|
||||||
|
}
|
||||||
|
this.statText?.setText(line);
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -590,8 +725,9 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
// board, plus a grab handle on top of it (the handle has to sit above the
|
// board, plus a grab handle on top of it (the handle has to sit above the
|
||||||
// stone, or you cannot see the thing you are about to drag). Real art rather
|
// stone, or you cannot see the thing you are about to drag). Real art rather
|
||||||
// than schematic markers because placing a mouth is mostly a question of what
|
// than schematic markers because placing a mouth is mostly a question of what
|
||||||
// stone ends up covering which stretch of path.
|
// stone ends up covering which stretch of path. `dim` fades a mouth on a
|
||||||
drawMouth(path, s, dir) {
|
// path that isn't the one currently being edited.
|
||||||
|
drawMouth(path, s, dir, dim = 1) {
|
||||||
const p = path.pointAt(s);
|
const p = path.pointAt(s);
|
||||||
const [x, y] = this.toBoard(p.x, p.y);
|
const [x, y] = this.toBoard(p.x, p.y);
|
||||||
const ang = Math.atan2(p.ty, p.tx);
|
const ang = Math.atan2(p.ty, p.tx);
|
||||||
|
|
@ -602,17 +738,18 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
.setOrigin(PORTAL_ORIGIN_X, 0.5)
|
.setOrigin(PORTAL_ORIGIN_X, 0.5)
|
||||||
.setRotation(rot)
|
.setRotation(rot)
|
||||||
.setScale(K)
|
.setScale(K)
|
||||||
.setDepth(depth);
|
.setDepth(depth)
|
||||||
|
.setAlpha(dim);
|
||||||
if (dir < 0) img.setTint(0xffd9a0); // exits read warmer, as in game
|
if (dir < 0) img.setTint(0xffd9a0); // exits read warmer, as in game
|
||||||
this.portalImgs.push(img);
|
this.portalImgs.push(img);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.g.fillStyle(0x0a0704, 0.95);
|
this.g.fillStyle(0x0a0704, 0.95 * dim);
|
||||||
this.g.fillCircle(x, y, 46 * K);
|
this.g.fillCircle(x, y, 46 * K);
|
||||||
}
|
}
|
||||||
const t = this.gTop;
|
const t = this.gTop;
|
||||||
t.fillStyle(dir > 0 ? 0x9fd8ff : 0xffd54a, 1);
|
t.fillStyle(dir > 0 ? 0x9fd8ff : 0xffd54a, dim);
|
||||||
t.lineStyle(2, 0x000000, 0.85);
|
t.lineStyle(2, 0x000000, 0.85 * dim);
|
||||||
t.fillCircle(x, y, 7);
|
t.fillCircle(x, y, 7);
|
||||||
t.strokeCircle(x, y, 7);
|
t.strokeCircle(x, y, 7);
|
||||||
}
|
}
|
||||||
|
|
@ -634,20 +771,66 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
g.lineStyle(1, 0x6b5638, 0.7);
|
g.lineStyle(1, 0x6b5638, 0.7);
|
||||||
g.strokeRect(bx0, by0, bx1 - bx0, by1 - by0);
|
g.strokeRect(bx0, by0, bx1 - bx0, by1 - by0);
|
||||||
|
|
||||||
let path = null;
|
|
||||||
try { path = buildPath(lvl.points); } catch (_) { path = null; }
|
|
||||||
this.path = path;
|
|
||||||
|
|
||||||
for (const img of this.portalImgs) img.destroy();
|
for (const img of this.portalImgs) img.destroy();
|
||||||
this.portalImgs = [];
|
this.portalImgs = [];
|
||||||
|
|
||||||
if (path) {
|
// Every path draws — inactive ones dimmed — so laying a second lane
|
||||||
const tunnels = normalizeTunnels(lvl.tunnels, path.length);
|
// against the first, or checking a tunnel handoff between them, doesn't
|
||||||
|
// require flipping tabs back and forth. `this.path` keeps meaning "the
|
||||||
|
// path the tunnel/point tools act on," now specifically the active one.
|
||||||
|
this.path = null;
|
||||||
|
lvl.paths.forEach((pd, i) => {
|
||||||
|
const active = i === this.activePathIdx;
|
||||||
|
const built = this.drawOnePathPreview(pd, active);
|
||||||
|
if (active) this.path = built;
|
||||||
|
});
|
||||||
|
|
||||||
|
// frog + the clearance ring the lint enforces (must clear EVERY path)
|
||||||
|
const [fx, fy] = this.toBoard(lvl.frog[0], lvl.frog[1]);
|
||||||
|
this.frogImg?.destroy();
|
||||||
|
if (this.textures.exists('zuma-frog')) {
|
||||||
|
this.frogImg = this.add.image(fx, fy, 'zuma-frog', 0)
|
||||||
|
.setScale(TUNING.FROG_SCALE * K).setDepth(6);
|
||||||
|
} else {
|
||||||
|
g.fillStyle(0x3f7d3a, 1);
|
||||||
|
g.fillCircle(fx, fy, 100 * TUNING.FROG_SCALE * K);
|
||||||
|
}
|
||||||
|
const clear = TUNING.FROG_CLEARANCE * K;
|
||||||
|
const frogOk = this.geo?.paths?.every((r) => r.minFrog >= TUNING.FROG_CLEARANCE) ?? true;
|
||||||
|
g.lineStyle(2, frogOk ? 0x6fd47e : 0xd9403a, 0.8);
|
||||||
|
dashedCircle(g, fx, fy, clear);
|
||||||
|
|
||||||
|
// control points for the active path only — first is the lead-in, last is the pit
|
||||||
|
const activePts = this.activePath().points;
|
||||||
|
activePts.forEach(([px, py], i) => {
|
||||||
|
const [x, y] = this.toBoard(px, py);
|
||||||
|
const last = i === activePts.length - 1;
|
||||||
|
const color = i === this.selected ? 0xffd54a : last ? 0xd9403a : i === 0 ? 0x6fd47e : 0x9fd8ff;
|
||||||
|
g.fillStyle(color, 1);
|
||||||
|
g.fillPoints([
|
||||||
|
new Phaser.Geom.Point(x, y - 8), new Phaser.Geom.Point(x + 8, y),
|
||||||
|
new Phaser.Geom.Point(x, y + 8), new Phaser.Geom.Point(x - 8, y),
|
||||||
|
], true);
|
||||||
|
if (i === this.selected) { g.lineStyle(2, 0xffffff, 0.9); g.strokeCircle(x, y, 14); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// One path's polyline, ghost chain, tunnels and pit — dimmed when it isn't
|
||||||
|
// the path currently being edited. Returns the built arc-length curve so
|
||||||
|
// draw() can hand it to nearestMouth/handleTunnelClick/slideMouth via
|
||||||
|
// this.path without rebuilding it.
|
||||||
|
drawOnePathPreview(pd, active) {
|
||||||
|
const g = this.g;
|
||||||
|
const dim = active ? 1 : 0.38;
|
||||||
|
let path = null;
|
||||||
|
try { path = buildPath(pd.points); } catch (_) { return null; }
|
||||||
|
|
||||||
|
const tunnels = normalizeTunnels(pd.tunnels, path.length);
|
||||||
const { visible, hidden } = pathSpans(path.length, tunnels);
|
const { visible, hidden } = pathSpans(path.length, tunnels);
|
||||||
|
|
||||||
// Buried runs, on the layer underneath everything, so anything live that
|
// Buried runs, on the layer underneath everything, so anything live that
|
||||||
// crosses them lands on top — the same ordering the game uses.
|
// crosses them lands on top — the same ordering the game uses.
|
||||||
this.gUnder.lineStyle(12 * K, 0x000000, 0.45);
|
this.gUnder.lineStyle(12 * K, 0x000000, 0.45 * dim);
|
||||||
for (const [s0, s1] of hidden) {
|
for (const [s0, s1] of hidden) {
|
||||||
for (let s = s0; s < s1; s += 34) {
|
for (let s = s0; s < s1; s += 34) {
|
||||||
const a = path.pointAt(s);
|
const a = path.pointAt(s);
|
||||||
|
|
@ -660,7 +843,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
|
|
||||||
// the groove, at true scaled widths, broken at every tunnel mouth
|
// the groove, at true scaled widths, broken at every tunnel mouth
|
||||||
const stroke = (w, color) => {
|
const stroke = (w, color) => {
|
||||||
g.lineStyle(w * K, color, 1);
|
g.lineStyle(w * K, color, dim);
|
||||||
for (const [s0, s1] of visible) {
|
for (const [s0, s1] of visible) {
|
||||||
const pts = sampleRange(path, s0, s1);
|
const pts = sampleRange(path, s0, s1);
|
||||||
g.beginPath();
|
g.beginPath();
|
||||||
|
|
@ -681,19 +864,19 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
// Submerged marbles are drawn hollow: that is the part of the chain the
|
// Submerged marbles are drawn hollow: that is the part of the chain the
|
||||||
// frog will not be able to touch.
|
// frog will not be able to touch.
|
||||||
const R = TUNING.BALL_RADIUS * K;
|
const R = TUNING.BALL_RADIUS * K;
|
||||||
for (let i = 0; i < lvl.quota; i++) {
|
for (let i = 0; i < pd.quota; i++) {
|
||||||
const s = path.length - i * TUNING.BALL_SPACING;
|
const s = path.length - i * TUNING.BALL_SPACING;
|
||||||
if (s < 0) break;
|
if (s < 0) break;
|
||||||
const p = path.pointAt(s);
|
const p = path.pointAt(s);
|
||||||
const [x, y] = this.toBoard(p.x, p.y);
|
const [x, y] = this.toBoard(p.x, p.y);
|
||||||
const color = BALL_COLORS[i % Math.max(1, lvl.colors)];
|
const color = BALL_COLORS[i % Math.max(1, this.level.colors)];
|
||||||
if (isHidden(tunnels, s)) {
|
if (isHidden(tunnels, s)) {
|
||||||
g.lineStyle(2, color, 0.5);
|
g.lineStyle(2, color, 0.5 * dim);
|
||||||
g.strokeCircle(x, y, R);
|
g.strokeCircle(x, y, R);
|
||||||
} else {
|
} else {
|
||||||
g.fillStyle(color, 0.9);
|
g.fillStyle(color, 0.9 * dim);
|
||||||
g.fillCircle(x, y, R);
|
g.fillCircle(x, y, R);
|
||||||
g.lineStyle(1, 0x000000, 0.4);
|
g.lineStyle(1, 0x000000, 0.4 * dim);
|
||||||
g.strokeCircle(x, y, R);
|
g.strokeCircle(x, y, R);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -703,20 +886,20 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
const end = path.pointAt(path.length);
|
const end = path.pointAt(path.length);
|
||||||
const [hx, hy] = this.toBoard(end.x, end.y);
|
const [hx, hy] = this.toBoard(end.x, end.y);
|
||||||
if (this.textures.exists(PIT_KEY)) {
|
if (this.textures.exists(PIT_KEY)) {
|
||||||
this.portalImgs.push(this.add.image(hx, hy, PIT_KEY).setScale(K).setDepth(6));
|
this.portalImgs.push(this.add.image(hx, hy, PIT_KEY).setScale(K).setDepth(6).setAlpha(dim));
|
||||||
} else {
|
} else {
|
||||||
g.fillStyle(0x0a0704, 1); g.fillCircle(hx, hy, PIT.OUTER * K);
|
g.fillStyle(0x0a0704, dim); g.fillCircle(hx, hy, PIT.OUTER * K);
|
||||||
g.fillStyle(0x050505, 1); g.fillCircle(hx, hy, PIT.MOUTH * K);
|
g.fillStyle(0x050505, dim); g.fillCircle(hx, hy, PIT.MOUTH * K);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the maws themselves, over the chain, exactly as the game stacks them
|
// the maws themselves, over the chain, exactly as the game stacks them
|
||||||
for (const t of tunnels) {
|
for (const t of tunnels) {
|
||||||
this.drawMouth(path, t.enter, 1);
|
this.drawMouth(path, t.enter, 1, dim);
|
||||||
this.drawMouth(path, t.exit, -1);
|
this.drawMouth(path, t.exit, -1, dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
// a half-placed tunnel: show where the entrance would go
|
// a half-placed tunnel: only ever exists on the path being actively edited
|
||||||
if (this.pendingEnter !== null) {
|
if (active && this.pendingEnter !== null) {
|
||||||
const p = path.pointAt(this.pendingEnter);
|
const p = path.pointAt(this.pendingEnter);
|
||||||
const [x, y] = this.toBoard(p.x, p.y);
|
const [x, y] = this.toBoard(p.x, p.y);
|
||||||
g.lineStyle(3, 0xffd54a, 0.95);
|
g.lineStyle(3, 0xffd54a, 0.95);
|
||||||
|
|
@ -724,34 +907,8 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
g.fillStyle(0xffd54a, 0.85);
|
g.fillStyle(0xffd54a, 0.85);
|
||||||
g.fillCircle(x, y, 5);
|
g.fillCircle(x, y, 5);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// frog + the clearance ring the lint enforces
|
return path;
|
||||||
const [fx, fy] = this.toBoard(lvl.frog[0], lvl.frog[1]);
|
|
||||||
this.frogImg?.destroy();
|
|
||||||
if (this.textures.exists('zuma-frog')) {
|
|
||||||
this.frogImg = this.add.image(fx, fy, 'zuma-frog', 0)
|
|
||||||
.setScale(TUNING.FROG_SCALE * K).setDepth(6);
|
|
||||||
} else {
|
|
||||||
g.fillStyle(0x3f7d3a, 1);
|
|
||||||
g.fillCircle(fx, fy, 100 * TUNING.FROG_SCALE * K);
|
|
||||||
}
|
|
||||||
const clear = TUNING.FROG_CLEARANCE * K;
|
|
||||||
g.lineStyle(2, this.geo && this.geo.minFrog < TUNING.FROG_CLEARANCE ? 0xd9403a : 0x6fd47e, 0.8);
|
|
||||||
dashedCircle(g, fx, fy, clear);
|
|
||||||
|
|
||||||
// control points — first is the lead-in, last is the pit
|
|
||||||
lvl.points.forEach(([px, py], i) => {
|
|
||||||
const [x, y] = this.toBoard(px, py);
|
|
||||||
const last = i === lvl.points.length - 1;
|
|
||||||
const color = i === this.selected ? 0xffd54a : last ? 0xd9403a : i === 0 ? 0x6fd47e : 0x9fd8ff;
|
|
||||||
g.fillStyle(color, 1);
|
|
||||||
g.fillPoints([
|
|
||||||
new Phaser.Geom.Point(x, y - 8), new Phaser.Geom.Point(x + 8, y),
|
|
||||||
new Phaser.Geom.Point(x, y + 8), new Phaser.Geom.Point(x - 8, y),
|
|
||||||
], true);
|
|
||||||
if (i === this.selected) { g.lineStyle(2, 0xffffff, 0.9); g.strokeCircle(x, y, 14); }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test play / export ─────────────────────────────────────────────────────
|
// ── Test play / export ─────────────────────────────────────────────────────
|
||||||
|
|
@ -774,25 +931,30 @@ export default class ZumaEditor extends Phaser.Scene {
|
||||||
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportLevel() {
|
levelFile() {
|
||||||
if (this.errs.length) { playSound(this, SFX.MASTERMIND_DENIED); return; }
|
return `level-${String(this.level.level).padStart(3, '0')}.json`;
|
||||||
this.download(`zuma-level-${this.level.level}.json`, this.level);
|
|
||||||
playSound(this, SFX.UI_CHIME);
|
|
||||||
this.flash(`Saved zuma-level-${this.level.level}.json`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exportBank() {
|
manifestEntry() {
|
||||||
|
return { level: this.level.level, name: this.level.name, file: this.levelFile() };
|
||||||
|
}
|
||||||
|
|
||||||
|
exportLevel() {
|
||||||
if (this.errs.length) { playSound(this, SFX.MASTERMIND_DENIED); return; }
|
if (this.errs.length) { playSound(this, SFX.MASTERMIND_DENIED); return; }
|
||||||
const levels = this.bank.filter((l) => l.level !== this.level.level).map(clone);
|
const file = this.levelFile();
|
||||||
levels.push(clone(this.level));
|
this.download(file, this.level);
|
||||||
levels.sort((a, b) => a.level - b.level);
|
|
||||||
this.download('zuma.json', {
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
count: levels.length,
|
|
||||||
levels,
|
|
||||||
});
|
|
||||||
playSound(this, SFX.UI_CHIME);
|
playSound(this, SFX.UI_CHIME);
|
||||||
this.flash('Saved zuma.json — drop it into data/ (genZuma.js will overwrite it)');
|
this.flash(`Saved ${file} — drop it into assets/gamedata/zuma/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportManifest() {
|
||||||
|
if (this.errs.length) { playSound(this, SFX.MASTERMIND_DENIED); return; }
|
||||||
|
const levels = this.manifest.filter((m) => m.level !== this.level.level).map((m) => ({ ...m }));
|
||||||
|
levels.push(this.manifestEntry());
|
||||||
|
levels.sort((a, b) => a.level - b.level);
|
||||||
|
this.download('levels.json', { version: 1, levels });
|
||||||
|
playSound(this, SFX.UI_CHIME);
|
||||||
|
this.flash('Saved levels.json — drop it into assets/gamedata/zuma/');
|
||||||
}
|
}
|
||||||
|
|
||||||
flash(msg) {
|
flash(msg) {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
// progress or history writes for it (see ZumaEditor.testPlay).
|
// progress or history writes for it (see ZumaEditor.testPlay).
|
||||||
this.testLevel = data.testLevel ?? null;
|
this.testLevel = data.testLevel ?? null;
|
||||||
this.returnToEditor = !!data.returnToEditor;
|
this.returnToEditor = !!data.returnToEditor;
|
||||||
this.bank = [];
|
this.manifest = []; // lightweight { level, name, file } entries
|
||||||
|
this.levelCache = new Map(); // level number -> fetched full def
|
||||||
this.levelsCompleted = 0;
|
this.levelsCompleted = 0;
|
||||||
this.canPersist = true;
|
this.canPersist = true;
|
||||||
this.view = 'select';
|
this.view = 'select';
|
||||||
|
|
@ -93,7 +94,8 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
this.ballSprites = new Map(); // ball id -> { img, icon }
|
this.ballSprites = new Map(); // ball id -> { img, icon }
|
||||||
this.flightSprites = new Map(); // flight id -> img
|
this.flightSprites = new Map(); // flight id -> img
|
||||||
this.lastClearX = null; // where the last pop/explosion landed —
|
this.lastClearX = null; // where the last pop/explosion landed —
|
||||||
this.lastClearY = null; // the victory fireworks start from here
|
this.lastClearY = null; // the victory fireworks start from here,
|
||||||
|
this.lastClearPathIdx = null; // and which path's pit they travel to
|
||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
async create() {
|
||||||
|
|
@ -102,17 +104,17 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
|
|
||||||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, BG).setDepth(-2);
|
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, BG).setDepth(-2);
|
||||||
|
|
||||||
const raw = this.cache.json.get('zuma');
|
const raw = this.cache.json.get('zuma-levels');
|
||||||
this.bank = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
this.manifest = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
if (this.testLevel) {
|
if (this.testLevel) {
|
||||||
this.bank = [this.testLevel];
|
this.manifest = [{ level: this.testLevel.level, name: this.testLevel.name }];
|
||||||
this.canPersist = false;
|
this.canPersist = false;
|
||||||
this.layer = this.add.layer();
|
this.layer = this.add.layer();
|
||||||
this.rollCirc = 2 * Math.PI * TUNING.BALL_RADIUS;
|
this.rollCirc = 2 * Math.PI * TUNING.BALL_RADIUS;
|
||||||
this.buildTextures();
|
this.buildTextures();
|
||||||
this.bindInput();
|
this.bindInput();
|
||||||
this.playLevel(this.testLevel.level);
|
this.playLevel(this.testLevel.level, this.testLevel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,7 +394,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
this.frogCurrent = null;
|
this.frogCurrent = null;
|
||||||
this.frogNext = null;
|
this.frogNext = null;
|
||||||
this.laserGfx = null;
|
this.laserGfx = null;
|
||||||
this.pitFall = null;
|
this.pitFalls = null;
|
||||||
this.quotaGfx = null;
|
this.quotaGfx = null;
|
||||||
this.scoreText = null;
|
this.scoreText = null;
|
||||||
this.effectsText = null;
|
this.effectsText = null;
|
||||||
|
|
@ -434,7 +436,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
|
|
||||||
const cx = GAME_WIDTH / 2;
|
const cx = GAME_WIDTH / 2;
|
||||||
|
|
||||||
if (!this.bank.length) {
|
if (!this.manifest.length) {
|
||||||
const msg = this.add.text(cx, 520, 'No levels found.\nRun: node tools/genZuma.js', {
|
const msg = this.add.text(cx, 520, 'No levels found.\nRun: node tools/genZuma.js', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center',
|
fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
@ -444,11 +446,11 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextLevel = Math.min(this.levelsCompleted + 1, this.bank.length);
|
const nextLevel = Math.min(this.levelsCompleted + 1, this.manifest.length);
|
||||||
const bg = this.add.rectangle(cx, 442, 320, 56, 0x000000, 0.55)
|
const bg = this.add.rectangle(cx, 442, 320, 56, 0x000000, 0.55)
|
||||||
.setStrokeStyle(2, 0x6b5638, 0.8)
|
.setStrokeStyle(2, 0x6b5638, 0.8)
|
||||||
.setOrigin(0.5);
|
.setOrigin(0.5);
|
||||||
const prog = this.add.text(cx, 442, `Completed ${this.levelsCompleted} / ${this.bank.length}`, {
|
const prog = this.add.text(cx, 442, `Completed ${this.levelsCompleted} / ${this.manifest.length}`, {
|
||||||
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex,
|
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.layer.add([bg, prog]);
|
this.layer.add([bg, prog]);
|
||||||
|
|
@ -460,7 +462,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
const left = cx - gridW / 2 + SIZE / 2;
|
const left = cx - gridW / 2 + SIZE / 2;
|
||||||
const top = 550;
|
const top = 550;
|
||||||
|
|
||||||
this.bank.forEach((l, i) => {
|
this.manifest.forEach((l, i) => {
|
||||||
const col = i % COLS;
|
const col = i % COLS;
|
||||||
const row = Math.floor(i / COLS);
|
const row = Math.floor(i / COLS);
|
||||||
const x = left + col * (SIZE + GAP);
|
const x = left + col * (SIZE + GAP);
|
||||||
|
|
@ -544,14 +546,29 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
doResetProgress() {
|
doResetProgress() {
|
||||||
api.post('/puzzles/zuma/reset').catch(() => { /* best effort */ });
|
api.post('/puzzles/zuma/reset').catch(() => { /* best effort */ });
|
||||||
this.levelsCompleted = 0;
|
this.levelsCompleted = 0;
|
||||||
try { this.bank.forEach((l) => localStorage.removeItem(`zuma-stars-${l.level}`)); } catch (_) { /* ignore */ }
|
try { this.manifest.forEach((l) => localStorage.removeItem(`zuma-stars-${l.level}`)); } catch (_) { /* ignore */ }
|
||||||
this.showLevelSelect();
|
this.showLevelSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Play a level ────────────────────────────────────────────────────────────
|
// ── Play a level ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
playLevel(level) {
|
// Level payloads are fetched on demand (assets/gamedata/zuma/level-NNN.json,
|
||||||
const def = this.bank.find((l) => l.level === level);
|
// per the manifest's `file` field) rather than loaded up front — memoized
|
||||||
|
// so re-entering an already-played level is free.
|
||||||
|
async fetchLevel(level) {
|
||||||
|
if (this.levelCache.has(level)) return this.levelCache.get(level);
|
||||||
|
const entry = this.manifest.find((m) => m.level === level);
|
||||||
|
if (!entry) return null;
|
||||||
|
const res = await fetch(`assets/gamedata/zuma/${entry.file}`);
|
||||||
|
const json = await res.json();
|
||||||
|
this.levelCache.set(level, json);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `preloadedDef` is set for editor Test Play, which already has the whole
|
||||||
|
// draft level in hand and skips the fetch entirely.
|
||||||
|
async playLevel(level, preloadedDef = null) {
|
||||||
|
const def = preloadedDef ?? await this.fetchLevel(level);
|
||||||
if (!def) return;
|
if (!def) return;
|
||||||
this.view = 'play';
|
this.view = 'play';
|
||||||
this.level = level;
|
this.level = level;
|
||||||
|
|
@ -609,8 +626,12 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPath() {
|
drawPath() {
|
||||||
const path = this.state.path;
|
for (const p of this.state.paths) this.drawOnePath(p);
|
||||||
const tunnels = this.state.tunnels;
|
}
|
||||||
|
|
||||||
|
drawOnePath(p) {
|
||||||
|
const path = p.path;
|
||||||
|
const tunnels = p.tunnels;
|
||||||
const { visible, hidden } = pathSpans(path.length, tunnels);
|
const { visible, hidden } = pathSpans(path.length, tunnels);
|
||||||
|
|
||||||
// Buried runs first and BELOW the path layer: a dashed scar showing where
|
// Buried runs first and BELOW the path layer: a dashed scar showing where
|
||||||
|
|
@ -660,25 +681,27 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
// exit gets a brighter ember so the two ends read differently at a glance:
|
// exit gets a brighter ember so the two ends read differently at a glance:
|
||||||
// one is swallowing, the other is spitting the chain back out.
|
// one is swallowing, the other is spitting the chain back out.
|
||||||
drawPortals() {
|
drawPortals() {
|
||||||
for (const t of this.state.tunnels) {
|
for (const p of this.state.paths) {
|
||||||
this.addPortal(t.enter, 1);
|
for (const t of p.tunnels) {
|
||||||
this.addPortal(t.exit, -1);
|
this.addPortal(p, t.enter, 1);
|
||||||
|
this.addPortal(p, t.exit, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addPortal(s, dir) {
|
addPortal(p, s, dir) {
|
||||||
const p = this.state.path.pointAt(s);
|
const pt = p.path.pointAt(s);
|
||||||
const ang = Math.atan2(p.ty, p.tx);
|
const ang = Math.atan2(pt.ty, pt.tx);
|
||||||
const rot = dir > 0 ? ang : ang + Math.PI;
|
const rot = dir > 0 ? ang : ang + Math.PI;
|
||||||
for (const [key, depth] of [[PORTAL_STONE, D.portalStone], [PORTAL_MOUTH, D.portalMouth]]) {
|
for (const [key, depth] of [[PORTAL_STONE, D.portalStone], [PORTAL_MOUTH, D.portalMouth]]) {
|
||||||
const img = this.add.image(p.x, p.y, key)
|
const img = this.add.image(pt.x, pt.y, key)
|
||||||
.setOrigin(PORTAL_ORIGIN_X, 0.5)
|
.setOrigin(PORTAL_ORIGIN_X, 0.5)
|
||||||
.setRotation(rot)
|
.setRotation(rot)
|
||||||
.setDepth(depth);
|
.setDepth(depth);
|
||||||
this.layer.add(img);
|
this.layer.add(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
const glow = this.add.image(p.x, p.y, 'zuma-glow')
|
const glow = this.add.image(pt.x, pt.y, 'zuma-glow')
|
||||||
.setTint(dir > 0 ? 0xff8a2b : 0xffc45a)
|
.setTint(dir > 0 ? 0xff8a2b : 0xffc45a)
|
||||||
.setBlendMode(Phaser.BlendModes.ADD)
|
.setBlendMode(Phaser.BlendModes.ADD)
|
||||||
.setScale(dir > 0 ? 2.6 : 3.4)
|
.setScale(dir > 0 ? 2.6 : 3.4)
|
||||||
|
|
@ -695,7 +718,8 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
// motion is vapour turning slowly in the mouth — the old skull throbbed on a
|
// motion is vapour turning slowly in the mouth — the old skull throbbed on a
|
||||||
// scale tween, which a hole cannot do without looking like it is breathing.
|
// scale tween, which a hole cannot do without looking like it is breathing.
|
||||||
drawPit() {
|
drawPit() {
|
||||||
const end = this.state.path.pointAt(this.state.path.length);
|
for (const p of this.state.paths) {
|
||||||
|
const end = p.path.pointAt(p.path.length);
|
||||||
const pit = this.add.image(end.x, end.y, PIT_KEY).setDepth(D.pit);
|
const pit = this.add.image(end.x, end.y, PIT_KEY).setDepth(D.pit);
|
||||||
const swirl = this.add.image(end.x, end.y, PIT_SWIRL)
|
const swirl = this.add.image(end.x, end.y, PIT_SWIRL)
|
||||||
.setBlendMode(Phaser.BlendModes.ADD)
|
.setBlendMode(Phaser.BlendModes.ADD)
|
||||||
|
|
@ -709,22 +733,17 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
targets: swirl, alpha: 0.22, duration: 2600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
|
targets: swirl, alpha: 0.22, duration: 2600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The chain going over the edge. It does NOT cut across to the pit: every
|
// The chain going over the edge. It does NOT cut across to the pit: every
|
||||||
// marble keeps its place on the path and the whole formation rolls the last
|
// marble keeps its place on the path and the whole formation rolls the last
|
||||||
// stretch in arc-length, still spinning off the tangent, so the chain pours
|
// stretch in arc-length, still spinning off the tangent, so the chain pours
|
||||||
// over the lip in the order and along the line it was already travelling.
|
// over the lip in the order and along the line it was already travelling.
|
||||||
// Driven from update() rather than tweened, because the thing being animated
|
// Driven from update() rather than tweened, because the thing being animated
|
||||||
// is one shared `advance` along the path, not per-sprite x/y.
|
// is one shared `advance` along the path, not per-sprite x/y. Multi-path
|
||||||
|
// levels run every path's fall in parallel, each into its own pit — losing
|
||||||
|
// on one path does not spare the others.
|
||||||
startPitFall() {
|
startPitFall() {
|
||||||
const path = this.state.path;
|
|
||||||
const L = path.length;
|
|
||||||
const balls = this.state.balls;
|
|
||||||
const rear = balls[balls.length - 1];
|
|
||||||
const span = rear ? L - rear.s : 0;
|
|
||||||
// Long chains are drained faster rather than made to take longer.
|
|
||||||
const speed = Math.max(TUNE.PIT_FALL_SPEED, span / TUNE.PIT_FALL_MAX_S);
|
|
||||||
|
|
||||||
// Any leftover tween (the insert squeeze, the power-up icon pulse) would
|
// Any leftover tween (the insert squeeze, the power-up icon pulse) would
|
||||||
// fight the per-frame writes below.
|
// fight the per-frame writes below.
|
||||||
for (const [, spr] of this.ballSprites) {
|
for (const [, spr] of this.ballSprites) {
|
||||||
|
|
@ -739,28 +758,57 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
this.flightSprites = new Map();
|
this.flightSprites = new Map();
|
||||||
|
|
||||||
this.pitFall = {
|
this.pitFalls = [];
|
||||||
|
let maxMs = 0;
|
||||||
|
for (const p of this.state.paths) {
|
||||||
|
const L = p.path.length;
|
||||||
|
const balls = p.balls;
|
||||||
|
const rear = balls[balls.length - 1];
|
||||||
|
const span = rear ? L - rear.s : 0;
|
||||||
|
if (!span) continue;
|
||||||
|
// Long chains are drained faster rather than made to take longer.
|
||||||
|
const speed = Math.max(TUNE.PIT_FALL_SPEED, span / TUNE.PIT_FALL_MAX_S);
|
||||||
|
this.pitFalls.push({
|
||||||
|
path: p.path,
|
||||||
|
balls,
|
||||||
|
tunnels: p.tunnels,
|
||||||
advance: 0,
|
advance: 0,
|
||||||
span,
|
span,
|
||||||
speed,
|
speed,
|
||||||
// Each marble shrinks over the last stretch of ITS OWN run. The front one
|
// Each marble shrinks over the last stretch of ITS OWN run. The front
|
||||||
// is already sitting on the pit when the level ends, so a ramp measured
|
// one is already sitting on the pit when the level ends, so a ramp
|
||||||
// from the pit centre would pop it to a dot on the very first frame;
|
// measured from the pit centre would pop it to a dot on the very first
|
||||||
// measured from where it starts, it drops away from full size like the
|
// frame; measured from where it starts, it drops away from full size
|
||||||
// rest of them, just sooner.
|
// like the rest of them, just sooner.
|
||||||
ramp: new Map(balls.map((b) => [b.id, Math.max(1, Math.min(TUNE.PIT_SWALLOW, L - b.s))])),
|
ramp: new Map(balls.map((b) => [b.id, Math.max(1, Math.min(TUNE.PIT_SWALLOW, L - b.s))])),
|
||||||
};
|
});
|
||||||
return span ? (span / speed) * 1000 : 0;
|
maxMs = Math.max(maxMs, (span / speed) * 1000);
|
||||||
|
}
|
||||||
|
return maxMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
stepPitFall(dtMs) {
|
stepPitFall(dtMs) {
|
||||||
const pf = this.pitFall;
|
for (const pf of this.pitFalls) this.stepOneFall(pf, dtMs);
|
||||||
const path = this.state.path;
|
|
||||||
|
// Once every path's fall has covered its span everything is in, so the
|
||||||
|
// outro ends unconditionally — leaving pitFalls set would keep update()
|
||||||
|
// short-circuited for the rest of the scene's life.
|
||||||
|
if (this.pitFalls.every((pf) => pf.advance >= pf.span)) {
|
||||||
|
for (const [id, spr] of this.ballSprites) {
|
||||||
|
this.destroyMarble(spr);
|
||||||
|
this.ballSprites.delete(id);
|
||||||
|
}
|
||||||
|
this.pitFalls = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stepOneFall(pf, dtMs) {
|
||||||
|
const path = pf.path;
|
||||||
const L = path.length;
|
const L = path.length;
|
||||||
const tun = this.state.tunnels;
|
const tun = pf.tunnels;
|
||||||
pf.advance = Math.min(pf.span, pf.advance + pf.speed * (dtMs / 1000));
|
pf.advance = Math.min(pf.span, pf.advance + pf.speed * (dtMs / 1000));
|
||||||
|
|
||||||
for (const b of this.state.balls) {
|
for (const b of pf.balls) {
|
||||||
const spr = this.ballSprites.get(b.id);
|
const spr = this.ballSprites.get(b.id);
|
||||||
if (!spr) continue;
|
if (!spr) continue;
|
||||||
const s = Math.min(L, b.s + pf.advance);
|
const s = Math.min(L, b.s + pf.advance);
|
||||||
|
|
@ -785,17 +833,6 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
spr.gloss.setScale(scale).setAlpha(vis * dim);
|
spr.gloss.setScale(scale).setAlpha(vis * dim);
|
||||||
spr.icon?.setPosition(p.x, p.y).setScale(scale).setAlpha(vis * dim);
|
spr.icon?.setPosition(p.x, p.y).setScale(scale).setAlpha(vis * dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once the chain has covered its span everything is in, so the outro ends
|
|
||||||
// unconditionally — leaving pitFall set would keep update() short-circuited
|
|
||||||
// for the rest of the scene's life.
|
|
||||||
if (pf.advance >= pf.span) {
|
|
||||||
for (const [id, spr] of this.ballSprites) {
|
|
||||||
this.destroyMarble(spr);
|
|
||||||
this.ballSprites.delete(id);
|
|
||||||
}
|
|
||||||
this.pitFall = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Four independently depth-sorted pieces (see D above): base disc, the marble
|
// Four independently depth-sorted pieces (see D above): base disc, the marble
|
||||||
|
|
@ -862,7 +899,9 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.fillStyle(0x0a120b, 0.9);
|
g.fillStyle(0x0a120b, 0.9);
|
||||||
g.fillRoundedRect(x - 3, y - 3, W + 6, H + 6, 8);
|
g.fillRoundedRect(x - 3, y - 3, W + 6, H + 6, 8);
|
||||||
const toCome = (st.quota - st.spawned) / st.quota;
|
const totalQuota = st.paths.reduce((a, p) => a + p.quota, 0);
|
||||||
|
const totalSpawned = st.paths.reduce((a, p) => a + p.spawned, 0);
|
||||||
|
const toCome = (totalQuota - totalSpawned) / totalQuota;
|
||||||
g.fillStyle(COLORS.gold, 1);
|
g.fillStyle(COLORS.gold, 1);
|
||||||
g.fillRoundedRect(x, y, Math.max(2, W * toCome), H, 6);
|
g.fillRoundedRect(x, y, Math.max(2, W * toCome), H, 6);
|
||||||
g.lineStyle(2, 0x6b5638, 1);
|
g.lineStyle(2, 0x6b5638, 1);
|
||||||
|
|
@ -901,7 +940,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
update(time, delta) {
|
update(time, delta) {
|
||||||
// The lost-chain outro keeps running after overlayUp is set: the engine is
|
// The lost-chain outro keeps running after overlayUp is set: the engine is
|
||||||
// frozen ('lost' makes step() a no-op) but the marbles are still falling.
|
// frozen ('lost' makes step() a no-op) but the marbles are still falling.
|
||||||
if (this.pitFall) { this.stepPitFall(delta); return; }
|
if (this.pitFalls) { this.stepPitFall(delta); return; }
|
||||||
if (this.view !== 'play' || this.overlayUp || !this.state) return;
|
if (this.view !== 'play' || this.overlayUp || !this.state) return;
|
||||||
const st = this.state;
|
const st = this.state;
|
||||||
const events = step(st, delta);
|
const events = step(st, delta);
|
||||||
|
|
@ -954,7 +993,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
case 'inserted': {
|
case 'inserted': {
|
||||||
playSound(this, 'sfx-zuma-hit');
|
playSound(this, 'sfx-zuma-hit');
|
||||||
// squeeze-in: the sprite appears via syncChain, then pops to size
|
// squeeze-in: the sprite appears via syncChain, then pops to size
|
||||||
const ball = this.state.balls.find((b) => b.id === e.id);
|
const ball = this.state.paths[e.pathIdx].balls.find((b) => b.id === e.id);
|
||||||
if (ball) this.makeBallSprite(ball, true);
|
if (ball) this.makeBallSprite(ball, true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -967,14 +1006,14 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
this.floatText(e.x, e.y - 20, `+${e.score}`, '#ffd54a');
|
this.floatText(e.x, e.y - 20, `+${e.score}`, '#ffd54a');
|
||||||
if (e.cause === 'chain') this.floatText(e.x, e.y - 64, 'CHAIN!', '#6fd47e', 36);
|
if (e.cause === 'chain') this.floatText(e.x, e.y - 64, 'CHAIN!', '#6fd47e', 36);
|
||||||
else if (e.combo > 1) this.floatText(e.x, e.y - 64, `COMBO x${e.combo}`, '#6fd47e', 34);
|
else if (e.combo > 1) this.floatText(e.x, e.y - 64, `COMBO x${e.combo}`, '#6fd47e', 34);
|
||||||
this.lastClearX = e.x; this.lastClearY = e.y;
|
this.lastClearX = e.x; this.lastClearY = e.y; this.lastClearPathIdx = e.pathIdx;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'explosion':
|
case 'explosion':
|
||||||
playScifiExplode(this);
|
playScifiExplode(this);
|
||||||
this.burst(e.x, e.y, 0xffa726, 16, 2.2);
|
this.burst(e.x, e.y, 0xffa726, 16, 2.2);
|
||||||
this.floatText(e.x, e.y - 20, `+${e.score}`, '#ffa726', 36);
|
this.floatText(e.x, e.y - 20, `+${e.score}`, '#ffa726', 36);
|
||||||
this.lastClearX = e.x; this.lastClearY = e.y;
|
this.lastClearX = e.x; this.lastClearY = e.y; this.lastClearPathIdx = e.pathIdx;
|
||||||
break;
|
break;
|
||||||
case 'powerup':
|
case 'powerup':
|
||||||
playSound(this, SFX.SCIFI_REVEAL);
|
playSound(this, SFX.SCIFI_REVEAL);
|
||||||
|
|
@ -1014,11 +1053,12 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
|
|
||||||
syncChain() {
|
syncChain() {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const b of this.state.balls) {
|
for (const p of this.state.paths) {
|
||||||
|
for (const b of p.balls) {
|
||||||
seen.add(b.id);
|
seen.add(b.id);
|
||||||
const spr = this.makeBallSprite(b);
|
const spr = this.makeBallSprite(b);
|
||||||
// roll phase from arc-length, roll direction from the path tangent
|
// roll phase from arc-length, roll direction from the path tangent
|
||||||
const t = this.state.path.pointAt(b.s);
|
const t = p.path.pointAt(b.s);
|
||||||
this.rollMarble(spr, b.x, b.y, b.s, t.tx, t.ty);
|
this.rollMarble(spr, b.x, b.y, b.s, t.tx, t.ty);
|
||||||
if (spr.icon) spr.icon.setPosition(b.x, b.y);
|
if (spr.icon) spr.icon.setPosition(b.x, b.y);
|
||||||
// Tunnels: b.vis ramps 1 → 0 just inside a mouth so the marble sinks
|
// Tunnels: b.vis ramps 1 → 0 just inside a mouth so the marble sinks
|
||||||
|
|
@ -1031,6 +1071,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
spr.icon?.setVisible(vis > 0.02);
|
spr.icon?.setVisible(vis > 0.02);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (const [id, spr] of this.ballSprites) {
|
for (const [id, spr] of this.ballSprites) {
|
||||||
if (!seen.has(id)) {
|
if (!seen.has(id)) {
|
||||||
this.destroyMarble(spr);
|
this.destroyMarble(spr);
|
||||||
|
|
@ -1128,13 +1169,13 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classic Zuma's clear finale: a chain of explosions runs from wherever the
|
// Classic Zuma's clear finale: a chain of explosions runs from wherever the
|
||||||
// last match landed down to the pit, each cashing in a slice of the time
|
// last match landed down to that path's own pit, each cashing in a slice of
|
||||||
// bonus already folded into `score`, and the win jingle lands right as the
|
// the time bonus already folded into `score`, and the win jingle lands right
|
||||||
// chain reaches the edge. The soundtrack ducks for the whole sequence —
|
// as the chain reaches the edge. The soundtrack ducks for the whole sequence
|
||||||
// same trick as the level-start fanfare (see MusicPlayer.pause/resume).
|
// — same trick as the level-start fanfare (see MusicPlayer.pause/resume).
|
||||||
playVictoryFireworks(timeBonus, onDone) {
|
playVictoryFireworks(timeBonus, onDone) {
|
||||||
this.music?.pause();
|
this.music?.pause();
|
||||||
const path = this.state.path;
|
const path = this.state.paths[this.lastClearPathIdx ?? 0].path;
|
||||||
const from = nearestS(path, this.lastClearX ?? this.state.frog.x, this.lastClearY ?? this.state.frog.y).s;
|
const from = nearestS(path, this.lastClearX ?? this.state.frog.x, this.lastClearY ?? this.state.frog.y).s;
|
||||||
const span = Math.max(0, path.length - from);
|
const span = Math.max(0, path.length - from);
|
||||||
const T = TUNE;
|
const T = TUNE;
|
||||||
|
|
@ -1215,7 +1256,7 @@ export default class ZumaGame extends Phaser.Scene {
|
||||||
this.layer.add(hint);
|
this.layer.add(hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNext = this.level < this.bank.length;
|
const hasNext = this.level < this.manifest.length;
|
||||||
if (hasNext) {
|
if (hasNext) {
|
||||||
const next = new Button(this, cx, cy + 80, `Next Level (${this.level + 1})`, () => this.playLevel(this.level + 1),
|
const next = new Button(this, cx, cy + 80, `Next Level (${this.level + 1})`, () => this.playLevel(this.level + 1),
|
||||||
{ width: 340, height: 60, fontSize: 26 }).setDepth(D.overlayUI);
|
{ width: 340, height: 60, fontSize: 26 }).setDepth(D.overlayUI);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
// Zuma — pure game engine (no Phaser, no DOM, no timers).
|
// Zuma — pure game engine (no Phaser, no DOM, no timers).
|
||||||
// A marble-shooter: a chain of colored balls rolls along a curved path toward a
|
// A marble-shooter: one or more chains of colored balls roll along curved
|
||||||
// bottomless pit; the player fires balls into the chain, popping runs of 3+.
|
// paths toward their own bottomless pits; the player fires balls from one
|
||||||
// The scene (or a headless script) drives all timing through step(); every
|
// shared frog into whichever chain a shot lands nearest to, popping runs of
|
||||||
// transition returns an ordered event list the renderer replays as FX.
|
// 3+. The scene (or a headless script) drives all timing through step();
|
||||||
|
// every transition returns an ordered event list the renderer replays as FX.
|
||||||
|
//
|
||||||
|
// Multi-path levels (def.paths.length > 1) are N fully independent chains —
|
||||||
|
// each with its own path, tunnels, spawner and quota — sharing exactly one
|
||||||
|
// frog, one shot queue, and one score. There is no mechanism to aim at a
|
||||||
|
// particular path: physics decides which chain a shot lands on, purely by
|
||||||
|
// proximity, which is why levels with more than one path are expected to keep
|
||||||
|
// their lanes apart (opposite sides of the screen, or staggered by tunnels)
|
||||||
|
// rather than relying on some "switch target" input. Losing on any one path
|
||||||
|
// loses the whole level; winning requires clearing every path.
|
||||||
//
|
//
|
||||||
// All geometry lives in path-space: each chain ball has an arc-length position
|
// All geometry lives in path-space: each chain ball has an arc-length position
|
||||||
// `s` along the level's Catmull-Rom path (front of chain = largest s).
|
// `s` along its path's Catmull-Rom curve (front of chain = largest s).
|
||||||
// Segments are derived, never stored — a gap exists between neighbors more
|
// Segments are derived, never stored — a gap exists between neighbors more
|
||||||
// than BALL_SPACING + GAP_EPS apart. Screen positions are cached on each ball
|
// than BALL_SPACING + GAP_EPS apart. Screen positions are cached on each ball
|
||||||
// (b.x, b.y) every tick for flight collision and rendering.
|
// (b.x, b.y) every tick for flight collision and rendering.
|
||||||
|
|
@ -53,7 +63,7 @@ export const TUNING = {
|
||||||
export const POWER_KINDS = ['slow', 'reverse', 'accuracy', 'explosion'];
|
export const POWER_KINDS = ['slow', 'reverse', 'accuracy', 'explosion'];
|
||||||
|
|
||||||
// ── Tunnels ──────────────────────────────────────────────────────────────────
|
// ── Tunnels ──────────────────────────────────────────────────────────────────
|
||||||
// A tunnel is an arc-length interval [enter, exit] the path runs through
|
// A tunnel is an arc-length interval [enter, exit] a path runs through
|
||||||
// underground. Nothing about chain movement changes — balls keep their `s` and
|
// underground. Nothing about chain movement changes — balls keep their `s` and
|
||||||
// keep rolling — but while a ball is strictly inside the interval it is
|
// keep rolling — but while a ball is strictly inside the interval it is
|
||||||
// *submerged*: not drawn, not hit by flights, not seen by the laser sight, not
|
// *submerged*: not drawn, not hit by flights, not seen by the laser sight, not
|
||||||
|
|
@ -245,16 +255,19 @@ export function buildPath(points, step = 4) {
|
||||||
// One implementation shared by genZuma.js (which refuses to write a failing
|
// One implementation shared by genZuma.js (which refuses to write a failing
|
||||||
// bank), verifyZuma.js and the editor's live validation strip, so the three
|
// bank), verifyZuma.js and the editor's live validation strip, so the three
|
||||||
// can't drift. All thresholds derive from TUNING — they move with ball size.
|
// can't drift. All thresholds derive from TUNING — they move with ball size.
|
||||||
|
// Runs once per path in def.paths; a multi-path level's frog has to clear
|
||||||
|
// EVERY path, and each path independently needs enough length for its own
|
||||||
|
// quota and enough curve radius throughout.
|
||||||
|
|
||||||
export const LEVEL_BOUNDS = { x0: 40, y0: 40, x1: 1880, y1: 1040 };
|
export const LEVEL_BOUNDS = { x0: 40, y0: 40, x1: 1880, y1: 1040 };
|
||||||
const LEADIN_S = 200; // the off-screen lead-in is exempt from the bounds check
|
const LEADIN_S = 200; // the off-screen lead-in is exempt from the bounds check
|
||||||
|
|
||||||
export function validateLevel(def) {
|
function validateOnePath(pd, frog) {
|
||||||
const errs = [];
|
const errs = [];
|
||||||
const path = buildPath(def.points);
|
const path = buildPath(pd.points);
|
||||||
const need = def.quota * TUNING.BALL_SPACING * 1.6;
|
const need = pd.quota * TUNING.BALL_SPACING * 1.6;
|
||||||
if (path.length < need) {
|
if (path.length < need) {
|
||||||
errs.push(`path ${path.length.toFixed(0)}px too short for quota ${def.quota} (needs ${need.toFixed(0)})`);
|
errs.push(`path ${path.length.toFixed(0)}px too short for quota ${pd.quota} (needs ${need.toFixed(0)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let minFrog = Infinity;
|
let minFrog = Infinity;
|
||||||
|
|
@ -262,20 +275,20 @@ export function validateLevel(def) {
|
||||||
let minRadiusS = 0;
|
let minRadiusS = 0;
|
||||||
let outOfBounds = null;
|
let outOfBounds = null;
|
||||||
for (let i = 0; i < path.samples.length; i++) {
|
for (let i = 0; i < path.samples.length; i++) {
|
||||||
const p = path.samples[i];
|
const s = path.samples[i];
|
||||||
minFrog = Math.min(minFrog, Math.hypot(p.x - def.frog[0], p.y - def.frog[1]));
|
minFrog = Math.min(minFrog, Math.hypot(s.x - frog[0], s.y - frog[1]));
|
||||||
if (!outOfBounds && p.s > LEADIN_S && (p.x < LEVEL_BOUNDS.x0 || p.x > LEVEL_BOUNDS.x1
|
if (!outOfBounds && s.s > LEADIN_S && (s.x < LEVEL_BOUNDS.x0 || s.x > LEVEL_BOUNDS.x1
|
||||||
|| p.y < LEVEL_BOUNDS.y0 || p.y > LEVEL_BOUNDS.y1)) {
|
|| s.y < LEVEL_BOUNDS.y0 || s.y > LEVEL_BOUNDS.y1)) {
|
||||||
outOfBounds = p;
|
outOfBounds = s;
|
||||||
}
|
}
|
||||||
if (i > 0 && i < path.samples.length - 1 && p.s > LEADIN_S) {
|
if (i > 0 && i < path.samples.length - 1 && s.s > LEADIN_S) {
|
||||||
const a = path.samples[i - 1], c = path.samples[i + 1];
|
const a = path.samples[i - 1], c = path.samples[i + 1];
|
||||||
const v1x = p.x - a.x, v1y = p.y - a.y, v2x = c.x - p.x, v2y = c.y - p.y;
|
const v1x = s.x - a.x, v1y = s.y - a.y, v2x = c.x - s.x, v2y = c.y - s.y;
|
||||||
const l1 = Math.hypot(v1x, v1y), l2 = Math.hypot(v2x, v2y);
|
const l1 = Math.hypot(v1x, v1y), l2 = Math.hypot(v2x, v2y);
|
||||||
if (l1 > 0.01 && l2 > 0.01) {
|
if (l1 > 0.01 && l2 > 0.01) {
|
||||||
const cos = Math.max(-1, Math.min(1, (v1x * v2x + v1y * v2y) / (l1 * l2)));
|
const cos = Math.max(-1, Math.min(1, (v1x * v2x + v1y * v2y) / (l1 * l2)));
|
||||||
const theta = Math.acos(cos);
|
const theta = Math.acos(cos);
|
||||||
if (theta > 1e-4 && l1 / theta < minRadius) { minRadius = l1 / theta; minRadiusS = p.s; }
|
if (theta > 1e-4 && l1 / theta < minRadius) { minRadius = l1 / theta; minRadiusS = s.s; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,8 +307,8 @@ export function validateLevel(def) {
|
||||||
// Tunnels. The mouths have to sit on real, on-screen path (not the lead-in,
|
// Tunnels. The mouths have to sit on real, on-screen path (not the lead-in,
|
||||||
// not on top of the hole), tunnels may not touch each other, and enough of
|
// not on top of the hole), tunnels may not touch each other, and enough of
|
||||||
// the chain has to stay above ground for the frog to have targets at all.
|
// the chain has to stay above ground for the frog to have targets at all.
|
||||||
const tunnels = normalizeTunnels(def.tunnels, path.length);
|
const tunnels = normalizeTunnels(pd.tunnels, path.length);
|
||||||
if (Array.isArray(def.tunnels) && def.tunnels.length !== tunnels.length) {
|
if (Array.isArray(pd.tunnels) && pd.tunnels.length !== tunnels.length) {
|
||||||
errs.push('a tunnel has exit <= enter');
|
errs.push('a tunnel has exit <= enter');
|
||||||
}
|
}
|
||||||
let hiddenLen = 0;
|
let hiddenLen = 0;
|
||||||
|
|
@ -325,34 +338,68 @@ export function validateLevel(def) {
|
||||||
return { errs, length: path.length, minFrog, minRadius, minRadiusS, tunnels, hiddenFrac };
|
return { errs, length: path.length, minFrog, minRadius, minRadiusS, tunnels, hiddenFrac };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range check on the non-geometric fields, shared with verifyZuma.js.
|
// Returns { errs, paths } where `paths` is one validateOnePath() result per
|
||||||
|
// def.paths entry (each error therein prefixed "path N: " once there is more
|
||||||
|
// than one), plus path[0]'s stats spread at the top level for callers written
|
||||||
|
// before multi-path existed (genZuma.js's console summary) — new callers
|
||||||
|
// (the editor) should read `paths[activeIdx]` instead.
|
||||||
|
export function validateLevel(def) {
|
||||||
|
const results = (def.paths ?? []).map((pd) => validateOnePath(pd, def.frog));
|
||||||
|
const multi = results.length > 1;
|
||||||
|
const errs = results.flatMap((r, i) => r.errs.map((e) => (multi ? `path ${i + 1}: ${e}` : e)));
|
||||||
|
const first = results[0] ?? { length: 0, minFrog: 0, minRadius: 0, minRadiusS: 0, tunnels: [], hiddenFrac: 0 };
|
||||||
|
return {
|
||||||
|
errs, paths: results,
|
||||||
|
length: first.length, minFrog: first.minFrog, minRadius: first.minRadius,
|
||||||
|
minRadiusS: first.minRadiusS, tunnels: first.tunnels, hiddenFrac: first.hiddenFrac,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range check on the non-geometric fields, shared with verifyZuma.js. Colors
|
||||||
|
// and starScores are level-wide; quota/introBalls/pushSpeed/powerUpRate are
|
||||||
|
// checked per path.
|
||||||
export function validateLevelParams(def) {
|
export function validateLevelParams(def) {
|
||||||
const errs = [];
|
const errs = [];
|
||||||
if (!(def.colors >= 4 && def.colors <= 6)) errs.push('colors must be 4..6');
|
if (!(def.colors >= 4 && def.colors <= 6)) errs.push('colors must be 4..6');
|
||||||
if (!(def.quota >= 20)) errs.push('quota must be >= 20');
|
|
||||||
if (!(def.introBalls < def.quota)) errs.push('introBalls must be < quota');
|
|
||||||
if (!(def.pushSpeed >= 10 && def.pushSpeed <= 400)) errs.push('pushSpeed must be 10..400');
|
|
||||||
if (!(def.powerUpRate >= 0 && def.powerUpRate <= 0.2)) errs.push('powerUpRate must be 0..0.2');
|
|
||||||
if (!(Array.isArray(def.starScores) && def.starScores.length === 3
|
if (!(Array.isArray(def.starScores) && def.starScores.length === 3
|
||||||
&& def.starScores[0] < def.starScores[1] && def.starScores[1] < def.starScores[2])) {
|
&& def.starScores[0] < def.starScores[1] && def.starScores[1] < def.starScores[2])) {
|
||||||
errs.push('starScores must be 3 ascending values');
|
errs.push('starScores must be 3 ascending values');
|
||||||
}
|
}
|
||||||
|
const paths = def.paths ?? [];
|
||||||
|
if (!paths.length) errs.push('level must have at least one path');
|
||||||
|
const multi = paths.length > 1;
|
||||||
|
paths.forEach((pd, i) => {
|
||||||
|
const tag = (m) => (multi ? `path ${i + 1}: ${m}` : m);
|
||||||
|
if (!(pd.quota >= 20)) errs.push(tag('quota must be >= 20'));
|
||||||
|
if (!(pd.introBalls < pd.quota)) errs.push(tag('introBalls must be < quota'));
|
||||||
|
if (!(pd.pushSpeed >= 10 && pd.pushSpeed <= 400)) errs.push(tag('pushSpeed must be 10..400'));
|
||||||
|
if (!(pd.powerUpRate >= 0 && pd.powerUpRate <= 0.2)) errs.push(tag('powerUpRate must be 0..0.2'));
|
||||||
|
});
|
||||||
return errs;
|
return errs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Level / state construction ───────────────────────────────────────────────
|
// ── Level / state construction ───────────────────────────────────────────────
|
||||||
|
|
||||||
export function createLevel(def, seed) {
|
export function createLevel(def, seed) {
|
||||||
const path = buildPath(def.points);
|
const paths = def.paths.map((pd, idx) => {
|
||||||
const state = {
|
const path = buildPath(pd.points);
|
||||||
def,
|
return {
|
||||||
|
idx,
|
||||||
path,
|
path,
|
||||||
tunnels: normalizeTunnels(def.tunnels, path.length),
|
tunnels: normalizeTunnels(pd.tunnels, path.length),
|
||||||
frog: { x: def.frog[0], y: def.frog[1] },
|
quota: pd.quota,
|
||||||
quota: def.quota,
|
introBalls: pd.introBalls ?? 8,
|
||||||
|
pushSpeed: pd.pushSpeed,
|
||||||
|
powerUpRate: pd.powerUpRate ?? 0,
|
||||||
spawned: 0,
|
spawned: 0,
|
||||||
balls: [], // front-first: balls[0] has the largest s
|
balls: [], // front-first: balls[0] has the largest s
|
||||||
flights: [], // fired balls in screen space
|
};
|
||||||
|
});
|
||||||
|
const state = {
|
||||||
|
def,
|
||||||
|
paths,
|
||||||
|
frog: { x: def.frog[0], y: def.frog[1] },
|
||||||
|
flights: [], // fired balls in screen space — shared by every path
|
||||||
rng: makeRng((seed ?? def.seed ?? 1) >>> 0),
|
rng: makeRng((seed ?? def.seed ?? 1) >>> 0),
|
||||||
status: 'intro', // 'intro' | 'playing' | 'won' | 'lost'
|
status: 'intro', // 'intro' | 'playing' | 'won' | 'lost'
|
||||||
score: 0,
|
score: 0,
|
||||||
|
|
@ -372,9 +419,14 @@ function levelColor(state) {
|
||||||
return Math.floor(state.rng() * state.def.colors);
|
return Math.floor(state.rng() * state.def.colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Union of colors still on the board across every path — this, not any one
|
||||||
|
// path's own chain, is what the shooter draws from. It's the reason firing at
|
||||||
|
// whichever path a shot happens to land nearest never strands the player with
|
||||||
|
// an unmatchable color: as long as SOME path still carries a color, current/
|
||||||
|
// next can be recolored to it.
|
||||||
export function colorsPresent(state) {
|
export function colorsPresent(state) {
|
||||||
const set = new Set();
|
const set = new Set();
|
||||||
for (const b of state.balls) set.add(b.color);
|
for (const p of state.paths) for (const b of p.balls) set.add(b.color);
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,26 +472,27 @@ export function findRun(balls, idx) {
|
||||||
|
|
||||||
// Screen positions, plus the two tunnel flags the renderer reads: `hidden` is
|
// Screen positions, plus the two tunnel flags the renderer reads: `hidden` is
|
||||||
// the gameplay truth (inert while submerged) and `vis` the cosmetic ramp that
|
// the gameplay truth (inert while submerged) and `vis` the cosmetic ramp that
|
||||||
// sinks a marble into the maw instead of blinking it away.
|
// sinks a marble into the maw instead of blinking it away. One path at a time.
|
||||||
function syncPositions(state) {
|
function syncPositions(p) {
|
||||||
const tun = state.tunnels ?? [];
|
const tun = p.tunnels ?? [];
|
||||||
for (const b of state.balls) {
|
for (const b of p.balls) {
|
||||||
const p = state.path.pointAt(b.s);
|
const pt = p.path.pointAt(b.s);
|
||||||
b.x = p.x; b.y = p.y;
|
b.x = pt.x; b.y = pt.y;
|
||||||
b.hidden = tun.length ? isHidden(tun, b.s) : false;
|
b.hidden = tun.length ? isHidden(tun, b.s) : false;
|
||||||
b.vis = tun.length ? visibilityAt(tun, b.s) : 1;
|
b.vis = tun.length ? visibilityAt(tun, b.s) : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Chain movement ────────────────────────────────────────────────────────────
|
// ── Chain movement ────────────────────────────────────────────────────────────
|
||||||
// Per tick: the rearmost (spawner-fed) segment drives forward; per gap, a
|
// Per path, per tick: the rearmost (spawner-fed) segment drives forward; per
|
||||||
// matching pair pulls the front side backward, a non-matching pair sends the
|
// gap, a matching pair pulls the front side backward, a non-matching pair
|
||||||
// rear side forward to catch up. Reverse overrides everything backward.
|
// sends the rear side forward to catch up. Reverse overrides everything
|
||||||
// Contacts merge implicitly (exact spacing); closed gaps clank + match-check.
|
// backward. Contacts merge implicitly (exact spacing); closed gaps clank +
|
||||||
|
// match-check. Every path moves independently — no cross-path interaction.
|
||||||
|
|
||||||
function moveSegments(state, dtMs, events) {
|
function moveSegments(state, p, dtMs, events) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
const balls = state.balls;
|
const balls = p.balls;
|
||||||
if (!balls.length) return;
|
if (!balls.length) return;
|
||||||
const dt = dtMs / 1000;
|
const dt = dtMs / 1000;
|
||||||
const now = state.elapsedMs;
|
const now = state.elapsedMs;
|
||||||
|
|
@ -449,8 +502,8 @@ function moveSegments(state, dtMs, events) {
|
||||||
if (now < state.effects.reverseUntil) {
|
if (now < state.effects.reverseUntil) {
|
||||||
vel.fill(-T.REVERSE_SPEED);
|
vel.fill(-T.REVERSE_SPEED);
|
||||||
} else {
|
} else {
|
||||||
let base = state.def.pushSpeed;
|
let base = p.pushSpeed;
|
||||||
if (state.status === 'intro') base *= T.INTRO_SPEED_MULT;
|
if (p.spawned < p.introBalls) base *= T.INTRO_SPEED_MULT;
|
||||||
if (now < state.effects.slowUntil) base *= T.SLOW_MULT;
|
if (now < state.effects.slowUntil) base *= T.SLOW_MULT;
|
||||||
vel[segs.length - 1] += base;
|
vel[segs.length - 1] += base;
|
||||||
for (let g = 0; g < segs.length - 1; g++) {
|
for (let g = 0; g < segs.length - 1; g++) {
|
||||||
|
|
@ -494,13 +547,13 @@ function moveSegments(state, dtMs, events) {
|
||||||
const gap = balls[fi].s - balls[fi + 1].s;
|
const gap = balls[fi].s - balls[fi + 1].s;
|
||||||
if (gap > T.BALL_SPACING + T.GAP_EPS) continue;
|
if (gap > T.BALL_SPACING + T.GAP_EPS) continue;
|
||||||
if (gap < T.BALL_SPACING) balls[fi + 1].s = balls[fi].s - T.BALL_SPACING;
|
if (gap < T.BALL_SPACING) balls[fi + 1].s = balls[fi].s - T.BALL_SPACING;
|
||||||
const p = state.path.pointAt(balls[fi].s);
|
const pt = p.path.pointAt(balls[fi].s);
|
||||||
events.push({ type: 'clank', x: p.x, y: p.y });
|
events.push({ type: 'clank', pathIdx: p.idx, x: pt.x, y: pt.y });
|
||||||
if (balls[fi].color === balls[fi + 1].color) {
|
if (balls[fi].color === balls[fi + 1].color) {
|
||||||
const run = findRun(balls, fi);
|
const run = findRun(balls, fi);
|
||||||
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
||||||
state.combo += 1;
|
state.combo += 1;
|
||||||
popRun(state, run.lo, run.hi, 'chain', events);
|
popRun(state, p, run.lo, run.hi, 'chain', events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -508,58 +561,73 @@ function moveSegments(state, dtMs, events) {
|
||||||
|
|
||||||
// ── Spawning ──────────────────────────────────────────────────────────────────
|
// ── Spawning ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function spawnColor(state) {
|
// Last-call colors are drawn from the whole board (every path), same as the
|
||||||
if (state.quota - state.spawned <= TUNING.LASTCALL_COUNT) {
|
// shooter — the guarantee is "the player can still find a match somewhere,"
|
||||||
|
// not "this path's own chain still has one."
|
||||||
|
function spawnColor(state, p) {
|
||||||
|
if (p.quota - p.spawned <= TUNING.LASTCALL_COUNT) {
|
||||||
const present = colorsPresent(state);
|
const present = colorsPresent(state);
|
||||||
if (present.size) return pickPresent(state, present);
|
if (present.size) return pickPresent(state, present);
|
||||||
}
|
}
|
||||||
return levelColor(state);
|
return levelColor(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnBalls(state, events) {
|
function spawnBalls(state, p, events) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
while (state.spawned < state.quota) {
|
while (p.spawned < p.quota) {
|
||||||
const rear = state.balls[state.balls.length - 1];
|
const rear = p.balls[p.balls.length - 1];
|
||||||
if (rear && rear.s < T.BALL_SPACING) break;
|
if (rear && rear.s < T.BALL_SPACING) break;
|
||||||
const color = spawnColor(state);
|
const color = spawnColor(state, p);
|
||||||
let power = null;
|
let power = null;
|
||||||
if (state.rng() < (state.def.powerUpRate ?? 0)) {
|
if (state.rng() < p.powerUpRate) {
|
||||||
power = POWER_KINDS[Math.floor(state.rng() * POWER_KINDS.length)];
|
power = POWER_KINDS[Math.floor(state.rng() * POWER_KINDS.length)];
|
||||||
}
|
}
|
||||||
const b = { id: state.nextId++, color, power, s: rear ? rear.s - T.BALL_SPACING : 0, x: 0, y: 0 };
|
const b = { id: state.nextId++, color, power, s: rear ? rear.s - T.BALL_SPACING : 0, x: 0, y: 0 };
|
||||||
const p = state.path.pointAt(b.s);
|
const pt = p.path.pointAt(b.s);
|
||||||
b.x = p.x; b.y = p.y;
|
b.x = pt.x; b.y = pt.y;
|
||||||
state.balls.push(b);
|
p.balls.push(b);
|
||||||
state.spawned++;
|
p.spawned++;
|
||||||
events.push({ type: 'spawn', id: b.id });
|
events.push({ type: 'spawn', pathIdx: p.idx, id: b.id });
|
||||||
if (state.status === 'intro' && state.spawned >= (state.def.introBalls ?? 8)) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The level-wide intro→playing switch: every path fast-feeds independently
|
||||||
|
// (see moveSegments) until ITS OWN spawned count clears its own introBalls,
|
||||||
|
// but firing/swapping stay locked until ALL paths have — one shared 'ready'
|
||||||
|
// moment rather than the frog going live mid-fast-forward on a path that
|
||||||
|
// finished its intro early.
|
||||||
|
function checkReady(state, events) {
|
||||||
|
if (state.status !== 'intro') return;
|
||||||
|
if (state.paths.every((p) => p.spawned >= p.introBalls)) {
|
||||||
state.status = 'playing';
|
state.status = 'playing';
|
||||||
events.push({ type: 'ready' });
|
events.push({ type: 'ready' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ── Popping, power-ups, scoring ───────────────────────────────────────────────
|
// ── Popping, power-ups, scoring ───────────────────────────────────────────────
|
||||||
|
|
||||||
export function popRun(state, lo, hi, cause, events) {
|
export function popRun(state, p, lo, hi, cause, events) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
const popped = state.balls.splice(lo, hi - lo + 1);
|
const popped = p.balls.splice(lo, hi - lo + 1);
|
||||||
const mid = popped[Math.floor(popped.length / 2)];
|
const mid = popped[Math.floor(popped.length / 2)];
|
||||||
let score = popped.length * T.SCORE_BALL * Math.max(1, state.combo);
|
let score = popped.length * T.SCORE_BALL * Math.max(1, state.combo);
|
||||||
if (cause === 'chain') score += T.SCORE_CHAIN_BONUS;
|
if (cause === 'chain') score += T.SCORE_CHAIN_BONUS;
|
||||||
state.score += score;
|
state.score += score;
|
||||||
events.push({
|
events.push({
|
||||||
type: 'pop', ids: popped.map((b) => b.id), color: mid.color,
|
type: 'pop', pathIdx: p.idx, ids: popped.map((b) => b.id), color: mid.color,
|
||||||
score, combo: state.combo, x: mid.x, y: mid.y, cause,
|
score, combo: state.combo, x: mid.x, y: mid.y, cause,
|
||||||
});
|
});
|
||||||
const powers = popped.filter((b) => b.power);
|
const powers = popped.filter((b) => b.power);
|
||||||
for (const b of powers) applyPower(state, b, events);
|
for (const b of powers) applyPower(state, p, b, events);
|
||||||
recolorShooter(state, events);
|
recolorShooter(state, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPower(state, ball, events) {
|
// Explosion blasts stay scoped to the path the popped ball belonged to — a
|
||||||
|
// chain reaction cannot hop to a different path, same as it cannot reach
|
||||||
|
// through a tunnel.
|
||||||
|
function applyPower(state, p, ball, events) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
events.push({ type: 'powerup', kind: ball.power, x: ball.x, y: ball.y });
|
events.push({ type: 'powerup', pathIdx: p.idx, kind: ball.power, x: ball.x, y: ball.y });
|
||||||
if (ball.power === 'slow') state.effects.slowUntil = state.elapsedMs + T.SLOW_MS;
|
if (ball.power === 'slow') state.effects.slowUntil = state.elapsedMs + T.SLOW_MS;
|
||||||
else if (ball.power === 'reverse') state.effects.reverseUntil = state.elapsedMs + T.REVERSE_MS;
|
else if (ball.power === 'reverse') state.effects.reverseUntil = state.elapsedMs + T.REVERSE_MS;
|
||||||
else if (ball.power === 'accuracy') state.effects.accuracyUntil = state.elapsedMs + T.ACCURACY_MS;
|
else if (ball.power === 'accuracy') state.effects.accuracyUntil = state.elapsedMs + T.ACCURACY_MS;
|
||||||
|
|
@ -567,34 +635,34 @@ function applyPower(state, ball, events) {
|
||||||
// blast radius around the popped ball; chained power balls trigger too.
|
// blast radius around the popped ball; chained power balls trigger too.
|
||||||
// A blast is stopped dead by a tunnel mouth — submerged marbles are out of
|
// A blast is stopped dead by a tunnel mouth — submerged marbles are out of
|
||||||
// play, so the chain reaction cannot reach through the ground to them.
|
// play, so the chain reaction cannot reach through the ground to them.
|
||||||
const tun = state.tunnels ?? [];
|
const tun = p.tunnels ?? [];
|
||||||
const queue = [ball];
|
const queue = [ball];
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const src = queue.shift();
|
const src = queue.shift();
|
||||||
const caught = state.balls.filter(
|
const caught = p.balls.filter(
|
||||||
(b) => !(tun.length && isHidden(tun, b.s))
|
(b) => !(tun.length && isHidden(tun, b.s))
|
||||||
&& Math.hypot(b.x - src.x, b.y - src.y) <= T.EXPLOSION_RADIUS
|
&& Math.hypot(b.x - src.x, b.y - src.y) <= T.EXPLOSION_RADIUS
|
||||||
);
|
);
|
||||||
if (!caught.length) continue;
|
if (!caught.length) continue;
|
||||||
const ids = new Set(caught.map((b) => b.id));
|
const ids = new Set(caught.map((b) => b.id));
|
||||||
// splice in place: callers hold references to state.balls across popRun
|
// splice in place: callers hold references to p.balls across popRun
|
||||||
for (let i = state.balls.length - 1; i >= 0; i--) {
|
for (let i = p.balls.length - 1; i >= 0; i--) {
|
||||||
if (ids.has(state.balls[i].id)) state.balls.splice(i, 1);
|
if (ids.has(p.balls[i].id)) p.balls.splice(i, 1);
|
||||||
}
|
}
|
||||||
const score = caught.length * T.SCORE_BALL * Math.max(1, state.combo);
|
const score = caught.length * T.SCORE_BALL * Math.max(1, state.combo);
|
||||||
state.score += score;
|
state.score += score;
|
||||||
events.push({ type: 'explosion', ids: [...ids], score, x: src.x, y: src.y });
|
events.push({ type: 'explosion', pathIdx: p.idx, ids: [...ids], score, x: src.x, y: src.y });
|
||||||
for (const b of caught) {
|
for (const b of caught) {
|
||||||
if (b.power === 'explosion') queue.push(b);
|
if (b.power === 'explosion') queue.push(b);
|
||||||
else if (b.power) applyPower(state, b, events);
|
else if (b.power) applyPower(state, p, b, events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recolorShooter(state, events) {
|
function recolorShooter(state, events) {
|
||||||
if (!state.balls.length) return;
|
|
||||||
const present = colorsPresent(state);
|
const present = colorsPresent(state);
|
||||||
|
if (!present.size) return;
|
||||||
for (const slot of ['current', 'next']) {
|
for (const slot of ['current', 'next']) {
|
||||||
if (!present.has(state[slot])) {
|
if (!present.has(state[slot])) {
|
||||||
state[slot] = pickPresent(state, present);
|
state[slot] = pickPresent(state, present);
|
||||||
|
|
@ -630,13 +698,13 @@ export function swapBalls(state) {
|
||||||
state.next = t;
|
state.next = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wedge a fired ball into the chain at hitIdx. side: +1 in front of the hit
|
// Wedge a fired ball into path p's chain at hitIdx. side: +1 in front of the
|
||||||
// ball (higher s), -1 behind. The front portion is shoved toward the hole —
|
// hit ball (higher s), -1 behind. The front portion is shoved toward the
|
||||||
// shoves can slam segments together (clank + junction match) and can lose the
|
// hole — shoves can slam segments together (clank + junction match) and can
|
||||||
// level by pushing the front ball into the pit.
|
// lose the level by pushing the front ball into the pit.
|
||||||
export function insertBall(state, color, hitIdx, side, events) {
|
export function insertBall(state, p, color, hitIdx, side, events) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
const balls = state.balls;
|
const balls = p.balls;
|
||||||
const hit = balls[hitIdx];
|
const hit = balls[hitIdx];
|
||||||
let insertIdx, s, push = true;
|
let insertIdx, s, push = true;
|
||||||
|
|
||||||
|
|
@ -670,20 +738,20 @@ export function insertBall(state, color, hitIdx, side, events) {
|
||||||
balls[i].s = minS;
|
balls[i].s = minS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncPositions(state);
|
syncPositions(p);
|
||||||
events.push({ type: 'inserted', id: ball.id, idx: insertIdx, x: ball.x, y: ball.y });
|
events.push({ type: 'inserted', pathIdx: p.idx, id: ball.id, idx: insertIdx, x: ball.x, y: ball.y });
|
||||||
|
|
||||||
// shove-closed gaps
|
// shove-closed gaps
|
||||||
for (const frontId of prevGaps) {
|
for (const frontId of prevGaps) {
|
||||||
const fi = balls.findIndex((b) => b.id === frontId);
|
const fi = balls.findIndex((b) => b.id === frontId);
|
||||||
if (fi < 0 || fi + 1 >= balls.length) continue;
|
if (fi < 0 || fi + 1 >= balls.length) continue;
|
||||||
if (balls[fi].s - balls[fi + 1].s > T.BALL_SPACING + T.GAP_EPS) continue;
|
if (balls[fi].s - balls[fi + 1].s > T.BALL_SPACING + T.GAP_EPS) continue;
|
||||||
events.push({ type: 'clank', x: balls[fi].x, y: balls[fi].y });
|
events.push({ type: 'clank', pathIdx: p.idx, x: balls[fi].x, y: balls[fi].y });
|
||||||
if (balls[fi].color === balls[fi + 1].color) {
|
if (balls[fi].color === balls[fi + 1].color) {
|
||||||
const run = findRun(balls, fi);
|
const run = findRun(balls, fi);
|
||||||
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
||||||
state.combo += 1;
|
state.combo += 1;
|
||||||
popRun(state, run.lo, run.hi, 'chain', events);
|
popRun(state, p, run.lo, run.hi, 'chain', events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -694,7 +762,7 @@ export function insertBall(state, color, hitIdx, side, events) {
|
||||||
const run = findRun(balls, idx);
|
const run = findRun(balls, idx);
|
||||||
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
||||||
state.combo = 1;
|
state.combo = 1;
|
||||||
popRun(state, run.lo, run.hi, 'shot', events);
|
popRun(state, p, run.lo, run.hi, 'shot', events);
|
||||||
} else {
|
} else {
|
||||||
state.combo = 0;
|
state.combo = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -702,32 +770,39 @@ export function insertBall(state, color, hitIdx, side, events) {
|
||||||
checkLose(state, events);
|
checkLose(state, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Steps every fired ball, checking every path's chain for the nearest hit —
|
||||||
|
// this is the entire "aiming" story for multi-path levels: a shot lands on
|
||||||
|
// whichever ball it physically reaches first, regardless of which path that
|
||||||
|
// ball is on.
|
||||||
function stepFlights(state, dtMs, events) {
|
function stepFlights(state, dtMs, events) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
const tun = state.tunnels ?? [];
|
|
||||||
for (let f = state.flights.length - 1; f >= 0; f--) {
|
for (let f = state.flights.length - 1; f >= 0; f--) {
|
||||||
const fl = state.flights[f];
|
const fl = state.flights[f];
|
||||||
const dist = fl.speed * (dtMs / 1000);
|
const dist = fl.speed * (dtMs / 1000);
|
||||||
const steps = Math.max(1, Math.ceil(dist / T.BALL_RADIUS));
|
const steps = Math.max(1, Math.ceil(dist / T.BALL_RADIUS));
|
||||||
const stepLen = dist / steps;
|
const stepLen = dist / steps;
|
||||||
|
let hitPath = null;
|
||||||
let hitIdx = -1;
|
let hitIdx = -1;
|
||||||
for (let k = 0; k < steps && hitIdx < 0; k++) {
|
for (let k = 0; k < steps && hitIdx < 0; k++) {
|
||||||
fl.x += fl.dx * stepLen;
|
fl.x += fl.dx * stepLen;
|
||||||
fl.y += fl.dy * stepLen;
|
fl.y += fl.dy * stepLen;
|
||||||
let best = Infinity;
|
let best = Infinity;
|
||||||
for (let i = 0; i < state.balls.length; i++) {
|
for (const p of state.paths) {
|
||||||
const b = state.balls[i];
|
const tun = p.tunnels;
|
||||||
|
for (let i = 0; i < p.balls.length; i++) {
|
||||||
|
const b = p.balls[i];
|
||||||
if (tun.length && isHidden(tun, b.s)) continue; // underground: shots pass over
|
if (tun.length && isHidden(tun, b.s)) continue; // underground: shots pass over
|
||||||
const d = Math.hypot(fl.x - b.x, fl.y - b.y);
|
const d = Math.hypot(fl.x - b.x, fl.y - b.y);
|
||||||
if (d < T.BALL_SPACING * T.HIT_PAD && d < best) { best = d; hitIdx = i; }
|
if (d < T.BALL_SPACING * T.HIT_PAD && d < best) { best = d; hitPath = p; hitIdx = i; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hitIdx >= 0) {
|
if (hitIdx >= 0) {
|
||||||
state.flights.splice(f, 1);
|
state.flights.splice(f, 1);
|
||||||
const b = state.balls[hitIdx];
|
const b = hitPath.balls[hitIdx];
|
||||||
const p = state.path.pointAt(b.s);
|
const pt = hitPath.path.pointAt(b.s);
|
||||||
const side = ((fl.x - b.x) * p.tx + (fl.y - b.y) * p.ty) >= 0 ? 1 : -1;
|
const side = ((fl.x - b.x) * pt.tx + (fl.y - b.y) * pt.ty) >= 0 ? 1 : -1;
|
||||||
insertBall(state, fl.color, hitIdx, side, events);
|
insertBall(state, hitPath, fl.color, hitIdx, side, events);
|
||||||
} else if (fl.x < -T.BOUNDS_PAD || fl.x > T.BOUNDS_W + T.BOUNDS_PAD
|
} else if (fl.x < -T.BOUNDS_PAD || fl.x > T.BOUNDS_W + T.BOUNDS_PAD
|
||||||
|| fl.y < -T.BOUNDS_PAD || fl.y > T.BOUNDS_H + T.BOUNDS_PAD) {
|
|| fl.y < -T.BOUNDS_PAD || fl.y > T.BOUNDS_H + T.BOUNDS_PAD) {
|
||||||
state.flights.splice(f, 1);
|
state.flights.splice(f, 1);
|
||||||
|
|
@ -736,20 +811,23 @@ function stepFlights(state, dtMs, events) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aiming helper for the laser sight: first chain hit along a ray from the frog.
|
// Aiming helper for the laser sight: first chain hit along a ray from the
|
||||||
|
// frog, across every path.
|
||||||
export function rayHit(state, angle) {
|
export function rayHit(state, angle) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
const tun = state.tunnels ?? [];
|
|
||||||
const dx = Math.cos(angle), dy = Math.sin(angle);
|
const dx = Math.cos(angle), dy = Math.sin(angle);
|
||||||
const max = Math.hypot(T.BOUNDS_W, T.BOUNDS_H);
|
const max = Math.hypot(T.BOUNDS_W, T.BOUNDS_H);
|
||||||
const stepLen = T.BALL_RADIUS / 2;
|
const stepLen = T.BALL_RADIUS / 2;
|
||||||
let x = state.frog.x + dx * T.FROG_MUZZLE;
|
let x = state.frog.x + dx * T.FROG_MUZZLE;
|
||||||
let y = state.frog.y + dy * T.FROG_MUZZLE;
|
let y = state.frog.y + dy * T.FROG_MUZZLE;
|
||||||
for (let d = 0; d < max; d += stepLen) {
|
for (let d = 0; d < max; d += stepLen) {
|
||||||
for (const b of state.balls) {
|
for (const p of state.paths) {
|
||||||
|
const tun = p.tunnels;
|
||||||
|
for (const b of p.balls) {
|
||||||
if (tun.length && isHidden(tun, b.s)) continue;
|
if (tun.length && isHidden(tun, b.s)) continue;
|
||||||
if (Math.hypot(x - b.x, y - b.y) < T.BALL_SPACING * T.HIT_PAD) return { x, y, hit: true };
|
if (Math.hypot(x - b.x, y - b.y) < T.BALL_SPACING * T.HIT_PAD) return { x, y, hit: true };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
x += dx * stepLen;
|
x += dx * stepLen;
|
||||||
y += dy * stepLen;
|
y += dy * stepLen;
|
||||||
}
|
}
|
||||||
|
|
@ -758,20 +836,27 @@ export function rayHit(state, angle) {
|
||||||
|
|
||||||
// ── Win / lose ────────────────────────────────────────────────────────────────
|
// ── Win / lose ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Any one path's chain reaching its own pit loses the whole level.
|
||||||
function checkLose(state, events) {
|
function checkLose(state, events) {
|
||||||
if (state.status === 'won' || state.status === 'lost') return;
|
if (state.status === 'won' || state.status === 'lost') return;
|
||||||
const front = state.balls[0];
|
for (const p of state.paths) {
|
||||||
if (front && front.s >= state.path.length - TUNING.HOLE_GRACE) {
|
const front = p.balls[0];
|
||||||
|
if (front && front.s >= p.path.length - TUNING.HOLE_GRACE) {
|
||||||
state.status = 'lost';
|
state.status = 'lost';
|
||||||
events.push({ type: 'lost' });
|
events.push({ type: 'lost' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every path's chain must be gone, plus the shared shot queue empty.
|
||||||
function checkWin(state, events) {
|
function checkWin(state, events) {
|
||||||
if (state.status !== 'playing') return;
|
if (state.status !== 'playing') return;
|
||||||
if (state.spawned >= state.quota && !state.balls.length && !state.flights.length) {
|
const allCleared = state.paths.every((p) => p.spawned >= p.quota && !p.balls.length);
|
||||||
|
if (allCleared && !state.flights.length) {
|
||||||
const T = TUNING;
|
const T = TUNING;
|
||||||
const parMs = state.quota * T.TIME_PAR_MS_PER_BALL;
|
const totalQuota = state.paths.reduce((a, p) => a + p.quota, 0);
|
||||||
|
const parMs = totalQuota * T.TIME_PAR_MS_PER_BALL;
|
||||||
const timeBonus = Math.max(0, Math.ceil((parMs - state.elapsedMs) / 1000)) * T.TIME_BONUS_PER_SEC;
|
const timeBonus = Math.max(0, Math.ceil((parMs - state.elapsedMs) / 1000)) * T.TIME_BONUS_PER_SEC;
|
||||||
state.score += timeBonus;
|
state.score += timeBonus;
|
||||||
state.status = 'won';
|
state.status = 'won';
|
||||||
|
|
@ -786,9 +871,10 @@ export function step(state, dtMs) {
|
||||||
if (state.status === 'won' || state.status === 'lost') return events;
|
if (state.status === 'won' || state.status === 'lost') return events;
|
||||||
const dt = Math.min(dtMs, TUNING.MAX_STEP_MS);
|
const dt = Math.min(dtMs, TUNING.MAX_STEP_MS);
|
||||||
state.elapsedMs += dt;
|
state.elapsedMs += dt;
|
||||||
moveSegments(state, dt, events);
|
for (const p of state.paths) moveSegments(state, p, dt, events);
|
||||||
spawnBalls(state, events);
|
for (const p of state.paths) spawnBalls(state, p, events);
|
||||||
syncPositions(state);
|
checkReady(state, events);
|
||||||
|
for (const p of state.paths) syncPositions(p);
|
||||||
checkLose(state, events);
|
checkLose(state, events);
|
||||||
if (state.status === 'lost') return events;
|
if (state.status === 'lost') return events;
|
||||||
stepFlights(state, dt, events);
|
stepFlights(state, dt, events);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export default class PreloadScene extends Phaser.Scene {
|
||||||
this.load.json('slots-artwork', 'data/slots-artwork.json');
|
this.load.json('slots-artwork', 'data/slots-artwork.json');
|
||||||
this.load.json('blockfighter', 'data/blockfighter.json');
|
this.load.json('blockfighter', 'data/blockfighter.json');
|
||||||
this.load.json('jewelquest', 'data/jewelquest.json');
|
this.load.json('jewelquest', 'data/jewelquest.json');
|
||||||
this.load.json('zuma', 'data/zuma.json');
|
this.load.json('zuma-levels', 'assets/gamedata/zuma/levels.json');
|
||||||
this.load.json('dotlink', 'data/dotlink.json');
|
this.load.json('dotlink', 'data/dotlink.json');
|
||||||
this.load.json('katamino', 'data/katamino.json');
|
this.load.json('katamino', 'data/katamino.json');
|
||||||
this.load.json('bookwork', 'data/bookwork.json');
|
this.load.json('bookwork', 'data/bookwork.json');
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@
|
||||||
// refuses to write a bank the game would consider unplayable.
|
// refuses to write a bank the game would consider unplayable.
|
||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
// node tools/genZuma.js [outFile]
|
// node tools/genZuma.js [outDir]
|
||||||
|
//
|
||||||
|
// Writes one file per level (assets/gamedata/zuma/level-NNN.json) plus a
|
||||||
|
// lightweight levels.json manifest, matching Peggle/Super Kart/Goo Tower's
|
||||||
|
// layout — the level select screen only ever needs level/name/file, so the
|
||||||
|
// manifest stays cheap to eager-load while the full per-level payload (path
|
||||||
|
// geometry, tunnels, etc.) is fetched on demand when that level is played.
|
||||||
//
|
//
|
||||||
// Geometry note: marbles are BALL_SPACING apart and turns must stay wider than
|
// Geometry note: marbles are BALL_SPACING apart and turns must stay wider than
|
||||||
// BALL_RADIUS * 1.7, so shapes are emitted as exact straights and circular arcs
|
// BALL_RADIUS * 1.7, so shapes are emitted as exact straights and circular arcs
|
||||||
|
|
@ -21,9 +27,9 @@ import { fileURLToPath } from 'node:url';
|
||||||
import { validateLevel, buildPath, TUNING } from '../src/games/zuma/ZumaLogic.js';
|
import { validateLevel, buildPath, TUNING } from '../src/games/zuma/ZumaLogic.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const OUT_FILE = process.argv[2]
|
const OUT_DIR = process.argv[2]
|
||||||
? path.resolve(process.argv[2])
|
? path.resolve(process.argv[2])
|
||||||
: path.join(__dirname, '../data/zuma.json');
|
: path.join(__dirname, '../assets/gamedata/zuma');
|
||||||
|
|
||||||
const rad = (deg) => (deg * Math.PI) / 180;
|
const rad = (deg) => (deg * Math.PI) / 180;
|
||||||
const rp = (pts) => pts.map(([x, y]) => [Math.round(x), Math.round(y)]);
|
const rp = (pts) => pts.map(([x, y]) => [Math.round(x), Math.round(y)]);
|
||||||
|
|
@ -228,11 +234,14 @@ for (const [level, name, shape, colors, quota, introBalls, pushSpeed, powerUpRat
|
||||||
const len = buildPath(points).length;
|
const len = buildPath(points).length;
|
||||||
const tunnels = (tunFrac ?? []).map(([a, b]) => [Math.round(len * a), Math.round(len * b)]);
|
const tunnels = (tunFrac ?? []).map(([a, b]) => [Math.round(len * a), Math.round(len * b)]);
|
||||||
const def = {
|
const def = {
|
||||||
level, name, shape, points, frog, tunnels, colors, quota, introBalls, pushSpeed, powerUpRate,
|
level, name, frog, colors,
|
||||||
background: BACKGROUNDS[(level - 1) % BACKGROUNDS.length],
|
background: BACKGROUNDS[(level - 1) % BACKGROUNDS.length],
|
||||||
seed: 1000 + level * 7919,
|
seed: 1000 + level * 7919,
|
||||||
starScores: starScores(quota, pushSpeed),
|
starScores: starScores(quota, pushSpeed),
|
||||||
|
paths: [{ shape, points, tunnels, quota, introBalls, pushSpeed, powerUpRate }],
|
||||||
};
|
};
|
||||||
|
// Every table row is still exactly one path — validateLevel/genZuma's own
|
||||||
|
// lint reports below read that one entry's stats (see ZumaLogic.validateLevel).
|
||||||
const { errs, length, minFrog, minRadius, hiddenFrac } = validateLevel(def);
|
const { errs, length, minFrog, minRadius, hiddenFrac } = validateLevel(def);
|
||||||
const cap = Math.floor(length / (TUNING.BALL_SPACING * 1.6));
|
const cap = Math.floor(length / (TUNING.BALL_SPACING * 1.6));
|
||||||
if (errs.length) {
|
if (errs.length) {
|
||||||
|
|
@ -246,13 +255,18 @@ for (const [level, name, shape, colors, quota, introBalls, pushSpeed, powerUpRat
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bad) {
|
if (bad) {
|
||||||
console.error(`\n${bad} invalid level(s) — not writing ${OUT_FILE}`);
|
console.error(`\n${bad} invalid level(s) — not writing ${OUT_DIR}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(OUT_FILE, JSON.stringify({
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
count: levels.length,
|
const manifest = { version: 1, levels: [] };
|
||||||
levels,
|
for (const def of levels) {
|
||||||
}, null, 1));
|
const file = `level-${String(def.level).padStart(3, '0')}.json`;
|
||||||
console.log(`\nWrote ${levels.length} levels to ${OUT_FILE}`);
|
fs.writeFileSync(path.join(OUT_DIR, file), `${JSON.stringify(def, null, 1)}\n`);
|
||||||
|
manifest.levels.push({ level: def.level, name: def.name, file });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(OUT_DIR, 'levels.json'), `${JSON.stringify(manifest, null, 1)}\n`);
|
||||||
|
|
||||||
|
console.log(`\nWrote ${levels.length} levels + manifest to ${OUT_DIR}`);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@
|
||||||
// 7. Win/lose state machine, recolor, last-call spawns.
|
// 7. Win/lose state machine, recolor, last-call spawns.
|
||||||
// 8. Determinism (seeded replay).
|
// 8. Determinism (seeded replay).
|
||||||
// 9. Tunnels: submerged chain is inert to flights, the laser and explosions.
|
// 9. Tunnels: submerged chain is inert to flights, the laser and explosions.
|
||||||
// 10. Level bank lint (data/zuma.json geometry + parameters).
|
// 10. Multi-path levels: independent spawners, shared shooter color pool,
|
||||||
// 11. Aimbot soak: every banked level is winnable, and the star curve sits
|
// physics-based shot targeting, either-path-loses, every-path-to-win.
|
||||||
|
// 11. Level bank lint (assets/gamedata/zuma/ manifest + per-level geometry/parameters).
|
||||||
|
// 12. Aimbot soak: every banked level is winnable, and the star curve sits
|
||||||
// above what mechanical play achieves.
|
// above what mechanical play achieves.
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
@ -48,28 +50,48 @@ const CURVY = [[0, 200], [400, 800], [800, 200], [1200, 800], [1600, 200]];
|
||||||
// into gapped ones and the insertion tests start asserting nonsense.
|
// into gapped ones and the insertion tests start asserting nonsense.
|
||||||
const SP = T.BALL_SPACING;
|
const SP = T.BALL_SPACING;
|
||||||
|
|
||||||
|
// Every existing single-path test passes flat overrides (points/tunnels/quota/
|
||||||
|
// introBalls/pushSpeed/powerUpRate) exactly as before genZuma.js's `paths`
|
||||||
|
// array existed — mkDef wraps them into a one-element paths array so none of
|
||||||
|
// those call sites need to change.
|
||||||
function mkDef(over = {}) {
|
function mkDef(over = {}) {
|
||||||
|
const { points, tunnels, quota, introBalls, pushSpeed, powerUpRate, shape, paths, ...rest } = over;
|
||||||
return {
|
return {
|
||||||
level: 1, name: 'Test', shape: 'line',
|
level: 1, name: 'Test',
|
||||||
points: STRAIGHT, frog: [800, 900],
|
frog: [800, 900], colors: 4, seed: 42, starScores: [100, 200, 300],
|
||||||
colors: 4, quota: 10, introBalls: 2,
|
paths: paths ?? [{
|
||||||
pushSpeed: 100, powerUpRate: 0, seed: 42,
|
shape: shape ?? 'line',
|
||||||
starScores: [100, 200, 300],
|
points: points ?? STRAIGHT,
|
||||||
...over,
|
tunnels,
|
||||||
|
quota: quota ?? 10,
|
||||||
|
introBalls: introBalls ?? 2,
|
||||||
|
pushSpeed: pushSpeed ?? 100,
|
||||||
|
powerUpRate: powerUpRate ?? 0,
|
||||||
|
}],
|
||||||
|
...rest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `over` used to be Object.assign'd straight onto state (back when `spawned`
|
||||||
|
// and `balls` lived at the top level); both now live on paths[0], so they're
|
||||||
|
// special-cased here and every existing call site keeps working unchanged.
|
||||||
function mkState(defOver = {}, over = {}) {
|
function mkState(defOver = {}, over = {}) {
|
||||||
const st = createLevel(mkDef(defOver));
|
const st = createLevel(mkDef(defOver));
|
||||||
Object.assign(st, over);
|
const { spawned, balls, ...rest } = over;
|
||||||
|
Object.assign(st, rest);
|
||||||
|
if (spawned !== undefined) st.paths[0].spawned = spawned;
|
||||||
|
if (balls !== undefined) st.paths[0].balls = balls;
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Aimbot ───────────────────────────────────────────────────────────────────
|
// ── Aimbot ───────────────────────────────────────────────────────────────────
|
||||||
// Deliberately mechanical: it never sets up a chain, never banks a shot off a
|
// Deliberately mechanical: it never sets up a chain, never banks a shot off a
|
||||||
// gap, and never holds a colour. Whatever it clears, a player clears.
|
// gap, and never holds a colour. Whatever it clears, a player clears. Scans
|
||||||
|
// every path for a target — the multi-path story is "physics decides which
|
||||||
|
// chain a shot lands on," so the bot has to consider all of them too.
|
||||||
|
|
||||||
// Walks the shot ray like rayHit does, but reports which ball it lands on.
|
// Walks the shot ray like rayHit does, but reports which ball (and which path)
|
||||||
|
// it lands on.
|
||||||
function firstHit(st, angle) {
|
function firstHit(st, angle) {
|
||||||
const dx = Math.cos(angle), dy = Math.sin(angle);
|
const dx = Math.cos(angle), dy = Math.sin(angle);
|
||||||
const stepLen = T.BALL_RADIUS / 2;
|
const stepLen = T.BALL_RADIUS / 2;
|
||||||
|
|
@ -77,28 +99,31 @@ function firstHit(st, angle) {
|
||||||
let x = st.frog.x + dx * T.FROG_MUZZLE;
|
let x = st.frog.x + dx * T.FROG_MUZZLE;
|
||||||
let y = st.frog.y + dy * T.FROG_MUZZLE;
|
let y = st.frog.y + dy * T.FROG_MUZZLE;
|
||||||
for (let d = 0; d < max; d += stepLen) {
|
for (let d = 0; d < max; d += stepLen) {
|
||||||
for (const b of st.balls) {
|
for (const p of st.paths) {
|
||||||
if (isHidden(st.tunnels, b.s)) continue; // submerged: the shot flies over
|
for (const b of p.balls) {
|
||||||
if (Math.hypot(x - b.x, y - b.y) < T.BALL_SPACING * T.HIT_PAD) return b;
|
if (isHidden(p.tunnels, b.s)) continue; // submerged: the shot flies over
|
||||||
|
if (Math.hypot(x - b.x, y - b.y) < T.BALL_SPACING * T.HIT_PAD) return { ball: b, path: p };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
x += dx * stepLen; y += dy * stepLen;
|
x += dx * stepLen; y += dy * stepLen;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rank every reachable ball: landing between two of the held colour beats
|
// Rank every reachable ball across every path: landing between two of the
|
||||||
// landing beside one, which beats landing on a bare match.
|
// held colour beats landing beside one, which beats landing on a bare match.
|
||||||
function chooseShot(st) {
|
function chooseShot(st) {
|
||||||
let best = null;
|
let best = null;
|
||||||
for (const target of st.balls) {
|
for (const p of st.paths) {
|
||||||
if (isHidden(st.tunnels, target.s)) continue;
|
for (const target of p.balls) {
|
||||||
|
if (isHidden(p.tunnels, target.s)) continue;
|
||||||
const angle = Math.atan2(target.y - st.frog.y, target.x - st.frog.x);
|
const angle = Math.atan2(target.y - st.frog.y, target.x - st.frog.x);
|
||||||
const hit = firstHit(st, angle);
|
const hit = firstHit(st, angle);
|
||||||
if (!hit) continue;
|
if (!hit) continue;
|
||||||
const i = st.balls.indexOf(hit);
|
const i = hit.path.balls.indexOf(hit.ball);
|
||||||
const prev = st.balls[i - 1], next = st.balls[i + 1];
|
const prev = hit.path.balls[i - 1], next = hit.path.balls[i + 1];
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (hit.color === st.current) {
|
if (hit.ball.color === st.current) {
|
||||||
score += 5;
|
score += 5;
|
||||||
if (prev?.color === st.current) score += 10;
|
if (prev?.color === st.current) score += 10;
|
||||||
if (next?.color === st.current) score += 10;
|
if (next?.color === st.current) score += 10;
|
||||||
|
|
@ -106,6 +131,7 @@ function chooseShot(st) {
|
||||||
if (prev?.color === st.current && next?.color === st.current) score += 8;
|
if (prev?.color === st.current && next?.color === st.current) score += 8;
|
||||||
if (score > 0 && (!best || score > best.score)) best = { angle, score };
|
if (score > 0 && (!best || score > best.score)) best = { angle, score };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,14 +152,16 @@ function aimbotPlay(def, seed) {
|
||||||
return { status: st.status, score: st.score };
|
return { status: st.status, score: st.score };
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec: [{ color, s, power? }] front-first (descending s)
|
// spec: [{ color, s, power? }] front-first (descending s). Defaults to
|
||||||
function mkChain(st, spec) {
|
// paths[0] — every pre-multi-path fixture is single-path.
|
||||||
st.balls = spec.map((b) => ({
|
function mkChain(st, spec, pathIdx = 0) {
|
||||||
|
const p = st.paths[pathIdx];
|
||||||
|
p.balls = spec.map((b) => ({
|
||||||
id: st.nextId++, color: b.color, power: b.power ?? null, s: b.s, x: 0, y: 0,
|
id: st.nextId++, color: b.color, power: b.power ?? null, s: b.s, x: 0, y: 0,
|
||||||
}));
|
}));
|
||||||
for (const b of st.balls) {
|
for (const b of p.balls) {
|
||||||
const p = st.path.pointAt(b.s);
|
const pt = p.path.pointAt(b.s);
|
||||||
b.x = p.x; b.y = p.y;
|
b.x = pt.x; b.y = pt.y;
|
||||||
}
|
}
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
@ -178,18 +206,19 @@ console.log('\n— Advance & spawning —');
|
||||||
let introSpawned = -1;
|
let introSpawned = -1;
|
||||||
for (let i = 0; i < 4000 && st.status === 'intro'; i++) {
|
for (let i = 0; i < 4000 && st.status === 'intro'; i++) {
|
||||||
step(st, 16);
|
step(st, 16);
|
||||||
if (st.status !== 'intro') introSpawned = st.spawned;
|
if (st.status !== 'intro') introSpawned = st.paths[0].spawned;
|
||||||
}
|
}
|
||||||
check('intro → playing at introBalls', st.status === 'playing' && introSpawned >= 3, `spawned ${introSpawned}`);
|
check('intro → playing at introBalls', st.status === 'playing' && introSpawned >= 3, `spawned ${introSpawned}`);
|
||||||
for (let i = 0; i < 4000 && st.spawned < 8; i++) step(st, 16);
|
for (let i = 0; i < 4000 && st.paths[0].spawned < 8; i++) step(st, 16);
|
||||||
check('spawn stops at quota', st.spawned === 8 && st.balls.length === 8);
|
check('spawn stops at quota', st.paths[0].spawned === 8 && st.paths[0].balls.length === 8);
|
||||||
check('spacing invariant after spawning', spacingOk(st.balls));
|
check('spacing invariant after spawning', spacingOk(st.paths[0].balls));
|
||||||
|
|
||||||
const st2 = mkState({}, { status: 'playing', spawned: 10 });
|
const st2 = mkState({}, { status: 'playing', spawned: 10 });
|
||||||
mkChain(st2, [{ color: 0, s: 600 + 2 * SP }, { color: 1, s: 600 + SP }, { color: 2, s: 600 }]);
|
mkChain(st2, [{ color: 0, s: 600 + 2 * SP }, { color: 1, s: 600 + SP }, { color: 2, s: 600 }]);
|
||||||
for (let i = 0; i < 20; i++) step(st2, 50); // 1s at pushSpeed 100
|
for (let i = 0; i < 20; i++) step(st2, 50); // 1s at pushSpeed 100
|
||||||
check('single segment drives at pushSpeed', near(st2.balls[0].s, 600 + 2 * SP + 100, 1), `got ${st2.balls[0].s.toFixed(1)}`);
|
check('single segment drives at pushSpeed', near(st2.paths[0].balls[0].s, 600 + 2 * SP + 100, 1),
|
||||||
check('spacing preserved while driving', spacingOk(st2.balls));
|
`got ${st2.paths[0].balls[0].s.toFixed(1)}`);
|
||||||
|
check('spacing preserved while driving', spacingOk(st2.paths[0].balls));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Insertion ─────────────────────────────────────────────────────────────
|
// ── 3. Insertion ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -202,37 +231,39 @@ console.log('\n— Insertion —');
|
||||||
);
|
);
|
||||||
|
|
||||||
let st = base(); let ev = [];
|
let st = base(); let ev = [];
|
||||||
insertBall(st, 3, 2, +1, ev);
|
insertBall(st, st.paths[0], 3, 2, +1, ev);
|
||||||
|
const balls = st.paths[0].balls;
|
||||||
check('front insert lands in front of hit ball',
|
check('front insert lands in front of hit ball',
|
||||||
st.balls[2].color === 3 && near(st.balls[2].s, 600 - SP, 0.01) && near(st.balls[3].s, 600 - 2 * SP, 0.01));
|
balls[2].color === 3 && near(balls[2].s, 600 - SP, 0.01) && near(balls[3].s, 600 - 2 * SP, 0.01));
|
||||||
check('front insert shoves balls ahead', near(st.balls[0].s, 600 + SP, 0.01) && near(st.balls[1].s, 600, 0.01));
|
check('front insert shoves balls ahead', near(balls[0].s, 600 + SP, 0.01) && near(balls[1].s, 600, 0.01));
|
||||||
check('front insert keeps spacing', spacingOk(st.balls) && st.balls.length === 6);
|
check('front insert keeps spacing', spacingOk(balls) && balls.length === 6);
|
||||||
check('non-matching insert resets combo, no pop', st.combo === 0 && !ev.some((e) => e.type === 'pop'));
|
check('non-matching insert resets combo, no pop', st.combo === 0 && !ev.some((e) => e.type === 'pop'));
|
||||||
|
|
||||||
st = base(); ev = [];
|
st = base(); ev = [];
|
||||||
insertBall(st, 3, 2, -1, ev);
|
insertBall(st, st.paths[0], 3, 2, -1, ev);
|
||||||
check('behind insert wedges after hit ball',
|
check('behind insert wedges after hit ball',
|
||||||
st.balls[3].color === 3 && near(st.balls[3].s, 600 - 2 * SP, 0.01) && near(st.balls[2].s, 600 - SP, 0.01));
|
st.paths[0].balls[3].color === 3 && near(st.paths[0].balls[3].s, 600 - 2 * SP, 0.01)
|
||||||
check('behind insert keeps spacing', spacingOk(st.balls));
|
&& near(st.paths[0].balls[2].s, 600 - SP, 0.01));
|
||||||
|
check('behind insert keeps spacing', spacingOk(st.paths[0].balls));
|
||||||
|
|
||||||
st = base(); ev = [];
|
st = base(); ev = [];
|
||||||
insertBall(st, 3, 4, -1, ev);
|
insertBall(st, st.paths[0], 3, 4, -1, ev);
|
||||||
check('tail attach adds at rear without shoving',
|
check('tail attach adds at rear without shoving',
|
||||||
near(st.balls[5].s, 600 - 5 * SP, 0.01) && near(st.balls[0].s, 600, 0.01));
|
near(st.paths[0].balls[5].s, 600 - 5 * SP, 0.01) && near(st.paths[0].balls[0].s, 600, 0.01));
|
||||||
|
|
||||||
// shove closes a gap → clank, no pop (junction colors differ)
|
// shove closes a gap → clank, no pop (junction colors differ)
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 900 }, { color: 1, s: 900 - SP }, { color: 2, s: 900 - 2.5 * SP },
|
[{ color: 0, s: 900 }, { color: 1, s: 900 - SP }, { color: 2, s: 900 - 2.5 * SP },
|
||||||
{ color: 3, s: 900 - 3.5 * SP }]);
|
{ color: 3, s: 900 - 3.5 * SP }]);
|
||||||
ev = [];
|
ev = [];
|
||||||
insertBall(st, 3, 2, +1, ev);
|
insertBall(st, st.paths[0], 3, 2, +1, ev);
|
||||||
check('shove-merge emits clank', ev.some((e) => e.type === 'clank'));
|
check('shove-merge emits clank', ev.some((e) => e.type === 'clank'));
|
||||||
check('shove-merge joins segments', segmentsOf(st.balls).length === 1 && spacingOk(st.balls));
|
check('shove-merge joins segments', segmentsOf(st.paths[0].balls).length === 1 && spacingOk(st.paths[0].balls));
|
||||||
check('shove-merge without matching junction does not pop', !ev.some((e) => e.type === 'pop'));
|
check('shove-merge without matching junction does not pop', !ev.some((e) => e.type === 'pop'));
|
||||||
|
|
||||||
// laser-sight ray helper
|
// laser-sight ray helper
|
||||||
st = mkChain(mkState({}, { status: 'playing' }), [{ color: 0, s: 600 }]);
|
st = mkChain(mkState({}, { status: 'playing' }), [{ color: 0, s: 600 }]);
|
||||||
const ball = st.balls[0];
|
const ball = st.paths[0].balls[0];
|
||||||
const ray = rayHit(st, Math.atan2(ball.y - st.frog.y, ball.x - st.frog.x));
|
const ray = rayHit(st, Math.atan2(ball.y - st.frog.y, ball.x - st.frog.x));
|
||||||
check('rayHit finds first chain ball', ray.hit && Math.hypot(ray.x - ball.x, ray.y - ball.y) < T.BALL_SPACING);
|
check('rayHit finds first chain ball', ray.hit && Math.hypot(ray.x - ball.x, ray.y - ball.y) < T.BALL_SPACING);
|
||||||
}
|
}
|
||||||
|
|
@ -243,27 +274,27 @@ console.log('\n— Matching —');
|
||||||
let st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
let st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 600 }, { color: 0, s: 600 - SP }, { color: 1, s: 600 - 2 * SP }, { color: 1, s: 600 - 3 * SP }]);
|
[{ color: 0, s: 600 }, { color: 0, s: 600 - SP }, { color: 1, s: 600 - 2 * SP }, { color: 1, s: 600 - 3 * SP }]);
|
||||||
let ev = [];
|
let ev = [];
|
||||||
insertBall(st, 0, 1, +1, ev);
|
insertBall(st, st.paths[0], 0, 1, +1, ev);
|
||||||
const pop = ev.find((e) => e.type === 'pop');
|
const pop = ev.find((e) => e.type === 'pop');
|
||||||
check('insert completing 3 pops the run', !!pop && pop.ids.length === 3 && st.balls.length === 2);
|
check('insert completing 3 pops the run', !!pop && pop.ids.length === 3 && st.paths[0].balls.length === 2);
|
||||||
check('3-pop score = 3 × SCORE_BALL', pop?.score === 3 * T.SCORE_BALL && st.score === 3 * T.SCORE_BALL);
|
check('3-pop score = 3 × SCORE_BALL', pop?.score === 3 * T.SCORE_BALL && st.score === 3 * T.SCORE_BALL);
|
||||||
check('shot pop sets combo to 1', pop?.combo === 1);
|
check('shot pop sets combo to 1', pop?.combo === 1);
|
||||||
|
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 600 + 2 * SP }, { color: 0, s: 600 + SP }, { color: 0, s: 600 }, { color: 0, s: 600 - SP }]);
|
[{ color: 0, s: 600 + 2 * SP }, { color: 0, s: 600 + SP }, { color: 0, s: 600 }, { color: 0, s: 600 - SP }]);
|
||||||
ev = [];
|
ev = [];
|
||||||
insertBall(st, 0, 1, -1, ev);
|
insertBall(st, st.paths[0], 0, 1, -1, ev);
|
||||||
const pop5 = ev.find((e) => e.type === 'pop');
|
const pop5 = ev.find((e) => e.type === 'pop');
|
||||||
check('2+2 around insert pops 5', !!pop5 && pop5.ids.length === 5 && st.balls.length === 0);
|
check('2+2 around insert pops 5', !!pop5 && pop5.ids.length === 5 && st.paths[0].balls.length === 0);
|
||||||
check('5-pop score', pop5?.score === 5 * T.SCORE_BALL);
|
check('5-pop score', pop5?.score === 5 * T.SCORE_BALL);
|
||||||
|
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 600 }, { color: 0, s: 600 - 2.5 * SP }, { color: 1, s: 600 - 3.5 * SP }]);
|
[{ color: 0, s: 600 }, { color: 0, s: 600 - 2.5 * SP }, { color: 1, s: 600 - 3.5 * SP }]);
|
||||||
ev = [];
|
ev = [];
|
||||||
insertBall(st, 0, 1, +1, ev);
|
insertBall(st, st.paths[0], 0, 1, +1, ev);
|
||||||
check('runs never cross a gap', !ev.some((e) => e.type === 'pop') && st.balls.length === 4);
|
check('runs never cross a gap', !ev.some((e) => e.type === 'pop') && st.paths[0].balls.length === 4);
|
||||||
|
|
||||||
const run = findRun(st.balls, 1);
|
const run = findRun(st.paths[0].balls, 1);
|
||||||
check('findRun bounded by the gap', run.lo === 1 && run.hi === 2);
|
check('findRun bounded by the gap', run.lo === 1 && run.hi === 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,7 +307,7 @@ console.log('\n— Pull-back & catch-up —');
|
||||||
let popEv = null, clankSeen = false, retreated = false;
|
let popEv = null, clankSeen = false, retreated = false;
|
||||||
for (let i = 0; i < 200 && !popEv; i++) {
|
for (let i = 0; i < 200 && !popEv; i++) {
|
||||||
const ev = step(st, 25);
|
const ev = step(st, 25);
|
||||||
if (st.balls.length && st.balls[0].s < 900 - 1) retreated = true;
|
if (st.paths[0].balls.length && st.paths[0].balls[0].s < 900 - 1) retreated = true;
|
||||||
if (ev.some((e) => e.type === 'clank')) clankSeen = true;
|
if (ev.some((e) => e.type === 'clank')) clankSeen = true;
|
||||||
popEv = ev.find((e) => e.type === 'pop') ?? popEv;
|
popEv = ev.find((e) => e.type === 'pop') ?? popEv;
|
||||||
}
|
}
|
||||||
|
|
@ -285,24 +316,24 @@ console.log('\n— Pull-back & catch-up —');
|
||||||
check('chain pop scores chain bonus', popEv?.cause === 'chain'
|
check('chain pop scores chain bonus', popEv?.cause === 'chain'
|
||||||
&& popEv?.score === 3 * T.SCORE_BALL + T.SCORE_CHAIN_BONUS);
|
&& popEv?.score === 3 * T.SCORE_BALL + T.SCORE_CHAIN_BONUS);
|
||||||
check('chain pop increments combo', popEv?.combo === 1);
|
check('chain pop increments combo', popEv?.combo === 1);
|
||||||
check('survivor remains after chain pop', st.balls.length === 1 && st.balls[0].color === 0);
|
check('survivor remains after chain pop', st.paths[0].balls.length === 1 && st.paths[0].balls[0].color === 0);
|
||||||
|
|
||||||
// non-matching gap → rear catches up, front stays put, clank without pop
|
// non-matching gap → rear catches up, front stays put, clank without pop
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 900 }, { color: 1, s: 900 - SP }, { color: 0, s: 900 - 5 * SP }, { color: 1, s: 900 - 6 * SP }]);
|
[{ color: 0, s: 900 }, { color: 1, s: 900 - SP }, { color: 0, s: 900 - 5 * SP }, { color: 1, s: 900 - 6 * SP }]);
|
||||||
let clankAt = null;
|
let clankAt = null;
|
||||||
for (let i = 0; i < 200 && !clankAt; i++) {
|
for (let i = 0; i < 200 && !clankAt; i++) {
|
||||||
const frontBefore = st.balls[0].s;
|
const frontBefore = st.paths[0].balls[0].s;
|
||||||
const ev = step(st, 25);
|
const ev = step(st, 25);
|
||||||
if (ev.some((e) => e.type === 'clank')) clankAt = { frontBefore, rearFront: st.balls[2].s };
|
if (ev.some((e) => e.type === 'clank')) clankAt = { frontBefore, rearFront: st.paths[0].balls[2].s };
|
||||||
}
|
}
|
||||||
check('non-matching gap: rear catches up to contact', !!clankAt && near(clankAt.rearFront, 900 - 2 * SP, 1.5),
|
check('non-matching gap: rear catches up to contact', !!clankAt && near(clankAt.rearFront, 900 - 2 * SP, 1.5),
|
||||||
clankAt ? `rear front at ${clankAt.rearFront.toFixed(1)}` : 'no clank');
|
clankAt ? `rear front at ${clankAt.rearFront.toFixed(1)}` : 'no clank');
|
||||||
check('non-matching gap: front segment stays put', !!clankAt && near(clankAt.frontBefore, 900, 0.01));
|
check('non-matching gap: front segment stays put', !!clankAt && near(clankAt.frontBefore, 900, 0.01));
|
||||||
check('no pop on non-matching junction', st.balls.length === 4);
|
check('no pop on non-matching junction', st.paths[0].balls.length === 4);
|
||||||
const before = st.balls[0].s;
|
const before = st.paths[0].balls[0].s;
|
||||||
for (let i = 0; i < 8; i++) step(st, 50);
|
for (let i = 0; i < 8; i++) step(st, 50);
|
||||||
check('merged chain resumes driving', st.balls[0].s > before + 30);
|
check('merged chain resumes driving', st.paths[0].balls[0].s > before + 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 6. Power-ups ─────────────────────────────────────────────────────────────
|
// ── 6. Power-ups ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -312,30 +343,30 @@ console.log('\n— Power-ups —');
|
||||||
let st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
let st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 800, power: 'slow' }, { color: 1, s: 800 - 3 * SP }]);
|
[{ color: 0, s: 800, power: 'slow' }, { color: 1, s: 800 - 3 * SP }]);
|
||||||
let ev = [];
|
let ev = [];
|
||||||
popRun(st, 0, 0, 'shot', ev);
|
popRun(st, st.paths[0], 0, 0, 'shot', ev);
|
||||||
check('slow power sets effect timer', st.effects.slowUntil === st.elapsedMs + T.SLOW_MS
|
check('slow power sets effect timer', st.effects.slowUntil === st.elapsedMs + T.SLOW_MS
|
||||||
&& ev.some((e) => e.type === 'powerup' && e.kind === 'slow'));
|
&& ev.some((e) => e.type === 'powerup' && e.kind === 'slow'));
|
||||||
const s0 = st.balls[0].s;
|
const s0 = st.paths[0].balls[0].s;
|
||||||
for (let i = 0; i < 20; i++) step(st, 50);
|
for (let i = 0; i < 20; i++) step(st, 50);
|
||||||
check('slow halves the drive (SLOW_MULT)', near(st.balls[0].s - s0, 100 * T.SLOW_MULT, 1.5),
|
check('slow halves the drive (SLOW_MULT)', near(st.paths[0].balls[0].s - s0, 100 * T.SLOW_MULT, 1.5),
|
||||||
`moved ${(st.balls[0].s - s0).toFixed(1)}`);
|
`moved ${(st.paths[0].balls[0].s - s0).toFixed(1)}`);
|
||||||
|
|
||||||
// reverse: chain rolls backward, then resumes forward when expired
|
// reverse: chain rolls backward, then resumes forward when expired
|
||||||
// (elapsedMs increments before the effect check, so 501 covers exactly 10 ticks)
|
// (elapsedMs increments before the effect check, so 501 covers exactly 10 ticks)
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }), [{ color: 0, s: 800 }]);
|
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }), [{ color: 0, s: 800 }]);
|
||||||
st.effects.reverseUntil = st.elapsedMs + 501;
|
st.effects.reverseUntil = st.elapsedMs + 501;
|
||||||
for (let i = 0; i < 10; i++) step(st, 50);
|
for (let i = 0; i < 10; i++) step(st, 50);
|
||||||
check('reverse rolls the chain backward', near(st.balls[0].s, 800 - T.REVERSE_SPEED * 0.5, 2),
|
check('reverse rolls the chain backward', near(st.paths[0].balls[0].s, 800 - T.REVERSE_SPEED * 0.5, 2),
|
||||||
`at ${st.balls[0].s.toFixed(1)}`);
|
`at ${st.paths[0].balls[0].s.toFixed(1)}`);
|
||||||
for (let i = 0; i < 10; i++) step(st, 50);
|
for (let i = 0; i < 10; i++) step(st, 50);
|
||||||
check('drive resumes after reverse expires', near(st.balls[0].s, 800 - T.REVERSE_SPEED * 0.5 + 100 * 0.5, 2),
|
check('drive resumes after reverse expires', near(st.paths[0].balls[0].s, 800 - T.REVERSE_SPEED * 0.5 + 100 * 0.5, 2),
|
||||||
`at ${st.balls[0].s.toFixed(1)}`);
|
`at ${st.paths[0].balls[0].s.toFixed(1)}`);
|
||||||
|
|
||||||
// accuracy: flag set on pop; fired flights move faster
|
// accuracy: flag set on pop; fired flights move faster
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||||
[{ color: 0, s: 800, power: 'accuracy' }, { color: 1, s: 800 - 3 * SP }]);
|
[{ color: 0, s: 800, power: 'accuracy' }, { color: 1, s: 800 - 3 * SP }]);
|
||||||
ev = [];
|
ev = [];
|
||||||
popRun(st, 0, 0, 'shot', ev);
|
popRun(st, st.paths[0], 0, 0, 'shot', ev);
|
||||||
check('accuracy power sets effect timer', st.effects.accuracyUntil === st.elapsedMs + T.ACCURACY_MS);
|
check('accuracy power sets effect timer', st.effects.accuracyUntil === st.elapsedMs + T.ACCURACY_MS);
|
||||||
const flight = fireBall(st, -Math.PI / 2);
|
const flight = fireBall(st, -Math.PI / 2);
|
||||||
check('accuracy speeds up shots', !!flight && near(flight.speed, T.SHOT_SPEED * T.ACCURACY_SHOT_MULT, 0.01));
|
check('accuracy speeds up shots', !!flight && near(flight.speed, T.SHOT_SPEED * T.ACCURACY_SHOT_MULT, 0.01));
|
||||||
|
|
@ -347,17 +378,17 @@ console.log('\n— Power-ups —');
|
||||||
{ color: 3, s: 900 - 5 * SP }, { color: 3, s: 900 - 6 * SP },
|
{ color: 3, s: 900 - 5 * SP }, { color: 3, s: 900 - 6 * SP },
|
||||||
]);
|
]);
|
||||||
ev = [];
|
ev = [];
|
||||||
popRun(st, 2, 2, 'shot', ev);
|
popRun(st, st.paths[0], 2, 2, 'shot', ev);
|
||||||
const boom = ev.find((e) => e.type === 'explosion');
|
const boom = ev.find((e) => e.type === 'explosion');
|
||||||
check('explosion pops everything in radius', !!boom && boom.ids.length === 4 && st.balls.length === 2);
|
check('explosion pops everything in radius', !!boom && boom.ids.length === 4 && st.paths[0].balls.length === 2);
|
||||||
check('explosion spares balls beyond radius', st.balls.every((b) => b.color === 3));
|
check('explosion spares balls beyond radius', st.paths[0].balls.every((b) => b.color === 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 7. State machine ─────────────────────────────────────────────────────────
|
// ── 7. State machine ─────────────────────────────────────────────────────────
|
||||||
console.log('\n— State machine —');
|
console.log('\n— State machine —');
|
||||||
{
|
{
|
||||||
let st = mkState({}, { status: 'playing', spawned: 10 });
|
let st = mkState({}, { status: 'playing', spawned: 10 });
|
||||||
mkChain(st, [{ color: 0, s: st.path.length - 20 }]);
|
mkChain(st, [{ color: 0, s: st.paths[0].path.length - 20 }]);
|
||||||
let lostEv = false;
|
let lostEv = false;
|
||||||
for (let i = 0; i < 10 && st.status === 'playing'; i++) {
|
for (let i = 0; i < 10 && st.status === 'playing'; i++) {
|
||||||
if (step(st, 50).some((e) => e.type === 'lost')) lostEv = true;
|
if (step(st, 50).some((e) => e.type === 'lost')) lostEv = true;
|
||||||
|
|
@ -365,7 +396,7 @@ console.log('\n— State machine —');
|
||||||
check('ball reaching the hole loses', st.status === 'lost' && lostEv);
|
check('ball reaching the hole loses', st.status === 'lost' && lostEv);
|
||||||
check('terminal state ignores further steps', step(st, 50).length === 0);
|
check('terminal state ignores further steps', step(st, 50).length === 0);
|
||||||
|
|
||||||
st = mkState({}, { status: 'playing', spawned: 10, balls: [], flights: [] });
|
st = mkState({}, { status: 'playing', spawned: 10, balls: [] });
|
||||||
const ev = step(st, 16);
|
const ev = step(st, 16);
|
||||||
const won = ev.find((e) => e.type === 'won');
|
const won = ev.find((e) => e.type === 'won');
|
||||||
const expectBonus = Math.max(0, Math.ceil((10 * T.TIME_PAR_MS_PER_BALL - st.elapsedMs) / 1000)) * T.TIME_BONUS_PER_SEC;
|
const expectBonus = Math.max(0, Math.ceil((10 * T.TIME_PAR_MS_PER_BALL - st.elapsedMs) / 1000)) * T.TIME_BONUS_PER_SEC;
|
||||||
|
|
@ -389,16 +420,16 @@ console.log('\n— State machine —');
|
||||||
[{ color: 0, s: 600 }, { color: 0, s: 552 }, { color: 2, s: 504 }, { color: 2, s: 456 }]);
|
[{ color: 0, s: 600 }, { color: 0, s: 552 }, { color: 2, s: 504 }, { color: 2, s: 456 }]);
|
||||||
st.current = 0; st.next = 0;
|
st.current = 0; st.next = 0;
|
||||||
const ev2 = [];
|
const ev2 = [];
|
||||||
popRun(st, 0, 1, 'shot', ev2);
|
popRun(st, st.paths[0], 0, 1, 'shot', ev2);
|
||||||
check('shooter recolors to colors still present', st.current === 2 && st.next === 2
|
check('shooter recolors to colors still present', st.current === 2 && st.next === 2
|
||||||
&& ev2.filter((e) => e.type === 'recolor').length === 2);
|
&& ev2.filter((e) => e.type === 'recolor').length === 2);
|
||||||
|
|
||||||
// last-call: final spawns only deal colors still on the board
|
// last-call: final spawns only deal colors still on the board
|
||||||
st = mkChain(mkState({}, { status: 'playing', spawned: 5 }),
|
st = mkChain(mkState({}, { status: 'playing', spawned: 5 }),
|
||||||
[{ color: 3, s: 2 * SP }, { color: 3, s: SP }]);
|
[{ color: 3, s: 2 * SP }, { color: 3, s: SP }]);
|
||||||
for (let i = 0; i < 400 && st.spawned < 10; i++) step(st, 50);
|
for (let i = 0; i < 400 && st.paths[0].spawned < 10; i++) step(st, 50);
|
||||||
check('last-call spawns restrict to present colors',
|
check('last-call spawns restrict to present colors',
|
||||||
st.spawned === 10 && st.balls.every((b) => b.color === 3));
|
st.paths[0].spawned === 10 && st.paths[0].balls.every((b) => b.color === 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 8. Determinism ───────────────────────────────────────────────────────────
|
// ── 8. Determinism ───────────────────────────────────────────────────────────
|
||||||
|
|
@ -414,7 +445,10 @@ console.log('\n— Determinism —');
|
||||||
if (tick === 300) fireBall(st, -Math.PI / 2);
|
if (tick === 300) fireBall(st, -Math.PI / 2);
|
||||||
for (const e of step(st, 25)) log.push(e.type + (e.ids ? `:${e.ids.length}` : ''));
|
for (const e of step(st, 25)) log.push(e.type + (e.ids ? `:${e.ids.length}` : ''));
|
||||||
}
|
}
|
||||||
return { log: log.join(','), score: st.score, status: st.status, balls: st.balls.map((b) => `${b.color}@${b.s.toFixed(2)}`).join('|') };
|
return {
|
||||||
|
log: log.join(','), score: st.score, status: st.status,
|
||||||
|
balls: st.paths[0].balls.map((b) => `${b.color}@${b.s.toFixed(2)}`).join('|'),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
const a = run(), b = run();
|
const a = run(), b = run();
|
||||||
check('identical seed + script → identical events', a.log === b.log);
|
check('identical seed + script → identical events', a.log === b.log);
|
||||||
|
|
@ -478,18 +512,18 @@ console.log('\n— Tunnels —');
|
||||||
|
|
||||||
let st = mkState({ tunnels: [[560, 660]], pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
let st = mkState({ tunnels: [[560, 660]], pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
||||||
mkChain(st, [{ color: 0, s: 600 }]);
|
mkChain(st, [{ color: 0, s: 600 }]);
|
||||||
shootAt(st, st.balls[0]);
|
shootAt(st, st.paths[0].balls[0]);
|
||||||
check('a shot flies over a submerged ball', runFlights(st) && st.balls.length === 1);
|
check('a shot flies over a submerged ball', runFlights(st) && st.paths[0].balls.length === 1);
|
||||||
|
|
||||||
st = mkState({ pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
st = mkState({ pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
||||||
mkChain(st, [{ color: 0, s: 600 }]);
|
mkChain(st, [{ color: 0, s: 600 }]);
|
||||||
shootAt(st, st.balls[0]);
|
shootAt(st, st.paths[0].balls[0]);
|
||||||
check('the same shot lands once the tunnel is gone', !runFlights(st) && st.balls.length === 2);
|
check('the same shot lands once the tunnel is gone', !runFlights(st) && st.paths[0].balls.length === 2);
|
||||||
|
|
||||||
st = mkState({ tunnels: [[560, 660]], pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
st = mkState({ tunnels: [[560, 660]], pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
||||||
mkChain(st, [{ color: 0, s: 600 }]);
|
mkChain(st, [{ color: 0, s: 600 }]);
|
||||||
step(st, 16);
|
step(st, 16);
|
||||||
const ball = st.balls[0];
|
const ball = st.paths[0].balls[0];
|
||||||
check('a submerged ball is flagged hidden and invisible', ball.hidden === true && ball.vis === 0);
|
check('a submerged ball is flagged hidden and invisible', ball.hidden === true && ball.vis === 0);
|
||||||
check('the laser sight ignores submerged chain',
|
check('the laser sight ignores submerged chain',
|
||||||
!rayHit(st, Math.atan2(ball.y - st.frog.y, ball.x - st.frog.x)).hit);
|
!rayHit(st, Math.atan2(ball.y - st.frog.y, ball.x - st.frog.x)).hit);
|
||||||
|
|
@ -502,20 +536,21 @@ console.log('\n— Tunnels —');
|
||||||
]);
|
]);
|
||||||
step(st, 16);
|
step(st, 16);
|
||||||
const ev = [];
|
const ev = [];
|
||||||
popRun(st, 2, 2, 'shot', ev);
|
popRun(st, st.paths[0], 2, 2, 'shot', ev);
|
||||||
const boom = ev.find((e) => e.type === 'explosion');
|
const boom = ev.find((e) => e.type === 'explosion');
|
||||||
check('an explosion cannot reach through a tunnel',
|
check('an explosion cannot reach through a tunnel',
|
||||||
!!boom && boom.ids.length === 2 && st.balls.length === 2,
|
!!boom && boom.ids.length === 2 && st.paths[0].balls.length === 2,
|
||||||
`caught ${boom?.ids.length}`);
|
`caught ${boom?.ids.length}`);
|
||||||
check('the submerged pair survives the blast', st.balls.every((b) => b.color === 2));
|
check('the submerged pair survives the blast', st.paths[0].balls.every((b) => b.color === 2));
|
||||||
|
|
||||||
// and the chain itself notices nothing
|
// and the chain itself notices nothing
|
||||||
st = mkState({ tunnels: [[400, 700]] }, { status: 'playing', spawned: 10 });
|
st = mkState({ tunnels: [[400, 700]] }, { status: 'playing', spawned: 10 });
|
||||||
mkChain(st, [{ color: 0, s: 300 }, { color: 1, s: 300 - SP }, { color: 2, s: 300 - 2 * SP }]);
|
mkChain(st, [{ color: 0, s: 300 }, { color: 1, s: 300 - SP }, { color: 2, s: 300 - 2 * SP }]);
|
||||||
for (let i = 0; i < 40; i++) step(st, 50); // 2s at pushSpeed 100
|
for (let i = 0; i < 40; i++) step(st, 50); // 2s at pushSpeed 100
|
||||||
check('the chain rolls straight on through a tunnel',
|
check('the chain rolls straight on through a tunnel',
|
||||||
near(st.balls[0].s, 500, 1) && spacingOk(st.balls), `front at ${st.balls[0].s.toFixed(1)}`);
|
near(st.paths[0].balls[0].s, 500, 1) && spacingOk(st.paths[0].balls),
|
||||||
check('every ball inside is submerged', st.balls.every((b) => b.hidden === (b.s > 400)));
|
`front at ${st.paths[0].balls[0].s.toFixed(1)}`);
|
||||||
|
check('every ball inside is submerged', st.paths[0].balls.every((b) => b.hidden === (b.s > 400)));
|
||||||
|
|
||||||
// ── lint ──
|
// ── lint ──
|
||||||
// A 4000px straight: TUNNEL.MIN_LEN plus the mouth clearances no longer fit
|
// A 4000px straight: TUNNEL.MIN_LEN plus the mouth clearances no longer fit
|
||||||
|
|
@ -542,16 +577,106 @@ console.log('\n— Tunnels —');
|
||||||
TUNNEL.MIN_LEN >= 2 * PORTAL_REACH);
|
TUNNEL.MIN_LEN >= 2 * PORTAL_REACH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 10. Level bank lint ──────────────────────────────────────────────────────
|
// ── 10. Multi-path ───────────────────────────────────────────────────────────
|
||||||
|
console.log('\n— Multi-path —');
|
||||||
|
{
|
||||||
|
// Two independent straight lanes, well apart on screen, frog centred between
|
||||||
|
// them — the "opposite sides" layout the feature is built around.
|
||||||
|
const LANE_A = [[0, 200], [400, 200], [800, 200], [1200, 200], [1600, 200]];
|
||||||
|
const LANE_B = [[0, 800], [400, 800], [800, 800], [1200, 800], [1600, 800]];
|
||||||
|
const twoLanes = (pushSpeed = 100) => ({
|
||||||
|
level: 1, name: 'Two Lanes', frog: [800, 500], colors: 4, seed: 99,
|
||||||
|
starScores: [100, 200, 300],
|
||||||
|
paths: [
|
||||||
|
{ shape: 'line', points: LANE_A, tunnels: [], quota: 10, introBalls: 2, pushSpeed, powerUpRate: 0 },
|
||||||
|
{ shape: 'line', points: LANE_B, tunnels: [], quota: 10, introBalls: 2, pushSpeed, powerUpRate: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// freeze both spawners at their quota so a hand-built chain fixture isn't
|
||||||
|
// added to mid-test — the same spawned:quota convention the single-path
|
||||||
|
// fixtures use, just applied to both paths.
|
||||||
|
const freeze = (st) => { for (const p of st.paths) p.spawned = p.quota; };
|
||||||
|
|
||||||
|
// independent spawners: both paths reach their own quota on their own clock
|
||||||
|
let st = createLevel(twoLanes(100));
|
||||||
|
for (let i = 0; i < 4000 && (st.paths[0].spawned < 10 || st.paths[1].spawned < 10); i++) step(st, 16);
|
||||||
|
check('each path spawns to its own quota independently',
|
||||||
|
st.paths[0].spawned === 10 && st.paths[1].spawned === 10
|
||||||
|
&& st.paths[0].balls.length === 10 && st.paths[1].balls.length === 10);
|
||||||
|
|
||||||
|
// shooter color pool unions across paths: path A all color 0, path B all
|
||||||
|
// color 1 — current/next must be reassignable to whatever is on EITHER lane
|
||||||
|
st = createLevel(twoLanes());
|
||||||
|
freeze(st);
|
||||||
|
mkChain(st, [{ color: 0, s: 300 }, { color: 0, s: 300 - SP }], 0);
|
||||||
|
mkChain(st, [{ color: 1, s: 300 }, { color: 1, s: 300 - SP }], 1);
|
||||||
|
st.status = 'playing';
|
||||||
|
st.current = 2; st.next = 3; // neither color is present anywhere
|
||||||
|
popRun(st, st.paths[0], 0, 0, 'shot', []);
|
||||||
|
check('shooter recolors from the union of both paths\' present colors',
|
||||||
|
(st.current === 0 || st.current === 1) && (st.next === 0 || st.next === 1));
|
||||||
|
|
||||||
|
// a shot lands on whichever path it is physically aimed at — no special
|
||||||
|
// targeting, purely the nearest ball to the ray
|
||||||
|
st = createLevel(twoLanes(0));
|
||||||
|
freeze(st);
|
||||||
|
mkChain(st, [{ color: 0, s: 600 }], 0);
|
||||||
|
mkChain(st, [{ color: 1, s: 600 }], 1);
|
||||||
|
st.status = 'playing';
|
||||||
|
const ballB = st.paths[1].balls[0];
|
||||||
|
st.current = 1;
|
||||||
|
fireBall(st, Math.atan2(ballB.y - st.frog.y, ballB.x - st.frog.x));
|
||||||
|
for (let i = 0; i < 200 && st.flights.length; i++) step(st, 16);
|
||||||
|
check('a shot lands on the path it is aimed at, not always path 0',
|
||||||
|
st.paths[1].balls.length === 2 && st.paths[0].balls.length === 1);
|
||||||
|
|
||||||
|
// either path (not just path 0) reaching its own pit loses the whole level
|
||||||
|
st = createLevel(twoLanes(100));
|
||||||
|
freeze(st);
|
||||||
|
mkChain(st, [{ color: 1, s: st.paths[1].path.length - 20 }], 1);
|
||||||
|
mkChain(st, [{ color: 0, s: 400 }], 0);
|
||||||
|
st.status = 'playing';
|
||||||
|
let lostEv = false;
|
||||||
|
for (let i = 0; i < 10 && st.status === 'playing'; i++) {
|
||||||
|
if (step(st, 50).some((e) => e.type === 'lost')) lostEv = true;
|
||||||
|
}
|
||||||
|
check('either path reaching the hole loses the whole level', st.status === 'lost' && lostEv);
|
||||||
|
|
||||||
|
// winning requires EVERY path cleared, not just one
|
||||||
|
st = createLevel(twoLanes());
|
||||||
|
freeze(st);
|
||||||
|
st.status = 'playing';
|
||||||
|
mkChain(st, [{ color: 0, s: 400 }], 1);
|
||||||
|
let won = step(st, 16).some((e) => e.type === 'won');
|
||||||
|
check('one path cleared but not the other does not win', !won && st.status === 'playing');
|
||||||
|
st.paths[1].balls = [];
|
||||||
|
won = step(st, 16).some((e) => e.type === 'won');
|
||||||
|
check('every path cleared wins', won && st.status === 'won');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 11. Level bank lint ──────────────────────────────────────────────────────
|
||||||
console.log('\n— Level bank —');
|
console.log('\n— Level bank —');
|
||||||
{
|
{
|
||||||
let bank = null;
|
const bankDir = join(__dirname, '../assets/gamedata/zuma');
|
||||||
|
let manifest = null;
|
||||||
try {
|
try {
|
||||||
bank = JSON.parse(readFileSync(join(__dirname, '../data/zuma.json'), 'utf8'));
|
manifest = JSON.parse(readFileSync(join(bankDir, 'levels.json'), 'utf8'));
|
||||||
} catch (_) { /* handled below */ }
|
} catch (_) { /* handled below */ }
|
||||||
check('bank exists (run genZuma.js)', !!bank);
|
check('manifest exists (run genZuma.js)', !!manifest);
|
||||||
if (bank) {
|
let levels = [];
|
||||||
const levels = bank.levels ?? [];
|
let manifestMismatch = '';
|
||||||
|
if (manifest) {
|
||||||
|
levels = (manifest.levels ?? []).map((entry) => {
|
||||||
|
const def = JSON.parse(readFileSync(join(bankDir, entry.file), 'utf8'));
|
||||||
|
if (!manifestMismatch && (entry.level !== def.level || entry.name !== def.name)) {
|
||||||
|
manifestMismatch = `${entry.file}: manifest says L${entry.level} "${entry.name}", `
|
||||||
|
+ `file says L${def.level} "${def.name}"`;
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
check('every manifest entry matches its level file', !manifestMismatch, manifestMismatch);
|
||||||
|
if (manifest) {
|
||||||
check('bank has 20 levels', levels.length === 20);
|
check('bank has 20 levels', levels.length === 20);
|
||||||
check('levels numbered 1..N contiguously', levels.every((l, i) => l.level === i + 1));
|
check('levels numbered 1..N contiguously', levels.every((l, i) => l.level === i + 1));
|
||||||
// Geometry and parameters both come from ZumaLogic — the same lint
|
// Geometry and parameters both come from ZumaLogic — the same lint
|
||||||
|
|
@ -567,11 +692,12 @@ console.log('\n— Level bank —');
|
||||||
check(`geometry lint (length, bounds, curvature ≥ ${(T.BALL_RADIUS * 1.7).toFixed(0)}px, frog clear ≥ ${T.FROG_CLEARANCE}px)`,
|
check(`geometry lint (length, bounds, curvature ≥ ${(T.BALL_RADIUS * 1.7).toFixed(0)}px, frog clear ≥ ${T.FROG_CLEARANCE}px)`,
|
||||||
geomOk, geomDetail);
|
geomOk, geomDetail);
|
||||||
|
|
||||||
const tunnelled = levels.filter((l) => (l.tunnels?.length ?? 0) > 0);
|
const tunnelCount = (l) => l.paths.reduce((a, p) => a + (p.tunnels?.length ?? 0), 0);
|
||||||
|
const tunnelled = levels.filter((l) => tunnelCount(l) > 0);
|
||||||
console.log(` .. ${tunnelled.length} of ${levels.length} levels carry tunnels`
|
console.log(` .. ${tunnelled.length} of ${levels.length} levels carry tunnels`
|
||||||
+ ` (${tunnelled.map((l) => `L${l.level}×${l.tunnels.length}`).join(' ')})`);
|
+ ` (${tunnelled.map((l) => `L${l.level}×${tunnelCount(l)}`).join(' ')})`);
|
||||||
|
|
||||||
// ── 11. Winnability soak ─────────────────────────────────────────────────
|
// ── 12. Winnability soak ─────────────────────────────────────────────────
|
||||||
// If a bot that only ever takes the best immediately available shot can
|
// If a bot that only ever takes the best immediately available shot can
|
||||||
// clear a level, a player can. This is what catches a quota the path can
|
// clear a level, a player can. This is what catches a quota the path can
|
||||||
// technically hold but nobody could actually survive.
|
// technically hold but nobody could actually survive.
|
||||||
|
|
@ -582,6 +708,7 @@ console.log('\n— Level bank —');
|
||||||
console.log('\n— Aimbot soak —');
|
console.log('\n— Aimbot soak —');
|
||||||
const SEEDS = 8;
|
const SEEDS = 8;
|
||||||
const FLOOR = 5; // per-level clears required out of SEEDS
|
const FLOOR = 5; // per-level clears required out of SEEDS
|
||||||
|
const totalQuota = (l) => l.paths.reduce((a, p) => a + p.quota, 0);
|
||||||
const rows = levels.map((l) => {
|
const rows = levels.map((l) => {
|
||||||
const runs = [];
|
const runs = [];
|
||||||
for (let k = 0; k < SEEDS; k++) runs.push(aimbotPlay(l, l.seed + k * 7919));
|
for (let k = 0; k < SEEDS; k++) runs.push(aimbotPlay(l, l.seed + k * 7919));
|
||||||
|
|
@ -601,7 +728,7 @@ console.log('\n— Level bank —');
|
||||||
console.log(` .. aimbot clear rate: ${(rate * 100).toFixed(0)}% (worst level ${Math.min(...rows.map((r) => r.wins))}/${SEEDS})`);
|
console.log(` .. aimbot clear rate: ${(rate * 100).toFixed(0)}% (worst level ${Math.min(...rows.map((r) => r.wins))}/${SEEDS})`);
|
||||||
check('bank-wide clear rate ≥ 80%', rate >= 0.8, `${(rate * 100).toFixed(0)}%`);
|
check('bank-wide clear rate ≥ 80%', rate >= 0.8, `${(rate * 100).toFixed(0)}%`);
|
||||||
|
|
||||||
const perMarble = rows.reduce((a, r, i) => a + r.mean / levels[i].quota, 0) / rows.length;
|
const perMarble = rows.reduce((a, r, i) => a + r.mean / totalQuota(levels[i]), 0) / rows.length;
|
||||||
console.log(` .. mean score per quota marble: ${perMarble.toFixed(1)}`);
|
console.log(` .. mean score per quota marble: ${perMarble.toFixed(1)}`);
|
||||||
// Medals are judged on the bot's MEAN run, not its best: best-of-N is a
|
// Medals are judged on the bot's MEAN run, not its best: best-of-N is a
|
||||||
// measure of chain luck, and the question here is where the star curve
|
// measure of chain luck, and the question here is where the star curve
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue