diff --git a/public/ui/frontend/campfire/ba-fw-ov.png b/public/ui/frontend/campfire/ba-fw-ov.png new file mode 100644 index 0000000..0748f60 Binary files /dev/null and b/public/ui/frontend/campfire/ba-fw-ov.png differ diff --git a/public/ui/frontend/campfire/ba-fw.png b/public/ui/frontend/campfire/ba-fw.png index 9479aae..3fd4d3d 100644 Binary files a/public/ui/frontend/campfire/ba-fw.png and b/public/ui/frontend/campfire/ba-fw.png differ diff --git a/public/ui/frontend/campfire/ba/fw_ov.png b/public/ui/frontend/campfire/ba/fw_ov.png new file mode 100644 index 0000000..0748f60 Binary files /dev/null and b/public/ui/frontend/campfire/ba/fw_ov.png differ diff --git a/public/ui/frontend/campfire/ne/bw_ov.png b/public/ui/frontend/campfire/ne/bw_ov.png new file mode 100644 index 0000000..671e74e Binary files /dev/null and b/public/ui/frontend/campfire/ne/bw_ov.png differ diff --git a/public/ui/frontend/campfire/ne/fw_ov.png b/public/ui/frontend/campfire/ne/fw_ov.png new file mode 100644 index 0000000..89e2f3e Binary files /dev/null and b/public/ui/frontend/campfire/ne/fw_ov.png differ diff --git a/public/ui/frontend/campfire/ne/nu3_ov.png b/public/ui/frontend/campfire/ne/nu3_ov.png new file mode 100644 index 0000000..3d01700 Binary files /dev/null and b/public/ui/frontend/campfire/ne/nu3_ov.png differ diff --git a/public/ui/frontend/campfire/pa-fw-ov.png b/public/ui/frontend/campfire/pa-fw-ov.png new file mode 100644 index 0000000..aa2102a Binary files /dev/null and b/public/ui/frontend/campfire/pa-fw-ov.png differ diff --git a/public/ui/frontend/campfire/pa-fw.png b/public/ui/frontend/campfire/pa-fw.png index e7823aa..c7b29dc 100644 Binary files a/public/ui/frontend/campfire/pa-fw.png and b/public/ui/frontend/campfire/pa-fw.png differ diff --git a/public/ui/frontend/campfire/pa/fw_ov.png b/public/ui/frontend/campfire/pa/fw_ov.png new file mode 100644 index 0000000..aa2102a Binary files /dev/null and b/public/ui/frontend/campfire/pa/fw_ov.png differ diff --git a/public/ui/frontend/campfire/so/bw_ov.png b/public/ui/frontend/campfire/so/bw_ov.png new file mode 100644 index 0000000..16c58fa Binary files /dev/null and b/public/ui/frontend/campfire/so/bw_ov.png differ diff --git a/public/ui/frontend/campfire/so/fw_ov.png b/public/ui/frontend/campfire/so/fw_ov.png new file mode 100644 index 0000000..3446251 Binary files /dev/null and b/public/ui/frontend/campfire/so/fw_ov.png differ diff --git a/public/ui/frontend/campfire/so/nu3_ov.png b/public/ui/frontend/campfire/so/nu3_ov.png new file mode 100644 index 0000000..2003c1d Binary files /dev/null and b/public/ui/frontend/campfire/so/nu3_ov.png differ diff --git a/public/ui/frontend/manifest.json b/public/ui/frontend/manifest.json index ca833e9..8c4cbec 100644 --- a/public/ui/frontend/manifest.json +++ b/public/ui/frontend/manifest.json @@ -1004,6 +1004,7 @@ "height": 309, "frameCount": 30, "rate": 1, + "drawMode": 3, "frames": [ { "x": 0, @@ -1345,6 +1346,7 @@ "height": 397, "frameCount": 26, "rate": 3, + "drawMode": 5, "frames": [ { "x": 0, @@ -1562,6 +1564,7 @@ "height": 397, "frameCount": 26, "rate": 3, + "drawMode": 5, "frames": [ { "x": 0, @@ -1779,6 +1782,7 @@ "height": 469, "frameCount": 18, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -1932,6 +1936,7 @@ "height": 1251, "frameCount": 54, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -2373,6 +2378,7 @@ "height": 433, "frameCount": 30, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -2624,6 +2630,7 @@ "height": 189, "frameCount": 31, "rate": 3, + "drawMode": 5, "frames": [ { "x": 0, @@ -2881,6 +2888,7 @@ "height": 189, "frameCount": 31, "rate": 3, + "drawMode": 5, "frames": [ { "x": 0, @@ -3138,6 +3146,7 @@ "height": 375, "frameCount": 31, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -3395,6 +3404,7 @@ "height": 805, "frameCount": 91, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -4132,6 +4142,7 @@ "height": 382, "frameCount": 51, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -4551,6 +4562,7 @@ "height": 183, "frameCount": 12, "rate": 3, + "drawMode": 5, "frames": [ { "x": 0, @@ -4656,6 +4668,7 @@ "height": 183, "frameCount": 12, "rate": 3, + "drawMode": 5, "frames": [ { "x": 0, @@ -4761,6 +4774,7 @@ "height": 233, "frameCount": 12, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -4866,6 +4880,7 @@ "height": 485, "frameCount": 12, "rate": 2, + "drawMode": 3, "frames": [ { "x": 0, @@ -4971,6 +4986,7 @@ "height": 676, "frameCount": 38, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -5284,6 +5300,7 @@ "height": 482, "frameCount": 38, "rate": 1, + "drawMode": 3, "frames": [ { "x": 0, @@ -5597,6 +5614,7 @@ "height": 448, "frameCount": 28, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -5830,6 +5848,7 @@ "height": 472, "frameCount": 28, "rate": 1, + "drawMode": 3, "frames": [ { "x": 0, @@ -6065,6 +6084,7 @@ "height": 183, "frameCount": 16, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -6202,6 +6222,7 @@ "height": 183, "frameCount": 16, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -6339,6 +6360,7 @@ "height": 404, "frameCount": 26, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -6552,10 +6574,11 @@ }, "fw": { "file": "campfire/ba-fw.png", - "width": 2033, - "height": 1122, + "width": 2010, + "height": 840, "frameCount": 64, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -6576,494 +6599,494 @@ { "x": 175, "y": 0, - "w": 89, + "w": 88, "h": 183, "anchorX": -39, "anchorY": -132 }, { - "x": 265, + "x": 264, "y": 0, - "w": 94, + "w": 90, "h": 183, "anchorX": -40, "anchorY": -132 }, { - "x": 360, + "x": 355, "y": 0, - "w": 99, + "w": 92, "h": 184, "anchorX": -40, "anchorY": -132 }, { - "x": 460, + "x": 448, "y": 0, - "w": 106, + "w": 95, "h": 185, "anchorX": -41, "anchorY": -131 }, { - "x": 567, + "x": 544, "y": 0, - "w": 115, + "w": 100, "h": 185, "anchorX": -43, "anchorY": -129 }, { - "x": 683, + "x": 645, "y": 0, - "w": 123, + "w": 106, "h": 188, "anchorX": -45, "anchorY": -128 }, { - "x": 807, + "x": 752, "y": 0, - "w": 133, + "w": 114, "h": 190, "anchorX": -48, "anchorY": -127 }, { - "x": 941, + "x": 867, "y": 0, - "w": 140, + "w": 120, "h": 189, "anchorX": -50, "anchorY": -126 }, { - "x": 1082, + "x": 988, "y": 0, - "w": 145, + "w": 124, "h": 189, "anchorX": -51, "anchorY": -126 }, { - "x": 1228, + "x": 1113, "y": 0, - "w": 146, + "w": 126, "h": 189, "anchorX": -51, "anchorY": -126 }, { - "x": 1375, + "x": 1240, "y": 0, - "w": 145, + "w": 125, "h": 190, "anchorX": -50, "anchorY": -127 }, { - "x": 1521, + "x": 1366, "y": 0, - "w": 141, + "w": 124, "h": 193, "anchorX": -49, "anchorY": -129 }, { - "x": 1663, + "x": 1491, "y": 0, - "w": 137, + "w": 121, "h": 194, "anchorX": -48, "anchorY": -131 }, { - "x": 1801, + "x": 1613, "y": 0, - "w": 133, + "w": 119, "h": 195, "anchorX": -47, "anchorY": -132 }, { - "x": 0, - "y": 196, - "w": 133, + "x": 1733, + "y": 0, + "w": 119, "h": 196, "anchorX": -49, "anchorY": -133 }, { - "x": 134, - "y": 196, - "w": 130, + "x": 1853, + "y": 0, + "w": 118, "h": 198, "anchorX": -51, "anchorY": -135 }, { - "x": 265, - "y": 196, - "w": 130, + "x": 0, + "y": 199, + "w": 120, "h": 198, "anchorX": -52, "anchorY": -136 }, { - "x": 396, - "y": 196, - "w": 146, - "h": 205, + "x": 121, + "y": 199, + "w": 137, + "h": 201, "anchorX": -58, - "anchorY": -141 + "anchorY": -137 }, { - "x": 543, - "y": 196, - "w": 162, - "h": 234, - "anchorX": -64, - "anchorY": -167 - }, - { - "x": 706, - "y": 196, - "w": 151, - "h": 254, - "anchorX": -64, - "anchorY": -186 - }, - { - "x": 858, - "y": 196, - "w": 148, - "h": 261, - "anchorX": -64, - "anchorY": -187 - }, - { - "x": 1007, - "y": 196, + "x": 259, + "y": 199, "w": 147, - "h": 259, - "anchorX": -65, - "anchorY": -180 + "h": 204, + "anchorX": -64, + "anchorY": -137 }, { - "x": 1155, - "y": 196, - "w": 146, - "h": 254, - "anchorX": -65, - "anchorY": -175 + "x": 407, + "y": 199, + "w": 139, + "h": 215, + "anchorX": -64, + "anchorY": -147 }, { - "x": 1302, - "y": 196, - "w": 145, - "h": 253, - "anchorX": -65, - "anchorY": -174 + "x": 547, + "y": 199, + "w": 138, + "h": 220, + "anchorX": -64, + "anchorY": -146 }, { - "x": 1448, - "y": 196, - "w": 145, - "h": 252, + "x": 686, + "y": 199, + "w": 140, + "h": 216, "anchorX": -65, - "anchorY": -173 + "anchorY": -137 }, { - "x": 1594, - "y": 196, - "w": 145, - "h": 252, + "x": 827, + "y": 199, + "w": 141, + "h": 214, "anchorX": -65, - "anchorY": -173 + "anchorY": -135 }, { - "x": 1740, - "y": 196, - "w": 145, - "h": 253, + "x": 969, + "y": 199, + "w": 140, + "h": 214, "anchorX": -65, - "anchorY": -173 + "anchorY": -135 }, { - "x": 1886, - "y": 196, - "w": 146, - "h": 253, + "x": 1110, + "y": 199, + "w": 140, + "h": 213, + "anchorX": -65, + "anchorY": -134 + }, + { + "x": 1251, + "y": 199, + "w": 140, + "h": 213, + "anchorX": -65, + "anchorY": -134 + }, + { + "x": 1392, + "y": 199, + "w": 140, + "h": 213, + "anchorX": -65, + "anchorY": -133 + }, + { + "x": 1533, + "y": 199, + "w": 141, + "h": 213, "anchorX": -66, - "anchorY": -173 + "anchorY": -133 + }, + { + "x": 1675, + "y": 199, + "w": 140, + "h": 213, + "anchorX": -66, + "anchorY": -133 + }, + { + "x": 1816, + "y": 199, + "w": 139, + "h": 213, + "anchorX": -66, + "anchorY": -133 }, { "x": 0, - "y": 458, - "w": 146, - "h": 253, + "y": 420, + "w": 140, + "h": 212, "anchorX": -66, - "anchorY": -173 + "anchorY": -132 }, { - "x": 147, - "y": 458, - "w": 146, - "h": 254, - "anchorX": -66, - "anchorY": -174 - }, - { - "x": 294, - "y": 458, - "w": 146, - "h": 254, - "anchorX": -66, - "anchorY": -174 - }, - { - "x": 441, - "y": 458, - "w": 146, - "h": 254, - "anchorX": -66, - "anchorY": -174 - }, - { - "x": 588, - "y": 458, - "w": 144, - "h": 247, - "anchorX": -63, - "anchorY": -167 - }, - { - "x": 733, - "y": 458, - "w": 146, - "h": 236, - "anchorX": -61, - "anchorY": -156 - }, - { - "x": 880, - "y": 458, - "w": 144, - "h": 231, - "anchorX": -60, - "anchorY": -151 - }, - { - "x": 1025, - "y": 458, - "w": 145, - "h": 230, - "anchorX": -61, - "anchorY": -150 - }, - { - "x": 1171, - "y": 458, - "w": 156, - "h": 228, - "anchorX": -68, - "anchorY": -148 - }, - { - "x": 1328, - "y": 458, + "x": 141, + "y": 420, "w": 141, + "h": 212, + "anchorX": -66, + "anchorY": -132 + }, + { + "x": 283, + "y": 420, + "w": 144, + "h": 212, + "anchorX": -63, + "anchorY": -132 + }, + { + "x": 428, + "y": 420, + "w": 141, + "h": 211, + "anchorX": -61, + "anchorY": -131 + }, + { + "x": 570, + "y": 420, + "w": 137, + "h": 210, + "anchorX": -60, + "anchorY": -130 + }, + { + "x": 708, + "y": 420, + "w": 138, + "h": 210, + "anchorX": -61, + "anchorY": -130 + }, + { + "x": 847, + "y": 420, + "w": 149, + "h": 210, + "anchorX": -68, + "anchorY": -130 + }, + { + "x": 997, + "y": 420, + "w": 130, "h": 211, "anchorX": -82, "anchorY": -131 }, { - "x": 1470, - "y": 458, - "w": 137, + "x": 1128, + "y": 420, + "w": 135, "h": 210, "anchorX": -87, "anchorY": -130 }, { - "x": 1608, - "y": 458, + "x": 1264, + "y": 420, "w": 128, "h": 209, "anchorX": -80, "anchorY": -129 }, { - "x": 1737, - "y": 458, - "w": 126, + "x": 1393, + "y": 420, + "w": 122, "h": 208, - "anchorX": -78, + "anchorX": -74, "anchorY": -128 }, { - "x": 1864, - "y": 458, - "w": 127, + "x": 1516, + "y": 420, + "w": 122, "h": 208, - "anchorX": -79, + "anchorX": -74, "anchorY": -128 }, { - "x": 0, - "y": 713, - "w": 126, + "x": 1639, + "y": 420, + "w": 122, "h": 207, - "anchorX": -78, + "anchorX": -74, "anchorY": -127 }, { - "x": 127, - "y": 713, + "x": 1762, + "y": 420, "w": 123, "h": 207, "anchorX": -75, "anchorY": -127 }, { - "x": 251, - "y": 713, - "w": 128, + "x": 1886, + "y": 420, + "w": 123, "h": 207, "anchorX": -75, "anchorY": -127 }, { - "x": 380, - "y": 713, - "w": 131, + "x": 0, + "y": 633, + "w": 122, "h": 207, "anchorX": -74, "anchorY": -127 }, { - "x": 512, - "y": 713, - "w": 131, + "x": 123, + "y": 633, + "w": 121, "h": 207, "anchorX": -73, "anchorY": -127 }, { - "x": 644, - "y": 713, - "w": 130, + "x": 245, + "y": 633, + "w": 120, "h": 207, "anchorX": -72, "anchorY": -127 }, { - "x": 775, - "y": 713, - "w": 128, + "x": 366, + "y": 633, + "w": 118, "h": 207, "anchorX": -70, "anchorY": -127 }, { - "x": 904, - "y": 713, - "w": 127, + "x": 485, + "y": 633, + "w": 116, "h": 207, "anchorX": -68, "anchorY": -128 }, { - "x": 1032, - "y": 713, - "w": 115, + "x": 602, + "y": 633, + "w": 110, "h": 206, "anchorX": -63, "anchorY": -128 }, { - "x": 1148, - "y": 713, - "w": 108, + "x": 713, + "y": 633, + "w": 106, "h": 203, "anchorX": -57, "anchorY": -129 }, { - "x": 1257, - "y": 713, + "x": 820, + "y": 633, "w": 111, "h": 199, "anchorX": -51, "anchorY": -130 }, { - "x": 1369, - "y": 713, + "x": 932, + "y": 633, "w": 113, "h": 197, "anchorX": -50, "anchorY": -130 }, { - "x": 1483, - "y": 713, + "x": 1046, + "y": 633, "w": 112, "h": 197, "anchorX": -49, "anchorY": -131 }, { - "x": 1596, - "y": 713, + "x": 1159, + "y": 633, "w": 92, "h": 197, "anchorX": -48, "anchorY": -132 }, { - "x": 1689, - "y": 713, + "x": 1252, + "y": 633, "w": 91, "h": 197, "anchorX": -47, "anchorY": -132 }, { - "x": 1781, - "y": 713, + "x": 1344, + "y": 633, "w": 91, "h": 199, "anchorX": -47, "anchorY": -133 }, { - "x": 1873, - "y": 713, + "x": 1436, + "y": 633, "w": 92, "h": 199, "anchorX": -48, "anchorY": -133 }, { - "x": 0, - "y": 921, + "x": 1529, + "y": 633, "w": 93, "h": 201, "anchorX": -49, "anchorY": -134 }, { - "x": 94, - "y": 921, + "x": 1623, + "y": 633, "w": 93, "h": 201, "anchorX": -49, "anchorY": -134 }, { - "x": 188, - "y": 921, + "x": 1717, + "y": 633, "w": 95, "h": 201, "anchorX": -50, @@ -7071,12 +7094,535 @@ } ] }, + "fw_ov": { + "file": "campfire/ba-fw-ov.png", + "width": 1942, + "height": 308, + "frameCount": 64, + "rate": 1, + "drawMode": 5, + "frames": [ + { + "x": 0, + "y": 0, + "w": 20, + "h": 37, + "anchorX": 27, + "anchorY": -44 + }, + { + "x": 21, + "y": 0, + "w": 21, + "h": 38, + "anchorX": 27, + "anchorY": -45 + }, + { + "x": 43, + "y": 0, + "w": 24, + "h": 37, + "anchorX": 26, + "anchorY": -46 + }, + { + "x": 68, + "y": 0, + "w": 28, + "h": 37, + "anchorX": 26, + "anchorY": -48 + }, + { + "x": 97, + "y": 0, + "w": 33, + "h": 37, + "anchorX": 26, + "anchorY": -50 + }, + { + "x": 131, + "y": 0, + "w": 37, + "h": 35, + "anchorX": 28, + "anchorY": -51 + }, + { + "x": 169, + "y": 0, + "w": 42, + "h": 39, + "anchorX": 30, + "anchorY": -54 + }, + { + "x": 212, + "y": 0, + "w": 49, + "h": 53, + "anchorX": 29, + "anchorY": -57 + }, + { + "x": 262, + "y": 0, + "w": 53, + "h": 66, + "anchorX": 32, + "anchorY": -65 + }, + { + "x": 316, + "y": 0, + "w": 53, + "h": 84, + "anchorX": 37, + "anchorY": -74 + }, + { + "x": 370, + "y": 0, + "w": 57, + "h": 107, + "anchorX": 37, + "anchorY": -84 + }, + { + "x": 428, + "y": 0, + "w": 51, + "h": 118, + "anchorX": 44, + "anchorY": -93 + }, + { + "x": 480, + "y": 0, + "w": 44, + "h": 125, + "anchorX": 51, + "anchorY": -101 + }, + { + "x": 525, + "y": 0, + "w": 38, + "h": 128, + "anchorX": 54, + "anchorY": -108 + }, + { + "x": 564, + "y": 0, + "w": 38, + "h": 129, + "anchorX": 51, + "anchorY": -114 + }, + { + "x": 603, + "y": 0, + "w": 37, + "h": 128, + "anchorX": 49, + "anchorY": -119 + }, + { + "x": 641, + "y": 0, + "w": 37, + "h": 128, + "anchorX": 47, + "anchorY": -122 + }, + { + "x": 679, + "y": 0, + "w": 35, + "h": 130, + "anchorX": 44, + "anchorY": -127 + }, + { + "x": 715, + "y": 0, + "w": 32, + "h": 132, + "anchorX": 46, + "anchorY": -131 + }, + { + "x": 748, + "y": 0, + "w": 32, + "h": 135, + "anchorX": 56, + "anchorY": -141 + }, + { + "x": 781, + "y": 0, + "w": 39, + "h": 136, + "anchorX": 59, + "anchorY": -167 + }, + { + "x": 821, + "y": 0, + "w": 36, + "h": 133, + "anchorX": 51, + "anchorY": -186 + }, + { + "x": 858, + "y": 0, + "w": 34, + "h": 139, + "anchorX": 50, + "anchorY": -187 + }, + { + "x": 893, + "y": 0, + "w": 30, + "h": 147, + "anchorX": 52, + "anchorY": -180 + }, + { + "x": 924, + "y": 0, + "w": 29, + "h": 150, + "anchorX": 52, + "anchorY": -175 + }, + { + "x": 954, + "y": 0, + "w": 28, + "h": 150, + "anchorX": 52, + "anchorY": -174 + }, + { + "x": 983, + "y": 0, + "w": 28, + "h": 149, + "anchorX": 52, + "anchorY": -173 + }, + { + "x": 1012, + "y": 0, + "w": 28, + "h": 149, + "anchorX": 52, + "anchorY": -173 + }, + { + "x": 1041, + "y": 0, + "w": 28, + "h": 149, + "anchorX": 52, + "anchorY": -173 + }, + { + "x": 1070, + "y": 0, + "w": 28, + "h": 150, + "anchorX": 52, + "anchorY": -173 + }, + { + "x": 1099, + "y": 0, + "w": 29, + "h": 151, + "anchorX": 51, + "anchorY": -173 + }, + { + "x": 1129, + "y": 0, + "w": 31, + "h": 151, + "anchorX": 49, + "anchorY": -174 + }, + { + "x": 1161, + "y": 0, + "w": 30, + "h": 151, + "anchorX": 50, + "anchorY": -174 + }, + { + "x": 1192, + "y": 0, + "w": 28, + "h": 151, + "anchorX": 52, + "anchorY": -174 + }, + { + "x": 1221, + "y": 0, + "w": 19, + "h": 142, + "anchorX": 61, + "anchorY": -167 + }, + { + "x": 1241, + "y": 0, + "w": 23, + "h": 127, + "anchorX": 62, + "anchorY": -156 + }, + { + "x": 1265, + "y": 0, + "w": 26, + "h": 120, + "anchorX": 58, + "anchorY": -151 + }, + { + "x": 1292, + "y": 0, + "w": 26, + "h": 118, + "anchorX": 58, + "anchorY": -150 + }, + { + "x": 1319, + "y": 0, + "w": 28, + "h": 131, + "anchorX": 60, + "anchorY": -148 + }, + { + "x": 1348, + "y": 0, + "w": 67, + "h": 143, + "anchorX": -8, + "anchorY": -102 + }, + { + "x": 1416, + "y": 0, + "w": 108, + "h": 121, + "anchorX": -58, + "anchorY": -75 + }, + { + "x": 1525, + "y": 0, + "w": 102, + "h": 113, + "anchorX": -72, + "anchorY": -63 + }, + { + "x": 1628, + "y": 0, + "w": 101, + "h": 118, + "anchorX": -78, + "anchorY": -61 + }, + { + "x": 1730, + "y": 0, + "w": 103, + "h": 119, + "anchorX": -79, + "anchorY": -61 + }, + { + "x": 1834, + "y": 0, + "w": 107, + "h": 117, + "anchorX": -78, + "anchorY": -60 + }, + { + "x": 0, + "y": 152, + "w": 117, + "h": 113, + "anchorX": -75, + "anchorY": -60 + }, + { + "x": 118, + "y": 152, + "w": 124, + "h": 111, + "anchorX": -71, + "anchorY": -61 + }, + { + "x": 243, + "y": 152, + "w": 126, + "h": 111, + "anchorX": -69, + "anchorY": -62 + }, + { + "x": 370, + "y": 152, + "w": 124, + "h": 114, + "anchorX": -66, + "anchorY": -65 + }, + { + "x": 495, + "y": 152, + "w": 120, + "h": 118, + "anchorX": -62, + "anchorY": -68 + }, + { + "x": 616, + "y": 152, + "w": 117, + "h": 122, + "anchorX": -59, + "anchorY": -71 + }, + { + "x": 734, + "y": 152, + "w": 115, + "h": 122, + "anchorX": -56, + "anchorY": -72 + }, + { + "x": 850, + "y": 152, + "w": 78, + "h": 144, + "anchorX": -26, + "anchorY": -92 + }, + { + "x": 929, + "y": 152, + "w": 33, + "h": 156, + "anchorX": 18, + "anchorY": -116 + }, + { + "x": 963, + "y": 152, + "w": 17, + "h": 150, + "anchorX": 38, + "anchorY": -127 + }, + { + "x": 981, + "y": 152, + "w": 11, + "h": 144, + "anchorX": 43, + "anchorY": -130 + }, + { + "x": 993, + "y": 152, + "w": 8, + "h": 141, + "anchorX": 44, + "anchorY": -131 + }, + { + "x": 1002, + "y": 152, + "w": 26, + "h": 144, + "anchorX": 8, + "anchorY": -115 + }, + { + "x": 1029, + "y": 152, + "w": 32, + "h": 132, + "anchorX": -17, + "anchorY": -101 + }, + { + "x": 1062, + "y": 152, + "w": 35, + "h": 130, + "anchorX": -24, + "anchorY": -100 + }, + { + "x": 1098, + "y": 152, + "w": 37, + "h": 128, + "anchorX": -28, + "anchorY": -100 + }, + { + "x": 1136, + "y": 152, + "w": 37, + "h": 130, + "anchorX": -26, + "anchorY": -102 + }, + { + "x": 1174, + "y": 152, + "w": 36, + "h": 132, + "anchorX": -23, + "anchorY": -105 + }, + { + "x": 1211, + "y": 152, + "w": 38, + "h": 134, + "anchorX": -23, + "anchorY": -107 + } + ] + }, "bw": { "file": "campfire/ba-bw.png", "width": 1813, "height": 201, "frameCount": 19, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -7240,6 +7786,7 @@ "height": 359, "frameCount": 26, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -7457,6 +8004,7 @@ "height": 359, "frameCount": 26, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -7674,6 +8222,7 @@ "height": 173, "frameCount": 9, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -7751,10 +8300,11 @@ }, "fw": { "file": "campfire/pa-fw.png", - "width": 2014, - "height": 1161, + "width": 2007, + "height": 1051, "frameCount": 80, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -7975,13 +8525,13 @@ { "x": 391, "y": 191, - "w": 103, + "w": 102, "h": 204, - "anchorX": -63, + "anchorX": -62, "anchorY": -122 }, { - "x": 495, + "x": 494, "y": 191, "w": 105, "h": 206, @@ -7989,39 +8539,39 @@ "anchorY": -124 }, { - "x": 601, + "x": 600, "y": 191, - "w": 107, + "w": 106, "h": 207, - "anchorX": -67, + "anchorX": -66, "anchorY": -125 }, { - "x": 709, + "x": 707, "y": 191, - "w": 109, + "w": 108, "h": 208, - "anchorX": -69, + "anchorX": -68, "anchorY": -126 }, { - "x": 819, + "x": 816, "y": 191, - "w": 110, + "w": 109, "h": 209, - "anchorX": -70, + "anchorX": -69, "anchorY": -127 }, { - "x": 930, + "x": 926, "y": 191, - "w": 111, + "w": 110, "h": 210, - "anchorX": -71, + "anchorX": -70, "anchorY": -128 }, { - "x": 1042, + "x": 1037, "y": 191, "w": 112, "h": 209, @@ -8029,7 +8579,7 @@ "anchorY": -128 }, { - "x": 1155, + "x": 1150, "y": 191, "w": 113, "h": 209, @@ -8037,7 +8587,7 @@ "anchorY": -127 }, { - "x": 1269, + "x": 1264, "y": 191, "w": 113, "h": 208, @@ -8045,7 +8595,7 @@ "anchorY": -126 }, { - "x": 1383, + "x": 1378, "y": 191, "w": 112, "h": 205, @@ -8053,23 +8603,23 @@ "anchorY": -123 }, { - "x": 1496, + "x": 1491, "y": 191, - "w": 110, + "w": 109, "h": 201, - "anchorX": -81, + "anchorX": -80, "anchorY": -119 }, { - "x": 1607, + "x": 1601, "y": 191, - "w": 107, + "w": 106, "h": 198, - "anchorX": -80, + "anchorX": -79, "anchorY": -116 }, { - "x": 1715, + "x": 1708, "y": 191, "w": 102, "h": 194, @@ -8077,333 +8627,984 @@ "anchorY": -112 }, { - "x": 1818, + "x": 1811, "y": 191, "w": 97, - "h": 202, + "h": 190, "anchorX": -78, - "anchorY": -120 + "anchorY": -108 }, { - "x": 1916, + "x": 1909, "y": 191, "w": 97, - "h": 219, + "h": 189, "anchorX": -75, - "anchorY": -137 + "anchorY": -107 }, { "x": 0, - "y": 411, + "y": 402, "w": 108, - "h": 242, + "h": 194, "anchorX": -67, - "anchorY": -160 + "anchorY": -112 }, { "x": 109, - "y": 411, + "y": 402, "w": 107, - "h": 259, + "h": 202, "anchorX": -47, - "anchorY": -177 + "anchorY": -120 }, { "x": 217, - "y": 411, + "y": 402, "w": 101, - "h": 266, + "h": 209, "anchorX": -35, - "anchorY": -184 + "anchorY": -127 }, { "x": 319, - "y": 411, + "y": 402, "w": 101, - "h": 265, + "h": 210, "anchorX": -33, - "anchorY": -183 + "anchorY": -128 }, { "x": 421, - "y": 411, + "y": 402, "w": 103, - "h": 259, + "h": 209, "anchorX": -33, - "anchorY": -177 + "anchorY": -127 }, { "x": 525, - "y": 411, + "y": 402, + "w": 107, + "h": 220, + "anchorX": -36, + "anchorY": -138 + }, + { + "x": 633, + "y": 402, "w": 111, - "h": 254, - "anchorX": -40, - "anchorY": -172 + "h": 227, + "anchorX": -39, + "anchorY": -145 }, { - "x": 637, - "y": 411, - "w": 144, - "h": 256, - "anchorX": -72, - "anchorY": -174 + "x": 745, + "y": 402, + "w": 115, + "h": 231, + "anchorX": -42, + "anchorY": -149 }, { - "x": 782, - "y": 411, - "w": 175, - "h": 256, - "anchorX": -102, - "anchorY": -174 + "x": 861, + "y": 402, + "w": 118, + "h": 230, + "anchorX": -45, + "anchorY": -148 }, { - "x": 958, - "y": 411, - "w": 179, - "h": 255, - "anchorX": -106, - "anchorY": -173 + "x": 980, + "y": 402, + "w": 122, + "h": 225, + "anchorX": -48, + "anchorY": -143 }, { - "x": 1138, - "y": 411, - "w": 156, - "h": 260, - "anchorX": -82, - "anchorY": -178 - }, - { - "x": 1295, - "y": 411, + "x": 1103, + "y": 402, "w": 125, - "h": 252, + "h": 213, "anchorX": -51, - "anchorY": -170 + "anchorY": -131 }, { - "x": 1421, - "y": 411, + "x": 1229, + "y": 402, "w": 128, - "h": 221, + "h": 196, "anchorX": -57, - "anchorY": -139 + "anchorY": -114 }, { - "x": 1550, - "y": 411, + "x": 1358, + "y": 402, "w": 140, "h": 193, "anchorX": -74, "anchorY": -111 }, { - "x": 1691, - "y": 411, - "w": 158, + "x": 1499, + "y": 402, + "w": 147, "h": 195, "anchorX": -87, "anchorY": -107 }, { - "x": 1850, - "y": 411, + "x": 1647, + "y": 402, "w": 151, "h": 203, "anchorX": -95, "anchorY": -105 }, { - "x": 0, - "y": 678, + "x": 1799, + "y": 402, "w": 150, "h": 199, "anchorX": -95, "anchorY": -102 }, { - "x": 151, - "y": 678, + "x": 0, + "y": 634, "w": 150, "h": 197, "anchorX": -95, "anchorY": -100 }, { - "x": 302, - "y": 678, + "x": 151, + "y": 634, "w": 151, "h": 197, "anchorX": -95, "anchorY": -100 }, { - "x": 454, - "y": 678, + "x": 303, + "y": 634, "w": 152, "h": 196, "anchorX": -95, "anchorY": -99 }, { - "x": 607, - "y": 678, + "x": 456, + "y": 634, "w": 153, "h": 197, "anchorX": -95, "anchorY": -100 }, { - "x": 761, - "y": 678, + "x": 610, + "y": 634, "w": 153, "h": 198, "anchorX": -95, "anchorY": -100 }, { - "x": 915, - "y": 678, + "x": 764, + "y": 634, "w": 153, "h": 200, "anchorX": -95, "anchorY": -102 }, { - "x": 1069, - "y": 678, + "x": 918, + "y": 634, "w": 153, "h": 205, "anchorX": -98, "anchorY": -107 }, { - "x": 1223, - "y": 678, + "x": 1072, + "y": 634, "w": 153, "h": 208, "anchorX": -102, "anchorY": -110 }, { - "x": 1377, - "y": 678, + "x": 1226, + "y": 634, "w": 154, - "h": 220, + "h": 208, "anchorX": -107, - "anchorY": -122 + "anchorY": -110 }, { - "x": 1532, - "y": 678, + "x": 1381, + "y": 634, "w": 153, - "h": 235, + "h": 210, "anchorX": -111, - "anchorY": -137 + "anchorY": -112 }, { - "x": 1686, - "y": 678, + "x": 1535, + "y": 634, "w": 151, - "h": 242, + "h": 212, "anchorX": -114, - "anchorY": -144 + "anchorY": -114 }, { - "x": 1838, - "y": 678, + "x": 1687, + "y": 634, "w": 148, - "h": 243, + "h": 211, "anchorX": -115, - "anchorY": -145 + "anchorY": -113 + }, + { + "x": 1836, + "y": 634, + "w": 143, + "h": 208, + "anchorX": -114, + "anchorY": -110 }, { "x": 0, - "y": 922, - "w": 143, - "h": 239, - "anchorX": -114, - "anchorY": -141 - }, - { - "x": 144, - "y": 922, + "y": 847, "w": 139, - "h": 230, + "h": 204, "anchorX": -113, - "anchorY": -132 + "anchorY": -106 }, { - "x": 284, - "y": 922, + "x": 140, + "y": 847, "w": 134, - "h": 217, + "h": 200, "anchorX": -111, - "anchorY": -119 + "anchorY": -102 }, { - "x": 419, - "y": 922, + "x": 275, + "y": 847, "w": 129, - "h": 202, + "h": 195, "anchorX": -108, - "anchorY": -104 + "anchorY": -97 }, { - "x": 549, - "y": 922, - "w": 128, + "x": 405, + "y": 847, + "w": 125, "h": 190, - "anchorX": -109, + "anchorX": -106, "anchorY": -92 }, { - "x": 678, - "y": 922, - "w": 129, + "x": 531, + "y": 847, + "w": 121, "h": 185, - "anchorX": -112, + "anchorX": -104, "anchorY": -87 }, { - "x": 808, - "y": 922, - "w": 130, + "x": 653, + "y": 847, + "w": 123, "h": 181, - "anchorX": -114, + "anchorX": -107, "anchorY": -83 }, { - "x": 939, - "y": 922, - "w": 131, + "x": 777, + "y": 847, + "w": 125, "h": 176, - "anchorX": -116, + "anchorX": -110, "anchorY": -78 }, { - "x": 1071, - "y": 922, - "w": 131, + "x": 903, + "y": 847, + "w": 125, "h": 173, - "anchorX": -117, + "anchorX": -111, "anchorY": -75 }, { - "x": 1203, - "y": 922, - "w": 131, + "x": 1029, + "y": 847, + "w": 125, "h": 172, - "anchorX": -117, + "anchorX": -111, "anchorY": -74 } ] }, + "fw_ov": { + "file": "campfire/pa-fw-ov.png", + "width": 2011, + "height": 232, + "frameCount": 80, + "rate": 1, + "drawMode": 5, + "frames": [ + { + "x": 0, + "y": 0, + "w": 10, + "h": 46, + "anchorX": -34, + "anchorY": -50 + }, + { + "x": 11, + "y": 0, + "w": 10, + "h": 46, + "anchorX": -32, + "anchorY": -50 + }, + { + "x": 22, + "y": 0, + "w": 9, + "h": 45, + "anchorX": -30, + "anchorY": -49 + }, + { + "x": 32, + "y": 0, + "w": 10, + "h": 46, + "anchorX": -29, + "anchorY": -49 + }, + { + "x": 43, + "y": 0, + "w": 10, + "h": 47, + "anchorX": -29, + "anchorY": -49 + }, + { + "x": 54, + "y": 0, + "w": 10, + "h": 49, + "anchorX": -29, + "anchorY": -48 + }, + { + "x": 65, + "y": 0, + "w": 9, + "h": 51, + "anchorX": -29, + "anchorY": -47 + }, + { + "x": 75, + "y": 0, + "w": 9, + "h": 52, + "anchorX": -29, + "anchorY": -45 + }, + { + "x": 85, + "y": 0, + "w": 9, + "h": 55, + "anchorX": -30, + "anchorY": -44 + }, + { + "x": 95, + "y": 0, + "w": 9, + "h": 57, + "anchorX": -31, + "anchorY": -42 + }, + { + "x": 105, + "y": 0, + "w": 8, + "h": 60, + "anchorX": -31, + "anchorY": -40 + }, + { + "x": 114, + "y": 0, + "w": 8, + "h": 62, + "anchorX": -32, + "anchorY": -37 + }, + { + "x": 123, + "y": 0, + "w": 8, + "h": 64, + "anchorX": -33, + "anchorY": -36 + }, + { + "x": 132, + "y": 0, + "w": 8, + "h": 65, + "anchorX": -33, + "anchorY": -36 + }, + { + "x": 141, + "y": 0, + "w": 7, + "h": 66, + "anchorX": -33, + "anchorY": -36 + }, + { + "x": 149, + "y": 0, + "w": 8, + "h": 66, + "anchorX": -34, + "anchorY": -37 + }, + { + "x": 158, + "y": 0, + "w": 8, + "h": 68, + "anchorX": -36, + "anchorY": -38 + }, + { + "x": 167, + "y": 0, + "w": 8, + "h": 68, + "anchorX": -38, + "anchorY": -38 + }, + { + "x": 176, + "y": 0, + "w": 8, + "h": 67, + "anchorX": -40, + "anchorY": -38 + }, + { + "x": 185, + "y": 0, + "w": 9, + "h": 65, + "anchorX": -42, + "anchorY": -38 + }, + { + "x": 195, + "y": 0, + "w": 9, + "h": 65, + "anchorX": -44, + "anchorY": -38 + }, + { + "x": 205, + "y": 0, + "w": 9, + "h": 63, + "anchorX": -46, + "anchorY": -36 + }, + { + "x": 215, + "y": 0, + "w": 10, + "h": 61, + "anchorX": -49, + "anchorY": -32 + }, + { + "x": 226, + "y": 0, + "w": 9, + "h": 60, + "anchorX": -51, + "anchorY": -29 + }, + { + "x": 236, + "y": 0, + "w": 10, + "h": 58, + "anchorX": -54, + "anchorY": -27 + }, + { + "x": 247, + "y": 0, + "w": 11, + "h": 57, + "anchorX": -57, + "anchorY": -29 + }, + { + "x": 259, + "y": 0, + "w": 11, + "h": 55, + "anchorX": -60, + "anchorY": -32 + }, + { + "x": 271, + "y": 0, + "w": 12, + "h": 53, + "anchorX": -63, + "anchorY": -36 + }, + { + "x": 284, + "y": 0, + "w": 13, + "h": 51, + "anchorX": -65, + "anchorY": -40 + }, + { + "x": 298, + "y": 0, + "w": 13, + "h": 49, + "anchorX": -67, + "anchorY": -44 + }, + { + "x": 312, + "y": 0, + "w": 15, + "h": 46, + "anchorX": -69, + "anchorY": -48 + }, + { + "x": 328, + "y": 0, + "w": 15, + "h": 43, + "anchorX": -70, + "anchorY": -51 + }, + { + "x": 344, + "y": 0, + "w": 18, + "h": 37, + "anchorX": -71, + "anchorY": -53 + }, + { + "x": 363, + "y": 0, + "w": 23, + "h": 31, + "anchorX": -72, + "anchorY": -53 + }, + { + "x": 387, + "y": 0, + "w": 29, + "h": 31, + "anchorX": -73, + "anchorY": -51 + }, + { + "x": 417, + "y": 0, + "w": 38, + "h": 31, + "anchorX": -76, + "anchorY": -48 + }, + { + "x": 456, + "y": 0, + "w": 44, + "h": 44, + "anchorX": -79, + "anchorY": -58 + }, + { + "x": 501, + "y": 0, + "w": 46, + "h": 59, + "anchorX": -81, + "anchorY": -72 + }, + { + "x": 548, + "y": 0, + "w": 45, + "h": 75, + "anchorX": -80, + "anchorY": -89 + }, + { + "x": 594, + "y": 0, + "w": 39, + "h": 92, + "anchorX": -76, + "anchorY": -105 + }, + { + "x": 634, + "y": 0, + "w": 35, + "h": 104, + "anchorX": -72, + "anchorY": -120 + }, + { + "x": 670, + "y": 0, + "w": 40, + "h": 113, + "anchorX": -64, + "anchorY": -137 + }, + { + "x": 711, + "y": 0, + "w": 49, + "h": 122, + "anchorX": -41, + "anchorY": -160 + }, + { + "x": 761, + "y": 0, + "w": 40, + "h": 120, + "anchorX": -14, + "anchorY": -177 + }, + { + "x": 802, + "y": 0, + "w": 28, + "h": 108, + "anchorX": -2, + "anchorY": -184 + }, + { + "x": 831, + "y": 0, + "w": 30, + "h": 89, + "anchorX": -5, + "anchorY": -183 + }, + { + "x": 862, + "y": 0, + "w": 41, + "h": 65, + "anchorX": -21, + "anchorY": -177 + }, + { + "x": 904, + "y": 0, + "w": 50, + "h": 47, + "anchorX": -40, + "anchorY": -172 + }, + { + "x": 955, + "y": 0, + "w": 78, + "h": 42, + "anchorX": -72, + "anchorY": -174 + }, + { + "x": 1034, + "y": 0, + "w": 110, + "h": 41, + "anchorX": -102, + "anchorY": -174 + }, + { + "x": 1145, + "y": 0, + "w": 117, + "h": 43, + "anchorX": -106, + "anchorY": -173 + }, + { + "x": 1263, + "y": 0, + "w": 95, + "h": 51, + "anchorX": -82, + "anchorY": -178 + }, + { + "x": 1359, + "y": 0, + "w": 51, + "h": 53, + "anchorX": -36, + "anchorY": -170 + }, + { + "x": 1411, + "y": 0, + "w": 34, + "h": 38, + "anchorX": -11, + "anchorY": -139 + }, + { + "x": 1446, + "y": 0, + "w": 70, + "h": 15, + "anchorX": -16, + "anchorY": -100 + }, + { + "x": 1517, + "y": 0, + "w": 135, + "h": 40, + "anchorX": -64, + "anchorY": -88 + }, + { + "x": 1653, + "y": 0, + "w": 53, + "h": 29, + "anchorX": -91, + "anchorY": -24 + }, + { + "x": 1707, + "y": 0, + "w": 58, + "h": 33, + "anchorX": -90, + "anchorY": -17 + }, + { + "x": 1766, + "y": 0, + "w": 65, + "h": 36, + "anchorX": -86, + "anchorY": -13 + }, + { + "x": 1832, + "y": 0, + "w": 76, + "h": 37, + "anchorX": -81, + "anchorY": -11 + }, + { + "x": 1909, + "y": 0, + "w": 90, + "h": 35, + "anchorX": -78, + "anchorY": -12 + }, + { + "x": 0, + "y": 123, + "w": 103, + "h": 31, + "anchorX": -75, + "anchorY": -15 + }, + { + "x": 104, + "y": 123, + "w": 112, + "h": 32, + "anchorX": -74, + "anchorY": -21 + }, + { + "x": 217, + "y": 123, + "w": 115, + "h": 40, + "anchorX": -74, + "anchorY": -40 + }, + { + "x": 333, + "y": 123, + "w": 114, + "h": 56, + "anchorX": -76, + "anchorY": -72 + }, + { + "x": 448, + "y": 123, + "w": 110, + "h": 71, + "anchorX": -79, + "anchorY": -100 + }, + { + "x": 559, + "y": 123, + "w": 104, + "h": 86, + "anchorX": -83, + "anchorY": -122 + }, + { + "x": 664, + "y": 123, + "w": 100, + "h": 93, + "anchorX": -88, + "anchorY": -137 + }, + { + "x": 765, + "y": 123, + "w": 96, + "h": 95, + "anchorX": -92, + "anchorY": -144 + }, + { + "x": 862, + "y": 123, + "w": 96, + "h": 94, + "anchorX": -95, + "anchorY": -145 + }, + { + "x": 959, + "y": 123, + "w": 99, + "h": 91, + "anchorX": -97, + "anchorY": -141 + }, + { + "x": 1059, + "y": 123, + "w": 106, + "h": 84, + "anchorX": -100, + "anchorY": -132 + }, + { + "x": 1166, + "y": 123, + "w": 113, + "h": 74, + "anchorX": -102, + "anchorY": -119 + }, + { + "x": 1280, + "y": 123, + "w": 120, + "h": 63, + "anchorX": -105, + "anchorY": -104 + }, + { + "x": 1401, + "y": 123, + "w": 125, + "h": 48, + "anchorX": -109, + "anchorY": -86 + }, + { + "x": 1527, + "y": 123, + "w": 126, + "h": 34, + "anchorX": -112, + "anchorY": -68 + }, + { + "x": 1654, + "y": 123, + "w": 123, + "h": 20, + "anchorX": -114, + "anchorY": -50 + }, + { + "x": 1778, + "y": 123, + "w": 118, + "h": 12, + "anchorX": -116, + "anchorY": -36 + }, + { + "x": 1897, + "y": 123, + "w": 113, + "h": 13, + "anchorX": -117, + "anchorY": -33 + }, + { + "x": 0, + "y": 219, + "w": 112, + "h": 13, + "anchorX": -117, + "anchorY": -32 + } + ] + }, "bw": { "file": "campfire/pa-bw.png", "width": 2045, "height": 597, "frameCount": 40, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -8735,6 +9936,7 @@ "height": 333, "frameCount": 32, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -9000,6 +10202,7 @@ "height": 333, "frameCount": 32, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -9265,6 +10468,7 @@ "height": 209, "frameCount": 12, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -9370,6 +10574,7 @@ "height": 77, "frameCount": 12, "rate": 1, + "drawMode": 3, "frames": [ { "x": 0, @@ -9475,6 +10680,7 @@ "height": 606, "frameCount": 52, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -9900,6 +11106,7 @@ "height": 68, "frameCount": 52, "rate": 1, + "drawMode": 3, "frames": [ { "x": 0, @@ -10325,6 +11532,7 @@ "height": 388, "frameCount": 30, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -10574,6 +11782,7 @@ "height": 73, "frameCount": 30, "rate": 1, + "drawMode": 3, "frames": [ { "x": 0, @@ -10825,6 +12034,7 @@ "height": 199, "frameCount": 21, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -11002,6 +12212,7 @@ "height": 199, "frameCount": 21, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -11179,6 +12390,7 @@ "height": 483, "frameCount": 21, "rate": 2, + "drawMode": 5, "frames": [ { "x": 0, @@ -11356,6 +12568,7 @@ "height": 1666, "frameCount": 121, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -12333,6 +13546,7 @@ "height": 460, "frameCount": 40, "rate": 1, + "drawMode": 5, "frames": [ { "x": 0, @@ -13551,12 +14765,30 @@ 9 ], "strings": { + "4007": "Barbarian", + "4008": "Paladin", + "4009": "Necromancer", + "4010": "Sorceress", + "4011": "Amazon", + "4012": "Druid", + "4013": "Assassin", + "4057": "Level", + "5017": "Level %d", "5101": "EXIT", "5102": "OK", "5103": "CANCEL", + "5171": "Connecting to Battle.net", + "5172": "Querying gateway", + "5173": "Connecting to the fastest server", + "5174": "Accessing your account", + "5175": "Checking versions", + "5176": "Verifying CD-key", + "5177": "Getting player profile", "5180": "Terms of Use", "5181": "AGREE", "5182": "DISAGREE", + "5204": "Battle.net Log In", + "5205": "To use an existing Battle.net account, enter the information below and click 'Log In'. Otherwise, click 'Create New Account'.", "5216": "Account name must be at least 2 characters.", "5217": "Account name can't be more than 15 characters.", "5218": "Passwords don't match.", @@ -13582,8 +14814,15 @@ "5238": "The passwords you have typed do not match. Please enter the same password twice.", "5239": "An account already exists with the name you have chosen. Please try again with a different name.", "5249": "Unable to Create Account", + "5334": "Gem Activated", + "5335": "Gem Deactivated", + "5336": "Perfect Gem Activated", "10834": "Verify Password", "11127": "PLEASE WRITE DOWN YOUR PASSWORD.", + "11132": "Expires Today", + "11133": "Expires in %d days", + "11134": "Character Expired", + "22731": "EXPANSION CHARACTER", "singlePlayer": "SINGLE PLAYER", "battleNet": "BATTLE.NET", "otherMultiplayer": "OTHER MULTIPLAYER", @@ -13591,9 +14830,17 @@ "credits": "CREDITS", "cinematics": "CINEMATICS", "gatewayPrefix": "GATEWAY: ", - "connectingToFastest": "Connecting to the fastest server...", - "checkingVersions": "Checking versions...", - "accessingAccount": "Accessing your account...", + "connectingToBattleNet": "Connecting to Battle.net", + "queryingGateway": "Querying gateway", + "connectingToFastest": "Connecting to the fastest server", + "accessingAccount": "Accessing your account", + "checkingVersions": "Checking versions", + "verifyingCdKey": "Verifying CD-key", + "gettingPlayerProfile": "Getting player profile", + "loginTitle": "Battle.net Log In", + "bnetLoginTitle": "Battle.net Log In", + "loginInstruction": "To use an existing Battle.net account, enter the information below and click 'Log In'. Otherwise, click 'Create New Account'.", + "bnetLoginInstruction": "To use an existing Battle.net account, enter the information below and click 'Log In'. Otherwise, click 'Create New Account'.", "accountName": "Account Name", "password": "Password", "verifyPassword": "Verify Password", @@ -13629,6 +14876,14 @@ "errAccountAlreadyExists": "An account already exists with the name you have chosen. Please try again with a different name.", "unableToCreateAccount": "Unable to Create Account", "termsOfUseText": "BATTLE.NET TERMS OF USE AGREEMENT\n\nTHE BATTLE.NET SERVICE IS PROVIDED TO YOU UNDER THE TERMS AND CONDITIONS OF THIS TERMS OF USE AGREEMENT (\"AGREEMENT\") AND ANY OPERATING RULES OR POLICIES THAT MAY BE PUBLISHED FROM TIME TO TIME BY BLIZZARD ENTERTAINMENT (\"BLIZZARD\").\n\nPLEASE READ THE FOLLOWING TERMS AND CONDITIONS CAREFULLY. BY CLICKING THE \"AGREE\" BUTTON BELOW, YOU ACKNOWLEDGE THAT YOU HAVE READ, UNDERSTOOD, AND AGREE TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS AGREEMENT.\n\n1. GRANT OF A LIMITED LICENSE TO USE THE SERVICE\nSubject to your agreement to and continuing compliance with this Agreement, Blizzard hereby grants, and you hereby accept, a limited, non-exclusive, non-transferable license to access and use the Battle.net Service solely for your personal, non-commercial entertainment purposes.\n\n2. ADDITIONAL LICENSE LIMITATIONS\nThe license granted to you in Section 1 is subject to the limitations set forth in Sections 1 and 2 (collectively, the \"License Limitations\"). Any use of the Service or the Game in violation of the License Limitations will be regarded as an infringement of Blizzard's copyrights in and to the Game. You agree that you will not, under any circumstances:\n(a) use cheats, automation software (bots), hacks, mods or any other unauthorized third-party software designed to modify the Battle.net Service or the Game experience;\n(b) exploit the Game or any of its parts, including without limitation the Service, for any commercial purpose;\n(c) use any unauthorized third-party software that intercepts, \"mines\", or otherwise collects information from or through the Game or the Service;\n(d) modify or cause to be modified any files that are a part of the Game or the Service in any way not expressly authorized by Blizzard;\n(e) host, provide or develop matchmaking services for the Game or the Service, or intercept, emulate or redirect the communication protocols used by Blizzard in any way, for any purpose, including without limitation unauthorized play over the internet, network play, or as part of content aggregation networks;\n(f) facilitate, create or maintain any unauthorized connection to the Game or the Service, including without limitation any connection to any unauthorized server that emulates, or attempts to emulate, the Service.\n\n3. ELIGIBILITY AND ACCOUNT SECURITY\nWhen creating an account on Battle.net, you must choose an Account Name and a Password. You may not use an Account Name that is used by someone else, is vulgar or offensive, or otherwise violates the Terms of Use. You are solely responsible for maintaining the confidentiality of your Password and for all activities that occur under your Account.\n\n4. CODE OF CONDUCT\nAs with all things, your use of Battle.net is governed by certain rules of conduct. While using the Battle.net Service, you agree to abide by rules of fair play and respect toward other players. Harassment, threats, hate speech, impersonation of Blizzard employees, and disruptive behavior in chat channels or games may result in temporary or permanent suspension of your Battle.net Account.\n\n5. ACCOUNT EXPIRATION AND TERMINATION\nBattle.net accounts and characters are subject to periodic expiration if left inactive. Accounts that have not been logged into for a period of ninety (90) days may be automatically deleted. Blizzard reserves the right to terminate this Agreement and revoke your access to the Battle.net Service at any time if you breach any provision of this Agreement.\n\n6. DISCLAIMER OF WARRANTIES AND LIMITATION OF LIABILITY\nTHE BATTLE.NET SERVICE IS PROVIDED ON AN \"AS IS\" AND \"AS AVAILABLE\" BASIS FOR YOUR USE, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND THOSE ARISING FROM COURSE OF DEALING OR USAGE OF TRADE.\n\nBY CLICKING \"AGREE\" BELOW, YOU CONFIRM THAT YOU HAVE READ AND ACCEPTED THIS BATTLE.NET TERMS OF USE AGREEMENT IN ITS ENTIRETY.", + "levelLabel": "Level", + "levelFormat": "Level %d", + "expiresToday": "Expires Today", + "expiresInDays": "Expires in %d days", + "characterExpired": "Character Expired", + "gemActivated": "Gem Activated", + "gemDeactivated": "Gem Deactivated", + "perfectGemActivated": "Perfect Gem Activated", "chooseClassTitle": "Choose a class for your campaign", "characterName": "Character Name", "expansionCharacter": "EXPANSION CHARACTER", @@ -13638,7 +14893,6 @@ "createNewCharacter": "CREATE NEW\nCHARACTER", "convertToExpansion": "CONVERT TO\nEXPANSION", "deleteCharacter": "DELETE\nCHARACTER", - "expiresIn90Days": "Expires in 90 days", "classes": { "am": { "name": "Amazon", @@ -13693,7 +14947,8 @@ "hell": "Hell", "createGameBtn": "CREATE GAME", "gemActivated": "Gem Activated", - "gemDeactivated": "Gem Deactivated" + "gemDeactivated": "Gem Deactivated", + "perfectGemActivated": "Perfect Gem Activated" } } } \ No newline at end of file diff --git a/src/baker/packers/pack-frontend-ui.ts b/src/baker/packers/pack-frontend-ui.ts index 5e13ec9..98b1172 100644 --- a/src/baker/packers/pack-frontend-ui.ts +++ b/src/baker/packers/pack-frontend-ui.ts @@ -47,6 +47,7 @@ export interface PackedAnimStripMeta { readonly frameCount: number readonly rate?: number readonly animRate?: number + readonly drawMode?: number readonly frames: readonly PackedStripFrameMeta[] } @@ -233,6 +234,7 @@ function packFramesToGrid( options?: { readonly rate?: number readonly animRate?: number + readonly drawMode?: number }, ): { meta: PackedAnimStripMeta @@ -275,6 +277,7 @@ function packFramesToGrid( frameCount: frames.length, ...(options?.rate !== undefined ? { rate: options.rate } : {}), ...(options?.animRate !== undefined ? { animRate: options.animRate } : {}), + ...(options?.drawMode !== undefined ? { drawMode: options.drawMode } : {}), frames: frameMetas, }, pixels, @@ -631,10 +634,13 @@ async function main(): Promise { animStrips['logoBnetFire'] = packedFire.meta } - // Campfire `fire.dc6` at control anchor (345, 454) on charactercreationscreenEXP.dc6 + // Campfire `fire.dc6` at expansion control anchor (345, 470) on charactercreationscreenEXP.dc6 (0x6fa63080) const fireDc6 = await decodeUiDc6('data\\global\\ui\\FrontEnd\\fire.dc6') { - const packed = packFramesToGrid('campfire/fire-strip.png', fireDc6.groups[0]!.frames, 2048, { rate: 1 }) + const packed = packFramesToGrid('campfire/fire-strip.png', fireDc6.groups[0]!.frames, 2048, { + rate: 1, + drawMode: 3, + }) savePng(packed.meta.file, packed.meta.width, packed.meta.height, packed.pixels, fecharPl2.rgb, true) animStrips['campfireFlame'] = packed.meta } @@ -643,8 +649,9 @@ async function main(): Promise { // 5. 7-Class Campfire Multi-State Animations (fechar/pal.pl2) // ============================================================================ console.log('Packing 7-class campfire multi-state animations...') - // Per D2Launch.dll 0x6fa68df8, 0x6fa60c08 & 0x6fa5d780: + // Per D2Launch.dll 0x6fa68df8, 0x6fa60c08, 0x6fa65d9c, 0x6fa65db0 & 0x6fa5d780: // Note: ptAnimTable != NULL does NOT subtract frame0.offset! + // DrawMode: 5 = DRAWMODE_NORMAL, 3 = DRAWMODE_MODULATE (screen) interface ClassCampfireSpec { readonly id: string readonly dir: string @@ -654,7 +661,8 @@ async function main(): Promise { readonly base: string readonly rate: number readonly overlay?: string - readonly additiveOverlay?: boolean + readonly overlayDrawMode?: 3 | 5 + readonly compositeOverlay?: boolean }[] } @@ -667,7 +675,7 @@ async function main(): Promise { { key: 'nu1', base: 'amnu1.dc6', rate: 3 }, { key: 'nu2', base: 'amnu2.dc6', rate: 3 }, { key: 'nu3', base: 'amnu3.dc6', rate: 2 }, - { key: 'fw', base: 'amfw.dc6', rate: 1, overlay: 'amfws.dc6', additiveOverlay: false }, + { key: 'fw', base: 'amfw.dc6', rate: 1, overlay: 'amfws.dc6', compositeOverlay: true }, { key: 'bw', base: 'ambw.dc6', rate: 1 }, ], }, @@ -690,9 +698,9 @@ async function main(): Promise { states: [ { key: 'nu1', base: 'nenu1.dc6', rate: 3 }, { key: 'nu2', base: 'nenu2.dc6', rate: 3 }, - { key: 'nu3', base: 'nenu3.dc6', rate: 2, overlay: 'nenu3s.dc6', additiveOverlay: true }, - { key: 'fw', base: 'nefw.dc6', rate: 1, overlay: 'nefws.dc6', additiveOverlay: true }, - { key: 'bw', base: 'nebw.dc6', rate: 1, overlay: 'nebws.dc6', additiveOverlay: true }, + { key: 'nu3', base: 'nenu3.dc6', rate: 2, overlay: 'nenu3s.dc6', overlayDrawMode: 3 }, + { key: 'fw', base: 'nefw.dc6', rate: 1, overlay: 'nefws.dc6', overlayDrawMode: 3 }, + { key: 'bw', base: 'nebw.dc6', rate: 1, overlay: 'nebws.dc6', overlayDrawMode: 3 }, ], }, { @@ -703,7 +711,7 @@ async function main(): Promise { { key: 'nu1', base: 'banu1.dc6', rate: 2 }, { key: 'nu2', base: 'banu2.dc6', rate: 2 }, { key: 'nu3', base: 'banu3.dc6', rate: 1 }, - { key: 'fw', base: 'bafw.dc6', rate: 1, overlay: 'bafws.dc6', additiveOverlay: false }, + { key: 'fw', base: 'bafw.dc6', rate: 1, overlay: 'bafws.dc6', overlayDrawMode: 5 }, { key: 'bw', base: 'babw.dc6', rate: 1 }, ], }, @@ -715,7 +723,7 @@ async function main(): Promise { { key: 'nu1', base: 'panu1.dc6', rate: 2 }, { key: 'nu2', base: 'panu2.dc6', rate: 2 }, { key: 'nu3', base: 'panu3.dc6', rate: 2 }, - { key: 'fw', base: 'pafw.dc6', rate: 1, overlay: 'pafws.dc6', additiveOverlay: false }, + { key: 'fw', base: 'pafw.dc6', rate: 1, overlay: 'pafws.dc6', overlayDrawMode: 5 }, { key: 'bw', base: 'pabw.dc6', rate: 1 }, ], }, @@ -726,9 +734,9 @@ async function main(): Promise { states: [ { key: 'nu1', base: 'sonu1.dc6', rate: 2 }, { key: 'nu2', base: 'sonu2.dc6', rate: 2 }, - { key: 'nu3', base: 'sonu3.dc6', rate: 1, overlay: 'sonu3s.dc6', additiveOverlay: true }, - { key: 'fw', base: 'sofw.dc6', rate: 1, overlay: 'sofws.dc6', additiveOverlay: true }, - { key: 'bw', base: 'sobw.dc6', rate: 1, overlay: 'sobws.dc6', additiveOverlay: true }, + { key: 'nu3', base: 'sonu3.dc6', rate: 1, overlay: 'sonu3s.dc6', overlayDrawMode: 3 }, + { key: 'fw', base: 'sofw.dc6', rate: 1, overlay: 'sofws.dc6', overlayDrawMode: 3 }, + { key: 'bw', base: 'sobw.dc6', rate: 1, overlay: 'sobws.dc6', overlayDrawMode: 3 }, ], }, { @@ -755,22 +763,38 @@ async function main(): Promise { ? await decodeUiDc6(`data\\global\\ui\\FrontEnd\\${cls.dir}\\${st.overlay}`) : null - if (ovDc6 && !st.additiveOverlay) { + if (ovDc6 && st.compositeOverlay) { const composited = compositeOverlayFrames(baseDc6.groups[0]!.frames, ovDc6.groups[0]!.frames) const relFile = `campfire/${cls.id}-${st.key}.png` - const packed = packFramesToGrid(relFile, composited, 2048, { rate: st.rate }) + const packed = packFramesToGrid(relFile, composited, 2048, { rate: st.rate, drawMode: 5 }) savePng(relFile, packed.meta.width, packed.meta.height, packed.pixels, fecharPl2.rgb, true) campfireClasses[cls.id]![st.key] = packed.meta } else { const relFile = `campfire/${cls.id}-${st.key}.png` - const packed = packFramesToGrid(relFile, baseDc6.groups[0]!.frames, 2048, { rate: st.rate }) + const packed = packFramesToGrid(relFile, baseDc6.groups[0]!.frames, 2048, { + rate: st.rate, + drawMode: 5, + }) savePng(relFile, packed.meta.width, packed.meta.height, packed.pixels, fecharPl2.rgb, true) campfireClasses[cls.id]![st.key] = packed.meta - if (ovDc6 && st.additiveOverlay) { + if (ovDc6 && st.overlayDrawMode !== undefined) { const ovRelFile = `campfire/${cls.id}-${st.key}-ov.png` - const ovPacked = packFramesToGrid(ovRelFile, ovDc6.groups[0]!.frames, 2048, { rate: st.rate }) + const ovPacked = packFramesToGrid(ovRelFile, ovDc6.groups[0]!.frames, 2048, { + rate: st.rate, + drawMode: st.overlayDrawMode, + }) savePng(ovRelFile, ovPacked.meta.width, ovPacked.meta.height, ovPacked.pixels, fecharPl2.rgb, true) + // Also write nested subdir path (`campfire//_ov.png`) for path compatibility + mkdirSync(join(outDir, 'campfire', cls.id), { recursive: true }) + savePng( + `campfire/${cls.id}/${st.key}_ov.png`, + ovPacked.meta.width, + ovPacked.meta.height, + ovPacked.pixels, + fecharPl2.rgb, + true, + ) campfireClasses[cls.id]![`${st.key}_ov`] = ovPacked.meta } } @@ -920,12 +944,32 @@ async function main(): Promise { 'BY CLICKING "AGREE" BELOW, YOU CONFIRM THAT YOU HAVE READ AND ACCEPTED THIS BATTLE.NET TERMS OF USE AGREEMENT IN ITS ENTIRETY.', ].join('\n') + const s4007 = lookupById(4007) ?? 'Barbarian' + const s4008 = lookupById(4008) ?? 'Paladin' + const s4009 = lookupById(4009) ?? 'Necromancer' + const s4010 = lookupById(4010) ?? 'Sorceress' + const s4011 = lookupById(4011) ?? 'Amazon' + const s4012 = lookupById(4012) ?? 'Druid' + const s4013 = lookupById(4013) ?? 'Assassin' + const s4057 = lookupById(4057) ?? 'Level' + const s5017 = lookupById(5017) ?? 'Level %d' const s5101 = lookupById(5101) ?? 'EXIT' const s5102 = lookupById(5102) ?? 'OK' const s5103 = lookupById(5103) ?? 'CANCEL' + const s5171 = lookupById(5171) ?? 'Connecting to Battle.net' + const s5172 = lookupById(5172) ?? 'Querying gateway' + const s5173 = lookupById(5173) ?? 'Connecting to the fastest server' + const s5174 = lookupById(5174) ?? 'Accessing your account' + const s5175 = lookupById(5175) ?? 'Checking versions' + const s5176 = lookupById(5176) ?? 'Verifying CD-key' + const s5177 = lookupById(5177) ?? 'Getting player profile' const s5180 = lookupById(5180) ?? 'Terms of Use' const s5181 = lookupById(5181) ?? 'AGREE' const s5182 = lookupById(5182) ?? 'DISAGREE' + const s5204 = lookupById(5204) ?? 'Battle.net Log In' + const s5205 = + lookupById(5205) ?? + "To use an existing Battle.net account, enter the information below and click 'Log In'. Otherwise, click 'Create New Account'." const s5216 = lookupById(5216) ?? 'Account name must be at least 2 characters.' const s5217 = lookupById(5217) ?? "Account name can't be more than 15 characters." const s5218 = lookupById(5218) ?? "Passwords don't match." @@ -967,8 +1011,15 @@ async function main(): Promise { lookupById(5239) ?? 'An account already exists with the name you have chosen. Please try again with a different name.' const s5249 = lookupById(5249) ?? 'Unable to Create Account' + const s5334 = lookupById(5334) ?? 'Gem Activated' + const s5335 = lookupById(5335) ?? 'Gem Deactivated' + const s5336 = lookupById(5336) ?? 'Perfect Gem Activated' const s10834 = lookupById(10834) ?? 'Verify Password' const s11127 = lookupById(11127) ?? 'PLEASE WRITE DOWN YOUR PASSWORD.' + const s11132 = lookupById(11132) ?? 'Expires Today' + const s11133 = lookupById(11133) ?? 'Expires in %d days' + const s11134 = lookupById(11134) ?? 'Character Expired' + const s22731 = lookupById(22731) ?? 'EXPANSION CHARACTER' const strings = { singlePlayer: lookupByKey('strSinglePlayer') ?? 'SINGLE PLAYER', @@ -978,9 +1029,17 @@ async function main(): Promise { credits: 'CREDITS', cinematics: 'CINEMATICS', gatewayPrefix: 'GATEWAY: ', - connectingToFastest: 'Connecting to the fastest server...', - checkingVersions: 'Checking versions...', - accessingAccount: 'Accessing your account...', + connectingToBattleNet: s5171, + queryingGateway: s5172, + connectingToFastest: s5173, + accessingAccount: s5174, + checkingVersions: s5175, + verifyingCdKey: s5176, + gettingPlayerProfile: s5177, + loginTitle: s5204, + bnetLoginTitle: s5204, + loginInstruction: s5205, + bnetLoginInstruction: s5205, accountName: s5224, password: s5225, verifyPassword: s10834 || s5226, @@ -1016,12 +1075,38 @@ async function main(): Promise { errAccountAlreadyExists: s5239, unableToCreateAccount: s5249, termsOfUseText: defaultTermsOfUseText, + levelLabel: s4057, + levelFormat: s5017, + expiresToday: s11132, + expiresInDays: s11133, + characterExpired: s11134, + gemActivated: s5334, + gemDeactivated: s5335, + perfectGemActivated: s5336, + '4007': s4007, + '4008': s4008, + '4009': s4009, + '4010': s4010, + '4011': s4011, + '4012': s4012, + '4013': s4013, + '4057': s4057, + '5017': s5017, '5101': s5101, '5102': s5102, '5103': s5103, + '5171': s5171, + '5172': s5172, + '5173': s5173, + '5174': s5174, + '5175': s5175, + '5176': s5176, + '5177': s5177, '5180': s5180, '5181': s5181, '5182': s5182, + '5204': s5204, + '5205': s5205, '5216': s5216, '5217': s5217, '5218': s5218, @@ -1047,57 +1132,63 @@ async function main(): Promise { '5238': s5238, '5239': s5239, '5249': s5249, + '5334': s5334, + '5335': s5335, + '5336': s5336, '10834': s10834, '11127': s11127, + '11132': s11132, + '11133': s11133, + '11134': s11134, + '22731': s22731, chooseClassTitle: 'Choose a class for your campaign', characterName: 'Character Name', - expansionCharacter: 'EXPANSION CHARACTER', + expansionCharacter: s22731, hardcore: 'HARDCORE', ladderCharacter: 'LADDER CHARACTER', selectCharacterTitle: 'Select a Character', createNewCharacter: 'CREATE NEW\nCHARACTER', convertToExpansion: 'CONVERT TO\nEXPANSION', deleteCharacter: 'DELETE\nCHARACTER', - expiresIn90Days: 'Expires in 90 days', classes: { am: { - name: lookupByKey('Amazon') ?? 'Amazon', + name: s4011, desc: lookupByKey('strAmazonDesc') ?? 'Skilled with the spear and the bow, she is a very versatile fighter.', }, as: { - name: lookupByKey('Assassin') ?? 'Assassin', + name: s4013, desc: lookupByKey('AssasinDesc') ?? 'Schooled in the Martial Arts, her mind and body are deadly weapons.', }, ne: { - name: lookupByKey('Necromancer') ?? 'Necromancer', + name: s4009, desc: lookupByKey('strNecroDesc') ?? 'Summoning undead minions and cursing his enemies are his specialties.', }, ba: { - name: lookupByKey('Barbarian') ?? 'Barbarian', + name: s4007, desc: lookupByKey('strBarbDesc') ?? 'He is unequaled in close-quarters combat and mastery of weapons.', }, pa: { - name: lookupByKey('Paladin') ?? 'Paladin', + name: s4008, desc: lookupByKey('strPaladinDesc') ?? 'He is a natural party leader, holy man, and blessed warrior.', }, so: { - name: lookupByKey('Sorceress') ?? 'Sorceress', + name: s4010, desc: lookupByKey('strSorcDesc') ?? 'She has mastered the elemental magics -- fire, lightning, and ice.', }, dz: { - name: lookupByKey('Druid') ?? 'Druid', + name: s4012, desc: lookupByKey('DruidDesc') ?? 'Commanding the forces of nature, he summons wild beasts and raging storms to his side.', @@ -1126,8 +1217,9 @@ async function main(): Promise { nightmare: 'Nightmare', hell: 'Hell', createGameBtn: 'CREATE GAME', - gemActivated: 'Gem Activated', - gemDeactivated: 'Gem Deactivated', + gemActivated: s5334, + gemDeactivated: s5335, + perfectGemActivated: s5336, }, } diff --git a/src/client/i18n/lang.ts b/src/client/i18n/lang.ts index 19c2e10..72e00ba 100644 --- a/src/client/i18n/lang.ts +++ b/src/client/i18n/lang.ts @@ -37,74 +37,90 @@ setDefaultTblLangProvider( }, ) +/** + * Default language for entry pages without an explicit language suffix (`acts.html`, `arena.html`, + * `index.html`) or unrecognized paths, preserving backward compatibility with Chinese (`CHI` / `'zh'`) + * per Issue #503 & Issue #712. + */ +export const DEFAULT_GAME_LANG: GameLang = 'zh' +export const DEFAULT_TBL_LANG: TblLang = 'CHI' + +/** Canonical HTML entry pages that resolve to English (`ENG` / `'en'`). */ +export const ENGLISH_ENTRY_PAGES = [ + 'acts-en.html', + 'arena-en.html', + 'bnet.html', + 'play-en.html', +] as const + +/** + * Canonical HTML entry pages that resolve to Chinese (`CHI` / `'zh'`), including + * backward-compatible unsuffixed pages (`acts.html`, `arena.html`, `index.html`). + */ +export const CHINESE_ENTRY_PAGES = [ + 'acts-zh.html', + 'arena-zh.html', + 'acts.html', + 'arena.html', + 'index.html', +] as const + +/** + * Resolve `GameLang` from a URL pathname if it matches a known HTML entry page: + * - `acts-en.html`, `arena-en.html`, `bnet.html`, `play-en.html` -> `'en'` (`ENG`) + * - `acts-zh.html`, `arena-zh.html`, `acts.html`, `arena.html`, `index.html` -> `'zh'` (`CHI`) + * - Any other pathname -> `null` (caller falls back to query param / localStorage / `` / `DEFAULT_GAME_LANG`) + */ +export function resolveLangFromPathname(pathname: string): GameLang | null { + const path = (pathname || '').toLowerCase() + for (const page of ENGLISH_ENTRY_PAGES) { + if (path === page || path.endsWith(`/${page}`)) { + return 'en' + } + } + for (const page of CHINESE_ENTRY_PAGES) { + if (path === page || path.endsWith(`/${page}`)) { + return 'zh' + } + } + return null +} + +/** + * Detect `TblLang` (`'CHI'` | `'ENG'`) from a URL string using deterministic precedence: + * 1. Explicit `?lang=` query parameter (`en`/`eng`/`english` -> `'ENG'`; `zh`/`chi`/`zh-cn`/`cn`/`chinese` -> `'CHI'`). + * 2. Known HTML entry-page filename via `resolveLangFromPathname` (`acts-en.html` -> `'ENG'`, `acts-zh.html` / `acts.html` -> `'CHI'`). + * 3. Backward-compatible default `DEFAULT_TBL_LANG` (`'CHI'`) when an explicit URL string is provided without a recognized language suffix. + * 4. Runtime `detectInitialLang()` when called with no URL argument. + */ export function detectLangFromUrl(urlStr?: string): TblLang { if (typeof urlStr === 'string' && urlStr.trim() !== '') { - try { - const u = new URL(urlStr, 'http://localhost') - const q = u.searchParams.get('lang')?.trim().toLowerCase() - if (q === 'en' || q === 'eng' || q === 'english') return 'ENG' - if (q === 'zh' || q === 'chi' || q === 'zh-cn' || q === 'cn' || q === 'chinese') return 'CHI' - const path = u.pathname.toLowerCase() - if ( - path.endsWith('/acts-en.html') || - path.endsWith('acts-en.html') || - path.endsWith('/arena-en.html') || - path.endsWith('arena-en.html') || - path.endsWith('/bnet.html') || - path.endsWith('bnet.html') || - path.endsWith('/play-en.html') || - path.endsWith('play-en.html') - ) { - return 'ENG' - } - if ( - path.endsWith('/acts-zh.html') || - path.endsWith('acts-zh.html') || - path.endsWith('/arena-zh.html') || - path.endsWith('arena-zh.html') - ) { - return 'CHI' - } - return 'CHI' - } catch { - // fall through + const u = new URL(urlStr, 'http://localhost') + const q = u.searchParams.get('lang')?.trim().toLowerCase() + if (q === 'en' || q === 'eng' || q === 'english') return 'ENG' + if (q === 'zh' || q === 'chi' || q === 'zh-cn' || q === 'cn' || q === 'chinese') return 'CHI' + const fromPath = resolveLangFromPathname(u.pathname) + if (fromPath !== null) { + return fromPath === 'en' ? 'ENG' : 'CHI' } + return DEFAULT_TBL_LANG } return detectInitialLang() === 'en' ? 'ENG' : 'CHI' } export function detectInitialLang(): GameLang { if (typeof window !== 'undefined' && window.location) { - try { - const params = new URLSearchParams(window.location.search || '') - const q = params.get('lang')?.trim().toLowerCase() - if (q === 'en' || q === 'eng' || q === 'english') return 'en' - if (q === 'zh' || q === 'chi' || q === 'zh-cn' || q === 'cn' || q === 'chinese') return 'zh' - } catch { - // Ignore malformed query string - } + const params = new URLSearchParams(window.location.search || '') + const q = params.get('lang')?.trim().toLowerCase() + if (q === 'en' || q === 'eng' || q === 'english') return 'en' + if (q === 'zh' || q === 'chi' || q === 'zh-cn' || q === 'cn' || q === 'chinese') return 'zh' + const path = (window.location.pathname || '').toLowerCase() - if ( - path.endsWith('/acts-en.html') || - path.endsWith('acts-en.html') || - path.endsWith('/arena-en.html') || - path.endsWith('arena-en.html') || - path.endsWith('/bnet.html') || - path.endsWith('bnet.html') || - path.endsWith('/play-en.html') || - path.endsWith('play-en.html') - ) { - return 'en' + const fromPath = resolveLangFromPathname(path) + if (fromPath !== null) { + return fromPath } - if ( - path.endsWith('/acts-zh.html') || - path.endsWith('acts-zh.html') || - path.endsWith('/arena-zh.html') || - path.endsWith('arena-zh.html') - ) { - return 'zh' - } - if (path.endsWith('/play.html') || path.endsWith('play.html')) { + if (path === 'play.html' || path.endsWith('/play.html')) { try { const raw = window.localStorage?.getItem('d2web.settings.v1') if (raw) { @@ -114,7 +130,7 @@ export function detectInitialLang(): GameLang { } } } catch { - // Ignore storage errors + // Ignore storage access errors in sandboxed contexts } } } @@ -123,7 +139,7 @@ export function detectInitialLang(): GameLang { if (docLang.startsWith('en')) return 'en' if (docLang.startsWith('zh')) return 'zh' } - return 'zh' + return DEFAULT_GAME_LANG } export function getGameLang(): GameLang { @@ -154,20 +170,30 @@ function notifyListeners(): void { const gameLang = getGameLang() const tbl = gameLang === 'en' ? 'ENG' : 'CHI' setDefaultTblLang(tbl) - for (const fn of listeners) { + const errors: unknown[] = [] + for (const fn of [...listeners]) { try { fn(tbl) - } catch { - // ignore listener errors + } catch (err) { + errors.push(err) } } - for (const fn of gameLangListeners) { + for (const fn of [...gameLangListeners]) { try { fn(gameLang) - } catch { - // ignore listener errors + } catch (err) { + errors.push(err) } } + if (errors.length === 1) { + throw errors[0] + } + if (errors.length > 1) { + throw new AggregateError( + errors, + `${errors.length} language change listeners failed during notifyListeners`, + ) + } } export function setGameLang(lang: GameLang | TblLang | null): void { @@ -212,6 +238,8 @@ export function onLangChange(listener: (lang: GameLang) => void): () => void { } } +export const onGameLangChange = onLangChange + export function buildLangSwitchHref( targetLang?: TblLang | GameLang, currentPath?: string, diff --git a/src/client/main.ts b/src/client/main.ts index 8c3c94c..6d6183d 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -472,29 +472,55 @@ async function bootPlayApp(): Promise { frontendRenderer.render(performance.now()) } + let bnetConnectPending = false + frontendRenderer.setAsyncHandlers({ onConnect: async (st) => { - st.statusText = '' + const manifestStrings = frontendRenderer.manifest?.strings as + | Record + | undefined + bnetConnectPending = true + ;(st as { bnetConnectStage?: number }).bnetConnectStage = 0 + st.statusText = manifestStrings?.['5171'] ?? manifestStrings?.connectingToBattleNet ?? '' st.lastError = null st.loginError = null + toolbar.setConnectionState({ phase: 'connecting', rttMs: null }) frontendRenderer.render(performance.now()) + await Promise.resolve() + if (st.stage !== 'bnet_connecting') { + bnetConnectPending = false + return + } + ;(st as { bnetConnectStage?: number }).bnetConnectStage = 2 + st.statusText = manifestStrings?.['5173'] ?? manifestStrings?.connectingToFastest ?? '' + frontendRenderer.render(performance.now()) + await Promise.resolve() + if (st.stage === 'bnet_connecting') { + bnetConnectPending = false + ;(st as { bnetConnectStage?: number }).bnetConnectStage = 3 + st.statusText = '' + st.stage = 'bnet_login' + st.stageTicks = 0 + st.activeInput = st.accountName.trim().length > 0 ? 'accountPassword' : 'accountName' + frontendRenderer.render(performance.now()) + } }, onLogin: async (st, createNewAccount) => { const username = st.accountName.trim() const password = st.accountPassword if (!username) { - st.loginError = 'PLEASE ENTER AN ACCOUNT NAME.' - st.lastError = st.loginError - frontendRenderer.render(performance.now()) return } if (loginInFlight) return loginInFlight = true - st.statusText = '' + const manifestStrings = frontendRenderer.manifest?.strings as + | Record + | undefined + st.statusText = manifestStrings?.['5174'] ?? manifestStrings?.accessingAccount ?? '' st.loginError = null st.lastError = null frontendRenderer.render(performance.now()) - const targetHost = settingsStore.get().serverHost || st.gateway || 'www.laiseek.xyz' + const targetHost = settingsStore.get().serverHost || 'www.laiseek.xyz' try { const res = createNewAccount ? await session.createAccountAndLogin({ username, password, host: targetHost }) @@ -507,10 +533,12 @@ async function bootPlayApp(): Promise { if (res.realms[0]) { st.gateway = res.realms[0].title } + st.statusText = '' applyOnlineCharactersToFlowState(st, res.characters, { stayOnCharSelect: Boolean(createNewAccount), }) } catch (err) { + st.statusText = '' if (err instanceof BncsCreateAccountError) { applyCreateAccountServerError( st, @@ -529,10 +557,7 @@ async function bootPlayApp(): Promise { }, onCreateCharacter: async (st) => { const name = st.charName.trim() - if (!name || name.length < 2 || !st.selectedClass) { - st.charCreateError = 'CHARACTER NAME MUST BE 2-15 LETTERS.' - st.lastError = st.charCreateError - frontendRenderer.render(performance.now()) + if (!name || name.length < 2 || name.length > 15 || !st.selectedClass) { return } st.statusText = '' @@ -605,9 +630,6 @@ async function bootPlayApp(): Promise { resetHeroAvatarAnims(st) st.lobbySubPanel = 'create' st.createGameError = null - if (!isDifficultyUnlocked(selected, st.difficulty)) { - st.difficulty = 'normal' - } st.activeInput = 'gameName' } catch (err) { if (err instanceof SessionLostError) return @@ -645,32 +667,25 @@ async function bootPlayApp(): Promise { onCreateGame: async (st) => { const gameName = st.gameName.trim() if (!gameName) { - st.createGameError = 'PLEASE ENTER A GAME NAME.' - st.lastError = st.createGameError - frontendRenderer.render(performance.now()) return } const maxPlayers = Number.parseInt(st.maxPlayers.trim(), 10) if (!Number.isFinite(maxPlayers) || maxPlayers < 1 || maxPlayers > 8) { - st.createGameError = 'MAX NUMBER OF PLAYERS MUST BE 1-8.' - st.lastError = st.createGameError - frontendRenderer.render(performance.now()) return } let levelDiff = 0xff if (st.charDifferenceEnabled) { const parsedDiff = Number.parseInt(st.charDifference.trim(), 10) if (!Number.isFinite(parsedDiff) || parsedDiff < 1 || parsedDiff > 99) { - st.createGameError = 'CHARACTER DIFFERENCE MUST BE 1-99.' - st.lastError = st.createGameError - frontendRenderer.render(performance.now()) return } levelDiff = parsedDiff } const selectedChar = st.roster[st.selectedSlotIndex] if (selectedChar && !isDifficultyUnlocked(selectedChar, st.difficulty)) { - st.difficulty = 'normal' + throw new Error( + `Difficulty "${st.difficulty}" is not unlocked for character "${selectedChar.name}" (progression=${selectedChar.progression})`, + ) } const diffNum: 0 | 1 | 2 = st.difficulty === 'hell' ? 2 : st.difficulty === 'nightmare' ? 1 : 0 @@ -930,6 +945,15 @@ async function bootPlayApp(): Promise { if (!inGameView) { stepFlowFrame(flowState, nowMs) + if (bnetConnectPending && flowState.stage === 'bnet_connecting' && flowState.stageTicks >= 1) { + bnetConnectPending = false + ;(flowState as { bnetConnectStage?: number }).bnetConnectStage = 3 + flowState.statusText = '' + flowState.stage = 'bnet_login' + flowState.stageTicks = 0 + flowState.activeInput = + flowState.accountName.trim().length > 0 ? 'accountPassword' : 'accountName' + } if (flowState.stage === 'in_game' && (session.phase === 'ingame' || session.phase === 'replay')) { inGameView = true flowState.tickAccumulatorMs = 0 diff --git a/tests/client/frontend-i18n-manifest-batch8.test.ts b/tests/client/frontend-i18n-manifest-batch8.test.ts new file mode 100644 index 0000000..907b160 --- /dev/null +++ b/tests/client/frontend-i18n-manifest-batch8.test.ts @@ -0,0 +1,226 @@ +import { existsSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + detectLangFromUrl, + onGameLangChange, + onLangChange, + resolveLangFromPathname, + setGameLang, + setLang, +} from '../../src/client/i18n/lang.ts' + +const ROOT_DIR = resolve(import.meta.dirname, '../..') +const FRONTEND_DIR = resolve(ROOT_DIR, 'public/ui/frontend') +const MANIFEST_PATH = resolve(FRONTEND_DIR, 'manifest.json') +const MAIN_TS_PATH = resolve(ROOT_DIR, 'src/client/main.ts') + +function readPngHeader(filePath: string): { readonly width: number; readonly height: number } { + const buf = readFileSync(filePath) + expect(buf[0]).toBe(0x89) + expect(buf[1]).toBe(0x50) + expect(buf[2]).toBe(0x4e) + expect(buf[3]).toBe(0x47) + return { + width: buf.readUInt32BE(16), + height: buf.readUInt32BE(20), + } +} + +describe('Batch 7 & 8 Phase 2 — Campfire Overlays, Manifest TBL Strings, main.ts Guards & i18n Fail-Fast (#702, #712, #697, #698, #699, #700, #704, #707, #708, #710)', () => { + const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as { + readonly animStrips: Record< + string, + { + readonly file: string + readonly width: number + readonly height: number + readonly frameCount: number + readonly rate?: number + readonly drawMode?: number + } + > + readonly campfireClasses: Record< + string, + Record< + string, + { + readonly file: string + readonly width: number + readonly height: number + readonly frameCount: number + readonly rate?: number + readonly drawMode?: number + readonly frames: readonly unknown[] + } + > + > + readonly strings: Record & { + readonly lobby: Record + } + } + + afterEach(() => { + setGameLang(null) + }) + + it('#712: lang.ts notifyListeners propagates listener errors instead of swallowing them', () => { + const unsub = onLangChange(() => { + throw new Error('broken tbl listener') + }) + try { + expect(() => setLang('en')).toThrow('broken tbl listener') + } finally { + unsub() + } + + const calls: string[] = [] + const u1 = onGameLangChange((l) => { + calls.push(`first:${l}`) + throw new Error('first error') + }) + const u2 = onGameLangChange((l) => { + calls.push(`second:${l}`) + }) + try { + expect(() => setGameLang('zh')).toThrow('first error') + expect(calls).toEqual(['first:zh', 'second:zh']) + } finally { + u1() + u2() + } + + expect(resolveLangFromPathname('/acts-en.html')).toBe('en') + expect(resolveLangFromPathname('/acts-zh.html')).toBe('zh') + expect(resolveLangFromPathname('/acts.html')).toBe('zh') + expect(detectLangFromUrl('http://localhost/acts-en.html')).toBe('ENG') + expect(detectLangFromUrl('http://localhost/acts-zh.html')).toBe('CHI') + expect(detectLangFromUrl('http://localhost/acts.html')).toBe('CHI') + }) + + it('#702: bakes Barbarian (bafws.dc6) and Paladin (pafws.dc6) forward overlays with drawMode=5 and ne/so overlays with drawMode=3', () => { + const baFwOv = manifest.campfireClasses.ba?.fw_ov + const paFwOv = manifest.campfireClasses.pa?.fw_ov + expect(baFwOv).toBeDefined() + expect(paFwOv).toBeDefined() + expect(baFwOv?.drawMode).toBe(5) + expect(paFwOv?.drawMode).toBe(5) + expect(baFwOv?.frameCount).toBe(manifest.campfireClasses.ba?.fw?.frameCount) + expect(paFwOv?.frameCount).toBe(manifest.campfireClasses.pa?.fw?.frameCount) + + // Verify PNG files exist on disk under both flat (`campfire/ba-fw-ov.png`) and nested (`campfire/ba/fw_ov.png`) paths + for (const relPath of [ + baFwOv!.file, + paFwOv!.file, + 'campfire/ba/fw_ov.png', + 'campfire/pa/fw_ov.png', + ]) { + const fullPath = resolve(FRONTEND_DIR, relPath) + expect(existsSync(fullPath)).toBe(true) + const hdr = readPngHeader(fullPath) + expect(hdr.width).toBeGreaterThan(0) + expect(hdr.height).toBeGreaterThan(0) + } + + const baPng = readPngHeader(resolve(FRONTEND_DIR, baFwOv!.file)) + expect(baPng.width).toBe(baFwOv!.width) + expect(baPng.height).toBe(baFwOv!.height) + + const paPng = readPngHeader(resolve(FRONTEND_DIR, paFwOv!.file)) + expect(paPng.width).toBe(paFwOv!.width) + expect(paPng.height).toBe(paFwOv!.height) + + // Necromancer and Sorceress additive overlays use drawMode 3 (DRAWMODE_MODULATE) + for (const clsId of ['ne', 'so'] as const) { + for (const ovKey of ['nu3_ov', 'fw_ov', 'bw_ov'] as const) { + const ov = manifest.campfireClasses[clsId]?.[ovKey] + expect(ov).toBeDefined() + expect(ov?.drawMode).toBe(3) + } + } + + expect(manifest.animStrips.campfireFlame?.drawMode).toBe(3) + }) + + it('#698, #699, #700, #704, #708: manifest.json contains canonical .tbl strings without fake suffixes or hardcoded expiresIn90Days', () => { + const s = manifest.strings + + // #698: Character expiration .tbl 11132, 11133, 11134; hardcoded expiresIn90Days removed + expect(s.expiresIn90Days).toBeUndefined() + expect(s['11132']).toBe('Expires Today') + expect(s['11133']).toBe('Expires in %d days') + expect(s['11134']).toBe('Character Expired') + expect(s.expiresToday).toBe('Expires Today') + expect(s.expiresInDays).toBe('Expires in %d days') + expect(s.characterExpired).toBe('Character Expired') + + // #699: Canonical BNet connect status strings 5171..5177 without "..." suffixes + expect(s['5171']).toBe('Connecting to Battle.net') + expect(s['5172']).toBe('Querying gateway') + expect(s['5173']).toBe('Connecting to the fastest server') + expect(s['5174']).toBe('Accessing your account') + expect(s['5175']).toBe('Checking versions') + expect(s['5176']).toBe('Verifying CD-key') + expect(s['5177']).toBe('Getting player profile') + expect(s.connectingToFastest).toBe('Connecting to the fastest server') + expect(s.accessingAccount).toBe('Accessing your account') + expect(s.checkingVersions).toBe('Checking versions') + expect(String(s.connectingToFastest).endsWith('...')).toBe(false) + expect(String(s.accessingAccount).endsWith('...')).toBe(false) + expect(String(s.checkingVersions).endsWith('...')).toBe(false) + + // #700: Login title (5204) and instruction (5205) + expect(s['5204']).toBe('Battle.net Log In') + expect(s['5205']).toBe( + "To use an existing Battle.net account, enter the information below and click 'Log In'. Otherwise, click 'Create New Account'.", + ) + expect(s.loginTitle).toBe(s['5204']) + expect(s.loginInstruction).toBe(s['5205']) + + // #704: Chat gem .tbl 5334, 5335, 5336 + expect(s['5334']).toBe('Gem Activated') + expect(s['5335']).toBe('Gem Deactivated') + expect(s['5336']).toBe('Perfect Gem Activated') + expect(s.lobby.gemActivated).toBe('Gem Activated') + expect(s.lobby.gemDeactivated).toBe('Gem Deactivated') + expect(s.lobby.perfectGemActivated).toBe('Perfect Gem Activated') + + // #707 & #708: Create Account instruction (5223), Please Read paragraphs (5228..5230), Level & class names + expect(s['5223']).toBe('Enter the Name and Password for your new battle.net account.') + expect(s.createAccountInstruction).toBe(s['5223']) + expect(typeof s.pleaseReadPara1).toBe('string') + expect(typeof s.pleaseReadPara2).toBe('string') + expect(typeof s.pleaseReadPara3).toBe('string') + expect(s['4057']).toBe('Level') + expect(s['5017']).toBe('Level %d') + expect(s['4007']).toBe('Barbarian') + expect(s['4008']).toBe('Paladin') + expect(s['4009']).toBe('Necromancer') + expect(s['4010']).toBe('Sorceress') + expect(s['4011']).toBe('Amazon') + expect(s['4012']).toBe('Druid') + expect(s['4013']).toBe('Assassin') + }) + + it('#697, #699, #704, #707, #710: main.ts has zero invented English validation popups, zero st.gateway hostname fallback, and zero silent difficulty downgrade', () => { + const mainSrc = readFileSync(MAIN_TS_PATH, 'utf8') + + // #697 & #707: Zero invented English validation error strings + expect(mainSrc).not.toContain('PLEASE ENTER AN ACCOUNT NAME.') + expect(mainSrc).not.toContain('CHARACTER NAME MUST BE 2-15 LETTERS.') + expect(mainSrc).not.toContain('PLEASE ENTER A GAME NAME.') + expect(mainSrc).not.toContain('MAX NUMBER OF PLAYERS MUST BE 1-8.') + expect(mainSrc).not.toContain('CHARACTER DIFFERENCE MUST BE 1-99.') + + // #704: Never use display gateway name (`st.gateway`) as network hostname fallback + expect(mainSrc).not.toContain('|| st.gateway') + + // #710: Never silently downgrade locked difficulty to 'normal' + expect(mainSrc).not.toContain("st.difficulty = 'normal'") + expect(mainSrc).toContain('is not unlocked for character') + + // #699: onConnect wires canonical BNCS status stages (5171, 5173) into bnet_login + expect(mainSrc).toContain("manifestStrings?.['5171']") + expect(mainSrc).toContain("manifestStrings?.['5173']") + }) +}) diff --git a/tests/lang.test.ts b/tests/lang.test.ts new file mode 100644 index 0000000..900afde --- /dev/null +++ b/tests/lang.test.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { + CHINESE_ENTRY_PAGES, + DEFAULT_GAME_LANG, + DEFAULT_TBL_LANG, + ENGLISH_ENTRY_PAGES, + detectInitialLang, + detectLangFromUrl, + getGameLang, + getTblLang, + onGameLangChange, + onLangChange, + onTblLangChange, + resolveLangFromPathname, + setGameLang, + setLang, + setTblLang, +} from '../src/client/i18n/lang.ts' + +describe('i18n lang.ts — URL path resolution & fail-fast listener error propagation (Issue #712)', () => { + afterEach(() => { + setGameLang(null) + }) + + it('exports canonical entry page lists and resolves pathname deterministically', () => { + expect(DEFAULT_GAME_LANG).toBe('zh') + expect(DEFAULT_TBL_LANG).toBe('CHI') + expect(ENGLISH_ENTRY_PAGES).toContain('acts-en.html') + expect(ENGLISH_ENTRY_PAGES).toContain('bnet.html') + expect(CHINESE_ENTRY_PAGES).toContain('acts-zh.html') + expect(CHINESE_ENTRY_PAGES).toContain('acts.html') + + expect(resolveLangFromPathname('/acts-en.html')).toBe('en') + expect(resolveLangFromPathname('/arena-en.html')).toBe('en') + expect(resolveLangFromPathname('/bnet.html')).toBe('en') + expect(resolveLangFromPathname('/play-en.html')).toBe('en') + + expect(resolveLangFromPathname('/acts-zh.html')).toBe('zh') + expect(resolveLangFromPathname('/arena-zh.html')).toBe('zh') + expect(resolveLangFromPathname('/acts.html')).toBe('zh') + expect(resolveLangFromPathname('/arena.html')).toBe('zh') + expect(resolveLangFromPathname('/index.html')).toBe('zh') + + expect(resolveLangFromPathname('/play.html')).toBeNull() + expect(resolveLangFromPathname('/unknown-route')).toBeNull() + }) + + it('resolves detectLangFromUrl across query params, entry pages, and backward-compatible default', () => { + expect(detectLangFromUrl('http://localhost/acts-en.html')).toBe('ENG') + expect(detectLangFromUrl('http://localhost/acts-zh.html')).toBe('CHI') + expect(detectLangFromUrl('http://localhost/acts.html')).toBe('CHI') + expect(detectLangFromUrl('http://localhost/acts.html?lang=en')).toBe('ENG') + expect(detectLangFromUrl('http://localhost/acts-en.html?lang=zh')).toBe('CHI') + expect(detectLangFromUrl('http://localhost/unrecognized.html')).toBe('CHI') + expect(detectInitialLang()).toBe('zh') + }) + + it('propagates a single listener error fail-fast instead of silently swallowing it', () => { + const unsub = onLangChange((lang) => { + throw new Error(`Font atlas reload failed for ${lang}`) + }) + try { + expect(() => setLang('en')).toThrow('Font atlas reload failed for en') + expect(getGameLang()).toBe('en') + expect(getTblLang()).toBe('ENG') + } finally { + unsub() + } + }) + + it('notifies all remaining listeners and throws AggregateError when multiple listeners fail', () => { + const seen: string[] = [] + const unsub1 = onTblLangChange((tbl) => { + seen.push(`tbl:${tbl}`) + throw new Error('First tbl listener failure') + }) + const unsub2 = onGameLangChange((lang) => { + seen.push(`game:${lang}`) + throw new Error('Second gameLang listener failure') + }) + const unsub3 = onGameLangChange((lang) => { + seen.push(`ok:${lang}`) + }) + + try { + let caught: unknown = null + try { + setTblLang('ENG') + } catch (err) { + caught = err + } + expect(caught).toBeInstanceOf(AggregateError) + const agg = caught as AggregateError + expect(agg.errors).toHaveLength(2) + expect((agg.errors[0] as Error).message).toBe('First tbl listener failure') + expect((agg.errors[1] as Error).message).toBe('Second gameLang listener failure') + // All 3 listeners must have been invoked despite earlier listener throws + expect(seen).toEqual(['tbl:ENG', 'game:en', 'ok:en']) + } finally { + unsub1() + unsub2() + unsub3() + } + }) +})