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
|
||||
// PreloadScene). Mirrors the Peggle / Super Kart / Goo Tower editors: load a
|
||||
// level (from the banked data/zuma.json or any local .json) → edit → Test Play
|
||||
// → ⬇ Export Bank, then hand-drop the file into data/.
|
||||
//
|
||||
// Unlike those games Zuma has no per-level files — the whole 20-level bank is
|
||||
// one JSON preloaded under the cache key 'zuma' — so "export" upserts this
|
||||
// level into a copy of that bank and downloads the lot.
|
||||
// PreloadScene). Mirrors the Peggle / Super Kart / Goo Tower editors exactly:
|
||||
// one file per level (assets/gamedata/zuma/level-NNN.json) plus a lightweight
|
||||
// levels.json manifest ({level, name, file}), eager-loaded under the cache
|
||||
// key 'zuma-levels' while the per-level payloads are fetched on demand. Load
|
||||
// a level (from the manifest dropdown or any local .json) → edit → Test Play
|
||||
// → ⬇ Export Level (the level file itself — drop it straight into
|
||||
// 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
|
||||
// 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() {
|
||||
const raw = this.cache.json.get('zuma');
|
||||
this.bank = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
||||
const raw = this.cache.json.get('zuma-levels');
|
||||
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.registry.get('zuma-editor-state') ?? this.defaultLevel())
|
||||
: (this.bank[0] ? clone(this.bank[0]) : this.defaultLevel());
|
||||
: this.defaultLevel();
|
||||
|
||||
this.tool = 'move';
|
||||
this.selected = -1;
|
||||
|
|
@ -95,6 +99,8 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
this.portalImgs = [];
|
||||
this.undoStack = [];
|
||||
this.errs = [];
|
||||
this.activePathIdx = 0; // which of this.level.paths the tools below edit
|
||||
this.pathTabButtons = [];
|
||||
|
||||
buildPortalTextures(this);
|
||||
buildPitTextures(this);
|
||||
|
|
@ -120,22 +126,36 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
|
||||
defaultLevel() {
|
||||
return {
|
||||
level: (this.bank?.length ?? 0) + 1,
|
||||
level: (this.manifest?.length ?? 0) + 1,
|
||||
name: 'New Path',
|
||||
shape: 'custom',
|
||||
points: [[-80, 300], [300, 300], [800, 380], [1300, 520], [1600, 760]],
|
||||
frog: [960, 900],
|
||||
tunnels: [],
|
||||
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,
|
||||
introBalls: 8,
|
||||
pushSpeed: 30,
|
||||
powerUpRate: 0.06,
|
||||
seed: 12345,
|
||||
starScores: [640, 960, 1280],
|
||||
};
|
||||
}
|
||||
|
||||
activePath() {
|
||||
return this.level.paths[this.activePathIdx];
|
||||
}
|
||||
|
||||
// ── Coordinate transforms ──────────────────────────────────────────────────
|
||||
|
||||
toBoard(wx, wy) {
|
||||
|
|
@ -159,8 +179,14 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
fontFamily: FONT, fontSize: '30px', color: COLORS.goldHex,
|
||||
}).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 = {};
|
||||
let y = 92;
|
||||
let y = 172;
|
||||
for (const [id, label] of TOOLS) {
|
||||
this.toolButtons[id] = new Button(this, cx, y, label, () => this.setTool(id),
|
||||
{ width: bw, height: 50, fontSize: 19 });
|
||||
|
|
@ -170,10 +196,10 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
y += 16;
|
||||
new Button(this, cx, y, '▶ Test Play', () => this.testPlay(), { width: bw, height: 56, fontSize: 22 });
|
||||
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 });
|
||||
y += 58;
|
||||
new Button(this, cx, y, '⬇ Export Manifest', () => this.exportManifest(), { width: bw, height: 50, fontSize: 19 });
|
||||
y += 58;
|
||||
|
||||
const half = bw / 2 - 6;
|
||||
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', () => {
|
||||
this.pushUndo();
|
||||
this.level = this.defaultLevel();
|
||||
this.activePathIdx = 0;
|
||||
this.buildPathTabs();
|
||||
this.selected = -1;
|
||||
this.pendingEnter = null;
|
||||
this.syncForm();
|
||||
|
|
@ -216,6 +244,71 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
buildForm() {
|
||||
|
|
@ -268,12 +361,17 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
|
||||
this.loadSelect = q('#zu-load');
|
||||
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.loadSelect.addEventListener('change', (ev) => {
|
||||
const n = Number(ev.target.value);
|
||||
const def = this.bank.find((l) => l.level === n);
|
||||
if (def) this.loadLevel(clone(def), `bank L${n}`);
|
||||
...this.manifest.map((m) => `<option value="${m.file}">L${m.level} · ${m.name}</option>`)].join('');
|
||||
this.loadSelect.addEventListener('change', async (ev) => {
|
||||
const file = 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) => {
|
||||
|
|
@ -298,28 +396,35 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
syncForm() {
|
||||
if (!this.fields) return;
|
||||
const l = this.level;
|
||||
const p = this.activePath();
|
||||
this.fields.level.value = l.level;
|
||||
this.fields.name.value = l.name;
|
||||
this.fields.colors.value = l.colors;
|
||||
this.fields.quota.value = l.quota;
|
||||
this.fields.introBalls.value = l.introBalls;
|
||||
this.fields.pushSpeed.value = l.pushSpeed;
|
||||
this.fields.powerUpRate.value = l.powerUpRate;
|
||||
this.fields.quota.value = p.quota;
|
||||
this.fields.introBalls.value = p.introBalls;
|
||||
this.fields.pushSpeed.value = p.pushSpeed;
|
||||
this.fields.powerUpRate.value = p.powerUpRate;
|
||||
this.fields.seed.value = l.seed;
|
||||
this.fields.star1.value = l.starScores[0];
|
||||
this.fields.star2.value = l.starScores[1];
|
||||
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) {
|
||||
const v = this.fields[key].value;
|
||||
const num = Number(v);
|
||||
const PATH_FIELDS = new Set(['quota', 'introBalls', 'pushSpeed']);
|
||||
switch (key) {
|
||||
case 'name': this.level.name = String(v || 'Untitled'); break;
|
||||
case 'star1': case 'star2': case 'star3':
|
||||
this.level.starScores[Number(key.slice(4)) - 1] = Math.round(num) || 0; break;
|
||||
case 'powerUpRate': this.level.powerUpRate = num; break;
|
||||
default: this.level[key] = Math.round(num) || 0; break;
|
||||
case 'powerUpRate': this.activePath().powerUpRate = num; 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();
|
||||
}
|
||||
|
|
@ -331,18 +436,20 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
}
|
||||
|
||||
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`);
|
||||
return;
|
||||
}
|
||||
this.pushUndo();
|
||||
const d = this.defaultLevel();
|
||||
this.level = {
|
||||
...d, ...clone(data),
|
||||
starScores: Array.isArray(data.starScores) && data.starScores.length === 3
|
||||
? clone(data.starScores) : d.starScores,
|
||||
tunnels: Array.isArray(data.tunnels) ? clone(data.tunnels) : [],
|
||||
...d, ...migrated,
|
||||
starScores: Array.isArray(migrated.starScores) && migrated.starScores.length === 3
|
||||
? clone(migrated.starScores) : d.starScores,
|
||||
};
|
||||
this.activePathIdx = 0;
|
||||
this.buildPathTabs();
|
||||
this.selected = -1;
|
||||
this.pendingEnter = null;
|
||||
this.syncForm();
|
||||
|
|
@ -351,6 +458,23 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
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 y = Math.round(Phaser.Math.Clamp(wy, WORLD.y0, WORLD.y0 + WORLD.h));
|
||||
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.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.
|
||||
|
||||
tunnelsOf() {
|
||||
if (!Array.isArray(this.level.tunnels)) this.level.tunnels = [];
|
||||
return this.level.tunnels;
|
||||
const p = this.activePath();
|
||||
if (!Array.isArray(p.tunnels)) p.tunnels = [];
|
||||
return p.tunnels;
|
||||
}
|
||||
|
||||
// Nearest mouth to a world point, as { idx, end, s }, or null.
|
||||
|
|
@ -435,8 +560,9 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
this.pushUndo();
|
||||
this.tunnelsOf().push([a, b]);
|
||||
this.level.tunnels.sort((p, q) => p[0] - q[0]);
|
||||
const tunnels = this.tunnelsOf();
|
||||
tunnels.push([a, b]);
|
||||
tunnels.sort((p, q) => p[0] - q[0]);
|
||||
playSound(this, SFX.UI_CHIME);
|
||||
this.revalidate();
|
||||
}
|
||||
|
|
@ -461,7 +587,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
clearTunnels() {
|
||||
if (!this.tunnelsOf().length) return;
|
||||
this.pushUndo();
|
||||
this.level.tunnels = [];
|
||||
this.activePath().tunnels = [];
|
||||
this.pendingEnter = null;
|
||||
this.revalidate();
|
||||
}
|
||||
|
|
@ -481,7 +607,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
const max = maxPx / K;
|
||||
let best = -1;
|
||||
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);
|
||||
if (d < bd) { bd = d; best = i; }
|
||||
});
|
||||
|
|
@ -489,7 +615,7 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
}
|
||||
|
||||
handleClick(wx, wy) {
|
||||
const pts = this.level.points;
|
||||
const pts = this.activePath().points;
|
||||
switch (this.tool) {
|
||||
case 'move': {
|
||||
const i = this.nearestPoint(wx, wy);
|
||||
|
|
@ -536,17 +662,17 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
|
||||
deletePointAt(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.level.points.splice(i, 1);
|
||||
this.activePath().points.splice(i, 1);
|
||||
this.selected = -1;
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
deleteSelected() {
|
||||
if (this.selected < 0 || this.level.points.length <= 3) return;
|
||||
if (this.selected < 0 || this.activePath().points.length <= 3) return;
|
||||
this.pushUndo();
|
||||
this.level.points.splice(this.selected, 1);
|
||||
this.activePath().points.splice(this.selected, 1);
|
||||
this.selected = -1;
|
||||
this.revalidate();
|
||||
}
|
||||
|
|
@ -568,19 +694,28 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
}
|
||||
|
||||
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 */ }
|
||||
this.errs = [...validateLevelParams(this.level), ...geo.errs];
|
||||
this.geo = geo;
|
||||
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;
|
||||
this.statText?.setText(
|
||||
`length ${geo.length.toFixed(0)}px • quota ${this.level.quota}/${cap}`
|
||||
+ ` • min radius ${Number.isFinite(geo.minRadius) ? geo.minRadius.toFixed(0) : '—'}px`
|
||||
+ ` • frog clear ${geo.minFrog.toFixed(0)}px • ${this.level.points.length} points`
|
||||
+ ` • ${nTun} tunnel${nTun === 1 ? '' : 's'} (${((geo.hiddenFrac ?? 0) * 100).toFixed(0)}% buried)`
|
||||
);
|
||||
const ap = this.activePath();
|
||||
let line = `path ${this.activePathIdx + 1}/${this.level.paths.length}`
|
||||
+ ` • length ${active.length.toFixed(0)}px • quota ${ap.quota}/${cap}`
|
||||
+ ` • min radius ${Number.isFinite(active.minRadius) ? active.minRadius.toFixed(0) : '—'}px`
|
||||
+ ` • 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();
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
// 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
|
||||
// stone ends up covering which stretch of path.
|
||||
drawMouth(path, s, dir) {
|
||||
// stone ends up covering which stretch of path. `dim` fades a mouth on a
|
||||
// path that isn't the one currently being edited.
|
||||
drawMouth(path, s, dir, dim = 1) {
|
||||
const p = path.pointAt(s);
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
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)
|
||||
.setRotation(rot)
|
||||
.setScale(K)
|
||||
.setDepth(depth);
|
||||
.setDepth(depth)
|
||||
.setAlpha(dim);
|
||||
if (dir < 0) img.setTint(0xffd9a0); // exits read warmer, as in game
|
||||
this.portalImgs.push(img);
|
||||
}
|
||||
} else {
|
||||
this.g.fillStyle(0x0a0704, 0.95);
|
||||
this.g.fillStyle(0x0a0704, 0.95 * dim);
|
||||
this.g.fillCircle(x, y, 46 * K);
|
||||
}
|
||||
const t = this.gTop;
|
||||
t.fillStyle(dir > 0 ? 0x9fd8ff : 0xffd54a, 1);
|
||||
t.lineStyle(2, 0x000000, 0.85);
|
||||
t.fillStyle(dir > 0 ? 0x9fd8ff : 0xffd54a, dim);
|
||||
t.lineStyle(2, 0x000000, 0.85 * dim);
|
||||
t.fillCircle(x, y, 7);
|
||||
t.strokeCircle(x, y, 7);
|
||||
}
|
||||
|
|
@ -634,99 +771,21 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
g.lineStyle(1, 0x6b5638, 0.7);
|
||||
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();
|
||||
this.portalImgs = [];
|
||||
|
||||
if (path) {
|
||||
const tunnels = normalizeTunnels(lvl.tunnels, path.length);
|
||||
const { visible, hidden } = pathSpans(path.length, tunnels);
|
||||
// Every path draws — inactive ones dimmed — so laying a second lane
|
||||
// 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;
|
||||
});
|
||||
|
||||
// Buried runs, on the layer underneath everything, so anything live that
|
||||
// crosses them lands on top — the same ordering the game uses.
|
||||
this.gUnder.lineStyle(12 * K, 0x000000, 0.45);
|
||||
for (const [s0, s1] of hidden) {
|
||||
for (let s = s0; s < s1; s += 34) {
|
||||
const a = path.pointAt(s);
|
||||
const b = path.pointAt(Math.min(s1, s + 17));
|
||||
const [ax, ay] = this.toBoard(a.x, a.y);
|
||||
const [bx, by] = this.toBoard(b.x, b.y);
|
||||
this.gUnder.lineBetween(ax, ay, bx, by);
|
||||
}
|
||||
}
|
||||
|
||||
// the groove, at true scaled widths, broken at every tunnel mouth
|
||||
const stroke = (w, color) => {
|
||||
g.lineStyle(w * K, color, 1);
|
||||
for (const [s0, s1] of visible) {
|
||||
const pts = sampleRange(path, s0, s1);
|
||||
g.beginPath();
|
||||
const [sx, sy] = this.toBoard(pts[0].x, pts[0].y);
|
||||
g.moveTo(sx, sy);
|
||||
for (const p of pts) {
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
g.lineTo(x, y);
|
||||
}
|
||||
g.strokePath();
|
||||
}
|
||||
};
|
||||
for (const { w, color } of PATH_STYLE.bands) stroke(w, color);
|
||||
|
||||
// Ghost chain: the whole quota packed back from the pit — the shape of
|
||||
// the moment before you lose. Makes "does this path hold the marbles I
|
||||
// asked for, and where does the crunch happen" something you can see.
|
||||
// Submerged marbles are drawn hollow: that is the part of the chain the
|
||||
// frog will not be able to touch.
|
||||
const R = TUNING.BALL_RADIUS * K;
|
||||
for (let i = 0; i < lvl.quota; i++) {
|
||||
const s = path.length - i * TUNING.BALL_SPACING;
|
||||
if (s < 0) break;
|
||||
const p = path.pointAt(s);
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
const color = BALL_COLORS[i % Math.max(1, lvl.colors)];
|
||||
if (isHidden(tunnels, s)) {
|
||||
g.lineStyle(2, color, 0.5);
|
||||
g.strokeCircle(x, y, R);
|
||||
} else {
|
||||
g.fillStyle(color, 0.9);
|
||||
g.fillCircle(x, y, R);
|
||||
g.lineStyle(1, 0x000000, 0.4);
|
||||
g.strokeCircle(x, y, R);
|
||||
}
|
||||
}
|
||||
|
||||
// the pit at the far end — the real art, so its footprint over the path
|
||||
// is what the author sees
|
||||
const end = path.pointAt(path.length);
|
||||
const [hx, hy] = this.toBoard(end.x, end.y);
|
||||
if (this.textures.exists(PIT_KEY)) {
|
||||
this.portalImgs.push(this.add.image(hx, hy, PIT_KEY).setScale(K).setDepth(6));
|
||||
} else {
|
||||
g.fillStyle(0x0a0704, 1); g.fillCircle(hx, hy, PIT.OUTER * K);
|
||||
g.fillStyle(0x050505, 1); g.fillCircle(hx, hy, PIT.MOUTH * K);
|
||||
}
|
||||
|
||||
// the maws themselves, over the chain, exactly as the game stacks them
|
||||
for (const t of tunnels) {
|
||||
this.drawMouth(path, t.enter, 1);
|
||||
this.drawMouth(path, t.exit, -1);
|
||||
}
|
||||
|
||||
// a half-placed tunnel: show where the entrance would go
|
||||
if (this.pendingEnter !== null) {
|
||||
const p = path.pointAt(this.pendingEnter);
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
g.lineStyle(3, 0xffd54a, 0.95);
|
||||
dashedCircle(g, x, y, 26 * K * 1.6);
|
||||
g.fillStyle(0xffd54a, 0.85);
|
||||
g.fillCircle(x, y, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// frog + the clearance ring the lint enforces
|
||||
// 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')) {
|
||||
|
|
@ -737,13 +796,15 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
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);
|
||||
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 — first is the lead-in, last is the pit
|
||||
lvl.points.forEach(([px, py], i) => {
|
||||
// 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 === lvl.points.length - 1;
|
||||
const last = i === activePts.length - 1;
|
||||
const color = i === this.selected ? 0xffd54a : last ? 0xd9403a : i === 0 ? 0x6fd47e : 0x9fd8ff;
|
||||
g.fillStyle(color, 1);
|
||||
g.fillPoints([
|
||||
|
|
@ -754,6 +815,102 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Buried runs, on the layer underneath everything, so anything live that
|
||||
// crosses them lands on top — the same ordering the game uses.
|
||||
this.gUnder.lineStyle(12 * K, 0x000000, 0.45 * dim);
|
||||
for (const [s0, s1] of hidden) {
|
||||
for (let s = s0; s < s1; s += 34) {
|
||||
const a = path.pointAt(s);
|
||||
const b = path.pointAt(Math.min(s1, s + 17));
|
||||
const [ax, ay] = this.toBoard(a.x, a.y);
|
||||
const [bx, by] = this.toBoard(b.x, b.y);
|
||||
this.gUnder.lineBetween(ax, ay, bx, by);
|
||||
}
|
||||
}
|
||||
|
||||
// the groove, at true scaled widths, broken at every tunnel mouth
|
||||
const stroke = (w, color) => {
|
||||
g.lineStyle(w * K, color, dim);
|
||||
for (const [s0, s1] of visible) {
|
||||
const pts = sampleRange(path, s0, s1);
|
||||
g.beginPath();
|
||||
const [sx, sy] = this.toBoard(pts[0].x, pts[0].y);
|
||||
g.moveTo(sx, sy);
|
||||
for (const p of pts) {
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
g.lineTo(x, y);
|
||||
}
|
||||
g.strokePath();
|
||||
}
|
||||
};
|
||||
for (const { w, color } of PATH_STYLE.bands) stroke(w, color);
|
||||
|
||||
// Ghost chain: the whole quota packed back from the pit — the shape of
|
||||
// the moment before you lose. Makes "does this path hold the marbles I
|
||||
// asked for, and where does the crunch happen" something you can see.
|
||||
// Submerged marbles are drawn hollow: that is the part of the chain the
|
||||
// frog will not be able to touch.
|
||||
const R = TUNING.BALL_RADIUS * K;
|
||||
for (let i = 0; i < pd.quota; i++) {
|
||||
const s = path.length - i * TUNING.BALL_SPACING;
|
||||
if (s < 0) break;
|
||||
const p = path.pointAt(s);
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
const color = BALL_COLORS[i % Math.max(1, this.level.colors)];
|
||||
if (isHidden(tunnels, s)) {
|
||||
g.lineStyle(2, color, 0.5 * dim);
|
||||
g.strokeCircle(x, y, R);
|
||||
} else {
|
||||
g.fillStyle(color, 0.9 * dim);
|
||||
g.fillCircle(x, y, R);
|
||||
g.lineStyle(1, 0x000000, 0.4 * dim);
|
||||
g.strokeCircle(x, y, R);
|
||||
}
|
||||
}
|
||||
|
||||
// the pit at the far end — the real art, so its footprint over the path
|
||||
// is what the author sees
|
||||
const end = path.pointAt(path.length);
|
||||
const [hx, hy] = this.toBoard(end.x, end.y);
|
||||
if (this.textures.exists(PIT_KEY)) {
|
||||
this.portalImgs.push(this.add.image(hx, hy, PIT_KEY).setScale(K).setDepth(6).setAlpha(dim));
|
||||
} else {
|
||||
g.fillStyle(0x0a0704, dim); g.fillCircle(hx, hy, PIT.OUTER * K);
|
||||
g.fillStyle(0x050505, dim); g.fillCircle(hx, hy, PIT.MOUTH * K);
|
||||
}
|
||||
|
||||
// the maws themselves, over the chain, exactly as the game stacks them
|
||||
for (const t of tunnels) {
|
||||
this.drawMouth(path, t.enter, 1, dim);
|
||||
this.drawMouth(path, t.exit, -1, dim);
|
||||
}
|
||||
|
||||
// a half-placed tunnel: only ever exists on the path being actively edited
|
||||
if (active && this.pendingEnter !== null) {
|
||||
const p = path.pointAt(this.pendingEnter);
|
||||
const [x, y] = this.toBoard(p.x, p.y);
|
||||
g.lineStyle(3, 0xffd54a, 0.95);
|
||||
dashedCircle(g, x, y, 26 * K * 1.6);
|
||||
g.fillStyle(0xffd54a, 0.85);
|
||||
g.fillCircle(x, y, 5);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// ── Test play / export ─────────────────────────────────────────────────────
|
||||
|
||||
testPlay() {
|
||||
|
|
@ -774,25 +931,30 @@ export default class ZumaEditor extends Phaser.Scene {
|
|||
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||
}
|
||||
|
||||
exportLevel() {
|
||||
if (this.errs.length) { playSound(this, SFX.MASTERMIND_DENIED); return; }
|
||||
this.download(`zuma-level-${this.level.level}.json`, this.level);
|
||||
playSound(this, SFX.UI_CHIME);
|
||||
this.flash(`Saved zuma-level-${this.level.level}.json`);
|
||||
levelFile() {
|
||||
return `level-${String(this.level.level).padStart(3, '0')}.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; }
|
||||
const levels = this.bank.filter((l) => l.level !== this.level.level).map(clone);
|
||||
levels.push(clone(this.level));
|
||||
levels.sort((a, b) => a.level - b.level);
|
||||
this.download('zuma.json', {
|
||||
generatedAt: new Date().toISOString(),
|
||||
count: levels.length,
|
||||
levels,
|
||||
});
|
||||
const file = this.levelFile();
|
||||
this.download(file, this.level);
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
// progress or history writes for it (see ZumaEditor.testPlay).
|
||||
this.testLevel = data.testLevel ?? null;
|
||||
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.canPersist = true;
|
||||
this.view = 'select';
|
||||
|
|
@ -93,7 +94,8 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
this.ballSprites = new Map(); // ball id -> { img, icon }
|
||||
this.flightSprites = new Map(); // flight id -> img
|
||||
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() {
|
||||
|
|
@ -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);
|
||||
|
||||
const raw = this.cache.json.get('zuma');
|
||||
this.bank = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
||||
const raw = this.cache.json.get('zuma-levels');
|
||||
this.manifest = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level);
|
||||
|
||||
if (this.testLevel) {
|
||||
this.bank = [this.testLevel];
|
||||
this.manifest = [{ level: this.testLevel.level, name: this.testLevel.name }];
|
||||
this.canPersist = false;
|
||||
this.layer = this.add.layer();
|
||||
this.rollCirc = 2 * Math.PI * TUNING.BALL_RADIUS;
|
||||
this.buildTextures();
|
||||
this.bindInput();
|
||||
this.playLevel(this.testLevel.level);
|
||||
this.playLevel(this.testLevel.level, this.testLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -392,7 +394,7 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
this.frogCurrent = null;
|
||||
this.frogNext = null;
|
||||
this.laserGfx = null;
|
||||
this.pitFall = null;
|
||||
this.pitFalls = null;
|
||||
this.quotaGfx = null;
|
||||
this.scoreText = null;
|
||||
this.effectsText = null;
|
||||
|
|
@ -434,7 +436,7 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
|
||||
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', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center',
|
||||
}).setOrigin(0.5);
|
||||
|
|
@ -444,11 +446,11 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
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)
|
||||
.setStrokeStyle(2, 0x6b5638, 0.8)
|
||||
.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,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add([bg, prog]);
|
||||
|
|
@ -460,7 +462,7 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
const left = cx - gridW / 2 + SIZE / 2;
|
||||
const top = 550;
|
||||
|
||||
this.bank.forEach((l, i) => {
|
||||
this.manifest.forEach((l, i) => {
|
||||
const col = i % COLS;
|
||||
const row = Math.floor(i / COLS);
|
||||
const x = left + col * (SIZE + GAP);
|
||||
|
|
@ -544,14 +546,29 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
doResetProgress() {
|
||||
api.post('/puzzles/zuma/reset').catch(() => { /* best effort */ });
|
||||
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();
|
||||
}
|
||||
|
||||
// ── Play a level ────────────────────────────────────────────────────────────
|
||||
|
||||
playLevel(level) {
|
||||
const def = this.bank.find((l) => l.level === level);
|
||||
// Level payloads are fetched on demand (assets/gamedata/zuma/level-NNN.json,
|
||||
// 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;
|
||||
this.view = 'play';
|
||||
this.level = level;
|
||||
|
|
@ -609,8 +626,12 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
drawPath() {
|
||||
const path = this.state.path;
|
||||
const tunnels = this.state.tunnels;
|
||||
for (const p of this.state.paths) this.drawOnePath(p);
|
||||
}
|
||||
|
||||
drawOnePath(p) {
|
||||
const path = p.path;
|
||||
const tunnels = p.tunnels;
|
||||
const { visible, hidden } = pathSpans(path.length, tunnels);
|
||||
|
||||
// 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:
|
||||
// one is swallowing, the other is spitting the chain back out.
|
||||
drawPortals() {
|
||||
for (const t of this.state.tunnels) {
|
||||
this.addPortal(t.enter, 1);
|
||||
this.addPortal(t.exit, -1);
|
||||
for (const p of this.state.paths) {
|
||||
for (const t of p.tunnels) {
|
||||
this.addPortal(p, t.enter, 1);
|
||||
this.addPortal(p, t.exit, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPortal(s, dir) {
|
||||
const p = this.state.path.pointAt(s);
|
||||
const ang = Math.atan2(p.ty, p.tx);
|
||||
addPortal(p, s, dir) {
|
||||
const pt = p.path.pointAt(s);
|
||||
const ang = Math.atan2(pt.ty, pt.tx);
|
||||
const rot = dir > 0 ? ang : ang + Math.PI;
|
||||
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)
|
||||
.setRotation(rot)
|
||||
.setDepth(depth);
|
||||
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)
|
||||
.setBlendMode(Phaser.BlendModes.ADD)
|
||||
.setScale(dir > 0 ? 2.6 : 3.4)
|
||||
|
|
@ -695,19 +718,21 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
// 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.
|
||||
drawPit() {
|
||||
const end = this.state.path.pointAt(this.state.path.length);
|
||||
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)
|
||||
.setBlendMode(Phaser.BlendModes.ADD)
|
||||
.setAlpha(0.5)
|
||||
.setDepth(D.pitSwirl);
|
||||
this.layer.add([pit, swirl]);
|
||||
this.tweens.add({
|
||||
targets: swirl, angle: 360, duration: 17000, repeat: -1, ease: 'Linear',
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: swirl, alpha: 0.22, duration: 2600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
|
||||
});
|
||||
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 swirl = this.add.image(end.x, end.y, PIT_SWIRL)
|
||||
.setBlendMode(Phaser.BlendModes.ADD)
|
||||
.setAlpha(0.5)
|
||||
.setDepth(D.pitSwirl);
|
||||
this.layer.add([pit, swirl]);
|
||||
this.tweens.add({
|
||||
targets: swirl, angle: 360, duration: 17000, repeat: -1, ease: 'Linear',
|
||||
});
|
||||
this.tweens.add({
|
||||
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
|
||||
|
|
@ -715,16 +740,10 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
// 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.
|
||||
// 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() {
|
||||
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
|
||||
// fight the per-frame writes below.
|
||||
for (const [, spr] of this.ballSprites) {
|
||||
|
|
@ -739,28 +758,57 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
}
|
||||
this.flightSprites = new Map();
|
||||
|
||||
this.pitFall = {
|
||||
advance: 0,
|
||||
span,
|
||||
speed,
|
||||
// Each marble shrinks over the last stretch of ITS OWN run. The front one
|
||||
// is already sitting on the pit when the level ends, so a ramp measured
|
||||
// from the pit centre would pop it to a dot on the very first frame;
|
||||
// measured from where it starts, it drops away from full size 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))])),
|
||||
};
|
||||
return span ? (span / speed) * 1000 : 0;
|
||||
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,
|
||||
span,
|
||||
speed,
|
||||
// Each marble shrinks over the last stretch of ITS OWN run. The front
|
||||
// one is already sitting on the pit when the level ends, so a ramp
|
||||
// measured from the pit centre would pop it to a dot on the very first
|
||||
// frame; measured from where it starts, it drops away from full size
|
||||
// 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))])),
|
||||
});
|
||||
maxMs = Math.max(maxMs, (span / speed) * 1000);
|
||||
}
|
||||
return maxMs;
|
||||
}
|
||||
|
||||
stepPitFall(dtMs) {
|
||||
const pf = this.pitFall;
|
||||
const path = this.state.path;
|
||||
for (const pf of this.pitFalls) this.stepOneFall(pf, dtMs);
|
||||
|
||||
// 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 tun = this.state.tunnels;
|
||||
const tun = pf.tunnels;
|
||||
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);
|
||||
if (!spr) continue;
|
||||
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.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
|
||||
|
|
@ -862,7 +899,9 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
g.clear();
|
||||
g.fillStyle(0x0a120b, 0.9);
|
||||
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.fillRoundedRect(x, y, Math.max(2, W * toCome), H, 6);
|
||||
g.lineStyle(2, 0x6b5638, 1);
|
||||
|
|
@ -901,7 +940,7 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
update(time, delta) {
|
||||
// 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.
|
||||
if (this.pitFall) { this.stepPitFall(delta); return; }
|
||||
if (this.pitFalls) { this.stepPitFall(delta); return; }
|
||||
if (this.view !== 'play' || this.overlayUp || !this.state) return;
|
||||
const st = this.state;
|
||||
const events = step(st, delta);
|
||||
|
|
@ -954,7 +993,7 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
case 'inserted': {
|
||||
playSound(this, 'sfx-zuma-hit');
|
||||
// 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);
|
||||
break;
|
||||
}
|
||||
|
|
@ -967,14 +1006,14 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
this.floatText(e.x, e.y - 20, `+${e.score}`, '#ffd54a');
|
||||
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);
|
||||
this.lastClearX = e.x; this.lastClearY = e.y;
|
||||
this.lastClearX = e.x; this.lastClearY = e.y; this.lastClearPathIdx = e.pathIdx;
|
||||
break;
|
||||
}
|
||||
case 'explosion':
|
||||
playScifiExplode(this);
|
||||
this.burst(e.x, e.y, 0xffa726, 16, 2.2);
|
||||
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;
|
||||
case 'powerup':
|
||||
playSound(this, SFX.SCIFI_REVEAL);
|
||||
|
|
@ -1014,21 +1053,23 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
|
||||
syncChain() {
|
||||
const seen = new Set();
|
||||
for (const b of this.state.balls) {
|
||||
seen.add(b.id);
|
||||
const spr = this.makeBallSprite(b);
|
||||
// roll phase from arc-length, roll direction from the path tangent
|
||||
const t = this.state.path.pointAt(b.s);
|
||||
this.rollMarble(spr, b.x, b.y, b.s, t.tx, t.ty);
|
||||
if (spr.icon) spr.icon.setPosition(b.x, b.y);
|
||||
// Tunnels: b.vis ramps 1 → 0 just inside a mouth so the marble sinks
|
||||
// into the maw instead of blinking out. The power-up icon runs its own
|
||||
// pulse tween, so it is toggled rather than alpha-driven.
|
||||
const vis = b.vis ?? 1;
|
||||
if (vis < 1 || spr.base.alpha < 1) {
|
||||
spr.base.setAlpha(vis);
|
||||
spr.gloss.setAlpha(vis);
|
||||
spr.icon?.setVisible(vis > 0.02);
|
||||
for (const p of this.state.paths) {
|
||||
for (const b of p.balls) {
|
||||
seen.add(b.id);
|
||||
const spr = this.makeBallSprite(b);
|
||||
// roll phase from arc-length, roll direction from the path tangent
|
||||
const t = p.path.pointAt(b.s);
|
||||
this.rollMarble(spr, b.x, b.y, b.s, t.tx, t.ty);
|
||||
if (spr.icon) spr.icon.setPosition(b.x, b.y);
|
||||
// Tunnels: b.vis ramps 1 → 0 just inside a mouth so the marble sinks
|
||||
// into the maw instead of blinking out. The power-up icon runs its own
|
||||
// pulse tween, so it is toggled rather than alpha-driven.
|
||||
const vis = b.vis ?? 1;
|
||||
if (vis < 1 || spr.base.alpha < 1) {
|
||||
spr.base.setAlpha(vis);
|
||||
spr.gloss.setAlpha(vis);
|
||||
spr.icon?.setVisible(vis > 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [id, spr] of this.ballSprites) {
|
||||
|
|
@ -1128,13 +1169,13 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// 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
|
||||
// bonus already folded into `score`, and the win jingle lands right as the
|
||||
// chain reaches the edge. The soundtrack ducks for the whole sequence —
|
||||
// same trick as the level-start fanfare (see MusicPlayer.pause/resume).
|
||||
// last match landed down to that path's own pit, each cashing in a slice of
|
||||
// the time bonus already folded into `score`, and the win jingle lands right
|
||||
// as the chain reaches the edge. The soundtrack ducks for the whole sequence
|
||||
// — same trick as the level-start fanfare (see MusicPlayer.pause/resume).
|
||||
playVictoryFireworks(timeBonus, onDone) {
|
||||
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 span = Math.max(0, path.length - from);
|
||||
const T = TUNE;
|
||||
|
|
@ -1215,7 +1256,7 @@ export default class ZumaGame extends Phaser.Scene {
|
|||
this.layer.add(hint);
|
||||
}
|
||||
|
||||
const hasNext = this.level < this.bank.length;
|
||||
const hasNext = this.level < this.manifest.length;
|
||||
if (hasNext) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
// 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
|
||||
// bottomless pit; the player fires balls into the chain, popping runs of 3+.
|
||||
// The scene (or a headless script) drives all timing through step(); every
|
||||
// transition returns an ordered event list the renderer replays as FX.
|
||||
// A marble-shooter: one or more chains of colored balls roll along curved
|
||||
// paths toward their own bottomless pits; the player fires balls from one
|
||||
// shared frog into whichever chain a shot lands nearest to, popping runs of
|
||||
// 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
|
||||
// `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
|
||||
// than BALL_SPACING + GAP_EPS apart. Screen positions are cached on each ball
|
||||
// (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'];
|
||||
|
||||
// ── 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
|
||||
// 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
|
||||
|
|
@ -245,16 +255,19 @@ export function buildPath(points, step = 4) {
|
|||
// 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
|
||||
// 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 };
|
||||
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 path = buildPath(def.points);
|
||||
const need = def.quota * TUNING.BALL_SPACING * 1.6;
|
||||
const path = buildPath(pd.points);
|
||||
const need = pd.quota * TUNING.BALL_SPACING * 1.6;
|
||||
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;
|
||||
|
|
@ -262,20 +275,20 @@ export function validateLevel(def) {
|
|||
let minRadiusS = 0;
|
||||
let outOfBounds = null;
|
||||
for (let i = 0; i < path.samples.length; i++) {
|
||||
const p = path.samples[i];
|
||||
minFrog = Math.min(minFrog, Math.hypot(p.x - def.frog[0], p.y - def.frog[1]));
|
||||
if (!outOfBounds && p.s > LEADIN_S && (p.x < LEVEL_BOUNDS.x0 || p.x > LEVEL_BOUNDS.x1
|
||||
|| p.y < LEVEL_BOUNDS.y0 || p.y > LEVEL_BOUNDS.y1)) {
|
||||
outOfBounds = p;
|
||||
const s = path.samples[i];
|
||||
minFrog = Math.min(minFrog, Math.hypot(s.x - frog[0], s.y - frog[1]));
|
||||
if (!outOfBounds && s.s > LEADIN_S && (s.x < LEVEL_BOUNDS.x0 || s.x > LEVEL_BOUNDS.x1
|
||||
|| s.y < LEVEL_BOUNDS.y0 || s.y > LEVEL_BOUNDS.y1)) {
|
||||
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 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);
|
||||
if (l1 > 0.01 && l2 > 0.01) {
|
||||
const cos = Math.max(-1, Math.min(1, (v1x * v2x + v1y * v2y) / (l1 * l2)));
|
||||
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,
|
||||
// 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.
|
||||
const tunnels = normalizeTunnels(def.tunnels, path.length);
|
||||
if (Array.isArray(def.tunnels) && def.tunnels.length !== tunnels.length) {
|
||||
const tunnels = normalizeTunnels(pd.tunnels, path.length);
|
||||
if (Array.isArray(pd.tunnels) && pd.tunnels.length !== tunnels.length) {
|
||||
errs.push('a tunnel has exit <= enter');
|
||||
}
|
||||
let hiddenLen = 0;
|
||||
|
|
@ -325,34 +338,68 @@ export function validateLevel(def) {
|
|||
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) {
|
||||
const errs = [];
|
||||
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
|
||||
&& def.starScores[0] < def.starScores[1] && def.starScores[1] < def.starScores[2])) {
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Level / state construction ───────────────────────────────────────────────
|
||||
|
||||
export function createLevel(def, seed) {
|
||||
const path = buildPath(def.points);
|
||||
const paths = def.paths.map((pd, idx) => {
|
||||
const path = buildPath(pd.points);
|
||||
return {
|
||||
idx,
|
||||
path,
|
||||
tunnels: normalizeTunnels(pd.tunnels, path.length),
|
||||
quota: pd.quota,
|
||||
introBalls: pd.introBalls ?? 8,
|
||||
pushSpeed: pd.pushSpeed,
|
||||
powerUpRate: pd.powerUpRate ?? 0,
|
||||
spawned: 0,
|
||||
balls: [], // front-first: balls[0] has the largest s
|
||||
};
|
||||
});
|
||||
const state = {
|
||||
def,
|
||||
path,
|
||||
tunnels: normalizeTunnels(def.tunnels, path.length),
|
||||
paths,
|
||||
frog: { x: def.frog[0], y: def.frog[1] },
|
||||
quota: def.quota,
|
||||
spawned: 0,
|
||||
balls: [], // front-first: balls[0] has the largest s
|
||||
flights: [], // fired balls in screen space
|
||||
flights: [], // fired balls in screen space — shared by every path
|
||||
rng: makeRng((seed ?? def.seed ?? 1) >>> 0),
|
||||
status: 'intro', // 'intro' | 'playing' | 'won' | 'lost'
|
||||
score: 0,
|
||||
|
|
@ -372,9 +419,14 @@ function levelColor(state) {
|
|||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -420,26 +472,27 @@ export function findRun(balls, idx) {
|
|||
|
||||
// Screen positions, plus the two tunnel flags the renderer reads: `hidden` is
|
||||
// the gameplay truth (inert while submerged) and `vis` the cosmetic ramp that
|
||||
// sinks a marble into the maw instead of blinking it away.
|
||||
function syncPositions(state) {
|
||||
const tun = state.tunnels ?? [];
|
||||
for (const b of state.balls) {
|
||||
const p = state.path.pointAt(b.s);
|
||||
b.x = p.x; b.y = p.y;
|
||||
// sinks a marble into the maw instead of blinking it away. One path at a time.
|
||||
function syncPositions(p) {
|
||||
const tun = p.tunnels ?? [];
|
||||
for (const b of p.balls) {
|
||||
const pt = p.path.pointAt(b.s);
|
||||
b.x = pt.x; b.y = pt.y;
|
||||
b.hidden = tun.length ? isHidden(tun, b.s) : false;
|
||||
b.vis = tun.length ? visibilityAt(tun, b.s) : 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chain movement ────────────────────────────────────────────────────────────
|
||||
// Per tick: the rearmost (spawner-fed) segment drives forward; per gap, a
|
||||
// matching pair pulls the front side backward, a non-matching pair sends the
|
||||
// rear side forward to catch up. Reverse overrides everything backward.
|
||||
// Contacts merge implicitly (exact spacing); closed gaps clank + match-check.
|
||||
// Per path, per tick: the rearmost (spawner-fed) segment drives forward; per
|
||||
// gap, a matching pair pulls the front side backward, a non-matching pair
|
||||
// sends the rear side forward to catch up. Reverse overrides everything
|
||||
// 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 balls = state.balls;
|
||||
const balls = p.balls;
|
||||
if (!balls.length) return;
|
||||
const dt = dtMs / 1000;
|
||||
const now = state.elapsedMs;
|
||||
|
|
@ -449,8 +502,8 @@ function moveSegments(state, dtMs, events) {
|
|||
if (now < state.effects.reverseUntil) {
|
||||
vel.fill(-T.REVERSE_SPEED);
|
||||
} else {
|
||||
let base = state.def.pushSpeed;
|
||||
if (state.status === 'intro') base *= T.INTRO_SPEED_MULT;
|
||||
let base = p.pushSpeed;
|
||||
if (p.spawned < p.introBalls) base *= T.INTRO_SPEED_MULT;
|
||||
if (now < state.effects.slowUntil) base *= T.SLOW_MULT;
|
||||
vel[segs.length - 1] += base;
|
||||
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;
|
||||
if (gap > T.BALL_SPACING + T.GAP_EPS) continue;
|
||||
if (gap < T.BALL_SPACING) balls[fi + 1].s = balls[fi].s - T.BALL_SPACING;
|
||||
const p = state.path.pointAt(balls[fi].s);
|
||||
events.push({ type: 'clank', x: p.x, y: p.y });
|
||||
const pt = p.path.pointAt(balls[fi].s);
|
||||
events.push({ type: 'clank', pathIdx: p.idx, x: pt.x, y: pt.y });
|
||||
if (balls[fi].color === balls[fi + 1].color) {
|
||||
const run = findRun(balls, fi);
|
||||
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
||||
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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function spawnColor(state) {
|
||||
if (state.quota - state.spawned <= TUNING.LASTCALL_COUNT) {
|
||||
// Last-call colors are drawn from the whole board (every path), same as the
|
||||
// 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);
|
||||
if (present.size) return pickPresent(state, present);
|
||||
}
|
||||
return levelColor(state);
|
||||
}
|
||||
|
||||
function spawnBalls(state, events) {
|
||||
function spawnBalls(state, p, events) {
|
||||
const T = TUNING;
|
||||
while (state.spawned < state.quota) {
|
||||
const rear = state.balls[state.balls.length - 1];
|
||||
while (p.spawned < p.quota) {
|
||||
const rear = p.balls[p.balls.length - 1];
|
||||
if (rear && rear.s < T.BALL_SPACING) break;
|
||||
const color = spawnColor(state);
|
||||
const color = spawnColor(state, p);
|
||||
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)];
|
||||
}
|
||||
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);
|
||||
b.x = p.x; b.y = p.y;
|
||||
state.balls.push(b);
|
||||
state.spawned++;
|
||||
events.push({ type: 'spawn', id: b.id });
|
||||
if (state.status === 'intro' && state.spawned >= (state.def.introBalls ?? 8)) {
|
||||
state.status = 'playing';
|
||||
events.push({ type: 'ready' });
|
||||
}
|
||||
const pt = p.path.pointAt(b.s);
|
||||
b.x = pt.x; b.y = pt.y;
|
||||
p.balls.push(b);
|
||||
p.spawned++;
|
||||
events.push({ type: 'spawn', pathIdx: p.idx, id: b.id });
|
||||
}
|
||||
}
|
||||
|
||||
// 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';
|
||||
events.push({ type: 'ready' });
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 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)];
|
||||
let score = popped.length * T.SCORE_BALL * Math.max(1, state.combo);
|
||||
if (cause === 'chain') score += T.SCORE_CHAIN_BONUS;
|
||||
state.score += score;
|
||||
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,
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
|
|
@ -567,34 +635,34 @@ function applyPower(state, ball, events) {
|
|||
// 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
|
||||
// play, so the chain reaction cannot reach through the ground to them.
|
||||
const tun = state.tunnels ?? [];
|
||||
const tun = p.tunnels ?? [];
|
||||
const queue = [ball];
|
||||
while (queue.length) {
|
||||
const src = queue.shift();
|
||||
const caught = state.balls.filter(
|
||||
const caught = p.balls.filter(
|
||||
(b) => !(tun.length && isHidden(tun, b.s))
|
||||
&& Math.hypot(b.x - src.x, b.y - src.y) <= T.EXPLOSION_RADIUS
|
||||
);
|
||||
if (!caught.length) continue;
|
||||
const ids = new Set(caught.map((b) => b.id));
|
||||
// splice in place: callers hold references to state.balls across popRun
|
||||
for (let i = state.balls.length - 1; i >= 0; i--) {
|
||||
if (ids.has(state.balls[i].id)) state.balls.splice(i, 1);
|
||||
// splice in place: callers hold references to p.balls across popRun
|
||||
for (let i = p.balls.length - 1; i >= 0; i--) {
|
||||
if (ids.has(p.balls[i].id)) p.balls.splice(i, 1);
|
||||
}
|
||||
const score = caught.length * T.SCORE_BALL * Math.max(1, state.combo);
|
||||
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) {
|
||||
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) {
|
||||
if (!state.balls.length) return;
|
||||
const present = colorsPresent(state);
|
||||
if (!present.size) return;
|
||||
for (const slot of ['current', 'next']) {
|
||||
if (!present.has(state[slot])) {
|
||||
state[slot] = pickPresent(state, present);
|
||||
|
|
@ -630,13 +698,13 @@ export function swapBalls(state) {
|
|||
state.next = t;
|
||||
}
|
||||
|
||||
// Wedge a fired ball into the chain at hitIdx. side: +1 in front of the hit
|
||||
// ball (higher s), -1 behind. The front portion is shoved toward the hole —
|
||||
// shoves can slam segments together (clank + junction match) and can lose the
|
||||
// level by pushing the front ball into the pit.
|
||||
export function insertBall(state, color, hitIdx, side, events) {
|
||||
// Wedge a fired ball into path p's chain at hitIdx. side: +1 in front of the
|
||||
// hit ball (higher s), -1 behind. The front portion is shoved toward the
|
||||
// hole — shoves can slam segments together (clank + junction match) and can
|
||||
// lose the level by pushing the front ball into the pit.
|
||||
export function insertBall(state, p, color, hitIdx, side, events) {
|
||||
const T = TUNING;
|
||||
const balls = state.balls;
|
||||
const balls = p.balls;
|
||||
const hit = balls[hitIdx];
|
||||
let insertIdx, s, push = true;
|
||||
|
||||
|
|
@ -670,20 +738,20 @@ export function insertBall(state, color, hitIdx, side, events) {
|
|||
balls[i].s = minS;
|
||||
}
|
||||
}
|
||||
syncPositions(state);
|
||||
events.push({ type: 'inserted', id: ball.id, idx: insertIdx, x: ball.x, y: ball.y });
|
||||
syncPositions(p);
|
||||
events.push({ type: 'inserted', pathIdx: p.idx, id: ball.id, idx: insertIdx, x: ball.x, y: ball.y });
|
||||
|
||||
// shove-closed gaps
|
||||
for (const frontId of prevGaps) {
|
||||
const fi = balls.findIndex((b) => b.id === frontId);
|
||||
if (fi < 0 || fi + 1 >= balls.length) 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) {
|
||||
const run = findRun(balls, fi);
|
||||
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
||||
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);
|
||||
if (run.hi - run.lo + 1 >= T.MATCH_MIN) {
|
||||
state.combo = 1;
|
||||
popRun(state, run.lo, run.hi, 'shot', events);
|
||||
popRun(state, p, run.lo, run.hi, 'shot', events);
|
||||
} else {
|
||||
state.combo = 0;
|
||||
}
|
||||
|
|
@ -702,32 +770,39 @@ export function insertBall(state, color, hitIdx, side, 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) {
|
||||
const T = TUNING;
|
||||
const tun = state.tunnels ?? [];
|
||||
for (let f = state.flights.length - 1; f >= 0; f--) {
|
||||
const fl = state.flights[f];
|
||||
const dist = fl.speed * (dtMs / 1000);
|
||||
const steps = Math.max(1, Math.ceil(dist / T.BALL_RADIUS));
|
||||
const stepLen = dist / steps;
|
||||
let hitPath = null;
|
||||
let hitIdx = -1;
|
||||
for (let k = 0; k < steps && hitIdx < 0; k++) {
|
||||
fl.x += fl.dx * stepLen;
|
||||
fl.y += fl.dy * stepLen;
|
||||
let best = Infinity;
|
||||
for (let i = 0; i < state.balls.length; i++) {
|
||||
const b = state.balls[i];
|
||||
if (tun.length && isHidden(tun, b.s)) continue; // underground: shots pass over
|
||||
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; }
|
||||
for (const p of state.paths) {
|
||||
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
|
||||
const d = Math.hypot(fl.x - b.x, fl.y - b.y);
|
||||
if (d < T.BALL_SPACING * T.HIT_PAD && d < best) { best = d; hitPath = p; hitIdx = i; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hitIdx >= 0) {
|
||||
state.flights.splice(f, 1);
|
||||
const b = state.balls[hitIdx];
|
||||
const p = state.path.pointAt(b.s);
|
||||
const side = ((fl.x - b.x) * p.tx + (fl.y - b.y) * p.ty) >= 0 ? 1 : -1;
|
||||
insertBall(state, fl.color, hitIdx, side, events);
|
||||
const b = hitPath.balls[hitIdx];
|
||||
const pt = hitPath.path.pointAt(b.s);
|
||||
const side = ((fl.x - b.x) * pt.tx + (fl.y - b.y) * pt.ty) >= 0 ? 1 : -1;
|
||||
insertBall(state, hitPath, fl.color, hitIdx, side, events);
|
||||
} 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) {
|
||||
state.flights.splice(f, 1);
|
||||
|
|
@ -736,19 +811,22 @@ 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) {
|
||||
const T = TUNING;
|
||||
const tun = state.tunnels ?? [];
|
||||
const dx = Math.cos(angle), dy = Math.sin(angle);
|
||||
const max = Math.hypot(T.BOUNDS_W, T.BOUNDS_H);
|
||||
const stepLen = T.BALL_RADIUS / 2;
|
||||
let x = state.frog.x + dx * T.FROG_MUZZLE;
|
||||
let y = state.frog.y + dy * T.FROG_MUZZLE;
|
||||
for (let d = 0; d < max; d += stepLen) {
|
||||
for (const b of state.balls) {
|
||||
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 };
|
||||
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 (Math.hypot(x - b.x, y - b.y) < T.BALL_SPACING * T.HIT_PAD) return { x, y, hit: true };
|
||||
}
|
||||
}
|
||||
x += dx * stepLen;
|
||||
y += dy * stepLen;
|
||||
|
|
@ -758,20 +836,27 @@ export function rayHit(state, angle) {
|
|||
|
||||
// ── Win / lose ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Any one path's chain reaching its own pit loses the whole level.
|
||||
function checkLose(state, events) {
|
||||
if (state.status === 'won' || state.status === 'lost') return;
|
||||
const front = state.balls[0];
|
||||
if (front && front.s >= state.path.length - TUNING.HOLE_GRACE) {
|
||||
state.status = 'lost';
|
||||
events.push({ type: 'lost' });
|
||||
for (const p of state.paths) {
|
||||
const front = p.balls[0];
|
||||
if (front && front.s >= p.path.length - TUNING.HOLE_GRACE) {
|
||||
state.status = 'lost';
|
||||
events.push({ type: 'lost' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every path's chain must be gone, plus the shared shot queue empty.
|
||||
function checkWin(state, events) {
|
||||
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 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;
|
||||
state.score += timeBonus;
|
||||
state.status = 'won';
|
||||
|
|
@ -786,9 +871,10 @@ export function step(state, dtMs) {
|
|||
if (state.status === 'won' || state.status === 'lost') return events;
|
||||
const dt = Math.min(dtMs, TUNING.MAX_STEP_MS);
|
||||
state.elapsedMs += dt;
|
||||
moveSegments(state, dt, events);
|
||||
spawnBalls(state, events);
|
||||
syncPositions(state);
|
||||
for (const p of state.paths) moveSegments(state, p, dt, events);
|
||||
for (const p of state.paths) spawnBalls(state, p, events);
|
||||
checkReady(state, events);
|
||||
for (const p of state.paths) syncPositions(p);
|
||||
checkLose(state, events);
|
||||
if (state.status === 'lost') return 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('blockfighter', 'data/blockfighter.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('katamino', 'data/katamino.json');
|
||||
this.load.json('bookwork', 'data/bookwork.json');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@
|
|||
// refuses to write a bank the game would consider unplayable.
|
||||
//
|
||||
// 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
|
||||
// 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';
|
||||
|
||||
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.join(__dirname, '../data/zuma.json');
|
||||
: path.join(__dirname, '../assets/gamedata/zuma');
|
||||
|
||||
const rad = (deg) => (deg * Math.PI) / 180;
|
||||
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 tunnels = (tunFrac ?? []).map(([a, b]) => [Math.round(len * a), Math.round(len * b)]);
|
||||
const def = {
|
||||
level, name, shape, points, frog, tunnels, colors, quota, introBalls, pushSpeed, powerUpRate,
|
||||
level, name, frog, colors,
|
||||
background: BACKGROUNDS[(level - 1) % BACKGROUNDS.length],
|
||||
seed: 1000 + level * 7919,
|
||||
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 cap = Math.floor(length / (TUNING.BALL_SPACING * 1.6));
|
||||
if (errs.length) {
|
||||
|
|
@ -246,13 +255,18 @@ for (const [level, name, shape, colors, quota, introBalls, pushSpeed, powerUpRat
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fs.writeFileSync(OUT_FILE, JSON.stringify({
|
||||
generatedAt: new Date().toISOString(),
|
||||
count: levels.length,
|
||||
levels,
|
||||
}, null, 1));
|
||||
console.log(`\nWrote ${levels.length} levels to ${OUT_FILE}`);
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const manifest = { version: 1, levels: [] };
|
||||
for (const def of levels) {
|
||||
const file = `level-${String(def.level).padStart(3, '0')}.json`;
|
||||
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.
|
||||
// 8. Determinism (seeded replay).
|
||||
// 9. Tunnels: submerged chain is inert to flights, the laser and explosions.
|
||||
// 10. Level bank lint (data/zuma.json geometry + parameters).
|
||||
// 11. Aimbot soak: every banked level is winnable, and the star curve sits
|
||||
// 10. Multi-path levels: independent spawners, shared shooter color pool,
|
||||
// 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.
|
||||
|
||||
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.
|
||||
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 = {}) {
|
||||
const { points, tunnels, quota, introBalls, pushSpeed, powerUpRate, shape, paths, ...rest } = over;
|
||||
return {
|
||||
level: 1, name: 'Test', shape: 'line',
|
||||
points: STRAIGHT, frog: [800, 900],
|
||||
colors: 4, quota: 10, introBalls: 2,
|
||||
pushSpeed: 100, powerUpRate: 0, seed: 42,
|
||||
starScores: [100, 200, 300],
|
||||
...over,
|
||||
level: 1, name: 'Test',
|
||||
frog: [800, 900], colors: 4, seed: 42, starScores: [100, 200, 300],
|
||||
paths: paths ?? [{
|
||||
shape: shape ?? 'line',
|
||||
points: points ?? STRAIGHT,
|
||||
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 = {}) {
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Aimbot ───────────────────────────────────────────────────────────────────
|
||||
// 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) {
|
||||
const dx = Math.cos(angle), dy = Math.sin(angle);
|
||||
const stepLen = T.BALL_RADIUS / 2;
|
||||
|
|
@ -77,34 +99,38 @@ function firstHit(st, angle) {
|
|||
let x = st.frog.x + dx * T.FROG_MUZZLE;
|
||||
let y = st.frog.y + dy * T.FROG_MUZZLE;
|
||||
for (let d = 0; d < max; d += stepLen) {
|
||||
for (const b of st.balls) {
|
||||
if (isHidden(st.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 b;
|
||||
for (const p of st.paths) {
|
||||
for (const b of p.balls) {
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rank every reachable ball: landing between two of the held colour beats
|
||||
// landing beside one, which beats landing on a bare match.
|
||||
// Rank every reachable ball across every path: landing between two of the
|
||||
// held colour beats landing beside one, which beats landing on a bare match.
|
||||
function chooseShot(st) {
|
||||
let best = null;
|
||||
for (const target of st.balls) {
|
||||
if (isHidden(st.tunnels, target.s)) continue;
|
||||
const angle = Math.atan2(target.y - st.frog.y, target.x - st.frog.x);
|
||||
const hit = firstHit(st, angle);
|
||||
if (!hit) continue;
|
||||
const i = st.balls.indexOf(hit);
|
||||
const prev = st.balls[i - 1], next = st.balls[i + 1];
|
||||
let score = 0;
|
||||
if (hit.color === st.current) {
|
||||
score += 5;
|
||||
if (prev?.color === st.current) score += 10;
|
||||
if (next?.color === st.current) score += 10;
|
||||
for (const p of st.paths) {
|
||||
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 hit = firstHit(st, angle);
|
||||
if (!hit) continue;
|
||||
const i = hit.path.balls.indexOf(hit.ball);
|
||||
const prev = hit.path.balls[i - 1], next = hit.path.balls[i + 1];
|
||||
let score = 0;
|
||||
if (hit.ball.color === st.current) {
|
||||
score += 5;
|
||||
if (prev?.color === st.current) score += 10;
|
||||
if (next?.color === st.current) score += 10;
|
||||
}
|
||||
if (prev?.color === st.current && next?.color === st.current) score += 8;
|
||||
if (score > 0 && (!best || score > best.score)) best = { angle, score };
|
||||
}
|
||||
if (prev?.color === st.current && next?.color === st.current) score += 8;
|
||||
if (score > 0 && (!best || score > best.score)) best = { angle, score };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
|
@ -126,14 +152,16 @@ function aimbotPlay(def, seed) {
|
|||
return { status: st.status, score: st.score };
|
||||
}
|
||||
|
||||
// spec: [{ color, s, power? }] front-first (descending s)
|
||||
function mkChain(st, spec) {
|
||||
st.balls = spec.map((b) => ({
|
||||
// spec: [{ color, s, power? }] front-first (descending s). Defaults to
|
||||
// paths[0] — every pre-multi-path fixture is single-path.
|
||||
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,
|
||||
}));
|
||||
for (const b of st.balls) {
|
||||
const p = st.path.pointAt(b.s);
|
||||
b.x = p.x; b.y = p.y;
|
||||
for (const b of p.balls) {
|
||||
const pt = p.path.pointAt(b.s);
|
||||
b.x = pt.x; b.y = pt.y;
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
|
@ -178,18 +206,19 @@ console.log('\n— Advance & spawning —');
|
|||
let introSpawned = -1;
|
||||
for (let i = 0; i < 4000 && st.status === 'intro'; i++) {
|
||||
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}`);
|
||||
for (let i = 0; i < 4000 && st.spawned < 8; i++) step(st, 16);
|
||||
check('spawn stops at quota', st.spawned === 8 && st.balls.length === 8);
|
||||
check('spacing invariant after spawning', spacingOk(st.balls));
|
||||
for (let i = 0; i < 4000 && st.paths[0].spawned < 8; i++) step(st, 16);
|
||||
check('spawn stops at quota', st.paths[0].spawned === 8 && st.paths[0].balls.length === 8);
|
||||
check('spacing invariant after spawning', spacingOk(st.paths[0].balls));
|
||||
|
||||
const st2 = mkState({}, { status: 'playing', spawned: 10 });
|
||||
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
|
||||
check('single segment drives at pushSpeed', near(st2.balls[0].s, 600 + 2 * SP + 100, 1), `got ${st2.balls[0].s.toFixed(1)}`);
|
||||
check('spacing preserved while driving', spacingOk(st2.balls));
|
||||
check('single segment drives at pushSpeed', near(st2.paths[0].balls[0].s, 600 + 2 * SP + 100, 1),
|
||||
`got ${st2.paths[0].balls[0].s.toFixed(1)}`);
|
||||
check('spacing preserved while driving', spacingOk(st2.paths[0].balls));
|
||||
}
|
||||
|
||||
// ── 3. Insertion ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -202,37 +231,39 @@ console.log('\n— Insertion —');
|
|||
);
|
||||
|
||||
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',
|
||||
st.balls[2].color === 3 && near(st.balls[2].s, 600 - SP, 0.01) && near(st.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 keeps spacing', spacingOk(st.balls) && st.balls.length === 6);
|
||||
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(balls[0].s, 600 + SP, 0.01) && near(balls[1].s, 600, 0.01));
|
||||
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'));
|
||||
|
||||
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',
|
||||
st.balls[3].color === 3 && near(st.balls[3].s, 600 - 2 * SP, 0.01) && near(st.balls[2].s, 600 - SP, 0.01));
|
||||
check('behind insert keeps spacing', spacingOk(st.balls));
|
||||
st.paths[0].balls[3].color === 3 && near(st.paths[0].balls[3].s, 600 - 2 * SP, 0.01)
|
||||
&& near(st.paths[0].balls[2].s, 600 - SP, 0.01));
|
||||
check('behind insert keeps spacing', spacingOk(st.paths[0].balls));
|
||||
|
||||
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',
|
||||
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)
|
||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||
[{ color: 0, s: 900 }, { color: 1, s: 900 - SP }, { color: 2, s: 900 - 2.5 * SP },
|
||||
{ color: 3, s: 900 - 3.5 * SP }]);
|
||||
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 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'));
|
||||
|
||||
// laser-sight ray helper
|
||||
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));
|
||||
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 }),
|
||||
[{ color: 0, s: 600 }, { color: 0, s: 600 - SP }, { color: 1, s: 600 - 2 * SP }, { color: 1, s: 600 - 3 * SP }]);
|
||||
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');
|
||||
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('shot pop sets combo to 1', pop?.combo === 1);
|
||||
|
||||
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 }]);
|
||||
ev = [];
|
||||
insertBall(st, 0, 1, -1, ev);
|
||||
insertBall(st, st.paths[0], 0, 1, -1, ev);
|
||||
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);
|
||||
|
||||
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 }]);
|
||||
ev = [];
|
||||
insertBall(st, 0, 1, +1, ev);
|
||||
check('runs never cross a gap', !ev.some((e) => e.type === 'pop') && st.balls.length === 4);
|
||||
insertBall(st, st.paths[0], 0, 1, +1, ev);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +307,7 @@ console.log('\n— Pull-back & catch-up —');
|
|||
let popEv = null, clankSeen = false, retreated = false;
|
||||
for (let i = 0; i < 200 && !popEv; i++) {
|
||||
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;
|
||||
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'
|
||||
&& popEv?.score === 3 * T.SCORE_BALL + T.SCORE_CHAIN_BONUS);
|
||||
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
|
||||
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 }]);
|
||||
let clankAt = null;
|
||||
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);
|
||||
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),
|
||||
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('no pop on non-matching junction', st.balls.length === 4);
|
||||
const before = st.balls[0].s;
|
||||
check('no pop on non-matching junction', st.paths[0].balls.length === 4);
|
||||
const before = st.paths[0].balls[0].s;
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -312,30 +343,30 @@ console.log('\n— Power-ups —');
|
|||
let st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||
[{ color: 0, s: 800, power: 'slow' }, { color: 1, s: 800 - 3 * SP }]);
|
||||
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
|
||||
&& 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);
|
||||
check('slow halves the drive (SLOW_MULT)', near(st.balls[0].s - s0, 100 * T.SLOW_MULT, 1.5),
|
||||
`moved ${(st.balls[0].s - s0).toFixed(1)}`);
|
||||
check('slow halves the drive (SLOW_MULT)', near(st.paths[0].balls[0].s - s0, 100 * T.SLOW_MULT, 1.5),
|
||||
`moved ${(st.paths[0].balls[0].s - s0).toFixed(1)}`);
|
||||
|
||||
// reverse: chain rolls backward, then resumes forward when expired
|
||||
// (elapsedMs increments before the effect check, so 501 covers exactly 10 ticks)
|
||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }), [{ color: 0, s: 800 }]);
|
||||
st.effects.reverseUntil = st.elapsedMs + 501;
|
||||
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),
|
||||
`at ${st.balls[0].s.toFixed(1)}`);
|
||||
check('reverse rolls the chain backward', near(st.paths[0].balls[0].s, 800 - T.REVERSE_SPEED * 0.5, 2),
|
||||
`at ${st.paths[0].balls[0].s.toFixed(1)}`);
|
||||
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),
|
||||
`at ${st.balls[0].s.toFixed(1)}`);
|
||||
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.paths[0].balls[0].s.toFixed(1)}`);
|
||||
|
||||
// accuracy: flag set on pop; fired flights move faster
|
||||
st = mkChain(mkState({}, { status: 'playing', spawned: 10 }),
|
||||
[{ color: 0, s: 800, power: 'accuracy' }, { color: 1, s: 800 - 3 * SP }]);
|
||||
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);
|
||||
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));
|
||||
|
|
@ -347,17 +378,17 @@ console.log('\n— Power-ups —');
|
|||
{ color: 3, s: 900 - 5 * SP }, { color: 3, s: 900 - 6 * SP },
|
||||
]);
|
||||
ev = [];
|
||||
popRun(st, 2, 2, 'shot', ev);
|
||||
popRun(st, st.paths[0], 2, 2, 'shot', ev);
|
||||
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 spares balls beyond radius', st.balls.every((b) => b.color === 3));
|
||||
check('explosion pops everything in radius', !!boom && boom.ids.length === 4 && st.paths[0].balls.length === 2);
|
||||
check('explosion spares balls beyond radius', st.paths[0].balls.every((b) => b.color === 3));
|
||||
}
|
||||
|
||||
// ── 7. State machine ─────────────────────────────────────────────────────────
|
||||
console.log('\n— State machine —');
|
||||
{
|
||||
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;
|
||||
for (let i = 0; i < 10 && st.status === 'playing'; i++) {
|
||||
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('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 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;
|
||||
|
|
@ -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 }]);
|
||||
st.current = 0; st.next = 0;
|
||||
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
|
||||
&& ev2.filter((e) => e.type === 'recolor').length === 2);
|
||||
|
||||
// last-call: final spawns only deal colors still on the board
|
||||
st = mkChain(mkState({}, { status: 'playing', spawned: 5 }),
|
||||
[{ 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',
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
|
@ -414,7 +445,10 @@ console.log('\n— Determinism —');
|
|||
if (tick === 300) fireBall(st, -Math.PI / 2);
|
||||
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();
|
||||
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 });
|
||||
mkChain(st, [{ color: 0, s: 600 }]);
|
||||
shootAt(st, st.balls[0]);
|
||||
check('a shot flies over a submerged ball', runFlights(st) && st.balls.length === 1);
|
||||
shootAt(st, st.paths[0].balls[0]);
|
||||
check('a shot flies over a submerged ball', runFlights(st) && st.paths[0].balls.length === 1);
|
||||
|
||||
st = mkState({ pushSpeed: 0 }, { status: 'playing', spawned: 10 });
|
||||
mkChain(st, [{ color: 0, s: 600 }]);
|
||||
shootAt(st, st.balls[0]);
|
||||
check('the same shot lands once the tunnel is gone', !runFlights(st) && st.balls.length === 2);
|
||||
shootAt(st, st.paths[0].balls[0]);
|
||||
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 });
|
||||
mkChain(st, [{ color: 0, s: 600 }]);
|
||||
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('the laser sight ignores submerged chain',
|
||||
!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);
|
||||
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');
|
||||
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}`);
|
||||
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
|
||||
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 }]);
|
||||
for (let i = 0; i < 40; i++) step(st, 50); // 2s at pushSpeed 100
|
||||
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)}`);
|
||||
check('every ball inside is submerged', st.balls.every((b) => b.hidden === (b.s > 400)));
|
||||
near(st.paths[0].balls[0].s, 500, 1) && spacingOk(st.paths[0].balls),
|
||||
`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 ──
|
||||
// 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);
|
||||
}
|
||||
|
||||
// ── 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 —');
|
||||
{
|
||||
let bank = null;
|
||||
const bankDir = join(__dirname, '../assets/gamedata/zuma');
|
||||
let manifest = null;
|
||||
try {
|
||||
bank = JSON.parse(readFileSync(join(__dirname, '../data/zuma.json'), 'utf8'));
|
||||
manifest = JSON.parse(readFileSync(join(bankDir, 'levels.json'), 'utf8'));
|
||||
} catch (_) { /* handled below */ }
|
||||
check('bank exists (run genZuma.js)', !!bank);
|
||||
if (bank) {
|
||||
const levels = bank.levels ?? [];
|
||||
check('manifest exists (run genZuma.js)', !!manifest);
|
||||
let 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('levels numbered 1..N contiguously', levels.every((l, i) => l.level === i + 1));
|
||||
// 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)`,
|
||||
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`
|
||||
+ ` (${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
|
||||
// clear a level, a player can. This is what catches a quota the path can
|
||||
// technically hold but nobody could actually survive.
|
||||
|
|
@ -582,6 +708,7 @@ console.log('\n— Level bank —');
|
|||
console.log('\n— Aimbot soak —');
|
||||
const SEEDS = 8;
|
||||
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 runs = [];
|
||||
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})`);
|
||||
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)}`);
|
||||
// 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue