Level Adjustments
This commit is contained in:
parent
afd3dc9466
commit
58d51503eb
|
|
@ -4,7 +4,7 @@
|
|||
"width": 1200,
|
||||
"height": 900
|
||||
},
|
||||
"orangeCount": 25,
|
||||
"orangeCount": 15,
|
||||
"greenCount": 2,
|
||||
"pegs": [
|
||||
{
|
||||
|
|
@ -349,41 +349,40 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 20,
|
||||
"y": 530
|
||||
},
|
||||
{
|
||||
"x": 350,
|
||||
"y": 720
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
160
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 320,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 460,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 740,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 880,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
"anchors": [
|
||||
{
|
||||
"x": 1180,
|
||||
"y": 510
|
||||
},
|
||||
{
|
||||
"x": 890,
|
||||
"y": 710
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-161
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
|
|
|
|||
|
|
@ -168,13 +168,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 705,
|
||||
"y": 652,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 680,
|
||||
|
|
@ -182,13 +175,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 495,
|
||||
"y": 652,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 418,
|
||||
"y": 575,
|
||||
|
|
@ -258,43 +244,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 380,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 490,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 710,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 820,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 380,
|
||||
"y": 600
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 600
|
||||
},
|
||||
{
|
||||
"x": 820,
|
||||
"y": 600
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
55,
|
||||
55
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,13 +210,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 822,
|
||||
"y": 676,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 736,
|
||||
"y": 704,
|
||||
|
|
@ -224,20 +217,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 646,
|
||||
"y": 718,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 554,
|
||||
"y": 718,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 464,
|
||||
"y": 704,
|
||||
|
|
@ -245,13 +224,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 378,
|
||||
"y": 676,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 296,
|
||||
"y": 636,
|
||||
|
|
@ -321,36 +293,36 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 420,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 540,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 660,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 780,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 180,
|
||||
"y": 640
|
||||
},
|
||||
{
|
||||
"x": 460,
|
||||
"y": 660
|
||||
},
|
||||
{
|
||||
"x": 740,
|
||||
"y": 660
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 640
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
70,
|
||||
70,
|
||||
70
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,13 +77,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 699,
|
||||
"y": 717,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 662,
|
||||
"y": 660,
|
||||
|
|
@ -140,13 +133,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 640,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 710,
|
||||
|
|
@ -182,62 +168,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 247,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 333,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 420,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 780,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 867,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 953,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 340,
|
||||
|
|
@ -268,5 +198,33 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 200,
|
||||
"y": 690
|
||||
},
|
||||
{
|
||||
"x": 490,
|
||||
"y": 710
|
||||
},
|
||||
{
|
||||
"x": 780,
|
||||
"y": 710
|
||||
},
|
||||
{
|
||||
"x": 1050,
|
||||
"y": 690
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-55,
|
||||
55,
|
||||
-55
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,20 +14,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 898,
|
||||
"y": 326,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 845,
|
||||
"y": 381,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 782,
|
||||
"y": 424,
|
||||
|
|
@ -70,20 +56,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 355,
|
||||
"y": 381,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 302,
|
||||
"y": 326,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 261,
|
||||
"y": 263,
|
||||
|
|
@ -321,50 +293,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 200,
|
||||
"y": 250,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 250,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 520,
|
||||
"y": 250,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 250,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 250,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 250,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 280,
|
||||
"y": 330
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 330
|
||||
},
|
||||
{
|
||||
"x": 920,
|
||||
"y": 330
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,48 +217,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 220,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 372,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 524,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 676,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 828,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 980,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 620,
|
||||
|
|
@ -366,5 +324,28 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 180,
|
||||
"y": 300
|
||||
},
|
||||
{
|
||||
"x": 490,
|
||||
"y": 260
|
||||
},
|
||||
{
|
||||
"x": 800,
|
||||
"y": 300
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-55,
|
||||
-45
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,20 +203,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 907,
|
||||
"y": 704,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 853,
|
||||
"y": 704,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 809,
|
||||
"y": 672,
|
||||
|
|
@ -342,29 +328,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 150,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 250,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 350,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 200,
|
||||
"y": 720
|
||||
},
|
||||
{
|
||||
"x": 620,
|
||||
"y": 730
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 720
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
35,
|
||||
-35
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,20 +56,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 950,
|
||||
"y": 692,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 890,
|
||||
"y": 692,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 860,
|
||||
"y": 640,
|
||||
|
|
@ -196,20 +182,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 310,
|
||||
"y": 692,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 250,
|
||||
"y": 692,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 220,
|
||||
"y": 640,
|
||||
|
|
@ -287,62 +259,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 345,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 450,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 555,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 660,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 765,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 870,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 975,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 560,
|
||||
"y": 210,
|
||||
|
|
@ -359,5 +275,28 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 240,
|
||||
"y": 700
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 720
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 700
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-55,
|
||||
-55
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,27 +21,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 685,
|
||||
"y": 553,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 618,
|
||||
"y": 579,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 547,
|
||||
"y": 570,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 488,
|
||||
"y": 529,
|
||||
|
|
@ -154,13 +133,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 285,
|
||||
"y": 640,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 261,
|
||||
"y": 691,
|
||||
|
|
@ -182,27 +154,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 161,
|
||||
"y": 612,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 206,
|
||||
"y": 577,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 261,
|
||||
"y": 589,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1045,
|
||||
"y": 640,
|
||||
|
|
@ -231,27 +182,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 921,
|
||||
"y": 612,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 966,
|
||||
"y": 577,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1021,
|
||||
"y": 589,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 300,
|
||||
|
|
@ -293,36 +223,36 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 430,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 543,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 657,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 770,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 150,
|
||||
"y": 580
|
||||
},
|
||||
{
|
||||
"x": 450,
|
||||
"y": 600
|
||||
},
|
||||
{
|
||||
"x": 750,
|
||||
"y": 600
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 580
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
55,
|
||||
-55,
|
||||
55
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,34 +217,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 140,
|
||||
"y": 500,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 500,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 500,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1060,
|
||||
"y": 500,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 430,
|
||||
"y": 780,
|
||||
|
|
@ -275,5 +247,39 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 120,
|
||||
"y": 330
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 330
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 900,
|
||||
"y": 330
|
||||
},
|
||||
{
|
||||
"x": 1080,
|
||||
"y": 330
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,34 +238,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 852,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 796,
|
||||
"y": 779,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 404,
|
||||
"y": 779,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 348,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 299,
|
||||
"y": 693,
|
||||
|
|
@ -286,50 +258,52 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 350,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 460,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 740,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 850,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 240,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 240,
|
||||
"y": 240
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 205
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 240
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-30,
|
||||
-30
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 250,
|
||||
"y": 720
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 740
|
||||
},
|
||||
{
|
||||
"x": 950,
|
||||
"y": 720
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45,
|
||||
45
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,20 +77,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 726,
|
||||
"y": 660,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 678,
|
||||
"y": 692,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 625,
|
||||
"y": 712,
|
||||
|
|
@ -210,34 +196,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 180,
|
||||
"y": 660,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 260,
|
||||
"y": 693,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 340,
|
||||
"y": 727,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 420,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 760,
|
||||
"y": 220,
|
||||
|
|
@ -268,5 +226,28 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 150,
|
||||
"y": 640
|
||||
},
|
||||
{
|
||||
"x": 450,
|
||||
"y": 690
|
||||
},
|
||||
{
|
||||
"x": 750,
|
||||
"y": 670
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45,
|
||||
-40
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,27 +77,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 500,
|
||||
"y": 247,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 579,
|
||||
"y": 221,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 662,
|
||||
"y": 230,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 734,
|
||||
"y": 271,
|
||||
|
|
@ -237,50 +216,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 320,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 880,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 300,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 200,
|
||||
"y": 250
|
||||
},
|
||||
{
|
||||
"x": 500,
|
||||
"y": 230
|
||||
},
|
||||
{
|
||||
"x": 800,
|
||||
"y": 250
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-40,
|
||||
-40
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,13 +182,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 970,
|
||||
"y": 698,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 918,
|
||||
"y": 660,
|
||||
|
|
@ -301,34 +294,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 200,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 293,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 387,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 650,
|
||||
"y": 220,
|
||||
|
|
@ -359,5 +324,28 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 180,
|
||||
"y": 710
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 740
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 710
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45,
|
||||
45
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,20 +245,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 720,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 900,
|
||||
"y": 720,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 780,
|
||||
|
|
@ -282,5 +268,39 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 380
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 700
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-65
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 380
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 700
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
65
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,13 +196,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 730,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 520,
|
||||
"y": 717,
|
||||
|
|
@ -350,34 +343,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 805,
|
||||
"y": 748,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 728,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 472,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 395,
|
||||
"y": 748,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 332,
|
||||
"y": 687,
|
||||
|
|
@ -433,36 +398,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 280,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 920,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 740,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 720
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 750
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 720
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45,
|
||||
45
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,13 +210,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 503,
|
||||
"y": 236,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 669,
|
||||
"y": 304,
|
||||
|
|
@ -224,13 +217,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 697,
|
||||
"y": 236,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 766,
|
||||
"y": 401,
|
||||
|
|
@ -261,5 +247,28 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 350,
|
||||
"y": 250
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 215
|
||||
},
|
||||
{
|
||||
"x": 850,
|
||||
"y": 250
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-35,
|
||||
-35
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,50 +265,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 200,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 320,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 440,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 760,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 880,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 200,
|
||||
"y": 680
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 720
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 680
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,13 +63,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 350,
|
||||
"y": 230,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 375,
|
||||
"y": 312,
|
||||
|
|
@ -119,13 +112,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 560,
|
||||
"y": 230,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 585,
|
||||
"y": 312,
|
||||
|
|
@ -175,13 +161,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 770,
|
||||
"y": 230,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 795,
|
||||
"y": 312,
|
||||
|
|
@ -231,13 +210,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 980,
|
||||
"y": 230,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1005,
|
||||
"y": 312,
|
||||
|
|
@ -286,43 +258,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 180,
|
||||
"y": 200,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 516,
|
||||
"y": 200,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 684,
|
||||
"y": 200,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 852,
|
||||
"y": 200,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 200,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 180,
|
||||
"y": 230
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 200
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 230
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-25,
|
||||
-25
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,13 +203,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 770,
|
||||
"y": 764,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 470,
|
||||
"y": 695,
|
||||
|
|
@ -217,13 +210,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 430,
|
||||
"y": 764,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 340,
|
||||
"y": 470,
|
||||
|
|
@ -314,22 +300,31 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 543,
|
||||
"y": 780,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 657,
|
||||
"y": 780,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 300,
|
||||
"y": 720
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 750
|
||||
},
|
||||
{
|
||||
"x": 900,
|
||||
"y": 720
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
35,
|
||||
35
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,71 +237,52 @@
|
|||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 745,
|
||||
"y": 675,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 895,
|
||||
"y": 675,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1045,
|
||||
"y": 675,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 336,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 512,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 688,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 864,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 210,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 240
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 205
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 240
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-30,
|
||||
-30
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 700
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 730
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 700
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
45,
|
||||
-45
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,20 +133,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 475,
|
||||
"y": 700,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 570,
|
||||
"y": 700,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 660,
|
||||
"y": 480,
|
||||
|
|
@ -196,84 +182,42 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 940,
|
||||
"y": 700,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1035,
|
||||
"y": 700,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1130,
|
||||
"y": 700,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 260,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 460,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 740,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 940,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 760,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": false
|
||||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 710
|
||||
},
|
||||
{
|
||||
"x": 450,
|
||||
"y": 730
|
||||
},
|
||||
{
|
||||
"x": 740,
|
||||
"y": 730
|
||||
},
|
||||
{
|
||||
"x": 1030,
|
||||
"y": 710
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
40,
|
||||
-40,
|
||||
40
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,5 +317,39 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 170,
|
||||
"y": 300
|
||||
},
|
||||
{
|
||||
"x": 170,
|
||||
"y": 620
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-60
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 1030,
|
||||
"y": 300
|
||||
},
|
||||
{
|
||||
"x": 1030,
|
||||
"y": 620
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
60
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,34 +273,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 580,
|
||||
"y": 400,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 400,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 580,
|
||||
"y": 530,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 530,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 830,
|
||||
"y": 400,
|
||||
|
|
@ -331,5 +303,23 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 220,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"x": 980,
|
||||
"y": 760
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
25
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,20 +63,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 180,
|
||||
"y": 370,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 370,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 420,
|
||||
"y": 370,
|
||||
|
|
@ -105,20 +91,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 900,
|
||||
"y": 370,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 370,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 180,
|
||||
"y": 490,
|
||||
|
|
@ -175,13 +147,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 180,
|
||||
"y": 610,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 300,
|
||||
"y": 610,
|
||||
|
|
@ -224,13 +189,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1020,
|
||||
"y": 610,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 180,
|
||||
"y": 730,
|
||||
|
|
@ -287,286 +245,6 @@
|
|||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 720,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1080,
|
||||
"y": 310,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 720,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1080,
|
||||
"y": 430,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 720,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1080,
|
||||
"y": 550,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 720,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1080,
|
||||
"y": 670,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 240,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 360,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 720,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 840,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 1080,
|
||||
"y": 790,
|
||||
"shape": "circle",
|
||||
"r": 16,
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 210,
|
||||
|
|
@ -625,5 +303,49 @@
|
|||
}
|
||||
],
|
||||
"walls": [],
|
||||
"moving": []
|
||||
"moving": [],
|
||||
"curves": [
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 350
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 410
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 350
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"orangeEligible": true
|
||||
},
|
||||
{
|
||||
"anchors": [
|
||||
{
|
||||
"x": 160,
|
||||
"y": 610
|
||||
},
|
||||
{
|
||||
"x": 600,
|
||||
"y": 550
|
||||
},
|
||||
{
|
||||
"x": 1040,
|
||||
"y": 610
|
||||
}
|
||||
],
|
||||
"bends": [
|
||||
-50,
|
||||
-50
|
||||
],
|
||||
"orangeEligible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,19 @@ level of each friend's block in `levels.json`).
|
|||
- The intro shows on every fresh entry to that level (level select or "Next
|
||||
Level") but is skipped on Replay / Try Again.
|
||||
|
||||
## Brick curves
|
||||
|
||||
Wavy lines of peg-like brick sections (the classic Peggle curved walls).
|
||||
Authored in the editor with the **Brick Curve** tool; stored in the level file
|
||||
as a `curves` array (`anchors` + per-segment `bends` + `orangeEligible`) and
|
||||
tessellated into 36×22 brick sections at load. Each brick behaves exactly
|
||||
like a peg — same color pool (blue/orange/green/purple), scoring, fade, and
|
||||
fever sweep.
|
||||
|
||||
Bricks are **procedurally drawn** (not in the sprite sheet): color fill with a
|
||||
dark outer stroke and a light inner inset stroke, using the same palette as
|
||||
the pegs. The reserved sheet frames 9–15 could skin them later if wanted.
|
||||
|
||||
## Not covered by the sheet
|
||||
|
||||
- **Level editor** (`?peggle-editor=1`) intentionally keeps procedural circles —
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { TUNING, POWERS } from './PeggleLogic.js';
|
||||
import { TUNING, POWERS, tessellateCurves } from './PeggleLogic.js';
|
||||
|
||||
// Secret level editor — reached only via index.html?peggle-editor=1.
|
||||
// Board occupies the same logical 1200×900 space, at the same screen offset,
|
||||
|
|
@ -27,7 +27,7 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
level: 1, name: 'New Level', masterId: 'ethel', powerId: 'superguide',
|
||||
orangeCount: 25, greenCount: 2, background: '', file: 'level-001.json',
|
||||
};
|
||||
this.mode = 'place'; // place | line | arc
|
||||
this.mode = 'place'; // place | line | arc | curve
|
||||
this.snap = true;
|
||||
this.pending = null; // first click of a stamper
|
||||
this.dragIdx = -1;
|
||||
|
|
@ -36,6 +36,14 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
this.pegImages = [];
|
||||
this.stampCount = 8;
|
||||
this.stampSpan = 180;
|
||||
|
||||
// Brick curves: { anchors:[{x,y}], bends:[px per segment], orangeEligible }
|
||||
this.curves = [];
|
||||
this.selCurve = -1; // selected curve index
|
||||
this.drawingCurve = null; // anchors being placed by the Curve tool
|
||||
this.dragAnchor = null; // { curve, idx }
|
||||
this.dragBend = null; // { curve, seg }
|
||||
this.lastClick = { t: -9999, x: 0, y: 0 };
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -65,12 +73,17 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
const saved = this.registry.get('peggle-editor-state');
|
||||
if (saved) {
|
||||
this.pegs = saved.pegs.map((p) => ({ ...p }));
|
||||
this.curves = (saved.curves ?? []).map((c) => ({
|
||||
anchors: c.anchors.map((a) => ({ ...a })), bends: [...c.bends], orangeEligible: !!c.orangeEligible,
|
||||
}));
|
||||
this.meta = { ...saved.meta };
|
||||
}
|
||||
}
|
||||
|
||||
this.drawChrome();
|
||||
this.brickG = this.add.graphics().setDepth(D.peg - 1);
|
||||
this.pegLayer = this.add.container(0, 0).setDepth(D.peg);
|
||||
this.handleG = this.add.graphics().setDepth(D.marker + 1);
|
||||
this.markerG = this.add.graphics().setDepth(D.marker);
|
||||
this.buildToolbar();
|
||||
this.bindPointer();
|
||||
|
|
@ -124,7 +137,7 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
this.modeBtns = {};
|
||||
for (const [mode, label] of [['place', 'Place / Move'], ['line', 'Line Stamp'], ['arc', 'Arc Stamp']]) {
|
||||
for (const [mode, label] of [['place', 'Place / Move'], ['line', 'Line Stamp'], ['arc', 'Arc Stamp'], ['curve', 'Brick Curve']]) {
|
||||
this.modeBtns[mode] = new Button(this, cx, y, label, () => this.setMode(mode),
|
||||
{ width: 260, height: 54, fontSize: 20, variant: mode === this.mode ? 'solid' : 'ghost' });
|
||||
y += step;
|
||||
|
|
@ -140,7 +153,14 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
new Button(this, cx, y, 'Undo (Ctrl+Z)', () => this.undo(),
|
||||
{ width: 260, height: 54, fontSize: 20, variant: 'ghost' });
|
||||
y += step;
|
||||
new Button(this, cx, y, 'Clear Board', () => { this.pushUndo(); this.pegs = []; this.renderPegs(); },
|
||||
new Button(this, cx, y, 'Clear Board', () => {
|
||||
this.pushUndo();
|
||||
this.pegs = [];
|
||||
this.curves = [];
|
||||
this.selCurve = -1;
|
||||
this.drawingCurve = null;
|
||||
this.renderPegs();
|
||||
},
|
||||
{ width: 260, height: 54, fontSize: 20, variant: 'ghost', textColor: COLORS.dangerHex });
|
||||
y += step + 20;
|
||||
|
||||
|
|
@ -155,8 +175,8 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
y += step + 14;
|
||||
|
||||
this.add.text(cx, y + 4,
|
||||
'Left-click: place / drag\nRight-click: delete\nE: toggle orange-eligible\nEsc: cancel stamp', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex, align: 'center', lineSpacing: 6,
|
||||
'Left-click: place / drag · Right-click: delete\nE: toggle orange-eligible · Esc: cancel\n\nBrick Curve: click anchors, dbl-click/Enter\nto finish. Click bricks to select: drag ◆\nanchors, drag ● to bend, dbl-click ● to\nadd an anchor, Del removes the curve.', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, align: 'center', lineSpacing: 5,
|
||||
}).setOrigin(0.5, 0).setDepth(D.ui);
|
||||
|
||||
new Button(this, cx, GAME_HEIGHT - 60, 'Exit Editor', () => { window.location.search = ''; },
|
||||
|
|
@ -273,6 +293,13 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
this.pegs = (layout.pegs ?? []).map((p) => ({
|
||||
x: p.x, y: p.y, r: p.r ?? TUNING.PEG_R, orangeEligible: !!p.orangeEligible,
|
||||
}));
|
||||
this.curves = (layout.curves ?? []).map((c) => ({
|
||||
anchors: (c.anchors ?? []).map((a) => ({ x: a.x, y: a.y })),
|
||||
bends: [...(c.bends ?? [])],
|
||||
orangeEligible: !!c.orangeEligible,
|
||||
}));
|
||||
this.selCurve = -1;
|
||||
this.drawingCurve = null;
|
||||
this.meta.orangeCount = layout.orangeCount ?? 25;
|
||||
this.meta.greenCount = layout.greenCount ?? 2;
|
||||
if (fileName) this.meta.file = fileName;
|
||||
|
|
@ -311,9 +338,133 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
return -1;
|
||||
}
|
||||
|
||||
// ── Curve geometry helpers ──────────────────────────────────────────────────
|
||||
|
||||
// Chord-perpendicular unit vector and the bezier midpoint of a segment.
|
||||
segmentInfo(curve, seg) {
|
||||
const p0 = curve.anchors[seg];
|
||||
const p1 = curve.anchors[seg + 1];
|
||||
const dx = p1.x - p0.x;
|
||||
const dy = p1.y - p0.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const perp = { x: -dy / len, y: dx / len };
|
||||
const bend = curve.bends[seg] ?? 0;
|
||||
const cp = { x: (p0.x + p1.x) / 2 + perp.x * bend, y: (p0.y + p1.y) / 2 + perp.y * bend };
|
||||
// Quadratic bezier at t=0.5:
|
||||
const mid = { x: 0.25 * p0.x + 0.5 * cp.x + 0.25 * p1.x, y: 0.25 * p0.y + 0.5 * cp.y + 0.25 * p1.y };
|
||||
return { p0, p1, perp, bend, mid };
|
||||
}
|
||||
|
||||
curveBricks() {
|
||||
return tessellateCurves(this.curves);
|
||||
}
|
||||
|
||||
curveAt(pointer) {
|
||||
const x = pointer.x - BOARD_X;
|
||||
const y = pointer.y - BOARD_Y;
|
||||
const bricks = this.curveBricksByCurve ?? [];
|
||||
for (let ci = 0; ci < bricks.length; ci++) {
|
||||
for (const b of bricks[ci]) {
|
||||
if ((b.x - x) ** 2 + (b.y - y) ** 2 <= 22 * 22) return ci;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Hit-test the selected curve's handles. Returns {kind:'anchor'|'bend', idx}.
|
||||
handleAt(pointer) {
|
||||
if (this.selCurve < 0) return null;
|
||||
const curve = this.curves[this.selCurve];
|
||||
if (!curve) return null;
|
||||
const x = pointer.x - BOARD_X;
|
||||
const y = pointer.y - BOARD_Y;
|
||||
for (let i = 0; i < curve.anchors.length; i++) {
|
||||
const a = curve.anchors[i];
|
||||
if ((a.x - x) ** 2 + (a.y - y) ** 2 <= 14 * 14) return { kind: 'anchor', idx: i };
|
||||
}
|
||||
for (let s = 0; s < curve.anchors.length - 1; s++) {
|
||||
const { mid } = this.segmentInfo(curve, s);
|
||||
if ((mid.x - x) ** 2 + (mid.y - y) ** 2 <= 12 * 12) return { kind: 'bend', idx: s };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
isDoubleClick(pointer) {
|
||||
const now = this.time.now;
|
||||
const dbl = now - this.lastClick.t < 330
|
||||
&& Math.abs(pointer.x - this.lastClick.x) < 14
|
||||
&& Math.abs(pointer.y - this.lastClick.y) < 14;
|
||||
this.lastClick = { t: now, x: pointer.x, y: pointer.y };
|
||||
return dbl;
|
||||
}
|
||||
|
||||
finishCurve() {
|
||||
if (this.drawingCurve && this.drawingCurve.length >= 2) {
|
||||
this.pushUndo();
|
||||
this.curves.push({
|
||||
anchors: this.drawingCurve,
|
||||
bends: new Array(this.drawingCurve.length - 1).fill(0),
|
||||
orangeEligible: this.eligibleCheckbox?.checked ?? true,
|
||||
});
|
||||
this.selCurve = this.curves.length - 1;
|
||||
}
|
||||
this.drawingCurve = null;
|
||||
this.renderPegs();
|
||||
}
|
||||
|
||||
deleteAnchor(curve, idx) {
|
||||
this.pushUndo();
|
||||
curve.anchors.splice(idx, 1);
|
||||
curve.bends.splice(idx === 0 ? 0 : idx - 1, 1);
|
||||
if (curve.anchors.length < 2) {
|
||||
this.curves.splice(this.selCurve, 1);
|
||||
this.selCurve = -1;
|
||||
}
|
||||
this.renderPegs();
|
||||
}
|
||||
|
||||
insertAnchor(curve, seg) {
|
||||
this.pushUndo();
|
||||
const { mid, bend } = this.segmentInfo(curve, seg);
|
||||
curve.anchors.splice(seg + 1, 0, { x: Math.round(mid.x), y: Math.round(mid.y) });
|
||||
// Each half of a quadratic keeps ~1/4 of the original sagitta.
|
||||
curve.bends.splice(seg, 1, bend / 4, bend / 4);
|
||||
this.renderPegs();
|
||||
}
|
||||
|
||||
// ── Pointer / keys ──────────────────────────────────────────────────────────
|
||||
|
||||
bindPointer() {
|
||||
this.input.on('pointerdown', (pointer) => {
|
||||
if (!this.inBoard(pointer)) return;
|
||||
const dbl = this.isDoubleClick(pointer);
|
||||
|
||||
// Handles of the selected curve take priority over everything.
|
||||
const handle = this.handleAt(pointer);
|
||||
if (handle) {
|
||||
const curve = this.curves[this.selCurve];
|
||||
if (handle.kind === 'anchor') {
|
||||
if (pointer.rightButtonDown()) { this.deleteAnchor(curve, handle.idx); return; }
|
||||
this.pushUndo();
|
||||
this.dragAnchor = { curve, idx: handle.idx };
|
||||
} else {
|
||||
if (dbl) { this.insertAnchor(curve, handle.idx); return; }
|
||||
this.pushUndo();
|
||||
this.dragBend = { curve, seg: handle.idx };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mode === 'curve') {
|
||||
if (pointer.rightButtonDown()) { this.drawingCurve = null; this.renderPegs(); return; }
|
||||
if (dbl) { this.finishCurve(); return; }
|
||||
const { x, y } = this.toBoard(pointer);
|
||||
if (!this.drawingCurve) this.drawingCurve = [];
|
||||
this.drawingCurve.push({ x, y });
|
||||
this.renderPegs();
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = this.pegAt(pointer);
|
||||
|
||||
if (pointer.rightButtonDown()) {
|
||||
|
|
@ -322,11 +473,19 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
}
|
||||
|
||||
if (this.mode === 'place') {
|
||||
// A click on a curve's bricks selects that curve.
|
||||
const ci = this.curveAt(pointer);
|
||||
if (ci >= 0 && idx < 0) {
|
||||
this.selCurve = ci;
|
||||
this.renderPegs();
|
||||
return;
|
||||
}
|
||||
if (idx >= 0) {
|
||||
this.pushUndo();
|
||||
this.dragIdx = idx;
|
||||
this.hoverIdx = idx;
|
||||
} else {
|
||||
if (this.selCurve >= 0) { this.selCurve = -1; this.renderPegs(); return; }
|
||||
const { x, y } = this.toBoard(pointer);
|
||||
this.pushUndo();
|
||||
this.pegs.push({ x, y, r: TUNING.PEG_R, orangeEligible: this.eligibleCheckbox?.checked ?? true });
|
||||
|
|
@ -352,7 +511,22 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
});
|
||||
|
||||
this.input.on('pointermove', (pointer) => {
|
||||
if (this.dragIdx >= 0 && pointer.isDown) {
|
||||
if (this.dragAnchor && pointer.isDown) {
|
||||
const { x, y } = this.toBoard(pointer);
|
||||
this.dragAnchor.curve.anchors[this.dragAnchor.idx] = { x, y };
|
||||
this.renderPegs(false);
|
||||
} else if (this.dragBend && pointer.isDown) {
|
||||
const { curve, seg } = this.dragBend;
|
||||
const { p0, p1, perp } = this.segmentInfo(curve, seg);
|
||||
const bx = pointer.x - BOARD_X;
|
||||
const by = pointer.y - BOARD_Y;
|
||||
const midX = (p0.x + p1.x) / 2;
|
||||
const midY = (p0.y + p1.y) / 2;
|
||||
// Signed distance of the pointer from the chord midpoint, along perp.
|
||||
// The handle sits at half the control offset, so bend = 2× that.
|
||||
curve.bends[seg] = Math.round(2 * ((bx - midX) * perp.x + (by - midY) * perp.y));
|
||||
this.renderPegs(false);
|
||||
} else if (this.dragIdx >= 0 && pointer.isDown) {
|
||||
const { x, y } = this.toBoard(pointer);
|
||||
this.pegs[this.dragIdx].x = x;
|
||||
this.pegs[this.dragIdx].y = y;
|
||||
|
|
@ -363,12 +537,23 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
});
|
||||
|
||||
this.input.on('pointerup', () => {
|
||||
if (this.dragIdx >= 0) { this.dragIdx = -1; this.renderPegs(); }
|
||||
if (this.dragIdx >= 0 || this.dragAnchor || this.dragBend) {
|
||||
this.dragIdx = -1;
|
||||
this.dragAnchor = null;
|
||||
this.dragBend = null;
|
||||
this.renderPegs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindKeys() {
|
||||
this.input.keyboard?.on('keydown-E', () => {
|
||||
if (this.selCurve >= 0 && this.curves[this.selCurve]) {
|
||||
this.pushUndo();
|
||||
this.curves[this.selCurve].orangeEligible = !this.curves[this.selCurve].orangeEligible;
|
||||
this.renderPegs();
|
||||
return;
|
||||
}
|
||||
if (this.hoverIdx >= 0 && this.pegs[this.hoverIdx]) {
|
||||
this.pushUndo();
|
||||
this.pegs[this.hoverIdx].orangeEligible = !this.pegs[this.hoverIdx].orangeEligible;
|
||||
|
|
@ -376,6 +561,13 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
}
|
||||
});
|
||||
this.input.keyboard?.on('keydown-DELETE', () => {
|
||||
if (this.selCurve >= 0 && this.curves[this.selCurve]) {
|
||||
this.pushUndo();
|
||||
this.curves.splice(this.selCurve, 1);
|
||||
this.selCurve = -1;
|
||||
this.renderPegs();
|
||||
return;
|
||||
}
|
||||
if (this.hoverIdx >= 0 && this.pegs[this.hoverIdx]) {
|
||||
this.pushUndo();
|
||||
this.pegs.splice(this.hoverIdx, 1);
|
||||
|
|
@ -383,7 +575,13 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
this.renderPegs();
|
||||
}
|
||||
});
|
||||
this.input.keyboard?.on('keydown-ESC', () => { this.pending = null; this.markerG.clear(); });
|
||||
this.input.keyboard?.on('keydown-ENTER', () => this.finishCurve());
|
||||
this.input.keyboard?.on('keydown-ESC', () => {
|
||||
this.pending = null;
|
||||
this.markerG.clear();
|
||||
if (this.drawingCurve) { this.drawingCurve = null; this.renderPegs(); }
|
||||
else if (this.selCurve >= 0) { this.selCurve = -1; this.renderPegs(); }
|
||||
});
|
||||
this.input.keyboard?.on('keydown-Z', (ev) => { if (ev.ctrlKey || ev.metaKey) this.undo(); });
|
||||
}
|
||||
|
||||
|
|
@ -428,16 +626,21 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
// ── Undo ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pushUndo() {
|
||||
this.undoStack.push(JSON.stringify(this.pegs));
|
||||
this.undoStack.push(JSON.stringify({ pegs: this.pegs, curves: this.curves }));
|
||||
if (this.undoStack.length > 60) this.undoStack.shift();
|
||||
}
|
||||
|
||||
undo() {
|
||||
const prev = this.undoStack.pop();
|
||||
if (prev == null) return;
|
||||
this.pegs = JSON.parse(prev);
|
||||
const snap = JSON.parse(prev);
|
||||
this.pegs = snap.pegs ?? [];
|
||||
this.curves = snap.curves ?? [];
|
||||
this.dragIdx = -1;
|
||||
this.hoverIdx = -1;
|
||||
this.selCurve = -1;
|
||||
this.dragAnchor = null;
|
||||
this.dragBend = null;
|
||||
this.renderPegs();
|
||||
}
|
||||
|
||||
|
|
@ -454,14 +657,36 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
if ((p.x - q.x) ** 2 + (p.y - q.y) ** 2 < min * min) { bad.add(i); bad.add(j); }
|
||||
}
|
||||
}
|
||||
const eligible = this.pegs.filter((p) => p.orangeEligible).length;
|
||||
|
||||
// Bricks: tessellate per curve so adjacency within a curve is exempt.
|
||||
this.curveBricksByCurve = this.curves.map((c) => tessellateCurves([c]));
|
||||
const allBricks = this.curveBricksByCurve.flatMap((list, ci) => list.map((b, bi) => ({ ...b, ci, bi })));
|
||||
const badBricks = new Set();
|
||||
const boundR = Math.hypot(TUNING.BRICK_LEN / 2, TUNING.BRICK_WID / 2);
|
||||
for (const b of allBricks) {
|
||||
if (b.x < boundR || b.x > BOARD_W - boundR || b.y < MIN_Y || b.y > MAX_Y) badBricks.add(`${b.ci}:${b.bi}`);
|
||||
for (const p of this.pegs) {
|
||||
const min = p.r + TUNING.BRICK_WID / 2 + GAP;
|
||||
if ((p.x - b.x) ** 2 + (p.y - b.y) ** 2 < min * min) badBricks.add(`${b.ci}:${b.bi}`);
|
||||
}
|
||||
for (const o of allBricks) {
|
||||
if (o.ci === b.ci && Math.abs(o.bi - b.bi) <= 1) continue; // self/adjacent
|
||||
const min = TUNING.BRICK_LEN * 0.8;
|
||||
if ((o.x - b.x) ** 2 + (o.y - b.y) ** 2 < min * min) badBricks.add(`${b.ci}:${b.bi}`);
|
||||
}
|
||||
}
|
||||
|
||||
const eligible = this.pegs.filter((p) => p.orangeEligible).length
|
||||
+ allBricks.filter((b) => b.orangeEligible).length;
|
||||
const totalPegs = this.pegs.length + allBricks.length;
|
||||
const problems = [];
|
||||
if (bad.size) problems.push(`${bad.size} peg(s) overlap or sit out of bounds (shown red).`);
|
||||
if (badBricks.size) problems.push(`${badBricks.size} brick(s) overlap or sit out of bounds (shown red).`);
|
||||
if (eligible < this.meta.orangeCount) problems.push(`Only ${eligible} orange-eligible pegs — need ≥ ${this.meta.orangeCount}.`);
|
||||
if (this.pegs.length < this.meta.orangeCount + this.meta.greenCount + 1) {
|
||||
if (totalPegs < this.meta.orangeCount + this.meta.greenCount + 1) {
|
||||
problems.push(`Need ≥ ${this.meta.orangeCount + this.meta.greenCount + 1} pegs total for orange+green+purple.`);
|
||||
}
|
||||
return { bad, eligible, problems, valid: problems.length === 0 };
|
||||
return { bad, badBricks, eligible, bricks: allBricks.length, problems, valid: problems.length === 0 };
|
||||
}
|
||||
|
||||
renderPegs(fullValidate = true) {
|
||||
|
|
@ -472,26 +697,88 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
this.pegImages.push(img);
|
||||
}
|
||||
|
||||
const v = fullValidate ? this.validate() : { bad: new Set(), eligible: this.pegs.filter((p) => p.orangeEligible).length, problems: [], valid: true };
|
||||
this.lastValidation = fullValidate ? v : this.lastValidation;
|
||||
const v = fullValidate ? this.validate() : (this.lastValidation ?? this.validate());
|
||||
this.lastValidation = v;
|
||||
if (!fullValidate) this.curveBricksByCurve = this.curves.map((c) => tessellateCurves([c]));
|
||||
|
||||
for (let i = 0; i < this.pegs.length; i++) {
|
||||
const p = this.pegs[i];
|
||||
const img = this.pegImages[i];
|
||||
img.setPosition(BOARD_X + p.x, BOARD_Y + p.y).setDisplaySize(p.r * 2, p.r * 2);
|
||||
if (v.bad.has(i)) img.setTint(0xff4d4d);
|
||||
if (fullValidate && v.bad.has(i)) img.setTint(0xff4d4d);
|
||||
else img.setTint(p.orangeEligible ? 0xffb347 : 0x4d8dee);
|
||||
}
|
||||
|
||||
this.renderCurves(fullValidate ? v : null);
|
||||
|
||||
if (this.statusText) {
|
||||
const val = this.lastValidation ?? v;
|
||||
this.statusText.setText(
|
||||
`${this.pegs.length} pegs · ${val.eligible} orange-eligible (need ${this.meta.orangeCount}) · ` +
|
||||
`${val.valid ? '✓ valid' : '✗ fix issues to export'}`,
|
||||
`${this.pegs.length} pegs + ${v.bricks ?? 0} bricks · ${v.eligible} orange-eligible (need ${this.meta.orangeCount}) · ` +
|
||||
`${v.valid ? '✓ valid' : '✗ fix issues to export'}`,
|
||||
);
|
||||
this.statusText.setColor(val.valid ? '#9be7b4' : '#ff8f8f');
|
||||
this.statusText.setColor(v.valid ? '#9be7b4' : '#ff8f8f');
|
||||
}
|
||||
this.flashWarn(v.problems.join('\n'));
|
||||
}
|
||||
|
||||
// Bricks + (for the selected curve) anchor and bend handles.
|
||||
renderCurves(validation) {
|
||||
const g = this.brickG;
|
||||
const h = this.handleG;
|
||||
if (!g || !h) return;
|
||||
g.clear();
|
||||
h.clear();
|
||||
|
||||
const L = TUNING.BRICK_LEN;
|
||||
const W = TUNING.BRICK_WID;
|
||||
(this.curveBricksByCurve ?? []).forEach((bricks, ci) => {
|
||||
const curve = this.curves[ci];
|
||||
const selected = ci === this.selCurve;
|
||||
bricks.forEach((b, bi) => {
|
||||
const invalid = validation?.badBricks?.has(`${ci}:${bi}`);
|
||||
const fill = invalid ? 0xff4d4d : curve.orangeEligible ? 0xffb347 : 0x4d8dee;
|
||||
g.save();
|
||||
g.translateCanvas(BOARD_X + b.x, BOARD_Y + b.y);
|
||||
g.rotateCanvas(b.angle);
|
||||
g.fillStyle(fill, 1);
|
||||
g.fillRoundedRect(-L / 2, -W / 2, L, W, 7);
|
||||
g.lineStyle(selected ? 3 : 2, selected ? 0xffffff : 0x0a1522, selected ? 1 : 0.8);
|
||||
g.strokeRoundedRect(-L / 2, -W / 2, L, W, 7);
|
||||
g.restore();
|
||||
});
|
||||
});
|
||||
|
||||
// Drawing-in-progress preview polyline.
|
||||
if (this.drawingCurve?.length) {
|
||||
h.lineStyle(2, 0xf0c75e, 0.9);
|
||||
for (let i = 0; i < this.drawingCurve.length - 1; i++) {
|
||||
const a = this.drawingCurve[i];
|
||||
const b = this.drawingCurve[i + 1];
|
||||
h.lineBetween(BOARD_X + a.x, BOARD_Y + a.y, BOARD_X + b.x, BOARD_Y + b.y);
|
||||
}
|
||||
for (const a of this.drawingCurve) {
|
||||
h.fillStyle(0xf0c75e, 1);
|
||||
h.fillRect(BOARD_X + a.x - 6, BOARD_Y + a.y - 6, 12, 12);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles for the selected curve.
|
||||
const sel = this.curves[this.selCurve];
|
||||
if (sel) {
|
||||
for (let s = 0; s < sel.anchors.length - 1; s++) {
|
||||
const { mid } = this.segmentInfo(sel, s);
|
||||
h.fillStyle(0xffffff, 1);
|
||||
h.fillCircle(BOARD_X + mid.x, BOARD_Y + mid.y, 8);
|
||||
h.lineStyle(2, 0x0a1522, 1);
|
||||
h.strokeCircle(BOARD_X + mid.x, BOARD_Y + mid.y, 8);
|
||||
}
|
||||
for (const a of sel.anchors) {
|
||||
h.fillStyle(0xf0c75e, 1);
|
||||
h.fillRect(BOARD_X + a.x - 7, BOARD_Y + a.y - 7, 14, 14);
|
||||
h.lineStyle(2, 0x0a1522, 1);
|
||||
h.strokeRect(BOARD_X + a.x - 7, BOARD_Y + a.y - 7, 14, 14);
|
||||
}
|
||||
}
|
||||
this.flashWarn((this.lastValidation ?? v).problems.join('\n'));
|
||||
}
|
||||
|
||||
// ── Export / test ───────────────────────────────────────────────────────────
|
||||
|
|
@ -505,6 +792,11 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
pegs: this.pegs.map((p) => ({
|
||||
x: p.x, y: p.y, shape: 'circle', r: p.r, orangeEligible: p.orangeEligible,
|
||||
})),
|
||||
curves: this.curves.map((c) => ({
|
||||
anchors: c.anchors.map((a) => ({ x: a.x, y: a.y })),
|
||||
bends: [...c.bends],
|
||||
orangeEligible: !!c.orangeEligible,
|
||||
})),
|
||||
walls: [],
|
||||
moving: [],
|
||||
};
|
||||
|
|
@ -549,6 +841,9 @@ export default class PeggleEditor extends Phaser.Scene {
|
|||
if (!v.valid) { this.flashWarn(`Test blocked:\n${v.problems.join('\n')}`); return; }
|
||||
this.registry.set('peggle-editor-state', {
|
||||
pegs: this.pegs.map((p) => ({ ...p })),
|
||||
curves: this.curves.map((c) => ({
|
||||
anchors: c.anchors.map((a) => ({ ...a })), bends: [...c.bends], orangeEligible: !!c.orangeEligible,
|
||||
})),
|
||||
meta: { ...this.meta },
|
||||
});
|
||||
this.scene.start('PeggleGame', {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ const SPRITE_FRAME = { blue: 0, orange: 2, green: 4, purple: 6, ball: 8 }; // co
|
|||
const FELT = 0x0d1b2e;
|
||||
const BOARD_BG = 0x122a45;
|
||||
|
||||
function shade(color, f) {
|
||||
const r = Math.min(255, Math.round(((color >> 16) & 0xff) * f));
|
||||
const g = Math.min(255, Math.round(((color >> 8) & 0xff) * f));
|
||||
const b = Math.min(255, Math.round((color & 0xff) * f));
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
const D = { felt: -2, board: -1, peg: 5, bucket: 8, ball: 10, fx: 12, preview: 14, launcher: 16, ui: 30, banner: 40, overlay: 60, overlayUI: 62 };
|
||||
|
||||
export default class PeggleGame extends Phaser.Scene {
|
||||
|
|
@ -142,6 +149,26 @@ export default class PeggleGame extends Phaser.Scene {
|
|||
g.generateTexture('peggle-spark', 8, 8);
|
||||
g.destroy();
|
||||
}
|
||||
// Brick-curve sections: pre-styled per color (dark outer stroke, color
|
||||
// fill, light inner stroke — the classic Peggle brick look). Pre-tinting
|
||||
// keeps the stroke contrast that a runtime tint would flatten.
|
||||
const L = TUNING.BRICK_LEN;
|
||||
const W = TUNING.BRICK_WID;
|
||||
for (const [color, base] of Object.entries(PEG_TINT)) {
|
||||
for (const [suffix, tone] of [['', base], ['-lit', PEG_LIT[color]]]) {
|
||||
const key = `peggle-brick-${color}${suffix}`;
|
||||
if (this.textures.exists(key)) continue;
|
||||
const g = this.make.graphics({ add: false });
|
||||
g.fillStyle(tone, 1);
|
||||
g.fillRoundedRect(2, 2, L, W, 8);
|
||||
g.lineStyle(3, shade(tone, 0.45), 1);
|
||||
g.strokeRoundedRect(2, 2, L, W, 8);
|
||||
g.lineStyle(2, shade(tone, 1.45), 0.95);
|
||||
g.strokeRoundedRect(6.5, 6.5, L - 9, W - 9, 5);
|
||||
g.generateTexture(key, L + 4, W + 4);
|
||||
g.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
masterFor(entry) {
|
||||
|
|
@ -521,8 +548,16 @@ export default class PeggleGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// Skins a peg image from its logical state — spritesheet frame when the
|
||||
// sheet is present, tinted procedural circle otherwise.
|
||||
// sheet is present, tinted procedural circle otherwise. Brick-curve
|
||||
// sections use their own pre-styled textures, rotated to the curve tangent.
|
||||
applyPegVisual(img, peg) {
|
||||
if (peg.shape === 'brick') {
|
||||
img.setTexture(`peggle-brick-${peg.color}${peg.lit ? '-lit' : ''}`);
|
||||
img.clearTint();
|
||||
img.setDisplaySize(TUNING.BRICK_LEN + 4, TUNING.BRICK_WID + 4);
|
||||
img.setRotation(peg.angle);
|
||||
return;
|
||||
}
|
||||
if (this.useSprites) {
|
||||
img.setTexture(SPRITE_SHEET, SPRITE_FRAME[peg.color] + (peg.lit ? 1 : 0));
|
||||
img.clearTint();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export const TUNING = {
|
|||
STUCK_SPEED: 40,
|
||||
STUCK_TIME: 1.5,
|
||||
PEG_FADE_S: 1.5, // lit pegs clear out (and stop colliding) after this
|
||||
BRICK_LEN: 36, // brick-curve section size (fixed → even spacing)
|
||||
BRICK_WID: 22,
|
||||
MAX_FLIGHT_S: 45,
|
||||
BALLS_PER_LEVEL: 10,
|
||||
};
|
||||
|
|
@ -145,6 +147,65 @@ export function mulberry32(seed) {
|
|||
};
|
||||
}
|
||||
|
||||
// ── Brick curves ─────────────────────────────────────────────────────────────
|
||||
// A curve is { anchors: [{x,y}...], bends: [px per segment], orangeEligible }.
|
||||
// Each segment is a quadratic bezier whose control point is the chord midpoint
|
||||
// displaced perpendicular by the bend. Curves tessellate into fixed-size brick
|
||||
// "pegs" that flow through the exact same color/score/fade pipeline as circles.
|
||||
|
||||
function bezierPoint(p0, cp, p1, t) {
|
||||
const u = 1 - t;
|
||||
return {
|
||||
x: u * u * p0.x + 2 * u * t * cp.x + t * t * p1.x,
|
||||
y: u * u * p0.y + 2 * u * t * cp.y + t * t * p1.y,
|
||||
};
|
||||
}
|
||||
|
||||
// Deterministic, pure: curves → brick defs {x, y, shape:'brick', angle, orangeEligible}.
|
||||
export function tessellateCurves(curves = []) {
|
||||
const bricks = [];
|
||||
const LEN = TUNING.BRICK_LEN;
|
||||
for (const curve of curves) {
|
||||
const anchors = curve.anchors ?? [];
|
||||
const bends = curve.bends ?? [];
|
||||
for (let s = 0; s < anchors.length - 1; s++) {
|
||||
const p0 = anchors[s];
|
||||
const p1 = anchors[s + 1];
|
||||
const bend = bends[s] ?? 0;
|
||||
const chordX = p1.x - p0.x;
|
||||
const chordY = p1.y - p0.y;
|
||||
const chordLen = Math.hypot(chordX, chordY) || 1;
|
||||
const cp = {
|
||||
x: (p0.x + p1.x) / 2 + (-chordY / chordLen) * bend,
|
||||
y: (p0.y + p1.y) / 2 + (chordX / chordLen) * bend,
|
||||
};
|
||||
// Arc-length table over a fine sampling of the bezier.
|
||||
const SAMPLES = 96;
|
||||
const pts = [];
|
||||
const cum = [0];
|
||||
for (let i = 0; i <= SAMPLES; i++) {
|
||||
pts.push(bezierPoint(p0, cp, p1, i / SAMPLES));
|
||||
if (i > 0) cum.push(cum[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y));
|
||||
}
|
||||
const total = cum[SAMPLES];
|
||||
const count = Math.max(1, Math.floor(total / LEN));
|
||||
const pad = (total - count * LEN) / 2;
|
||||
for (let b = 0; b < count; b++) {
|
||||
const target = pad + LEN * (b + 0.5);
|
||||
let i = 1;
|
||||
while (i < SAMPLES && cum[i] < target) i++;
|
||||
const span = cum[i] - cum[i - 1] || 1;
|
||||
const f = (target - cum[i - 1]) / span;
|
||||
const x = pts[i - 1].x + (pts[i].x - pts[i - 1].x) * f;
|
||||
const y = pts[i - 1].y + (pts[i].y - pts[i - 1].y) * f;
|
||||
const angle = Math.atan2(pts[i].y - pts[i - 1].y, pts[i].x - pts[i - 1].x);
|
||||
bricks.push({ x, y, shape: 'brick', angle, orangeEligible: !!curve.orangeEligible });
|
||||
}
|
||||
}
|
||||
}
|
||||
return bricks;
|
||||
}
|
||||
|
||||
// ── Round construction ───────────────────────────────────────────────────────
|
||||
|
||||
export function multiplierFor(orangeCleared) {
|
||||
|
|
@ -174,16 +235,35 @@ export function createRound(levelDef, opts = {}) {
|
|||
const orangeCount = levelDef.orangeCount ?? 25;
|
||||
const greenCount = levelDef.greenCount ?? 2;
|
||||
|
||||
const pegs = levelDef.pegs.map((p, i) => ({
|
||||
id: i,
|
||||
x: p.x, y: p.y,
|
||||
r: p.r ?? TUNING.PEG_R,
|
||||
shape: p.shape ?? 'circle',
|
||||
color: 'blue',
|
||||
orangeEligible: !!p.orangeEligible,
|
||||
lit: false,
|
||||
removed: false,
|
||||
}));
|
||||
const halfLen = TUNING.BRICK_LEN / 2;
|
||||
const halfWid = TUNING.BRICK_WID / 2;
|
||||
const pegs = [
|
||||
...(levelDef.pegs ?? []),
|
||||
...tessellateCurves(levelDef.curves ?? []),
|
||||
].map((p, i) => (p.shape === 'brick'
|
||||
? {
|
||||
id: i,
|
||||
x: p.x, y: p.y,
|
||||
shape: 'brick',
|
||||
angle: p.angle,
|
||||
halfLen, halfWid,
|
||||
r: halfWid,
|
||||
boundR: Math.hypot(halfLen, halfWid),
|
||||
color: 'blue',
|
||||
orangeEligible: !!p.orangeEligible,
|
||||
lit: false,
|
||||
removed: false,
|
||||
}
|
||||
: {
|
||||
id: i,
|
||||
x: p.x, y: p.y,
|
||||
r: p.r ?? TUNING.PEG_R,
|
||||
shape: p.shape ?? 'circle',
|
||||
color: 'blue',
|
||||
orangeEligible: !!p.orangeEligible,
|
||||
lit: false,
|
||||
removed: false,
|
||||
}));
|
||||
|
||||
const eligible = pegs.filter((p) => p.orangeEligible).map((p) => p.id);
|
||||
for (const id of pickN(rng, eligible, Math.min(orangeCount, eligible.length))) {
|
||||
|
|
@ -353,10 +433,42 @@ export function launchBall(state, angle, opts = {}) {
|
|||
// ── Peg / geometry helpers ───────────────────────────────────────────────────
|
||||
|
||||
function collideBallPeg(ball, peg) {
|
||||
// Returns contact normal if overlapping, else null. Circles only in v1.
|
||||
// Returns contact normal if overlapping, else null.
|
||||
const dx = ball.x - peg.x;
|
||||
const dy = ball.y - peg.y;
|
||||
const rr = (ball.r ?? TUNING.BALL_R) + peg.r;
|
||||
const br = ball.r ?? TUNING.BALL_R;
|
||||
|
||||
if (peg.shape === 'brick') {
|
||||
// Circle vs oriented rect: work in the brick's local space.
|
||||
const bound = peg.boundR + br;
|
||||
if (dx * dx + dy * dy > bound * bound) return null;
|
||||
const ca = Math.cos(peg.angle);
|
||||
const sa = Math.sin(peg.angle);
|
||||
const lx = dx * ca + dy * sa;
|
||||
const ly = -dx * sa + dy * ca;
|
||||
const cx = Math.max(-peg.halfLen, Math.min(peg.halfLen, lx));
|
||||
const cy = Math.max(-peg.halfWid, Math.min(peg.halfWid, ly));
|
||||
let nxl = lx - cx;
|
||||
let nyl = ly - cy;
|
||||
const d2 = nxl * nxl + nyl * nyl;
|
||||
let depth;
|
||||
if (d2 >= br * br) return null;
|
||||
if (d2 < 0.000001) {
|
||||
// Ball center inside the brick — push out along the shallower axis.
|
||||
const pushX = peg.halfLen - Math.abs(lx);
|
||||
const pushY = peg.halfWid - Math.abs(ly);
|
||||
if (pushY <= pushX) { nxl = 0; nyl = ly >= 0 ? 1 : -1; depth = pushY + br; }
|
||||
else { nxl = lx >= 0 ? 1 : -1; nyl = 0; depth = pushX + br; }
|
||||
} else {
|
||||
const d = Math.sqrt(d2);
|
||||
nxl /= d;
|
||||
nyl /= d;
|
||||
depth = br - d;
|
||||
}
|
||||
return { nx: nxl * ca - nyl * sa, ny: nxl * sa + nyl * ca, depth };
|
||||
}
|
||||
|
||||
const rr = br + peg.r;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 >= rr * rr) return null;
|
||||
const d = Math.sqrt(d2) || 0.0001;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { writeFileSync, mkdirSync } from 'node:fs';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { TUNING } from '../src/games/peggle/PeggleLogic.js';
|
||||
import { TUNING, tessellateCurves } from '../src/games/peggle/PeggleLogic.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = join(__dirname, '..', 'assets', 'gamedata', 'peggle');
|
||||
|
|
@ -91,66 +91,86 @@ function settle(pegs) {
|
|||
}
|
||||
|
||||
function levelFile(pegs, over = {}) {
|
||||
const curves = over.curves ?? [];
|
||||
let placed = pegs;
|
||||
if (curves.length) {
|
||||
// Cull circle pegs that would collide with the brick curves.
|
||||
const bricks = tessellateCurves(curves);
|
||||
placed = pegs.filter((p) => {
|
||||
const min = p.r + TUNING.BRICK_WID / 2 + GAP + 4;
|
||||
return !bricks.some((b) => (p.x - b.x) ** 2 + (p.y - b.y) ** 2 < min * min);
|
||||
});
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
board: { width: TUNING.BOARD_W, height: TUNING.BOARD_H },
|
||||
orangeCount: 25,
|
||||
greenCount: 2,
|
||||
pegs,
|
||||
pegs: placed,
|
||||
walls: [],
|
||||
moving: [],
|
||||
...over,
|
||||
curves,
|
||||
};
|
||||
}
|
||||
|
||||
// Brick-curve shorthand: anchors + per-segment bends, orange-eligible.
|
||||
const wave = (anchors, bends, orangeEligible = true) => ({ anchors, bends, orangeEligible });
|
||||
|
||||
const el = { orangeEligible: true };
|
||||
|
||||
// ── Ethel (1-5): parlor arcs & chandeliers ───────────────────────────────────
|
||||
|
||||
function ethelLevel(v) {
|
||||
const pegs = [];
|
||||
const curves = [];
|
||||
switch (v) {
|
||||
case 0: // Tea Time — gentle nested arcs + bottom row
|
||||
case 0: // Tea Time — nested arcs, a steam-wisp brick curve up top
|
||||
pegs.push(
|
||||
...arc(600, 120, 420, Math.PI * 0.15, Math.PI * 0.85, 14, el),
|
||||
...arc(600, 60, 560, Math.PI * 0.20, Math.PI * 0.80, 13, el),
|
||||
...arc(600, 10, 700, Math.PI * 0.25, Math.PI * 0.75, 12, el),
|
||||
...line(160, 770, 1040, 770, 10),
|
||||
...line(320, 300, 880, 300, 5),
|
||||
);
|
||||
curves.push(wave([{ x: 350, y: 300 }, { x: 600, y: 270 }, { x: 850, y: 300 }], [-45, -45]));
|
||||
break;
|
||||
case 1: // Fine China — a saucer stack: opposing arc pairs
|
||||
case 1: // Fine China — arc pairs with a brick saucer bowl
|
||||
pegs.push(
|
||||
...arc(600, 40, 500, Math.PI * 0.22, Math.PI * 0.78, 12, el),
|
||||
...arc(600, 940, 500, -Math.PI * 0.76, -Math.PI * 0.24, 12, el),
|
||||
...arc(600, 470, 210, 0, Math.PI * 2 * (1 - 1 / 12), 12, el),
|
||||
...line(150, 480, 330, 480, 3),
|
||||
...line(870, 480, 1050, 480, 3),
|
||||
...line(380, 740, 820, 740, 5),
|
||||
);
|
||||
curves.push(wave([{ x: 380, y: 600 }, { x: 600, y: 600 }, { x: 820, y: 600 }], [55, 55]));
|
||||
break;
|
||||
case 2: // Doily Drop — lace fan of alternating arcs with side tassels
|
||||
case 2: // Doily Drop — lace fan with hanging brick swags
|
||||
pegs.push(
|
||||
...arc(600, 130, 350, Math.PI * 0.1, Math.PI * 0.9, 12, el),
|
||||
...arc(600, 130, 470, Math.PI * 0.14, Math.PI * 0.86, 13, el),
|
||||
...arc(600, 130, 590, Math.PI * 0.18, Math.PI * 0.82, 14, el),
|
||||
...line(180, 260, 180, 560, 4),
|
||||
...line(1020, 260, 1020, 560, 4),
|
||||
...line(420, 760, 780, 760, 4),
|
||||
);
|
||||
curves.push(wave(
|
||||
[{ x: 180, y: 640 }, { x: 460, y: 660 }, { x: 740, y: 660 }, { x: 1020, y: 640 }],
|
||||
[70, 70, 70],
|
||||
));
|
||||
break;
|
||||
case 3: // Lace & Grace — S-curve of two big arcs + hanging drops
|
||||
case 3: // Lace & Grace — S arcs with a ribbon wave along the bottom
|
||||
pegs.push(
|
||||
...arc(320, 300, 240, -Math.PI * 0.5, Math.PI * 0.5, 12, el),
|
||||
...arc(880, 560, 240, Math.PI * 0.5, Math.PI * 1.5, 12, el),
|
||||
...line(300, 640, 300, 780, 3, el),
|
||||
...line(600, 620, 600, 780, 3, el),
|
||||
...line(160, 210, 420, 210, 4),
|
||||
...line(780, 210, 1040, 210, 4),
|
||||
...grid(480, 340, 3, 2, 120, 100, el),
|
||||
);
|
||||
curves.push(wave(
|
||||
[{ x: 200, y: 690 }, { x: 490, y: 710 }, { x: 780, y: 710 }, { x: 1050, y: 690 }],
|
||||
[-55, 55, -55],
|
||||
));
|
||||
break;
|
||||
default: // Grandma's Attic — chandelier: arcs + hanging strands
|
||||
default: // Grandma's Attic — chandelier strands under a cobweb swag
|
||||
pegs.push(
|
||||
...arc(600, 90, 380, Math.PI * 0.15, Math.PI * 0.85, 12, el),
|
||||
...arc(600, 90, 520, Math.PI * 0.2, Math.PI * 0.8, 13, el),
|
||||
|
|
@ -159,30 +179,31 @@ function ethelLevel(v) {
|
|||
...line(600, 500, 600, 780, 5, el),
|
||||
...line(760, 470, 760, 770, 5, el),
|
||||
...line(920, 420, 920, 720, 5, el),
|
||||
...line(200, 250, 1000, 250, 6),
|
||||
);
|
||||
curves.push(wave([{ x: 280, y: 330 }, { x: 600, y: 330 }, { x: 920, y: 330 }], [50, 50]));
|
||||
break;
|
||||
}
|
||||
return levelFile(settle(pegs));
|
||||
return levelFile(settle(pegs), { curves });
|
||||
}
|
||||
|
||||
// ── Kona (6-10): bones, paws & toys ──────────────────────────────────────────
|
||||
|
||||
function konaLevel(v) {
|
||||
const pegs = [];
|
||||
const curves = [];
|
||||
switch (v) {
|
||||
case 0: // Fetch! — the dog bone
|
||||
case 0: // Fetch! — the dog bone under a thrown-ball arc
|
||||
pegs.push(
|
||||
...circle(310, 430, 95, 9, el),
|
||||
...circle(890, 430, 95, 9, el),
|
||||
...line(400, 390, 800, 390, 6, el),
|
||||
...line(400, 470, 800, 470, 6, el),
|
||||
...line(430, 430, 770, 430, 5, el),
|
||||
...grid(220, 240, 6, 1, 152, 0),
|
||||
...grid(300, 620, 5, 2, 150, 90),
|
||||
...line(140, 320, 220, 560, 3),
|
||||
...line(1060, 320, 980, 560, 3),
|
||||
);
|
||||
curves.push(wave([{ x: 180, y: 300 }, { x: 490, y: 260 }, { x: 800, y: 300 }], [-55, -45]));
|
||||
break;
|
||||
case 1: { // Paw Prints — big pad + four toes, twice
|
||||
const paw = (cx, cy, s) => [
|
||||
|
|
@ -196,11 +217,11 @@ function konaLevel(v) {
|
|||
...paw(390, 520, 1),
|
||||
...paw(880, 620, 0.8),
|
||||
...line(700, 240, 1050, 240, 4),
|
||||
...line(150, 740, 350, 740, 3),
|
||||
);
|
||||
curves.push(wave([{ x: 200, y: 720 }, { x: 620, y: 730 }, { x: 1040, y: 720 }], [35, -35]));
|
||||
break;
|
||||
}
|
||||
case 2: // Buried Bones — two crossed bones + dirt scatter
|
||||
case 2: // Buried Bones — crossed bones over brick dirt mounds
|
||||
pegs.push(
|
||||
...circle(280, 300, 60, 6, el),
|
||||
...circle(920, 640, 60, 6, el),
|
||||
|
|
@ -208,11 +229,11 @@ function konaLevel(v) {
|
|||
...circle(920, 300, 60, 6, el),
|
||||
...circle(280, 640, 60, 6, el),
|
||||
...line(850, 340, 350, 600, 8, el),
|
||||
...grid(240, 760, 8, 1, 105, 0),
|
||||
...line(560, 210, 640, 210, 2),
|
||||
);
|
||||
curves.push(wave([{ x: 240, y: 700 }, { x: 600, y: 720 }, { x: 960, y: 700 }], [-55, -55]));
|
||||
break;
|
||||
case 3: // Squeaky Toy — ball on a rope with small toys
|
||||
case 3: // Squeaky Toy — bouncing-path brick wave under the ball
|
||||
pegs.push(
|
||||
...circle(600, 430, 150, 13, el),
|
||||
...circle(600, 430, 70, 7, el),
|
||||
|
|
@ -221,10 +242,13 @@ function konaLevel(v) {
|
|||
...circle(980, 640, 65, 7, el),
|
||||
...line(160, 300, 360, 300, 3),
|
||||
...line(840, 300, 1040, 300, 3),
|
||||
...line(430, 740, 770, 740, 4),
|
||||
);
|
||||
curves.push(wave(
|
||||
[{ x: 150, y: 580 }, { x: 450, y: 600 }, { x: 750, y: 600 }, { x: 1020, y: 580 }],
|
||||
[55, -55, 55],
|
||||
));
|
||||
break;
|
||||
default: // Best in Show — kennel house + trophy rows
|
||||
default: // Best in Show — kennel house with bunting swags
|
||||
pegs.push(
|
||||
...line(600, 200, 320, 380, 5, el),
|
||||
...line(600, 200, 880, 380, 5, el),
|
||||
|
|
@ -232,58 +256,62 @@ function konaLevel(v) {
|
|||
...line(880, 380, 880, 700, 5, el),
|
||||
...line(320, 700, 880, 700, 8, el),
|
||||
...grid(460, 460, 3, 3, 140, 110, el),
|
||||
...line(140, 500, 240, 500, 2),
|
||||
...line(960, 500, 1060, 500, 2),
|
||||
...line(430, 780, 770, 780, 4),
|
||||
);
|
||||
curves.push(
|
||||
wave([{ x: 120, y: 330 }, { x: 300, y: 330 }], [45]),
|
||||
wave([{ x: 900, y: 330 }, { x: 1080, y: 330 }], [45]),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return levelFile(settle(pegs));
|
||||
return levelFile(settle(pegs), { curves });
|
||||
}
|
||||
|
||||
// ── Zanthor (11-15): stars, moons & sigils ───────────────────────────────────
|
||||
|
||||
function zanthorLevel(v) {
|
||||
const pegs = [];
|
||||
const curves = [];
|
||||
switch (v) {
|
||||
case 0: // Hocus Pocus — pentagram inside an arc
|
||||
case 0: // Hocus Pocus — pentagram between two wand-swish curves
|
||||
pegs.push(
|
||||
...pentagram(600, 480, 230, 8, el),
|
||||
...arc(600, 430, 400, Math.PI * 0.12, Math.PI * 0.88, 15, el),
|
||||
...line(240, 240, 460, 240, 3),
|
||||
...line(740, 240, 960, 240, 3),
|
||||
);
|
||||
curves.push(
|
||||
wave([{ x: 240, y: 240 }, { x: 600, y: 205 }, { x: 960, y: 240 }], [-30, -30]),
|
||||
wave([{ x: 250, y: 720 }, { x: 600, y: 740 }, { x: 950, y: 720 }], [45, 45]),
|
||||
);
|
||||
break;
|
||||
case 1: // Crescent Moon — offset arcs + scattered stars
|
||||
case 1: // Crescent Moon — moon with a comet-tail brick curve
|
||||
pegs.push(
|
||||
...arc(560, 460, 260, -Math.PI * 0.42, Math.PI * 0.42, 13, el),
|
||||
...arc(660, 460, 200, -Math.PI * 0.40, Math.PI * 0.40, 10, el),
|
||||
...pentagram(280, 300, 80, 3, el),
|
||||
...pentagram(920, 620, 80, 3, el),
|
||||
...line(180, 660, 420, 760, 4),
|
||||
...line(760, 220, 1000, 220, 4),
|
||||
);
|
||||
curves.push(wave([{ x: 150, y: 640 }, { x: 450, y: 690 }, { x: 750, y: 670 }], [45, -40]));
|
||||
break;
|
||||
case 2: // Crystal Ball — orb on a stand with facet lattice
|
||||
case 2: // Crystal Ball — orb under a curl of mystic mist
|
||||
pegs.push(
|
||||
...circle(600, 420, 200, 15, el),
|
||||
...grid(490, 340, 3, 3, 110, 90, el),
|
||||
...line(420, 680, 780, 680, 5, el),
|
||||
...line(470, 760, 730, 760, 4, el),
|
||||
...line(160, 300, 320, 300, 3),
|
||||
...line(880, 300, 1040, 300, 3),
|
||||
);
|
||||
curves.push(wave([{ x: 200, y: 250 }, { x: 500, y: 230 }, { x: 800, y: 250 }], [-40, -40]));
|
||||
break;
|
||||
case 3: // Twin Stars — two pentagrams joined by a beam
|
||||
case 3: // Twin Stars — pentagrams over a shooting-star trail
|
||||
pegs.push(
|
||||
...pentagram(330, 400, 170, 6, el),
|
||||
...pentagram(870, 560, 170, 6, el),
|
||||
...line(430, 470, 750, 500, 4, el),
|
||||
...line(200, 740, 480, 740, 4),
|
||||
...line(650, 220, 1000, 220, 4),
|
||||
);
|
||||
curves.push(wave([{ x: 180, y: 710 }, { x: 600, y: 740 }, { x: 1020, y: 710 }], [45, 45]));
|
||||
break;
|
||||
default: // Grand Sigil — double circle, cross & diagonals
|
||||
default: // Grand Sigil — circles & cross flanked by runic rails
|
||||
pegs.push(
|
||||
...circle(600, 470, 260, 18, el),
|
||||
...circle(600, 470, 130, 10, el),
|
||||
|
|
@ -293,19 +321,22 @@ function zanthorLevel(v) {
|
|||
...line(740, 470, 850, 470, 2, el),
|
||||
...line(300, 240, 420, 330, 2),
|
||||
...line(900, 240, 780, 330, 2),
|
||||
...line(300, 720, 420, 620, 2),
|
||||
...line(900, 720, 780, 620, 2),
|
||||
...line(480, 780, 720, 780, 3),
|
||||
);
|
||||
curves.push(
|
||||
wave([{ x: 160, y: 380 }, { x: 160, y: 700 }], [-65]),
|
||||
wave([{ x: 1040, y: 380 }, { x: 1040, y: 700 }], [65]),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return levelFile(settle(pegs));
|
||||
return levelFile(settle(pegs), { curves });
|
||||
}
|
||||
|
||||
// ── Fireball (16-20): rings, rays & bursts ───────────────────────────────────
|
||||
|
||||
function fireballLevel(v) {
|
||||
const pegs = [];
|
||||
const curves = [];
|
||||
switch (v) {
|
||||
case 0: // X-Ray Vision — concentric rings, orange core
|
||||
pegs.push(
|
||||
|
|
@ -314,11 +345,10 @@ function fireballLevel(v) {
|
|||
...circle(600, 470, 260, 20, el),
|
||||
...arc(600, 470, 345, Math.PI * -0.35, Math.PI * 1.35, 22),
|
||||
{ x: 600, y: 470, ...el },
|
||||
...line(160, 740, 400, 740, 3),
|
||||
...line(800, 740, 1040, 740, 3),
|
||||
);
|
||||
curves.push(wave([{ x: 160, y: 720 }, { x: 600, y: 750 }, { x: 1040, y: 720 }], [45, 45]));
|
||||
break;
|
||||
case 1: { // Solar Flare — core + radial rays
|
||||
case 1: { // Solar Flare — core + rays under a corona curve
|
||||
pegs.push(...circle(600, 470, 110, 9, el), { x: 600, y: 470, ...el });
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const a = (i * Math.PI * 2) / 8 + Math.PI / 8;
|
||||
|
|
@ -327,28 +357,28 @@ function fireballLevel(v) {
|
|||
600 + 400 * Math.cos(a), 470 + 400 * Math.sin(a), 4, el,
|
||||
));
|
||||
}
|
||||
curves.push(wave([{ x: 350, y: 250 }, { x: 600, y: 215 }, { x: 850, y: 250 }], [-35, -35]));
|
||||
break;
|
||||
}
|
||||
case 2: // Atomic Age — three overlapping orbit rings + nucleus
|
||||
case 2: // Atomic Age — orbit rings over an electron-path curve
|
||||
pegs.push(
|
||||
...circle(600, 470, 60, 6, el),
|
||||
...circle(440, 470, 190, 13, el),
|
||||
...circle(760, 470, 190, 13, el),
|
||||
...circle(600, 320, 190, 13, el),
|
||||
...line(200, 760, 440, 760, 3),
|
||||
...line(760, 760, 1000, 760, 3),
|
||||
);
|
||||
curves.push(wave([{ x: 200, y: 680 }, { x: 600, y: 720 }, { x: 1000, y: 680 }], [50, 50]));
|
||||
break;
|
||||
case 3: { // Meteor Shower — diagonal streaks with cores
|
||||
case 3: { // Meteor Shower — streaks under an atmosphere curve
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x0 = 140 + i * 210;
|
||||
pegs.push(...line(x0, 230, x0 + 150, 720, 7, el));
|
||||
pegs.push({ x: x0 + 160, y: 760, ...el });
|
||||
}
|
||||
pegs.push(...line(180, 200, 1020, 200, 6));
|
||||
curves.push(wave([{ x: 180, y: 230 }, { x: 600, y: 200 }, { x: 1020, y: 230 }], [-25, -25]));
|
||||
break;
|
||||
}
|
||||
default: // Supernova — rings + rays + corner shields
|
||||
default: // Supernova — rings + rays over a shockwave curve
|
||||
pegs.push(
|
||||
...circle(600, 470, 90, 8, el),
|
||||
...circle(600, 470, 210, 16, el),
|
||||
|
|
@ -363,26 +393,30 @@ function fireballLevel(v) {
|
|||
pegs.push(
|
||||
...arc(140, 210, 150, 0, Math.PI * 0.5, 4),
|
||||
...arc(1060, 210, 150, Math.PI * 0.5, Math.PI, 4),
|
||||
...line(430, 780, 770, 780, 4),
|
||||
);
|
||||
curves.push(wave([{ x: 300, y: 720 }, { x: 600, y: 750 }, { x: 900, y: 720 }], [35, 35]));
|
||||
break;
|
||||
}
|
||||
return levelFile(settle(pegs));
|
||||
return levelFile(settle(pegs), { curves });
|
||||
}
|
||||
|
||||
// ── Victor (21-25): boards & lattices ────────────────────────────────────────
|
||||
|
||||
function victorLevel(v) {
|
||||
const pegs = [];
|
||||
const curves = [];
|
||||
switch (v) {
|
||||
case 0: // Opening Move — light checker rows
|
||||
case 0: // Opening Move — checker rows between two brick waves
|
||||
pegs.push(
|
||||
...grid(220, 300, 6, 3, 150, 150, el),
|
||||
...grid(295, 375, 6, 3, 150, 150, el),
|
||||
...line(160, 210, 1040, 210, 6),
|
||||
);
|
||||
curves.push(
|
||||
wave([{ x: 160, y: 240 }, { x: 600, y: 205 }, { x: 1040, y: 240 }], [-30, -30]),
|
||||
wave([{ x: 160, y: 700 }, { x: 600, y: 730 }, { x: 1040, y: 700 }], [45, -45]),
|
||||
);
|
||||
break;
|
||||
case 1: { // Knight's Tour — L-shaped clusters
|
||||
case 1: { // Knight's Tour — L clusters over a zigzag rail
|
||||
const L = (x, y) => [
|
||||
...line(x, y, x, y + 180, 3, el),
|
||||
...line(x, y + 180, x + 190, y + 180, 3, el),
|
||||
|
|
@ -390,12 +424,14 @@ function victorLevel(v) {
|
|||
pegs.push(
|
||||
...L(240, 260), ...L(520, 220), ...L(800, 260),
|
||||
...L(380, 520), ...L(660, 480), ...L(940, 520),
|
||||
...line(160, 760, 460, 760, 4),
|
||||
...line(740, 760, 1040, 760, 4),
|
||||
);
|
||||
curves.push(wave(
|
||||
[{ x: 160, y: 710 }, { x: 450, y: 730 }, { x: 740, y: 730 }, { x: 1030, y: 710 }],
|
||||
[40, -40, 40],
|
||||
));
|
||||
break;
|
||||
}
|
||||
case 2: // Double Gambit — chevrons
|
||||
case 2: // Double Gambit — chevrons between curved side rails
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const y = 260 + i * 140;
|
||||
pegs.push(
|
||||
|
|
@ -403,8 +439,12 @@ function victorLevel(v) {
|
|||
...line(600, y + 90, 960, y, 6, el),
|
||||
);
|
||||
}
|
||||
curves.push(
|
||||
wave([{ x: 170, y: 300 }, { x: 170, y: 620 }], [-60]),
|
||||
wave([{ x: 1030, y: 300 }, { x: 1030, y: 620 }], [60]),
|
||||
);
|
||||
break;
|
||||
case 3: // Fortress — walls, towers & courtyard
|
||||
case 3: // Fortress — walls & towers ringed by a brick moat
|
||||
pegs.push(
|
||||
...line(220, 260, 980, 260, 9, el),
|
||||
...line(220, 700, 980, 700, 9, el),
|
||||
|
|
@ -413,19 +453,22 @@ function victorLevel(v) {
|
|||
...line(725, 330, 725, 630, 4, el),
|
||||
...line(980, 330, 980, 630, 4, el),
|
||||
...grid(330, 400, 2, 2, 100, 130, el),
|
||||
...grid(580, 400, 2, 2, 100, 130, el),
|
||||
...grid(830, 400, 2, 2, 100, 130, el),
|
||||
);
|
||||
curves.push(wave([{ x: 220, y: 760 }, { x: 980, y: 760 }], [25]));
|
||||
break;
|
||||
default: // Checkmate — the dense diagonal lattice
|
||||
default: // Checkmate — lattice woven with two long waves
|
||||
pegs.push(
|
||||
...grid(180, 250, 8, 5, 120, 120, el),
|
||||
...grid(240, 310, 8, 5, 120, 120, el),
|
||||
...line(160, 210, 1040, 210, 8),
|
||||
);
|
||||
curves.push(
|
||||
wave([{ x: 160, y: 350 }, { x: 600, y: 410 }, { x: 1040, y: 350 }], [50, 50]),
|
||||
wave([{ x: 160, y: 610 }, { x: 600, y: 550 }, { x: 1040, y: 610 }], [-50, -50]),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return levelFile(settle(pegs));
|
||||
return levelFile(settle(pegs), { curves });
|
||||
}
|
||||
|
||||
// ── Manifest + write ─────────────────────────────────────────────────────────
|
||||
|
|
@ -461,12 +504,14 @@ for (const block of BLOCKS) {
|
|||
levelNo++;
|
||||
const file = `level-${String(levelNo).padStart(3, '0')}.json`;
|
||||
const data = block.build(v);
|
||||
const eligible = data.pegs.filter((p) => p.orangeEligible).length;
|
||||
const bricks = tessellateCurves(data.curves ?? []);
|
||||
const eligible = data.pegs.filter((p) => p.orangeEligible).length
|
||||
+ bricks.filter((b) => b.orangeEligible).length;
|
||||
if (eligible < data.orangeCount) {
|
||||
throw new Error(`${file} (${block.names[v]}): only ${eligible} orange-eligible pegs (< ${data.orangeCount})`);
|
||||
throw new Error(`${file} (${block.names[v]}): only ${eligible} orange-eligible pegs+bricks (< ${data.orangeCount})`);
|
||||
}
|
||||
writeFileSync(join(OUT_DIR, file), `${JSON.stringify(data, null, 2)}\n`);
|
||||
console.log(`${file}: ${block.names[v]} [${block.masterId}] — ${data.pegs.length} pegs (${eligible} orange-eligible)`);
|
||||
console.log(`${file}: ${block.names[v]} [${block.masterId}] — ${data.pegs.length} pegs + ${bricks.length} bricks (${eligible} orange-eligible)`);
|
||||
manifestLevels.push({
|
||||
level: levelNo,
|
||||
name: block.names[v],
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { dirname, join } from 'node:path';
|
|||
import {
|
||||
TUNING, SCORING, POWERS,
|
||||
mulberry32, multiplierFor, createRound, cloneState,
|
||||
clampAim, aimToVelocity, bucketX, bucketWidth,
|
||||
clampAim, aimToVelocity, bucketX, bucketWidth, tessellateCurves,
|
||||
launchBall, stepSim, resolveFever,
|
||||
simulatePreview, scoreShot, zenBallOptimize, applyPower,
|
||||
} from '../src/games/peggle/PeggleLogic.js';
|
||||
|
|
@ -192,8 +192,10 @@ console.log('― rules');
|
|||
check('createRound assigns exactly orangeCount orange', count('orange') === lvl.orangeCount, `${count('orange')}`);
|
||||
check('createRound assigns exactly greenCount green', count('green') === lvl.greenCount, `${count('green')}`);
|
||||
check('createRound assigns exactly one purple', count('purple') === 1);
|
||||
// Peg ids run over circles then tessellated bricks, in order.
|
||||
const defs = [...lvl.pegs, ...tessellateCurves(lvl.curves ?? [])];
|
||||
check('orange pegs only on eligible slots',
|
||||
st.pegs.every((p) => p.color !== 'orange' || lvl.pegs[p.id].orangeEligible));
|
||||
st.pegs.every((p) => p.color !== 'orange' || defs[p.id].orangeEligible));
|
||||
check('purple/green never on the same peg', st.pegs[st.purpleId].color === 'purple');
|
||||
check('10 balls to start', st.ballsLeft === T.BALLS_PER_LEVEL);
|
||||
}
|
||||
|
|
@ -377,6 +379,90 @@ console.log('― powers');
|
|||
check('zen launch consumes the charge and fires', st.zenNext === 0 && evs.some((e) => e.type === 'powerFired' && e.powerId === 'zenball'));
|
||||
}
|
||||
|
||||
// ── 3a. Brick curves ─────────────────────────────────────────────────────────
|
||||
|
||||
console.log('― brick curves');
|
||||
|
||||
{
|
||||
// Straight segment: evenly spaced bricks along the chord, angle ≈ 0.
|
||||
const bricks = tessellateCurves([{ anchors: [{ x: 200, y: 400 }, { x: 560, y: 400 }], bends: [0], orangeEligible: true }]);
|
||||
check('straight curve brick count', bricks.length === Math.floor(360 / T.BRICK_LEN), `${bricks.length}`);
|
||||
check('straight curve bricks level and flat',
|
||||
bricks.every((b) => near(b.y, 400, 0.5) && near(b.angle, 0, 0.01)));
|
||||
const gaps = bricks.slice(1).map((b, i) => b.x - bricks[i].x);
|
||||
check('straight curve spacing = BRICK_LEN', gaps.every((g) => near(g, T.BRICK_LEN, 0.5)), gaps.join(','));
|
||||
check('curve bricks inherit eligibility', bricks.every((b) => b.orangeEligible));
|
||||
}
|
||||
|
||||
{
|
||||
// Bent segment: the middle brick bows toward the bend side (+perp is +y
|
||||
// for a left→right chord), ends stay near the anchors.
|
||||
const bricks = tessellateCurves([{ anchors: [{ x: 200, y: 400 }, { x: 800, y: 400 }], bends: [100], orangeEligible: false }]);
|
||||
const mid = bricks[Math.floor(bricks.length / 2)];
|
||||
check('bent curve bows at the middle', mid.y > 435 && mid.y < 465, `${mid.y}`);
|
||||
check('bent curve stays anchored at the ends', bricks[0].y < 420 && bricks[bricks.length - 1].y < 420);
|
||||
check('bent curve bricks rotate along the tangent',
|
||||
bricks[0].angle > 0.05 && bricks[bricks.length - 1].angle < -0.05,
|
||||
`${bricks[0].angle} .. ${bricks[bricks.length - 1].angle}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Ball bounces off a horizontal brick face like a peg (lights + restitution).
|
||||
const lvl = mkLevel([], {
|
||||
curves: [{ anchors: [{ x: 450, y: 500 }, { x: 750, y: 500 }], bends: [0], orangeEligible: true }],
|
||||
});
|
||||
const st = createRound(lvl, { seed: 7 });
|
||||
check('bricks join the peg list', st.pegs.length >= 8 && st.pegs.every((p) => p.shape === 'brick'));
|
||||
st.orangeTotal = 99; // keep the round alive
|
||||
inject(st, { x: 600, y: 430, vx: 0, vy: 200 });
|
||||
let vyBefore = null;
|
||||
let hit = null;
|
||||
for (let i = 0; i < 600 && !hit; i++) {
|
||||
vyBefore = st.balls[0]?.vy;
|
||||
hit = stepSim(st, 1 / 240).find((e) => e.type === 'pegHit') ?? null;
|
||||
}
|
||||
check('brick hit registered and lit', !!hit && st.pegs[hit.pegId].lit);
|
||||
check('brick bounce reflects upward with peg restitution',
|
||||
st.balls[0].vy < 0 && near(Math.abs(st.balls[0].vy / vyBefore), T.PEG_RESTITUTION, 0.06),
|
||||
`${Math.abs(st.balls[0]?.vy / vyBefore)}`);
|
||||
}
|
||||
|
||||
{
|
||||
// A 45° brick wall deflects a vertical drop sideways.
|
||||
const lvl = mkLevel([], {
|
||||
curves: [{ anchors: [{ x: 480, y: 380 }, { x: 720, y: 620 }], bends: [0], orangeEligible: false }],
|
||||
});
|
||||
const st = createRound(lvl, { seed: 7 });
|
||||
st.orangeTotal = 99;
|
||||
inject(st, { x: 600, y: 420, vx: 0, vy: 300 });
|
||||
let hit = false;
|
||||
for (let i = 0; i < 600 && !hit; i++) {
|
||||
hit = stepSim(st, 1 / 240).some((e) => e.type === 'pegHit');
|
||||
}
|
||||
check('45° brick deflects the ball sideways', hit && Math.abs(st.balls[0].vx) > 60, `vx ${st.balls[0]?.vx}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Round integration: brick curves join orange assignment and the fever sweep.
|
||||
const lvl = mkLevel([], {
|
||||
orangeCount: 5,
|
||||
curves: [{ anchors: [{ x: 300, y: 500 }, { x: 900, y: 500 }], bends: [-60], orangeEligible: true }],
|
||||
});
|
||||
const st = createRound(lvl, { seed: 11 });
|
||||
const orange = st.pegs.filter((p) => p.color === 'orange').length;
|
||||
check('bricks receive orange assignment', orange === 5, `${orange}`);
|
||||
check('bricks host the purple bonus', st.purpleId != null && st.pegs[st.purpleId].color === 'purple');
|
||||
|
||||
// Determinism replay over a level with curves.
|
||||
const logOf = () => {
|
||||
const s = createRound(lvl, { seed: 999 });
|
||||
const log = [...launchBall(s, 0.3)];
|
||||
for (let t = 0; t < 25 && s.phase === 'flight'; t += 1 / 60) log.push(...stepSim(s, 1 / 60));
|
||||
return log.filter((e) => e.type === 'pegHit').map((e) => `${e.pegId}:${e.points}`).join('|');
|
||||
};
|
||||
check('curved levels replay deterministically', logOf() === logOf());
|
||||
}
|
||||
|
||||
// ── 3b. New powers (for the next wave of friends) ────────────────────────────
|
||||
|
||||
console.log('― powers (new wave)');
|
||||
|
|
@ -702,10 +788,25 @@ console.log('― data lint');
|
|||
const tag = entry.file;
|
||||
check(`${tag}: board is ${T.BOARD_W}×${T.BOARD_H}`,
|
||||
lvl.board.width === T.BOARD_W && lvl.board.height === T.BOARD_H);
|
||||
const bricks = tessellateCurves(lvl.curves ?? []);
|
||||
check(`${tag}: enough orange-eligible pegs`,
|
||||
lvl.pegs.filter((p) => p.orangeEligible).length >= lvl.orangeCount);
|
||||
lvl.pegs.filter((p) => p.orangeEligible).length
|
||||
+ bricks.filter((b) => b.orangeEligible).length >= lvl.orangeCount);
|
||||
check(`${tag}: enough pegs for orange+green+purple`,
|
||||
lvl.pegs.length >= lvl.orangeCount + lvl.greenCount + 1);
|
||||
lvl.pegs.length + bricks.length >= lvl.orangeCount + lvl.greenCount + 1);
|
||||
if (lvl.curves?.length) {
|
||||
check(`${tag}: curve schema valid`,
|
||||
lvl.curves.every((c) => (c.anchors?.length ?? 0) >= 2 && (c.bends?.length ?? -1) === c.anchors.length - 1));
|
||||
const boundR = Math.hypot(T.BRICK_LEN / 2, T.BRICK_WID / 2);
|
||||
check(`${tag}: bricks in bounds and clear of launcher/bucket lanes`,
|
||||
bricks.every((b) => b.x >= boundR && b.x <= T.BOARD_W - boundR && b.y >= 140 && b.y <= 820),
|
||||
bricks.filter((b) => !(b.y >= 140 && b.y <= 820)).map((b) => `${Math.round(b.x)},${Math.round(b.y)}`).join(' '));
|
||||
const clash = lvl.pegs.some((p) => bricks.some((b) => {
|
||||
const min = (p.r ?? T.PEG_R) + T.BRICK_WID / 2;
|
||||
return (p.x - b.x) ** 2 + (p.y - b.y) ** 2 < min * min;
|
||||
}));
|
||||
check(`${tag}: bricks clear of circle pegs`, !clash);
|
||||
}
|
||||
check(`${tag}: pegs in bounds and clear of launcher/bucket lanes`,
|
||||
lvl.pegs.every((p) => p.x >= p.r && p.x <= T.BOARD_W - p.r && p.y >= 140 && p.y <= 820));
|
||||
let overlap = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue