Spaces:
Running
Running
File size: 239,485 Bytes
3597f5a 502e0c9 3597f5a 33f155c 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 502e0c9 3597f5a 8e1d8e7 3597f5a 502e0c9 3597f5a 8e1d8e7 502e0c9 3597f5a 502e0c9 3597f5a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 | ;
;
/* ============================================================================
MODEL LIBRARY — every unit, structure and world object as real geometry.
----------------------------------------------------------------------------
Models are welded from primitives and authored around the origin with their
feet on y=0, facing +X (which matches the sim's angle 0). A model's own
colours are near-neutral livery values; the per-instance tint multiplies
them, so the same mesh serves both armies and still shows its own panel
lines, shadow gaps and warning markings.
Turreted vehicles are two meshes — hull and turret — instanced separately so
the turret can track a target while the hull drives somewhere else, which is
the whole reason tanks read as tanks.
============================================================================ */
const C=(r,g,b)=>[r/255,g/255,b/255];
const MET =C(158,166,176), MET_D=C(96,104,114), MET_L=C(206,214,222);
const DARK =C(52,58,66), DARKER=C(32,36,42);
const TREAD =C(44,46,50), RUBBER=C(60,62,68);
const GLASS =C(120,200,255), LAMP =C(255,226,150);
const HOT =C(255,150,70), ENERGY=C(140,230,255);
const CONC =C(150,146,138), CONC_D=C(104,101,95);
const RUST =C(140,102,72), BONE =C(206,198,182);
/* Warm bone-amber, not leaf green. The reference material's whole character is
a pale amber plate field falling to an almost-black wet seam; a mid-green
base has neither the value range nor the warmth to get there, and no amount
of lighting recovers it. CLAW stays near-white so the tips read at 40px. */
const CHITIN=C(196,178,124), CHIT_D=C(96,86,58), CLAW=C(232,226,206);
/* Biological livery and flexible tissue never inherit vehicle plate or servo
textures. BIO_TEAM stays faction-tinted on chitin; BIO_LEG/BIO_MEM use an
organic relief and mark appendages for procedural secondary motion. */
const BIO_TEAM=C(198,174,220), BIO_LEG=C(148,132,88), BIO_MEM=C(150,138,96);
/* TEAM_* entries are the only surfaces that take the full faction colour:
hull flanks, turret cheeks, wing roots, roof panels. Keeping livery to a few
deliberate panels is what makes a unit read as "blue army" without erasing
its metal, glass and rubber. */
const TEAM_A=C(214,222,230), TEAM_B=C(160,170,182), TEAM_T=C(236,242,248);
/* Architectural palette. Structures are not vehicles: they want a wall
material and a roof material, both far calmer than hull plating. */
const WALL =C(176,180,188), WALL_D=C(124,128,136);
/* Anything painted SERVO is treated as a leg by the vertex stage and animates
through the walk cycle. Use it for shins, thighs, feet and hip rams only. */
const SERVO =C(132,138,148);
const ROOFC =C(150,154,160), ROOF_T=C(196,202,210);
/* Tower-only hard-surface palette. These references deliberately do not
replace MET/DARK globally: vehicles and legacy structures keep their current
finishes while defensive towers receive clearly separated armor, mechanism,
trim, livery, light, foundation and bore materials. */
const TWR_ARM=C(190,198,208), TWR_ARM_D=C(118,128,140);
const TWR_MACH=C(145,154,166), TWR_TRIM=C(220,226,234);
const TWR_COAT=C(68,76,88), TWR_TEAM=C(218,226,236);
const TWR_GLOW=C(92,214,255), TWR_PAD=C(116,120,128), TWR_BORE=C(13,15,18);
/* Landmark glowOk only passes MAT.TWR_GLOW. This brighter vertex is the
same tile, so HQ points read at command distance without a rail. */
const HQ_LAMP=C(236,250,255);
const LIT_WIN=C(255,230,170), COOL_WIN=C(170,230,255), FLICK_WIN=C(255,200,140);
const SHOP_WIN=C(255,245,220), NEON_CYAN=C(0,240,255), SOLAR_PANEL=C(35,50,80);
const COPPER_PAT=C(82,132,114), BRICK_RED=C(140,76,64);
const NOVA_MET=C(132,142,156), NOVA_CARB=C(44,48,56);
/* Stage S1 bespoke structure semantics. These are deliberately palette-local
selectors, rather than a renderer switch: HQ and Techlab can establish their
own material hierarchy now while the renderer still falls back to the live
V2 atlas. They are semantic-bake prototypes, NOT UV-authored map packs. */
const NOVA_HQ_ARM=C(152,158,166), NOVA_HQ_STRUCT=C(46,50,56);
const NOVA_HQ_ROOF=C(116,132,152), NOVA_HQ_GLASS=C(44,130,196);
const NOVA_RESEARCH_ARM=C(136,154,178), NOVA_RESEARCH_CORE=C(40,60,82);
const NOVA_RESEARCH_ROOF=C(104,122,144), NOVA_RESEARCH_GLASS=C(50,148,212);
/* Stage S2 keeps production landmarks as individual semantic packs. These are
palette selectors for the live V2 material atlas, not a claim that custom
UV-authored maps already exist. Their separation is intentional: when the
authored BaseAO/NRE/mask pack lands, it can replace just one landmark's
surfaces without accidentally recolouring another Nova building. */
const NOVA_FACTORY_ARM=C(132,154,180), NOVA_FACTORY_CORE=C(34,46,62);
const NOVA_FACTORY_ROOF=C(100,122,148), NOVA_FACTORY_GLASS=C(48,136,202);
const NOVA_REACTOR_ARM=C(124,150,176), NOVA_REACTOR_CORE=C(30,46,64);
const NOVA_REACTOR_TRIM=C(156,178,202), NOVA_REACTOR_GLOW=C(72,208,255);
const NOVA_AIR_DECK=C(58,76,100), NOVA_AIR_ARM=C(128,150,176);
const NOVA_AIR_ROOF=C(104,126,150), NOVA_AIR_GLASS=C(48,142,210);
const NOVA_FAB_ARM=C(130,152,176), NOVA_FAB_CORE=C(38,50,66);
const NOVA_FAB_ROOF=C(102,124,148), NOVA_FAB_GLASS=C(50,138,204);
/* Stage S3 extends the same isolated semantic treatment to real Nova economy
and support structures. These are still palette selectors for the live V2
material atlas, not a claim that bespoke UV-authored maps are complete. */
const NOVA_MEX_ARM=C(126,148,174), NOVA_MEX_CORE=C(34,50,68);
const NOVA_MEX_TRIM=C(160,180,202), NOVA_MEX_GLOW=C(22,70,92);
/* Service-deck plating. Its own palette entry so retiring it from a pad
cannot disturb TWR_COAT, which many other surfaces still use. */
const NOVA_DECK=C(111,119,131);
const NOVA_GEO_ARM=C(122,146,170), NOVA_GEO_CORE=C(30,46,62);
const NOVA_GEO_TRIM=C(154,178,202), NOVA_GEO_GLOW=C(76,210,255);
const NOVA_SILO_ARM=C(130,150,174), NOVA_SILO_CORE=C(36,48,62);
const NOVA_SILO_ROOF=C(104,124,148), NOVA_SILO_GLOW=C(64,196,248);
const NOVA_UPLINK_ARM=C(128,152,178), NOVA_UPLINK_CORE=C(32,48,66);
const NOVA_UPLINK_TRIM=C(162,184,208), NOVA_UPLINK_GLOW=C(70,210,255);
const NOVA_HARBOR_ARM=C(124,148,174), NOVA_HARBOR_CORE=C(32,48,66);
const NOVA_HARBOR_DECK=C(78,98,124), NOVA_HARBOR_GLASS=C(46,136,202);
/* Stage S4 gives the common Nova defenses distinct semantic palettes. These
remain live-V2 selectors, deliberately isolated per building family, so a
later authored bake can replace one asset without recolouring another. */
const NOVA_SENTINEL_ARM=C(138,160,186), NOVA_SENTINEL_CORE=C(36,48,64);
const NOVA_SENTINEL_TRIM=C(164,188,214), NOVA_SENTINEL_GLOW=C(72,212,255);
const NOVA_BUNKER_ARM=C(132,150,174), NOVA_BUNKER_CORE=C(34,44,58);
const NOVA_BUNKER_TRIM=C(156,176,200), NOVA_BUNKER_GLOW=C(68,198,246);
const NOVA_RAIL_ARM=C(144,166,194), NOVA_RAIL_CORE=C(30,42,60);
const NOVA_RAIL_TRIM=C(174,198,224), NOVA_RAIL_GLOW=C(84,224,255);
const DROP_CANOPY_GLASS=C(34,112,176); // dark pressure glazing stays blue under the overhead world light
const LEG_IRON=C(88,80,78), LEG_VENT=C(255,100,20);
const SYN_NANO_C=C(120,132,148), SYN_GOLD_C=C(212,175,55), SYN_FLOW=C(0,220,240);
const BROOD_MEMB=C(184,152,120);
const HEADLIGHT_C=C(255,250,220), TAILLIGHT_C=C(255,34,17);
/* Colour constants double as MATERIAL selectors: the builder tracks a current
material, and this wrapper sets it from whichever palette entry a primitive
was already using. That upgrades the entire model library to textured
surfaces without touching a single call site. */
const COL_MAT=new Map();
const COL_TEAM=new Set(); // palette entries painted in team livery
function bindMat(){
COL_MAT.set(MET,matResolve('hull')); COL_MAT.set(MET_D,matResolve('hull.dark')); COL_MAT.set(MET_L,matResolve('hull.trim'));
COL_MAT.set(DARK,matResolve('hull.dark'));COL_MAT.set(DARKER,matResolve('hull.dark'));
COL_MAT.set(TREAD,matResolve('hull.track')); COL_MAT.set(RUBBER,matResolve('hull.track'));
COL_MAT.set(GLASS,matResolve('glass.canopy')); COL_MAT.set(LAMP,matResolve('emissive.light'));
COL_MAT.set(HOT,matResolve('emissive.light')); COL_MAT.set(ENERGY,matResolve('emissive.energy'));
COL_MAT.set(CONC,MAT.CONC); COL_MAT.set(CONC_D,MAT.CONC);
COL_MAT.set(RUST,MAT.RUST); COL_MAT.set(BONE,MAT.CONC);
COL_MAT.set(CHITIN,matResolve('bio.shell')); COL_MAT.set(CHIT_D,matResolve('bio.shell')); COL_MAT.set(CLAW,matResolve('hull.trim'));
COL_MAT.set(BIO_TEAM,matResolve('bio.shell')); COL_MAT.set(BIO_LEG,matResolve('bio.limb')); COL_MAT.set(BIO_MEM,matResolve('bio.membrane'));
COL_MAT.set(TEAM_A,matResolve('hull')); COL_MAT.set(TEAM_B,matResolve('hull')); COL_MAT.set(TEAM_T,matResolve('hull.trim'));
COL_MAT.set(WALL,matResolve('structure.wall')); COL_MAT.set(WALL_D,matResolve('structure.wall'));
COL_MAT.set(SERVO,MAT.SERVO);
COL_MAT.set(ROOFC,matResolve('structure.roof')); COL_MAT.set(ROOF_T,matResolve('structure.roof'));
COL_MAT.set(TWR_ARM,MAT.TWR_ARMOR); COL_MAT.set(TWR_ARM_D,MAT.TWR_ARMOR);
COL_MAT.set(TWR_MACH,MAT.TWR_MACH); COL_MAT.set(TWR_TRIM,MAT.TWR_MACH);
COL_MAT.set(TWR_COAT,MAT.TWR_COAT); COL_MAT.set(TWR_TEAM,MAT.TWR_ARMOR);
COL_MAT.set(TWR_GLOW,MAT.TWR_GLOW); COL_MAT.set(HQ_LAMP,MAT.TWR_GLOW); COL_MAT.set(TWR_PAD,MAT.TWR_PAD);
COL_MAT.set(TWR_BORE,MAT.TWR_BORE);
COL_MAT.set(LIT_WIN,matResolve('window.warm')); COL_MAT.set(COOL_WIN,matResolve('window.cool'));
COL_MAT.set(FLICK_WIN,matResolve('window.flicker')); COL_MAT.set(SHOP_WIN,matResolve('window.shop'));
COL_MAT.set(NEON_CYAN,MAT.NEON_FACADE); COL_MAT.set(SOLAR_PANEL,matResolve('roof.solar'));
COL_MAT.set(COPPER_PAT,matResolve('roof.copper')); COL_MAT.set(BRICK_RED,MAT.BRICK_MASONRY);
COL_MAT.set(NOVA_MET,matResolve('faction.nova')); COL_MAT.set(NOVA_CARB,MAT.NOVA_CARBON);
/* HQ/Techlab own semantic surface sets until authored UV packs arrive.
Keeping the mappings local prevents a generic Nova-material edit from
accidentally changing every production building in the faction. */
COL_MAT.set(NOVA_HQ_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_HQ_STRUCT,MAT.TWR_COAT);
COL_MAT.set(NOVA_HQ_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_HQ_GLASS,matResolve('glass.canopy'));
COL_MAT.set(NOVA_RESEARCH_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_RESEARCH_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_RESEARCH_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_RESEARCH_GLASS,matResolve('glass.canopy'));
COL_MAT.set(NOVA_FACTORY_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_FACTORY_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_FACTORY_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_FACTORY_GLASS,matResolve('glass.canopy'));
COL_MAT.set(NOVA_REACTOR_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_REACTOR_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_REACTOR_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_REACTOR_GLOW,matResolve('emissive.energy'));
COL_MAT.set(NOVA_AIR_DECK,matResolve('structure.roof')); COL_MAT.set(NOVA_AIR_ARM,matResolve('faction.nova'));
COL_MAT.set(NOVA_AIR_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_AIR_GLASS,matResolve('glass.canopy'));
COL_MAT.set(NOVA_FAB_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_FAB_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_FAB_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_FAB_GLASS,matResolve('glass.canopy'));
COL_MAT.set(NOVA_MEX_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_MEX_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_MEX_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_MEX_GLOW,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_DECK,MAT.DECK_PLATE);
COL_MAT.set(NOVA_GEO_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_GEO_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_GEO_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_GEO_GLOW,matResolve('emissive.energy'));
COL_MAT.set(NOVA_SILO_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_SILO_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_SILO_ROOF,matResolve('structure.roof')); COL_MAT.set(NOVA_SILO_GLOW,matResolve('emissive.energy'));
COL_MAT.set(NOVA_UPLINK_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_UPLINK_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_UPLINK_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_UPLINK_GLOW,matResolve('emissive.energy'));
COL_MAT.set(NOVA_HARBOR_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_HARBOR_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_HARBOR_DECK,matResolve('structure.roof')); COL_MAT.set(NOVA_HARBOR_GLASS,matResolve('glass.canopy'));
COL_MAT.set(NOVA_SENTINEL_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_SENTINEL_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_SENTINEL_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_SENTINEL_GLOW,matResolve('emissive.energy'));
COL_MAT.set(NOVA_BUNKER_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_BUNKER_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_BUNKER_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_BUNKER_GLOW,matResolve('emissive.energy'));
COL_MAT.set(NOVA_RAIL_ARM,matResolve('faction.nova')); COL_MAT.set(NOVA_RAIL_CORE,MAT.NOVA_CARBON);
COL_MAT.set(NOVA_RAIL_TRIM,matResolve('hull.trim')); COL_MAT.set(NOVA_RAIL_GLOW,matResolve('emissive.energy'));
COL_MAT.set(DROP_CANOPY_GLASS,matResolve('glass.canopy'));
COL_MAT.set(LEG_IRON,matResolve('faction.legion')); COL_MAT.set(LEG_VENT,MAT.LEGION_THERMITE);
COL_MAT.set(SYN_NANO_C,matResolve('faction.syndicate')); COL_MAT.set(SYN_GOLD_C,MAT.SYN_GOLD);
COL_MAT.set(SYN_FLOW,matResolve('emissive.energy')); COL_MAT.set(BROOD_MEMB,matResolve('bio.membrane'));
COL_MAT.set(HEADLIGHT_C,MAT.UNIT_HEADLIGHT); COL_MAT.set(TAILLIGHT_C,MAT.UNIT_TAILLIGHT);
}
bindMat();
COL_TEAM.add(TEAM_A); COL_TEAM.add(TEAM_B); COL_TEAM.add(TEAM_T);
COL_TEAM.add(TWR_TEAM);
COL_TEAM.add(BIO_TEAM);
/* A named role is preferred, but legacy models still pass palette colours.
The fallback heuristics prevent an unregistered pale cyan face from becoming
dull steel when an experimental model is added before its palette binding. */
function matDetect(col,role){
if(role) return matResolve(role);
const known=COL_MAT.get(col); if(known!=null) return known;
if(!Array.isArray(col)) return MAT.PLATE;
const r=col[0]||0,g=col[1]||0,b=col[2]||0;
if(b>r*1.35&&b>g*1.08&&b>.42) return matResolve('glass');
if(r>.82&&g>.82&&b>.82) return matResolve('hull.trim');
if(r<.24&&g<.24&&b<.24) return matResolve('hull.dark');
return matResolve('hull');
}
/* Patch the builder so every legacy primitive picks its material from its
palette while new factories can request semantic material roles directly. */
for(const fn of ['box','cyl','sphere','wedge','extrude','ring','bevelBox','inset','tube','greeble']){
const orig=MeshBuilder.prototype[fn];
MeshBuilder.prototype[fn]=function(...args){
/* The colour argument sits at a different index per primitive. Scan from
the end: position vectors come first, while colour is always the last
normalized RGB tuple before optional yaw/flags. This lets new palette
colours get a sensible material fallback instead of silently inheriting
the previous part's material. */
for(let k=args.length-1;k>=0;k--){
const a=args[k];
if(Array.isArray(a)&&a.length===3&&a.every(v=>typeof v==='number'&&v>=0&&v<=1)){
this.m=matDetect(a); this.tm=COL_TEAM.has(a); break;
}
}
return orig.apply(this,args);
};
}
/* Tread bank: two blocks with road wheels between them. Repeated on nearly
every ground vehicle, so it earns its own function. */
function treads(m,len,wid,h,gauge,wheels){
for(const s of [-1,1]){
m.box(0,0,s*gauge,len,h,wid,TREAD);
m.box(0,h*0.34,s*gauge,len*1.03,h*0.34,wid*1.16,DARKER); // track links
const n=wheels||4;
for(let k=0;k<n;k++){
const x=-len/2+len*(k+0.5)/n;
m.cyl(x,h*0.5,s*gauge,h*0.42,h*0.42,wid*0.5,7,RUBBER);
}
}
}
/* Rectangular hull with chamfered nose — the base of most vehicles. */
function hullPlate(m,len,wid,h,y,col){
m.extrude(0,y,0,[[-len*0.5,-wid*0.5],[len*0.30,-wid*0.5],[len*0.5,-wid*0.26],
[len*0.5,wid*0.26],[len*0.30,wid*0.5],[-len*0.5,wid*0.5]],h,col);
}
function barrel(m,x,y,z,len,r,col){
m.cyl(x,y,z,r,r*0.86,0,8,col); // placeholder (kept for clarity)
}
/* Append a temporary +Y primitive after rotating it onto +X. Keeping this in
one helper matters: positions AND normals must move while UV/material data
stays untouched, and every index must be offset by the receiving mesh. */
function mergeAxisX(m,mm,x,y,z){
for(let i=0;i<mm.v.length;i+=VFLOATS){
const px=mm.v[i], py=mm.v[i+1], pz=mm.v[i+2];
mm.v[i]=py+x; mm.v[i+1]=-px+y; mm.v[i+2]=pz+z;
const nx=mm.v[i+3], ny=mm.v[i+4];
mm.v[i+3]=ny; mm.v[i+4]=-nx;
}
const o=m.n;
for(let i=0;i<mm.v.length;i++) m.v.push(mm.v[i]);
for(const ix of mm.i) m.i.push(ix+o);
m.n+=mm.n;
/* Match a native primitive call: the colour wrapper also leaves the active
material/team state on the receiving builder for any direct faces next. */
m.m=mm.m; m.tm=mm.tm;
return m;
}
/* Cylinders and hollow tubes along the weapon-forward +X axis. The native
primitives stand on +Y, which made callers that passed a yaw build antennae
instead of barrels. These explicit helpers make that mistake impossible. */
function cylX(m,x,y,z,len,r1,r2,seg,col,cap){
const mm=MB(); mm.m=m.m; mm.tm=m.tm;
mm.cyl(0,0,0,r1,r2,len,seg,col,cap);
return mergeAxisX(m,mm,x,y,z);
}
function tubeX(m,x,y,z,len,rOut,rIn,seg,col){
const mm=MB(); mm.m=m.m; mm.tm=m.tm;
mm.tube(0,0,0,rOut,rIn,len,seg,col);
return mergeAxisX(m,mm,x,y,z);
}
function ringX(m,x,y,z,r0,r1,seg,col){
const mm=MB(); mm.m=m.m; mm.tm=m.tm;
mm.ring(0,0,0,r0,r1,seg,col);
return mergeAxisX(m,mm,x,y,z);
}
/* A gun barrel pointing along +X, with an optional lateral offset. */
function gunX(m,x0,y,len,r,col,z){
const seg=8, zz=z||0;
/* No front cap: the following tube exposes a real shadowed bore instead of
painting a dark circle over a flat barrel end. */
cylX(m,x0,y,zz,len,r,r*0.88,seg,col,false);
m.box(x0+len*0.92,y-r*1.25,zz,len*0.16,r*2.5,r*2.6,col); // muzzle brake
tubeX(m,x0+len-.10,y,zz,Math.max(.48,r*.82),r*1.18,r*.56,10,TWR_BORE);
return m;
}
/* ============================================================================
DETAIL KIT — the vocabulary that makes a hull look manufactured
----------------------------------------------------------------------------
Reference points are C&C3 Tiberium Wars and Supreme Commander 2: heavy
faceted armour with visible plate breaks, chamfered edges everywhere, and a
dense scatter of small functional hardware — vents, hydraulics, sensor
masts, stowage, tow points, exhaust banks. Neither game relies on texture
for that read; it is all silhouette and surface break-up, which is exactly
what survives at command-view zoom.
Everything below is a compound of primitives so a unit can be assembled from
twenty-odd recognisable parts instead of six boxes.
============================================================================ */
/* Faceted armour hull: an N-sided tapered shell rather than a box. Two stacked
extrusions with the upper one inset give the plate break that reads as
layered armour from above. */
function hullShell(m,len,wid,hLow,hHigh,col,colTop,taperNose,taperTail){
const tn=taperNose===undefined?0.42:taperNose, tt=taperTail===undefined?0.62:taperTail;
const L=len*0.5, W=wid*0.5;
const prof=[
[-L, -W*tt],
[-L*0.55, -W],
[ L*0.42, -W],
[ L, -W*tn],
[ L, W*tn],
[ L*0.42, W],
[-L*0.55, W],
[-L, W*tt],
];
m.extrude(0,0,0,prof,hLow,col);
const k=0.80;
m.extrude(0,hLow,0,prof.map(p=>[p[0]*k,p[1]*k]),hHigh,colTop||col);
return m;
}
/* Angled armour plate with a lip — spaced armour, skirts, glacis add-ons. */
function armorPlate(m,x,y,z,w,h,thick,tilt,col,yaw){
const c=Math.cos(yaw||0), s=Math.sin(yaw||0);
m.box(x,y,z,w,h,thick,col,yaw||0);
m.box(x - Math.sin(yaw||0)*0, y+h, z, w*0.96, thick*0.7, thick*1.9, col, yaw||0);
for(const o of [-0.34,0.34]){ // bolt strip down the plate
const ox=w*o;
m.box(x+ox*c, y+h*0.5, z+ox*s, thick*0.9, h*0.7, thick*0.9, col, yaw||0);
}
return m;
}
/* Hydraulic ram: a pair of polished pistons in a housing. Legs, elevation
gear, suspension arms — anything that should look like it moves. */
function hydraulic(m,x,y,z,len,r,col,yaw){
const c=Math.cos(yaw||0), s=Math.sin(yaw||0);
m.cyl(x,y,z,r,r*0.95,len*0.6,8,col);
m.cyl(x,y+len*0.6,z,r*0.55,r*0.55,len*0.45,8,MET_L); // bright ram
m.cyl(x,y+len*0.55,z,r*1.18,r*1.18,len*0.10,8,DARKER); // gland nut
return m;
}
/* Louvred vent bank — engine decks, radiator faces, intake grilles. */
function ventBank(m,x,y,z,w,d,n,col,yaw){
const c=Math.cos(yaw||0), s=Math.sin(yaw||0);
m.inset(x,y,z,w,d,0.36,0.10,DARKER,yaw||0);
for(let k=0;k<n;k++){
const t=(k+0.5)/n-0.5, oz=t*d;
m.box(x - oz*s, y+0.22, z + oz*c, w*0.92, 0.28, d/n*0.45, col, yaw||0);
}
return m;
}
/* Sensor mast: pole, dish and a blade antenna. The single most effective way
to break a flat roofline and make a vehicle look crewed. */
function sensorMast(m,x,y,z,h,col){
m.cyl(x,y,z,0.20,0.14,h,6,DARKER);
m.cyl(x,y+h,z,0.9,0.22,0.18,9,col); // small dish
m.box(x,y+h*0.55,z,0.10,h*0.42,0.5,MET_L); // blade antenna
/* HQ_LAMP is TWR_GLOW, so landmark glowOk keeps the point. LAMP here
was a leftover orange cube that failed the gate on HQ and bloomed
on every factory/defense mast. */
m.cyl(x,y+h+0.22,z,.16,.16,.18,8,HQ_LAMP);
return m;
}
/* Exhaust bank: stacks with hollow bores and a heat shroud. */
function exhaust(m,x,y,z,n,r,h,col,spread){
for(let k=0;k<n;k++){
const oz=(k-(n-1)/2)*(spread||r*2.6);
m.cyl(x,y,z+oz,r*1.25,r*1.15,h*0.35,8,DARKER); // shroud
m.cyl(x,y+h*0.3,z+oz,r,r*0.92,h*0.7,8,col);
m.tube(x,y+h*0.98,z+oz,r*0.92,r*0.5,h*0.16,8,DARKER);
}
return m;
}
/* Stowage: tarps, crates and spare track links strapped to a hull. Clutter is
what stops a large flat panel reading as an untextured slab. */
function stowage(m,x,y,z,len,wid,col,yaw,seed){
let sd=seed||7;
const rr2=()=>{ sd=(sd*1664525+1013904223)&0x7fffffff; return (sd>>>10)/2097152; };
const c=Math.cos(yaw||0), s=Math.sin(yaw||0);
for(let k=0;k<4;k++){
const t=(k+0.5)/4-0.5, px=t*len, pz=(rr2()-0.5)*wid*0.6;
const bw=len/4*(0.55+rr2()*0.35), bh=0.5+rr2()*0.7, bd=wid*(0.3+rr2()*0.3);
m.bevelBox(x+px*c-pz*s, y, z+px*s+pz*c, bw,bh,bd, Math.min(bw,bd)*0.22, col, yaw||0);
}
return m;
}
/* Ammunition / equipment box with a lid lip and latches. */
function kitBox(m,x,y,z,w,h,d,col,yaw){
m.bevelBox(x,y,z,w,h,d,Math.min(w,d)*0.14,col,yaw||0);
m.box(x,y+h,z,w*1.04,h*0.14,d*1.04,DARKER,yaw||0);
for(const o of [-0.3,0.3]) m.box(x+w*o*Math.cos(yaw||0),y+h*0.55,z+w*o*Math.sin(yaw||0),
w*0.08,h*0.5,d*1.06,MET_L,yaw||0);
return m;
}
/* SupCom-style emissive strip: a thin glowing line along a hull edge. */
/* ---------------------------------------------------------------------------
ROOF EDGE / PARAPET
A roof that ends in a bare chamfer has nothing for the light to catch, so
from overhead the building's outline dissolves into the ground. A raised lip
with a darker reveal under it gives every roof a hard bright edge and a line
of shade beneath — the single cheapest piece of architectural detail there
is, and the one the eye uses to read a top-down building's extent.
--------------------------------------------------------------------------- */
function roofEdge(m,x,y,z,w,d,lip,col,trim){
const hw=w/2, hd=d/2, th=lip||1.6;
for(const s of [-1,1]){
m.box(x,y,z+s*(hd-th*0.5), w, th*1.5, th, col); // side parapets
m.box(x,y+th*1.5,z+s*(hd-th*0.5), w*0.995, th*0.36, th*1.25, trim||col);
m.box(x+s*(hw-th*0.5),y,z, th, th*1.5, d-th*2, col); // end parapets
m.box(x+s*(hw-th*0.5),y+th*1.5,z, th*1.25, th*0.36, d-th*2, trim||col);
}
}
function glowStrip(m,x,y,z,len,col,yaw){
m.box(x,y,z,len,0.22,0.34,col||ENERGY,yaw||0);
return m;
}
/* ---------------------------------------------------------------------------
DECK CROWN — roofEdge, but for vehicles.
roofEdge above exists because "from overhead the building's outline dissolves
into the ground". That is just as true of a tank, and nothing was doing it:
this camera looks almost straight down, so a hull's DECK is the surface a
player actually identifies a unit by, and an extrude cap gives it nothing to
catch light on. The raised rail draws a hard outline against terrain, the
nose chevrons state facing without needing a turret, and the rear kick plate
stops the back edge melting into the tracks. Trim is team colour and it lands
in the SAME PLACE on every chassis — that consistency is the other half of
the read, and it is why this is a shared helper and not per-model geometry.
--------------------------------------------------------------------------- */
function deckCrown(m,x,y,z,len,wid,col,trim){
const L=len*0.5, W=wid*0.5, th=Math.min(len,wid)*0.09;
for(const sd of [-1,1]){
m.box(x,y,z+sd*(W-th*0.5),len*0.94,th*0.85,th,col||MET_D); // deck rail
m.box(x,y+th*0.85,z+sd*(W-th*0.5),len*0.90,th*0.30,th*0.8,trim||MET_L); // bright reveal
m.box(x+L*0.44,y+th*0.2,z+sd*W*0.46,len*0.32,th*0.45,th*1.3,trim||MET_L,sd*0.44); // nose chevron
}
m.box(x-L*0.88,y,z,th,th*0.85,wid*0.88,col||MET_D); // rear kick plate
return m;
}
/* ============================================================================
HARD-SURFACE COMPONENT KIT
The roster's tell was never polygon count — it was that everything was built
from PRIMARY shapes: a box is a box, a cylinder is a cylinder, and the eye
reads that as a logo, not a machine. Real hardware is components: a wheel is
tire + rim + spokes + hub; a thruster is bell + throat + glow + gimbal; a
window is frame + mullions + glass. Each helper below builds the COMPONENT,
with the chamfers and material breaks already in it, so a model assembled
from these starts at "machine" instead of earning it one box at a time.
============================================================================ */
/* Chamfered cylinder — the single most-used hard-surface move. A straight
cylinder has a razor rim that catches no light; a 45-degree chamfer ring at
each end gives every drum, hub and housing a lit edge. */
function chamferCyl(m,x,y,z,r,h,seg,col,colRim,yaw){
const rim=Math.min(h*0.22,r*0.30);
m.cyl(x,y,z,r-rim,r,rim,seg,colRim||MET_L,yaw);
m.cyl(x,y+rim,z,r,r,h-rim*2,seg,col,yaw);
m.cyl(x,y+h-rim,z,r,r-rim,rim,seg,colRim||MET_L,yaw);
return m;
}
/* A real wheel: treaded tire, rim dish, spokes, hub cap. Axis is Z (side
mount) — the only orientation ground vehicles use. */
function wheelAsm(m,x,y,z,r,w,col){
const sd=z>=0?1:-1;
m.cyl(x,y,z,r,r,w,12,TREAD,Math.PI/2);
/* tread lugs around the face — reads even at 20px */
for(let k=0;k<8;k++){
const a=k/8*TAU;
m.box(x+Math.cos(a)*r*0.98,y+Math.sin(a)*r*0.98,z,w*0.9,r*0.22,r*0.20,DARKER,a);
}
m.cyl(x,y,z+sd*w*0.5,r*0.62,r*0.62,w*0.14,10,col||MET_L,Math.PI/2); // rim dish
for(let k=0;k<5;k++){
const a=k/5*TAU;
m.box(x+Math.cos(a)*r*0.34,y+Math.sin(a)*r*0.34,z+sd*w*0.56,r*0.5,r*0.16,w*0.10,MET_D,a);
}
m.cyl(x,y,z+sd*w*0.60,r*0.22,r*0.18,w*0.16,8,MET_D,Math.PI/2); // hub cap
return m;
}
/* Thruster: gimbal collar, bell that WIDENS toward the exit, recessed throat,
and a glow disc set inside the bell so the emissive reads as heat coming
from within, not paint on the back face. Points -X (exhaust rearward). */
function thrusterBell(m,x,y,z,r,len,col){
m.cyl(x,y,z,r*0.74,r*0.62,len*0.30,10,MET_D,Math.PI/2); // gimbal collar
m.cyl(x-len*0.30,y,z,r*0.62,r,len*0.70,12,col||MET_L,Math.PI/2); // bell (flares out)
m.tube(x-len,y,z,r*0.98,r*0.80,len*0.14,12,DARKER,Math.PI/2); // rim ring
m.cyl(x-len*0.86,y,z,r*0.72,r*0.72,len*0.10,10,HOT,Math.PI/2); // glow, recessed
return m;
}
/* Cockpit glazing: raised frame, glass panes, and mullions crossing them.
Glass with no frame reads as a blue sticker; the frame is what sells it. */
function canopyGlass(m,x,y,z,len,wid,hgt,col){
m.bevelBox(x,y,z,len,hgt*0.35,wid,0.24,col||MET_L); // sill
m.bevelBox(x,y+hgt*0.30,z,len*0.92,hgt*0.70,wid*0.88,0.30,GLASS); // panes
m.box(x,y+hgt*0.32,z,len*0.94,hgt*0.66,wid*0.10,col||MET_L); // centre mullion
m.box(x,y+hgt*0.32,z,len*0.10,hgt*0.66,wid*0.90,col||MET_L); // cross mullion
m.wedge(x+len*0.55,y+hgt*0.9,z,len*0.35,hgt*0.25,wid*0.9,col||MET_L,0,true); // brow
return m;
}
/* Recessed panel with a lip — the inner-extrusion move as a one-liner, for
flat faces that need a fabricated break without restructuring the hull. */
function insetPanel(m,x,y,z,len,wid,depth,col,yaw){
m.box(x,y,z,len,depth,wid,DARKER,yaw);
m.box(x,y+depth*0.55,z,len*0.86,depth*0.55,wid*0.86,col||MET_D,yaw);
return m;
}
/* Road wheels with return rollers and a drive sprocket — a proper running
gear instead of a plain slab, visible along the whole flank.
WEIGHT CLASS, NOT JUST SIZE. Every ground vehicle from a size-16 Lancer to a
size-26 Basilisk used the same proportions and the same wheel count, so a
light and a heavy were the identical machine viewed at two zoom levels — and
with nothing on screen to measure against, the player had no way to tell
which one was the expensive one. `cls` (0 light / 1 medium / 2 heavy) widens
the tracks, adds or removes a road wheel, and gives a heavy a second skirt
tier, so weight reads at a glance from the running gear alone. Defaults to
medium, which is what every existing caller was already getting. */
function runningGear(m,len,wid,h,gauge,wheels,col,cls){
const K=cls==null?1:cls;
wid*=[0.82,1.0,1.26][K]; wheels=(wheels||5)+[-1,0,1][K];
for(const s of [-1,1]){
m.box(0,0,s*gauge,len,h*0.86,wid,TREAD);
m.box(0,h*0.30,s*gauge,len*1.04,h*0.30,wid*1.20,DARKER); // track links
const n=wheels||5;
for(let k=0;k<n;k++){
const x=-len/2+len*(k+0.5)/n;
m.cyl(x,h*0.46,s*gauge,h*0.40,h*0.40,wid*0.52,8,RUBBER);
m.cyl(x,h*0.46,s*(gauge+wid*0.30),h*0.17,h*0.17,wid*0.12,7,MET_L); // hub
hydraulic(m,x,h*0.86,s*gauge*0.72,h*0.7,0.20,col||MET_D,0); // suspension arm
}
m.cyl( len*0.52,h*0.72,s*gauge,h*0.34,h*0.34,wid*0.55,9,MET_D); // drive sprocket
m.cyl(-len*0.52,h*0.72,s*gauge,h*0.30,h*0.30,wid*0.55,9,MET_D); // idler
for(let k=0;k<3;k++) // return rollers
m.cyl(-len*0.3+k*len*0.3,h*1.02,s*gauge,h*0.14,h*0.14,wid*0.42,7,MET_D);
if(K===2) armorPlate(m,0,h*1.3,s*(gauge+wid*0.5),len*0.94,h*0.8,0.42,0,col||MET_D,0);
}
return m;
}
/* ---------------------------------------------------------------------------
LEAN TRACK UNIT — the same read as runningGear(), for a quarter of the cost.
Measured, not guessed: runningGear() builds 1,136 / 1,680 / 2,320 triangles
for light / medium / heavy, and on a medium 840 of those are the per-wheel
hydraulic() suspension arms — three 8-segment cylinders each, hidden BEHIND
the track skirt where this camera never looks. A tier-1 unit's entire budget
is 1,400. That arithmetic is the real reason every tracked vehicle in the
roster ended up as a flat slab with a stick on it: there was nothing left to
spend on the part of the silhouette the player actually sees.
Same components, 6-segment wheels and flat arms: track run, links, road
wheels with hub faces, drive sprocket, idler, return run, and a chamfered
armour skirt. The livery stripe rides the TOP EDGE of that skirt, because
that edge is the one piece of running gear an overhead camera sees and the
deck is where a turret sits.
`cls` matches runningGear's contract (0 light / 1 medium / 2 heavy) so the
two are interchangeable at a call site.
--------------------------------------------------------------------------- */
function trackUnit(m,len,wid,h,gauge,wheels,col,cls,trim){
const K=cls==null?1:cls;
wid*=[0.84,1.0,1.24][K];
const n=Math.max(3,(wheels||5)+[-1,0,1][K]), C=col||MET_D;
for(const s of [-1,1]){
const z=s*gauge;
/* The run is the TOP and BOTTOM straps of the track, not a solid slab, so
the road wheels show through the gap between them — the single detail
that separates a track from a black rubber band at any zoom. */
m.box(0,0,z,len,h*0.20,wid,TREAD); // ground run
m.box(0,h*0.66,z,len*0.98,h*0.18,wid,TREAD); // return run
m.box(0,h*0.70,z,len*1.02,h*0.12,wid*1.16,DARKER); // links, proud of it
for(let k=0;k<n;k++){
const x=-len/2+len*(k+0.5)/n;
m.cyl(x,h*0.14,z,h*0.30,h*0.30,wid*0.56,6,RUBBER); // road wheel
m.box(x,h*0.30,z+s*wid*0.60,h*0.26,h*0.24,wid*0.14,MET_L); // hub face, outboard
}
m.cyl( len*0.50,h*0.30,z,h*0.40,h*0.40,wid*0.58,6,MET_D); // drive sprocket
m.cyl(-len*0.50,h*0.30,z,h*0.36,h*0.36,wid*0.58,6,MET_D); // idler
/* SKIRT. A chamfered plate spanning the upper run is what reads as armour
rather than as machinery, and its top edge is the only part of any
running gear an overhead camera sees — so the livery stripe lands there
and not on the deck, where a turret sits on top of it. */
m.bevelBox(0,h*0.86,z+s*wid*0.12,len*0.94,h*0.56,wid*0.86,h*0.16,C);
/* Two blocks, not a racing stripe down the whole skirt: livery has to be
readable AND deliberate, and a continuous band along both tracks is most
of the visible surface of the vehicle. */
for(const f of [-1,1])
m.box(f*len*0.28,h*1.42,z+s*wid*0.30,len*0.26,h*0.13,wid*0.26,trim||TEAM_A);
if(K===2) m.box(0,h*0.24,z+s*wid*0.62,len*0.88,h*0.40,wid*0.12,C); // heavy side plate
}
return m;
}
/* ---------------------------------------------------------------------------
GLACIS — a sloped nose plate that actually slopes forward.
MeshBuilder.wedge() ramps across its THIRD axis, so a wedge dropped in at
yaw 0 slopes SIDEWAYS. Every "glacis" authored in this library before this
helper is therefore a lateral A-frame on a hull whose armour was meant to
rake front-to-back, and it never got caught because a sloped shape still
looks sloped in a screenshot — you have to look at the SIDE elevation to see
that the nose is square. Yawing a quarter turn puts the ridge at the rear
and runs the slope down to the nose, which is what a glacis is.
Arguments are in vehicle axes: len along +X, wid across Z.
--------------------------------------------------------------------------- */
function glacisX(m,x,y,z,len,hgt,wid,col){
m.wedge(x,y,z,wid,hgt,len,col,Math.PI/2);
return m;
}
/* Layered turret: faceted body, mantlet, chamfered roof, bustle and hatch. */
function turretBody(m,len,wid,h,col,colTop){
const L=len*0.5, W=wid*0.5;
const prof=[[-L,-W*0.86],[-L*0.5,-W],[L*0.5,-W*0.9],[L,-W*0.44],
[L,W*0.44],[L*0.5,W*0.9],[-L*0.5,W],[-L,W*0.86]];
m.extrude(0,0,0,prof,h*0.72,col);
m.extrude(0,h*0.72,0,prof.map(p=>[p[0]*0.82,p[1]*0.80]),h*0.28,colTop||col);
m.box(L*0.86,h*0.30,0,len*0.16,h*0.62,wid*0.52,MET_D); // mantlet
return m;
}
/* ---------------------------------------------------------------------------
UNITS — indexed to match TYPES[]. Each entry returns {hull, turret?}.
Authored at roughly 10 world units per "size 10" so a unit's `size` field
scales it directly.
--------------------------------------------------------------------------- */
const UNIT_MESH=[]; // [type] = {hull:InstMesh, tur:InstMesh|null, s:scale}
const UNIT_GEO=[];
/* ============================================================================
INFANTRY — soldiers that look like soldiers.
The production tab says INFANTRY and the models were tiny mechs, which is a
promise broken twice: the player expects a trooper, and the battlefield
loses its scale reference — a tank only reads as big when a HUMAN stands
next to it. One shared builder, varied per kit, so every infantry unit is
the same species: helmet with a glass visor, armoured torso over a narrower
waist, pauldrons carrying the livery, a backpack, and a weapon held ACROSS
the body — the classic soldier read at any pixel size.
Legs are SERVO deliberately: the walk shader swings SERVO geometry below hip
height, so these troopers march with the same system that walks the mechs.
Arms and weapon stay rigid so the gun does not wobble. */
function mfTrooper(m,o){
o=o||{};
const H=o.h||8.6, W=o.w||1.0; // height, bulk factor
const hip=H*0.46, sh=H*0.74, hd=H*0.86;
/* legs — boot, shin, thigh, with a knee step between them */
for(const sd of [-1,1]){
m.bevelBox(0.25,0,sd*0.62*W,1.5*W,0.55,0.95*W,0.20,DARK); // boot
m.bevelBox(0.05,0.55,sd*0.62*W,0.80*W,hip*0.42,0.80*W,0.18,SERVO); // shin
m.cyl(0.05,hip*0.50,sd*0.62*W,0.46*W,0.46*W,0.5,7,MET_D,Math.PI/2); // knee
m.bevelBox(0.0,hip*0.55,sd*0.62*W,0.92*W,hip*0.48,0.92*W,0.20,SERVO); // thigh
m.bevelBox(0.0,hip*1.02,sd*0.62*W,1.0*W,0.5,1.0*W,0.16,MET_D); // hip guard
}
/* torso — armour OVER a waist, so there is a silhouette pinch */
m.bevelBox(0,hip,0,1.3*W,(sh-hip)*0.36,1.5*W,0.22,DARK); // waist
m.bevelBox(0.1,hip+(sh-hip)*0.30,0,1.9*W,(sh-hip)*0.74,2.1*W,0.30,o.chest||MET); // chest plate
/* RAKED GLACIS over the chest — the armour angle is what stops the torso
reading as a box. Sharp edge running downward, like a breastplate. */
m.wedge(0.95*W,hip+(sh-hip)*0.36,0,0.8*W,(sh-hip)*0.52,1.9*W,o.chest||MET_L);
insetPanel(m,0.95*W,hip+(sh-hip)*0.62,0,0.9*W,1.1*W,0.16,o.chest||MET_D,0); // chest vent
m.bevelBox(-0.55*W,hip+(sh-hip)*0.5,0,1.0*W,(sh-hip)*0.62,1.9*W,0.24,o.pack||MET_D); // backpack
/* pauldrons — the livery lands here, at the widest point */
for(const sd of [-1,1]){
m.bevelBox(0,sh-0.25,sd*1.25*W,1.35*W,0.62,0.95*W,0.30,TEAM_A);
m.wedge(0,sh+0.30,sd*1.25*W,1.30*W,0.42,0.92*W,TEAM_A,0,sd<0); // angled pauldron cap
m.bevelBox(0.15,sh-1.15,sd*1.30*W,0.72*W,1.1,0.62*W,0.20,SERVO); // upper arm
m.bevelBox(0.85*W,sh-1.55,sd*1.05*W,1.3*W,0.60,0.55*W,0.18,DARK); // forearm, reaching to the gun
}
/* head — helmet dome with a GLASS visor slot and a brow ridge */
m.bevelBox(0.1,sh+0.15,0,1.15*W,(hd-sh)*0.9,1.25*W,0.34,o.helm||MET_L);
m.box(0.62*W,sh+0.42,0,0.34,0.42,0.95*W,GLASS); // visor
m.box(0.55*W,sh+0.85,0,0.5,0.18,1.1*W,MET_D); // brow
if(o.antenna){ m.cyl(-0.35*W,hd,0.5*W,0.05,0.03,0.9,5,DARKER); }
return {hip,sh,hd};
}
function mdlStriker(){ // 0 — line rifleman
const m=MB();
const b=mfTrooper(m,{h:8.6,w:1.0,antenna:1});
/* rifle held across the chest: receiver, barrel with muzzle, magazine,
stock — pointed +X so the unit faces where it shoots */
const gy=b.hip+1.6;
m.bevelBox(1.35,gy,0.15,2.6,0.55,0.45,0.14,MET_D); // receiver
m.cyl(2.7,gy+0.12,0.15,0.14,0.12,1.7,6,DARKER,Math.PI/2); // barrel
m.tube(4.4,gy+0.12,0.15,0.20,0.10,0.28,6,MET_L,Math.PI/2); // muzzle
m.box(1.6,gy-0.55,0.15,0.4,0.6,0.35,MET_D); // magazine
m.box(0.3,gy+0.05,0.15,0.7,0.4,0.35,DARK); // stock
glowStrip(m,-0.55,b.sh+0.4,0,0.8,ENERGY,0); // pack light
return {hull:m.build(),tur:null,s:0.92};
}
function mdlRhino(){ // 1 — medium battle tank
const m=MB();
runningGear(m,13,3.2,2.8,3.5,5,MET_D);
/* Faceted hull with a distinct upper plate break, then hardware bolted all
over it: skirts, stowage, vents, a mast. The plate break plus the clutter
is what stops an overhead view reading it as a rounded rectangle. */
hullShell(m,12.4,7.4,2.2,1.5,MET,TEAM_A);
m.wedge(4.4,3.7,0,3.4,1.5,6.4,MET_L); // glacis
for(const sd of [-1,1]){
armorPlate(m,0.6,2.2,sd*4.1,9.6,1.9,0.42,0,MET_D,0); // spaced side skirts
m.box(-4.9,3.9,sd*2.0,2.0,0.5,1.0,TEAM_A); // livery flash
}
ventBank(m,-4.3,3.72,0,3.6,4.6,5,MET_D,0); // engine deck louvres
exhaust(m,-5.9,3.0,0,2,0.42,2.0,MET_D,1.9);
stowage(m,-1.4,3.72,3.0,5.0,1.6,RUST,0,3);
kitBox(m,-3.0,3.72,-2.5,2.2,0.9,1.5,MET_D,0);
sensorMast(m,-5.2,3.9,1.6,2.6,MET_L);
m.box(4.9,3.5,-2.4,1.0,0.7,1.0,GLASS); // driver's vision block
glowStrip(m,0.5,3.76,0,7.0,ENERGY,0);
const t=MB();
turretBody(t,7.2,6.0,2.9,MET,TEAM_T);
for(const sd of [-1,1]){
m.tm=0;
t.box(-0.4,1.1,sd*3.0,4.6,1.5,0.42,TEAM_A); // cheek livery
t.box(-2.2,2.1,sd*2.1,1.5,0.9,0.9,DARKER); // smoke launchers
t.cyl(-2.2,3.0,sd*2.1,0.26,0.24,0.9,6,DARKER);
}
gunX(t,2.6,1.5,7.6,0.52,MET_D);
t.cyl(4.6,1.5,0,0.72,0.66,1.2,9,MET_D); // fume extractor
t.tube(10.0,1.5,0,0.54,0.30,0.8,8,DARKER); // bore
t.cyl(0.2,2.9,-1.5,0.95,0.86,0.8,9,MET_D); // cupola
t.tube(0.2,3.7,-1.5,0.95,0.62,0.30,9,MET_L);
t.box(1.1,3.4,-1.5,1.9,0.34,0.34,DARK); // pintle MG
kitBox(t,-3.3,1.0,0,1.5,1.9,3.2,MET_D,0); // bustle rack
stowage(t,-3.3,2.9,0,2.6,2.6,RUST,0,9);
sensorMast(t,-1.2,3.2,1.9,1.9,MET_L);
return {hull:m.build(),tur:t.build(),s:1.0,turH:5.2,muzzle:10.2};
}
function mdlGoliath(){ // 2 — heavy assault tank
const m=MB();
runningGear(m,17,4.4,3.6,4.7,6,MET_D,2);
hullShell(m,16.4,9.8,2.8,1.9,MET,TEAM_A);
m.wedge(5.8,4.7,0,4.4,1.9,8.6,MET_L); // heavy glacis
armorPlate(m,6.0,4.7,0,3.0,1.5,0.5,0,MET_D,0); // applique brow
for(const sd of [-1,1]){
armorPlate(m,0.8,2.8,sd*5.4,13.0,2.4,0.5,0,MET_D,0); // full-length skirts
m.bevelBox(3.4,4.9,sd*4.4,4.6,1.5,2.0,0.3,MET_D); // sponson blisters
m.box(-6.4,5.1,sd*2.6,2.6,0.6,1.2,TEAM_A);
m.cyl(6.6,4.9,sd*3.6,0.30,0.26,1.1,6,MET_L); // tow points
}
ventBank(m,-5.6,4.72,0,4.6,6.4,7,MET_D,0);
exhaust(m,-7.6,3.6,0,3,0.50,2.6,MET_D,2.4);
stowage(m,-2.0,4.72,4.0,6.4,2.0,RUST,0,17);
kitBox(m,-4.2,4.72,-3.4,2.8,1.1,2.0,MET_D,0);
sensorMast(m,-6.6,5.1,2.4,3.2,MET_L);
glowStrip(m,0.5,4.78,0,9.5,ENERGY,0);
const t=MB();
turretBody(t,9.6,8.0,3.9,MET,TEAM_T);
for(const sd of [-1,1]){
t.box(-0.4,1.5,sd*4.0,6.6,2.0,0.5,TEAM_A);
t.box(-3.0,2.9,sd*2.9,2.0,1.1,1.1,DARKER);
for(let k=0;k<3;k++) t.cyl(-3.0+k*0.7,4.0,sd*2.9,0.24,0.22,1.0,6,DARKER);
armorPlate(t,1.6,0.2,sd*4.1,5.0,2.6,0.42,0,MET_D,0);
}
gunX(t,3.6,2.0,10.8,0.70,MET_D);
t.cyl(6.2,2.0,0,0.95,0.88,1.6,9,MET_D);
t.tube(14.3,2.0,0,0.72,0.40,1.0,8,DARKER);
t.cyl(0.6,3.9,-2.4,1.15,1.05,0.9,9,MET_D);
t.tube(0.6,4.8,-2.4,1.15,0.74,0.34,9,MET_L);
t.box(1.8,4.5,-2.4,2.4,0.4,0.4,DARK);
kitBox(t,-4.4,1.2,0,1.9,2.4,4.4,MET_D,0);
stowage(t,-4.4,3.6,0,3.4,3.6,RUST,0,21);
sensorMast(t,-1.8,4.3,2.6,2.3,MET_L);
return {hull:m.build(),tur:t.build(),s:1.0,turH:6.3,muzzle:14.4};
}
function mdlThumper(){ // 3 — self-propelled artillery
const m=MB();
runningGear(m,13,3.4,3.0,3.7,5,MET_D);
hullShell(m,12.4,7.6,2.0,1.3,MET,TEAM_A);
ventBank(m,-3.6,3.32,0,3.4,5.2,5,MET_D,0);
exhaust(m,-5.4,2.6,0,2,0.40,1.8,MET_D,2.0);
for(const sd of [-1,1]){
/* Deployed recoil spades are the artillery tell in plan view. */
m.box(-5.8,1.4,sd*4.0,2.2,2.8,0.9,MET_D);
hydraulic(m,-5.0,2.6,sd*3.6,2.2,0.28,MET_D,0);
m.box(0.4,3.36,sd*3.6,7.0,0.5,0.7,TEAM_A);
kitBox(m,2.6,3.32,sd*2.6,2.4,1.0,1.4,MET_D,0); // shell lockers
}
sensorMast(m,-4.6,3.5,1.8,2.4,MET_L);
const t=MB();
turretBody(t,5.6,5.8,2.6,MET,TEAM_T);
gunX(t,1.6,2.2,14.5,0.60,MET_D); // very long tube
t.cyl(4.2,2.2,0,1.05,0.95,2.6,9,MET_D); // recuperator
t.cyl(7.0,2.2,0,0.80,0.74,1.4,9,MET_D);
t.tube(16.4,2.2,0,0.62,0.34,1.0,8,DARKER);
for(const sd of [-1,1]){
t.cyl(1.4,1.1,sd*1.8,1.4,1.3,0.9,9,DARK); // trunnions
hydraulic(t,-1.4,1.1,sd*2.0,2.6,0.32,MET_D,0); // elevation rams
t.box(-0.6,2.7,sd*2.7,4.2,0.5,0.5,TEAM_A);
}
kitBox(t,-2.8,1.2,0,1.7,1.6,3.6,MET_D,0);
return {hull:m.build(),tur:t.build(),s:1.0,turH:5.4,muzzle:16.1};
}
function mdlCommander(){ // 4 — hero mech
const m=MB();
/* SupCom-style bot proportions: broad pauldrons, deep chest, chunky
digitigrade legs with visible hydraulics, and glowing power conduits. */
for(const sd of [-1,1]){
m.bevelBox(-1.0,0,sd*3.6,5.6,1.8,3.2,0.5,SERVO); // foot
m.box(-1.0,1.8,sd*3.6,4.0,0.6,2.6,SERVO);
m.extrude(-0.4,2.4,sd*3.6,[[-1.6,-1.7],[1.6,-1.7],[2.0,0],[1.6,1.7],[-1.6,1.7],[-2.0,0]],4.8,SERVO);
hydraulic(m,-1.6,2.4,sd*3.6,4.4,0.42,SERVO,0); // shin ram
m.bevelBox(0.2,7.2,sd*3.6,4.0,2.4,4.0,0.6,SERVO); // knee block
hydraulic(m,-1.4,9.6,sd*3.6,3.0,0.36,SERVO,0);
m.box(0.2,9.6,sd*3.6,3.4,2.6,3.4,SERVO); // thigh
}
m.extrude(0,12.2,0,[[-4.2,-4.6],[2.6,-5.0],[4.6,-2.4],[4.6,2.4],[2.6,5.0],[-4.2,4.6]],5.4,MET);
m.extrude(0,17.6,0,[[-3.4,-3.8],[2.2,-4.2],[3.8,-2.0],[3.8,2.0],[2.2,4.2],[-3.4,3.8]],2.2,TEAM_A);
m.wedge(3.2,19.8,0,4.0,2.4,7.6,TEAM_T); // chest plate
glowStrip(m,1.0,19.9,0,6.0,ENERGY,0);
for(const sd of [-1,1]){
m.bevelBox(0.4,17.0,sd*6.4,4.4,3.6,3.6,0.7,TEAM_A); // pauldron
armorPlate(m,0.4,20.6,sd*6.4,4.0,1.4,0.5,0,MET_D,0);
m.box(0.0,13.6,sd*6.6,3.0,4.0,3.0,MET_D); // upper arm
hydraulic(m,-1.2,13.6,sd*6.6,3.4,0.34,MET_L,0);
gunX(m,1.6,13.6,6.6,0.62,DARK,sd*6.6); // arm cannon
tubeX(m,8.4,13.6,sd*6.6,0.8,0.58,0.32,8,DARKER); // lateral muzzle collar
m.cyl(-2.6,19.4,sd*2.6,0.66,0.50,3.4,8,MET_D); // back vents
m.tube(-2.6,22.8,sd*2.6,0.62,0.36,0.4,8,ENERGY);
}
m.bevelBox(-3.6,17.6,0,3.2,3.4,6.4,0.6,DARK); // backpack
m.bevelBox(2.2,22.2,0,3.6,2.8,4.4,0.6,TEAM_A); // head
m.box(4.0,23.2,0,0.5,1.0,3.0,GLASS); // visor
sensorMast(m,-0.6,25.0,1.4,2.6,MET_L);
m.box(-1.2,25.0,0,0.9,2.6,0.9,LAMP); // command beacon
/* Nova's commander is the clean-energy flagship, not the generic biped the
three enemy bios branch from. A suspended command halo and twin capacitor
towers keep that identity legible even when the weapon is occluded. */
m.ring(-3.4,23.7,0,2.4,3.4,18,ENERGY);
for(const sd of [-1,1]){
m.cyl(-5.0,18.8,sd*3.1,1.05,.82,5.8,10,MET_D);
m.cyl(-5.0,24.7,sd*3.1,.78,.62,1.0,10,ENERGY);
m.box(-5.0,22.0,sd*3.1,.45,5.2,1.0,ENERGY);
}
return {hull:m.build(),tur:null,s:0.62};
}
function mdlWasp(){ // 5 — light air
const m=MB();
const low=[[-5.0,-1.55],[-3.0,-2.05],[2.5,-2.15],[5.5,-.72],[6.0,0],
[5.5,.72],[2.5,2.15],[-3.0,2.05],[-5.0,1.55]];
const mid=[[-4.5,-1.35],[-2.4,-1.72],[2.7,-1.66],[5.0,-.55],[5.4,0],
[5.0,.55],[2.7,1.66],[-2.4,1.72],[-4.5,1.35]];
m.extrude(0,0,0,low,1.35,MET_D); // armoured keel
m.extrude(0,1.35,0,mid,1.15,MET); // faceted fuselage
m.extrude(.15,2.50,0,mid.map(p=>[p[0]*.70,p[1]*.63]),.58,TEAM_A);
canopyGlass(m,2.55,2.88,0,3.5,2.45,1.35,MET_L); // framed canopy
m.box(-.65,3.10,0,4.25,.34,1.08,TEAM_T); // dorsal identity stripe
glowStrip(m,-.35,3.47,0,3.05,ENERGY,0);
ventBank(m,-1.9,3.12,0,1.6,1.30,3,DARK,0);
/* TWIN THRUSTERS — an aircraft with no visible propulsion reads as a paper
dart. Bells flare toward the exhaust with the glow recessed inside. */
for(const sd of [-1,1]) thrusterBell(m,-4.6,1.9,sd*1.05,0.62,1.7,MET_L);
for(const sd of [-1,1]){
const wing=sd>0
? [[-3.45,1.18],[2.40,1.34],[.85,6.95],[-4.15,6.08]]
: [[-3.45,-1.18],[-4.15,-6.08],[.85,-6.95],[2.40,-1.34]];
const panel=sd>0
? [[-2.72,1.72],[1.72,1.75],[.72,5.48],[-3.18,5.03]]
: [[-2.72,-1.72],[-3.18,-5.03],[.72,-5.48],[1.72,-1.75]];
m.extrude(0,1.02,0,wing,.62,MET_D); // one-piece load-bearing wing
m.extrude(0,1.64,0,panel,.24,TEAM_A); // restrained wing livery
m.box(-1.65,1.02,sd*3.55,4.50,.75,2.25,DARK); // engine pylon joins wing
cylX(m,-4.65,1.34,sd*3.75,5.55,1.13,.94,10,DARK,false); // long nacelle
ringX(m,-4.69,1.34,sd*3.75,.78,1.18,12,MET_L);
tubeX(m,-5.12,1.34,sd*3.75,.82,.92,.43,10,TWR_BORE); // actual hollow exhaust
ringX(m,-5.15,1.34,sd*3.75,.31,.48,10,HOT);
ringX(m,.43,1.34,sd*3.75,.67,1.00,10,MET_L); // intake lip
m.box(-1.10,1.96,sd*3.75,2.20,.42,1.26,TEAM_T); // nacelle livery saddle
ventBank(m,-1.05,2.39,sd*3.75,1.62,.90,3,MET_D,0);
m.bevelBox(-.30,.48,sd*5.35,2.85,.92,1.28,.24,DARKER); // attached missile rail
for(let k=0;k<3;k++)
cylX(m,-.92+k*.84,.58,sd*5.35,1.25,.24,.18,7,HOT,false);
m.box(2.20,.54,sd*1.55,2.15,.78,.86,DARK); // fixed gun receiver
gunX(m,2.75,.82,3.15,.27,MET_D,sd*1.55); // visible hollow cannon
m.box(-4.10,2.20,sd*1.45,2.25,1.85,.36,MET_L); // twin tail fins touch hull
m.box(-3.65,3.46,sd*.86,1.50,.28,.34,LAMP); // formation lamps
}
m.bevelBox(-4.15,1.58,0,2.35,1.25,3.55,.30,DARKER); // tail gearbox links fins
sensorMast(m,-3.85,2.82,0,1.55,MET_L);
m.greeble(-.40,3.10,0,3.3,1.05,.34,5,MET_D,0,51);
return {hull:m.build(),tur:null,s:1.0,air:1};
}
function mdlLongbow(){ // 6 — beam sniper
const m=MB();
runningGear(m,12.0,3.0,2.5,3.25,5,MET_D);
hullShell(m,11.8,7.0,1.9,1.25,MET,TEAM_A,0.34,0.72);
m.wedge(4.3,3.16,0,3.2,1.15,5.6,MET_L);
for(const sd of [-1,1]){
armorPlate(m,0.2,1.9,sd*3.72,8.6,1.6,0.38,0,MET_D,0);
m.box(-1.0,3.18,sd*3.18,6.2,0.44,0.62,TEAM_A);
/* Long capacitors keep the sniper role readable when the tracking weapon
is turned away from the camera. */
m.cyl(-3.8,3.24,sd*2.15,0.72,0.72,2.3,9,TWR_MACH);
m.ring(-3.8,5.0,sd*2.15,0.76,0.95,10,TWR_GLOW);
}
ventBank(m,-4.1,3.18,0,2.8,3.2,4,MET_D,0);
exhaust(m,-5.3,2.35,0,2,0.34,1.8,MET_D,1.7);
sensorMast(m,-4.7,3.28,1.15,2.2,MET_L);
glowStrip(m,0.0,3.24,0,6.4,ENERGY,0);
const t=MB();
turretBody(t,5.6,4.7,2.35,TWR_ARM,TWR_TEAM);
t.bevelBox(-2.3,0.65,0,2.1,1.5,3.8,0.35,TWR_COAT);
for(const sd of [-1,1]){
t.box(-0.3,1.0,sd*2.32,3.8,1.0,0.38,TWR_TEAM);
hydraulic(t,-1.4,0.15,sd*1.75,1.6,0.23,TWR_MACH,0);
}
gunX(t,1.75,1.25,9.7,0.42,TWR_MACH);
for(const x of [4.4,6.15,7.9]) ringX(t,x,1.25,0,0.60,0.88,12,TWR_GLOW);
cylX(t,0.7,1.25,0,2.15,0.92,0.72,10,TWR_COAT,false);
tubeX(t,11.0,1.25,0,0.72,0.60,0.31,10,TWR_BORE);
t.cyl(-0.9,2.36,-1.15,0.62,0.54,0.50,9,MET_L);
t.box(-0.9,2.86,-1.15,0.22,0.75,0.22,LAMP);
return {hull:m.build(),tur:t.build(),s:1.0,turH:4.45};
}
function mdlHornet(){ // 7 — rocket vehicle
const m=MB();
runningGear(m,11.6,3.0,2.55,3.25,5,MET_D);
hullShell(m,11.4,7.0,1.95,1.25,MET,TEAM_A,0.38,0.70);
m.wedge(4.1,3.2,0,3.1,1.2,5.5,MET_L);
ventBank(m,-3.9,3.22,0,2.8,3.8,4,MET_D,0);
exhaust(m,-5.1,2.4,0,2,0.34,1.8,MET_D,1.7);
for(const sd of [-1,1]){
armorPlate(m,0.0,1.95,sd*3.7,8.2,1.6,0.38,0,MET_D,0);
m.box(0.3,3.24,sd*3.15,6.0,0.45,0.62,TEAM_A);
kitBox(m,-2.4,3.22,sd*2.35,1.8,0.9,1.25,MET_D,0);
}
sensorMast(m,-4.5,3.3,1.1,2.0,MET_L);
const t=MB();
t.cyl(-1.5,0,0,2.5,2.15,1.2,12,TWR_PAD);
t.bevelBox(-0.9,1.2,0,3.2,2.2,5.5,0.45,TWR_COAT);
for(const sd of [-1,1]){
t.cyl(-0.7,1.5,sd*2.72,1.05,0.92,0.62,10,TWR_MACH);
hydraulic(t,-1.8,1.2,sd*2.15,2.0,0.28,TWR_MACH,0);
t.box(-1.0,3.35,sd*2.72,3.6,0.44,0.40,TWR_TEAM);
}
/* Six genuinely open launch tubes replace closed cargo-box silhouettes. */
for(let row=0;row<2;row++) for(let col=0;col<3;col++){
const y=2.55+row*1.55, z=-1.9+col*1.9;
cylX(t,-0.1,y,z,4.45,0.69,0.63,10,TWR_MACH,false);
ringX(t,4.28,y,z,0.66,0.84,10,TWR_TEAM);
tubeX(t,4.32,y,z,0.72,0.70,0.39,10,TWR_BORE);
t.box(-0.45,y+0.73,z,3.7,0.20,0.18,TWR_TRIM);
}
t.bevelBox(-2.6,2.0,0,1.5,3.4,4.5,0.32,MET_D);
return {hull:m.build(),tur:t.build(),s:1.0,turH:4.45};
}
function mdlTitan(){ // 8 — superheavy walker
const m=MB();
for(const sd of [-1,1]){
/* Every part below the hip uses SERVO. Mixing ordinary armour into a leg
leaves those plates behind when the vertex-stage gait moves the foot. */
m.bevelBox(-1.35,0,sd*6.10,9.6,2.15,5.4,.62,SERVO); // broad planted foot
m.box(2.95,.42,sd*6.10,2.25,1.25,4.75,SERVO); // reinforced toe
m.box(-5.55,.30,sd*6.10,1.25,1.05,4.40,SERVO); // heel counterweight
m.cyl(-1.15,2.15,sd*6.10,1.78,1.48,1.05,10,SERVO); // ankle bearing
m.extrude(-.55,3.10,sd*6.10,[[-2.45,-2.25],[1.65,-2.25],[2.35,-1.15],
[2.35,1.15],[1.65,2.25],[-2.45,2.25]],6.20,SERVO);
m.bevelBox(-.35,4.20,sd*6.10,3.65,3.80,4.95,.48,SERVO); // shin armour mass
hydraulic(m,-2.22,3.15,sd*6.10,5.65,.48,SERVO,0);
hydraulic(m,1.18,3.25,sd*6.10,5.35,.38,SERVO,0);
m.cyl(.35,9.30,sd*6.10,2.70,2.45,2.25,12,SERVO); // articulated knee drum
m.bevelBox(.25,10.15,sd*6.10,6.15,2.65,5.65,.68,SERVO);
m.extrude(-.15,11.10,sd*6.10,[[-2.20,-2.15],[1.90,-2.15],[2.40,-1.05],
[2.40,1.05],[1.90,2.15],[-2.20,2.15]],3.95,SERVO);
m.box(-1.45,11.65,sd*4.35,2.35,3.10,1.35,SERVO); // inner hip brace, connected
}
m.extrude(0,14.35,0,[[-6.7,-7.25],[3.8,-7.8],[7.1,-4.3],[7.1,4.3],
[3.8,7.8],[-6.7,7.25]],4.45,MET_D); // pelvis / hip bridge
m.extrude(.15,18.80,0,[[-6.0,-6.45],[3.6,-6.9],[6.15,-3.7],[6.15,3.7],
[3.6,6.9],[-6.0,6.45]],2.05,TEAM_A);
for(const sd of [-1,1]){
armorPlate(m,.20,18.88,sd*6.30,8.6,1.55,.58,0,MET_L,0); // hip skirt
m.cyl(-4.20,16.35,sd*5.15,.62,.54,2.05,8,DARKER);
}
m.bevelBox(-4.55,16.35,0,4.65,3.25,10.75,.72,DARKER); // reactor bridge
ventBank(m,-4.45,19.76,0,3.30,7.65,6,MET_D,0);
m.extrude(0,20.85,0,[[-5.9,-6.25],[3.4,-7.15],[6.15,-4.25],[6.15,4.25],
[3.4,7.15],[-5.9,6.25]],6.30,MET); // high faceted torso
m.extrude(.35,27.15,0,[[-4.9,-5.35],[3.0,-5.95],[5.2,-3.35],[5.2,3.35],
[3.0,5.95],[-4.9,5.35]],2.15,TEAM_A); // shoulder deck livery
m.wedge(4.65,23.25,0,4.25,4.15,10.2,TEAM_T,0,true); // sloped chest glacis
m.bevelBox(1.35,27.15,0,6.80,1.30,8.65,.42,MET_L); // collar armour
glowStrip(m,4.85,27.48,0,5.0,ENERGY,0);
for(const sd of [-1,1]){
m.bevelBox(.65,23.05,sd*7.55,6.45,4.35,4.45,.72,MET_D); // attached shoulder receiver
m.box(-2.60,23.75,sd*7.55,1.55,2.45,4.05,DARKER); // ammunition bustle
m.greeble(-2.60,26.18,sd*7.55,1.20,3.15,.56,4,MET_L,0,sd>0?81:82);
m.box(3.05,24.28,sd*7.55,3.65,2.20,2.50,DARK); // trunnion housing
gunX(m,4.70,25.15,11.25,.82,MET_D,sd*7.55); // correctly separated cannon
m.box(.50,27.48,sd*7.55,5.55,.42,3.55,TEAM_A); // shoulder ownership panel
m.cyl(-5.10,23.15,sd*4.60,.92,.72,5.35,9,MET_D); // rear heat exchanger
m.tube(-5.10,28.42,sd*4.60,.72,.38,.52,9,TWR_BORE);
m.box(-4.55,21.45,sd*6.00,2.10,4.80,1.10,DARK); // torso side grille
for(let k=0;k<4;k++)
m.box(-4.05,22.00+k*.88,sd*6.58,1.45,.36,.32,MET_L);
}
m.bevelBox(-4.80,21.75,0,3.25,5.35,7.30,.65,DARK); // attached power backpack
m.greeble(-4.80,27.10,0,2.40,5.20,.70,6,MET_D,0,91);
m.bevelBox(3.05,29.30,0,5.15,3.65,6.30,.65,MET_L); // command head
m.wedge(5.75,30.18,0,1.55,1.70,5.10,GLASS,0,true); // visor block
m.box(6.55,30.45,0,.48,1.08,4.35,ENERGY); // readable visor strip
sensorMast(m,-.30,32.15,1.65,2.50,MET_L);
m.cyl(-1.65,32.20,-1.55,.62,.40,2.85,8,MET_D);
m.tube(-1.65,34.95,-1.55,.44,.22,.38,8,LAMP);
kitBox(m,-4.80,20.65,0,2.05,1.28,3.60,MET_D,0);
return {hull:m.build(),tur:null,s:0.62};
}
function mdlPyro(){ // 10 — flame trooper
const m=MB();
/* Same species as the Striker, bulked out: a flame suit is sealed and
heavy, so the trooper is wider, the helmet is a full hood, and the
backpack is the fuel plant — twin tanks with caps and bands, plumbed to
the projector. The pilot light is the tell at range. */
const b=mfTrooper(m,{h:8.2,w:1.22,chest:RUST,helm:MET_D,pack:MET_D});
for(const sd of [-1,1]){ // fuel tanks
m.cyl(-1.15,b.hip+0.4,sd*0.62,0.52,0.52,3.4,8,RUST);
m.cyl(-1.15,b.hip+3.8,sd*0.62,0.52,0.30,0.5,8,MET_D); // caps
m.cyl(-1.15,b.hip+1.9,sd*0.62,0.56,0.56,0.22,8,MET_L); // bands
}
m.cyl(-0.6,b.sh-0.4,0,0.16,0.14,1.9,5,DARKER,Math.PI/2); // feed hose
/* projector: short, fat, with a shroud and a pilot flame */
const gy=b.hip+1.5;
m.bevelBox(1.5,gy,0.15,2.0,0.7,0.6,0.16,MET_D);
m.tube(3.1,gy+0.1,0.15,0.34,0.20,0.9,7,MET_L,Math.PI/2); // shroud
m.cyl(3.9,gy+0.1,0.15,0.12,0.10,0.5,5,HOT,Math.PI/2); // pilot light
m.box(1.0,gy-0.5,0.15,0.5,0.5,0.4,DARK); // grip
glowStrip(m,3.4,gy+0.45,0.15,0.7,HOT,0);
return {hull:m.build(),tur:null,s:0.95};
}
function mdlVulture(){ // 10 — anti-air gun vehicle
const m=MB();
runningGear(m,11,2.8,2.6,3.1,4,MET_D);
hullShell(m,11.0,6.8,2.0,1.2,MET,TEAM_A);
ventBank(m,-3.2,3.22,0,2.8,4.4,4,MET_D,0);
exhaust(m,-4.8,2.4,0,2,0.36,1.6,MET_D,1.8);
for(const sd of [-1,1]) m.box(0.2,3.26,sd*3.2,6.4,0.5,0.6,TEAM_A);
kitBox(m,3.0,3.22,0,2.2,0.9,3.4,MET_D,0);
const t=MB();
t.cyl(0,0,0,2.8,2.4,2.4,12,MET);
t.extrude(0,2.4,0,[[-2.4,-2.2],[1.4,-2.6],[2.8,-1.2],[2.8,1.2],[1.4,2.6],[-2.4,2.2]],1.0,TEAM_T);
for(const sd of [-1,1]){
/* Twin short high-elevation barrels plus a tracking dish: an AA mount has
to be unmistakable from above or it gets misused as a gun tank. */
t.box(2.6,2.4,sd*1.4,5.6,1.0,1.0,MET_D);
t.tube(5.6,2.9,sd*1.4,0.48,0.26,0.8,7,DARKER);
t.box(0.6,3.6,sd*2.2,2.8,0.42,0.5,TEAM_A);
hydraulic(t,-0.8,2.4,sd*2.0,1.8,0.26,MET_D,0);
kitBox(t,-2.0,2.4,sd*2.0,1.5,1.0,1.1,DARKER,0); // ammo feed boxes
}
t.box(-2.9,2.4,0,1.6,2.2,2.6,DARK);
t.cyl(-2.9,4.6,0,2.9,0.7,0.45,12,MET_L); // tracking dish
t.cyl(-2.9,5.05,0,0.30,0.22,1.0,6,DARKER);
t.box(-2.9,6.05,0,0.28,0.34,0.28,LAMP);
return {hull:m.build(),tur:t.build(),s:1.0,turH:4.4};
}
function mdlBulwark(){ // 11 — shield generator vehicle
const m=MB();
/* The only vehicle still calling the legacy treads()/hullPlate() pair, so it
was the only one with no road wheels, no suspension, no hubs and a flat
unchamfered deck — eight primitives for a 90-mass tier-2 shield platform
that is meant to look like the toughest thing on the field. */
runningGear(m,13,3.6,2.8,3.8,5,MET_D,2);
hullShell(m,13.0,8.4,2.4,1.6,MET,TEAM_A);
deckCrown(m,0,4.06,0,12.4,8.0,MET_D,TEAM_T);
m.bevelBox(0,5.8,0,6.2,3.0,7.2,0.5,TEAM_T); // livery reactor housing
for(const s of [-1,1]) m.box(0,4.4,s*4.3,11.0,1.6,0.7,TEAM_A);
for(const s of [-1,1]) for(const f of [-1,1])
m.cyl(f*2.4,8.8,s*2.4,0.6,0.5,3.4,7,MET_D); // emitter pylons
m.sphere(0,10.4,0,3.0,10,ENERGY,0.7,false); // field core
m.box(-5.0,5.4,0,2.0,1.4,5.0,DARK);
return {hull:m.build(),tur:null,s:1.0};
}
/* ============================================================================
ARTHROPOD KIT — the Brood is grown, and it has to look grown.
The swarm was three spheres and six boxes. Read from the game's near-top-down
camera that is a green blob with twigs, and no amount of colour work fixes a
silhouette that has no anatomy in it. Real arthropods read the way they do
because of three things, and none of them were present:
JOINTED LIMBS WITH THE KNEE ABOVE THE BODY. This is the single strongest
signal. A spider's femur angles UP and OUT from the hip to a raised knee,
and only then does the tibia come down to the ground — so the body hangs
slung between its legs rather than sitting on them. A straight peg leg
reads as furniture; a raised knee reads as alive and as carrying weight.
SEGMENTED, OVERLAPPING CARAPACE. Chitin is not one smooth surface, it is a
stack of plates each slightly proud of the one behind, so every segment
catches a separate highlight and throws a shadow onto its neighbour.
That stack is what makes the mass look hard and heavy instead of soft.
TAPER EVERYWHERE. Nothing on a living thing is a constant-radius tube.
All limb geometry is built in BIO_LEG on purpose: the vertex stage keys its
two-oscillator spring bend off that material id (see VS3D in mesh.js), so
legs, antennae and tendrils get weighted follow-through with no bones, no CPU
skinning and no extra draw calls. Body masses are CHITIN, which the same
shader gives a breathing pulse. Building the anatomy correctly is therefore
also what animates it.
============================================================================ */
/* A tapered segment between two arbitrary points — the primitive the rest of
this kit is made of. cyl() only builds Y-axis frustums, which cannot express
a limb that goes out, up, and then down again. */
function boneSeg(m,a,b,r0,r1,seg,col){
const dx=b[0]-a[0], dy=b[1]-a[1], dz=b[2]-a[2];
const len=Math.hypot(dx,dy,dz)||1e-4;
const ax=dx/len, ay=dy/len, az=dz/len;
/* Any vector not parallel to the axis gives a stable basis. Picking the axis
we are LEAST aligned with avoids the degenerate cross product. */
const up = Math.abs(ay)<0.9 ? [0,1,0] : [1,0,0];
let ux=up[1]*az-up[2]*ay, uy=up[2]*ax-up[0]*az, uz=up[0]*ay-up[1]*ax;
let l=Math.hypot(ux,uy,uz)||1; ux/=l; uy/=l; uz/=l;
const vx=ay*uz-az*uy, vy=az*ux-ax*uz, vz=ax*uy-ay*ux;
const n=seg||6, A=[], B=[];
for(let k=0;k<n;k++){
const th=k/n*Math.PI*2, c=Math.cos(th), s=Math.sin(th);
A.push([a[0]+(ux*c+vx*s)*r0, a[1]+(uy*c+vy*s)*r0, a[2]+(uz*c+vz*s)*r0]);
B.push([b[0]+(ux*c+vx*s)*r1, b[1]+(uy*c+vy*s)*r1, b[2]+(uz*c+vz*s)*r1]);
}
for(let k=0;k<n;k++){ const j=(k+1)%n; m.quad(A[k],A[j],B[j],B[k],col); }
return m;
}
/* One walking leg: coxa out from the body, femur out and UP to a raised knee,
tibia down and further out, tarsus tapering to a point on the ground.
`side` is -1/+1, `k` staggers each leg's splay so they fan instead of
marching in parallel. Everything is BIO_LEG so the shader flexes it. */
function broodLeg(m,x,y,z,side,reach,drop,knee,th,col,gaitPhase,hip){
const c=col||BIO_LEG;
/* SEVEN SEGMENTS ON SEVEN BONES.
The count matters because each joint is a place the limb can change
direction, and it is that repeated change of direction — with a knuckle
swelling at every one — that the eye reads as a leg carrying weight rather
than as a bent rod. But the count alone is not enough: with the segments
welded rigidly to the body they still swung as one hook. Each is now a
RIGID BODY hinged to its parent, so the limb protracts from the hip, flexes
at the knee and plants its foot in sequence, the way a hinge chain does.
`gaitPhase` puts diagonal legs in antiphase — the alternating-tripod gait
every six- and eight-legged animal actually walks. */
const R=reach, D=drop, S=side;
/* THE HIP HAS TO BE INSIDE THE SHELL.
Every socket used to be a fraction of REACH — 0.10R for the socket, 0.34R
for the coxa — which silently assumed the body was as wide as a third of
the leg's span. It was, but only by accident: carapace() was bulging its
keel and segment plates out the +Z FLANK, and that bulge happened to reach
the coxa. Correcting the keel to the spine took ~2.2 units of half-width
off the flank and the legs came away from the body, worst at the waist
where the shell is narrowest — which is exactly the middle pair.
`hip` is the real answer: the lateral distance from the centreline where
this animal's shell actually is. The socket buries itself inside that, the
coxa sits on it, and the rest of the chain measures its span from there
instead of from a number that was never about the body at all. */
const H=(hip==null? 0.34*R : hip)*S; // shell surface, signed
const P=[
[x, y, z+H*0.30], // socket, inside the shell
[x+R*0.05, y+knee*0.34, z+H], // coxa — emerges AT the shell
[x+R*0.10, y+knee*0.82, z+H+S*0.34*R], // trochanter
[x+R*0.12, y+knee*1.06, z+H+S*0.76*R], // femur head — the high knee
[x+R*0.02, y+knee*0.58, z+H+S*1.10*R], // patella
[x-R*0.08, y-D*0.30, z+H+S*1.32*R], // tibia foot
[x-R*0.18, y-D*0.80, z+H+S*1.42*R], // metatarsus
[x-R*0.32, y-D, z+H+S*1.48*R], // tarsus tip
];
const RD=[th*1.20, th*1.34, th*1.26, th*1.10, th*0.86, th*0.62, th*0.40, th*0.07];
/* Hip sweeps fore/aft about the vertical; every joint below it flexes about
the horizontal, which is how a real limb folds. Amplitude falls off down
the chain — a hip travels, a tarsus only flicks — and the phase lag is what
makes the leg unfurl along its length instead of folding as one piece. */
const g=gaitPhase||0;
const AX=[[0,1,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0]];
const AMP=[0.26,0.10,0.15,0.21,0.17,0.12,0.09];
const LAG=[0.00,0.22,0.42,0.66,0.92,1.16,1.38];
let parent=null; const bone=[];
for(let k=0;k<7;k++){
/* Sign the swing by side so both flanks step outward together rather than
the whole animal crabbing sideways. */
const ax=AX[k][0]?[S,0,0]:[0,1,0];
parent=m.joint(P[k],parent,ax,g+LAG[k],AMP[k]*(k?1:S),0);
bone.push(parent);
}
/* FOUR SIDES, NOT SEVEN. A limb segment is a couple of pixels wide on a phone
at play zoom, and seven-sided tubes on seven segments across eight legs is
where this model's triangle budget actually went — the first version cost
5,860 triangles, nearly three times the TITAN, on the unit that spawns in
the largest numbers in the game. Silhouette is set by the JOINTS, not by
the roundness of the tubes between them, so this costs nothing visually. */
for(let k=0;k<P.length-1;k++){
m.bone(bone[k]);
const claw=k>=P.length-2;
boneSeg(m,P[k],P[k+1],RD[k],RD[k+1],claw?4:5,claw?CLAW:c);
}
/* Knuckles ride the bone BELOW them, so a joint stays welded to the segment
it drives and never tears open as the limb folds. Only the three that
actually read — hip, knee and ankle — get a sphere. */
for(const k of [3,5]){
m.bone(bone[k]);
m.sphere(P[k][0],P[k][1],P[k][2],RD[k]*1.14,4,c,0.86,false);
}
/* One spur, on the knee, and only when the limb is big enough for it to be
more than three pixels. On chaff it is pure cost. */
if(th>0.66){
m.bone(bone[3]);
chitinBlade(m,P[3][0],P[3][1],P[3][2],R*0.20,th*0.36,S*0.30,Math.PI*0.88,CLAW,-0.24);
}
m.bone(-1);
return m;
}
/* ---------------------------------------------------------------------------
SCULPTED CARAPACE.
A stack of overlapping spheres was the first attempt and it is wrong for the
same reason it is wrong in any sculpting package: the intersections show, the
cross-section is stuck circular, and the silhouette is made of arcs. This is
one continuous skin instead, with the anatomy written into it as
displacement — so the segment ridges, the tubercles and the dorsal keel are
part of the surface and light like it.
`profile(v)` gives the body's half-width down its length; the section is
flattened dorsoventrally and given a flat-ish underside, because an insect
is wider than it is tall and rides low over its legs.
--------------------------------------------------------------------------- */
function carapace(m,x,y,z,len,wid,hgt,col,o){
o=o||{};
const segs=o.segs==null?5:o.segs, ridge=o.ridge==null?0.055:o.ridge;
const bump=o.bump==null?0.05:o.bump, keel=o.keel||0, seed=(o.seed||11)>>>0;
const taperF=o.nose==null?0.55:o.nose, taperB=o.tail==null?0.30:o.tail;
/* Cheap deterministic value noise — the tubercle field. Sampled in surface
space so the bumps travel with the form instead of sliding across it. */
const h=(a,b)=>{ let n=(a*374761393+b*668265263+seed)>>>0;
n=(n^(n>>>13))*1274126177>>>0; return ((n^(n>>>16))>>>0)/4294967296; };
const noise=(a,b)=>{
const ia=Math.floor(a), ib=Math.floor(b), fa=a-ia, fb=b-ib;
const sa=fa*fa*(3-2*fa), sb=fb*fb*(3-2*fb);
return (h(ia,ib)*(1-sa)+h(ia+1,ib)*sa)*(1-sb)+(h(ia,ib+1)*(1-sa)+h(ia+1,ib+1)*sa)*sb;
};
/* DECLARE THE MATERIAL BEFORE SCULPTING.
`mat()` tracks a CURRENT material and the bindMat patch list only rewrites
the primitives it names — sculpt() and quad() are not on it, so every
carapace inherited whatever the previous primitive happened to leave
behind. Measured: the Ravager's entire body, 1,112 vertices, was MAT.PLATE.
That is the one material id the organic path keys off, so the animal that
was rebuilt for subsurface translucency and a breathing pulse was getting
neither — it was shading as painted metal. One line, six organisms. */
const CMAT=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col));
if(CMAT!=null) m.mat(CMAT);
m.sculpt(x,y,z,o.u||18,o.v||16,(u,v)=>{
const th=u*Math.PI*2, ct=Math.cos(th), st=Math.sin(th);
/* Body profile: a fat ellipse biased toward the tail, pinched at both ends
by different amounts so the head is blunt and the abdomen is full. */
const s=Math.sin(Math.PI*Math.pow(v,0.86));
let r=Math.pow(s,0.62)*(1-taperF*Math.pow(1-v,2.2))*(1-taperB*Math.pow(v,3.0));
/* WAIST. The single most important line on an arthropod: the pinch between
cephalothorax and abdomen is what makes two body masses out of one lump,
and without it any amount of surface detail still reads as a bean. */
if(o.waist) r*=1-o.waist*Math.exp(-Math.pow((v-(o.waistAt||0.45))/(o.waistW||0.10),2));
/* SEGMENT PLATES, not ripples. A smoothstepped sawtooth gives each segment
a flat field and a hard step down to the next, which is what casts the
shadow that makes chitin look like overlapping armour rather than a
corrugated tube. Deepest along the spine, gone under the belly. */
/* DORSAL IS +Y, WHICH IS `st`. This mask was `ct` — the +Z flank — so every
organism's segment plates, second noise octave and "dorsal keel" were
displaced onto ONE SIDE of the body instead of along the spine. Measured
on the Sovereign's body params at keel 0.40: z ran -5.94..+9.30, a
3.4-unit bulge out the right flank, while the back stayed smooth. The
relief was all still there; it was just ninety degrees from where the
silhouette needed it, which is why these animals read as beans from the
side and as a lopsided ridge from above. Six organisms, one axis. */
const dorsal=Math.max(0,st);
const seg=v*segs, f=seg-Math.floor(seg);
const plate=f*f*(3-2*f);
r+=ridge*(plate-0.5)*2*dorsal;
r+=bump*(noise(u*13,v*17)-0.5)*2*(0.30+0.70*dorsal);
r+=bump*0.55*(noise(u*29,v*37)-0.5)*2*dorsal; // finer second octave
r+=keel*Math.pow(Math.max(0,st),2.4)*Math.sin(Math.PI*v);
r=Math.max(0.04,r);
/* Flattened section with a broad flat underside: |cos| shaping on the
lower half keeps the belly from ballooning into a circle. */
const py=st*hgt*r*(st<0?0.48:1.0);
return [(v-0.5)*len, py, ct*wid*r];
},
/* THE MATERIAL IS A VALUE BREAK, NOT A HUE.
What makes reference chitin read as chitin is not its colour, it is the
range: pale bone-amber plate FIELDS against seams so dark they are almost
black, with the seam reading wet. One flat vertex colour cannot express
that, and no amount of atlas texture puts the dark in the right place
because the atlas does not know where this body's segments are. Painted
from the SAME plate function that displaced the surface, so the dark can
never drift off the crease it belongs in. */
(u,v)=>{
const th=u*Math.PI*2, st=Math.sin(th);
const seg=v*segs, f=seg-Math.floor(seg);
/* Distance from the nearest seam, 0 at the seam and 1 mid-plate. */
const d=Math.min(f,1-f)*2;
/* Bias the mask WIDE: the pale field should own most of each segment and
the dark should be confined to the crease itself, or the animal just
reads as uniformly dark and all the sculpting is lost. */
let k=Math.pow(d,0.42); k=k*k*(3-2*k);
k*=0.70+0.30*Math.max(0,st); // belly stays darker
k*=0.80+0.20*noise(u*23,v*31); // pore stipple
k=0.18+0.82*k; // never fully black
const lo=o.seam||[0.10,0.11,0.09], hi=col;
return [lo[0]+(hi[0]-lo[0])*k, lo[1]+(hi[1]-lo[1])*k, lo[2]+(hi[2]-lo[2])*k];
},false,false);
return m;
}
/* Tubercles — the bumpy relief all over the reference carapaces. Deterministic
from `seed` so a model is identical every build. */
function tubercles(m,x,y,z,rx,rz,n,r,col,seed){
let s=(seed||7)>>>0;
const rnd=()=>{ s=(s*1664525+1013904223)>>>0; return s/4294967296; };
for(let k=0;k<n;k++){
const a=rnd()*Math.PI*2, d=Math.sqrt(rnd());
m.sphere(x+Math.cos(a)*rx*d, y+r*0.5*(0.6+rnd()*0.8), z+Math.sin(a)*rz*d,
r*(0.55+rnd()*0.7),4,col,0.62,true);
}
return m;
}
/* A curved blade: mandible, raptorial claw, dorsal spike. Built as a chain so
it actually curves instead of being a straight cone. */
function chitinBlade(m,x,y,z,len,th,curve,yaw,col,rise){
/* `rise` is pitch, and it is not optional decoration: without it every blade
lies flat in the XZ plane, so a rank of dorsal spines renders as a set of
horizontal rods lying along the back like scaffolding instead of raking
upward off it. Positive rise sweeps the tip up; the curve term still bends
it sideways, so a mandible and a spine come off the same builder. */
const c=col||CLAW, cs=Math.cos(yaw||0), sn=Math.sin(yaw||0), N=3, up=rise==null?0.10:rise;
let px=x, py=y, pz=z, pr=th;
for(let k=1;k<=N;k++){
const t=k/N, sweep=curve*t*t*len;
const nx=x+(len*t)*cs-sweep*sn, nz=z+(len*t)*sn+sweep*cs;
const ny=y+len*up*t*(1.35-t*0.35);
const nr=th*(1-t*0.94);
boneSeg(m,[px,py,pz],[nx,ny,nz],pr,nr,5,c);
px=nx; py=ny; pz=nz; pr=nr;
}
return m;
}
/* Antenna / tendril: long, very thin, swept back and up. Pure BIO_LEG, so the
shader's spring bend is what actually sells it — these are the parts that
trail and settle as the creature moves. */
function tendril(m,x,y,z,len,th,side,rise,col,gaitPhase){
/* Three hinged links rather than one swept curve. A tendril's whole character
is that it LAGS — the base follows the head, the tip follows the base a
beat later — and that only happens if the links are separate rigid bodies
hinged to each other. Amplitude climbs toward the tip because a free end
travels furthest, which is the opposite of a weight-bearing leg. */
const c=col||BIO_LEG, N=4, g=gaitPhase||0;
const pt=k=>{ const t=k/N;
return [x-len*t*0.55, y+rise*Math.sin(t*1.35), z+side*len*t*0.26]; };
let parent=null; const bone=[];
for(let k=0;k<3;k++){
parent=m.joint(pt(k*2),parent,[side*0.35,0.30,0.88],g+k*0.55,0.16+k*0.10,0);
bone.push(parent);
}
for(let k=0;k<N;k++){
m.bone(bone[Math.min(2,k>>1)]);
boneSeg(m,pt(k),pt(k+1),th*(1-k/N*0.88),th*(1-(k+1)/N*0.88),3,c);
}
m.bone(-1);
return m;
}
/* The red eye cluster from the reference — small, numerous, dead ahead. */
function eyeCluster(m,x,y,z,r,rows,col){
for(let a=0;a<rows;a++) for(const sd of [-1,1]){
m.sphere(x+a*r*0.22, y+r*(0.55-a*0.85), z+sd*r*(1.05-a*0.28), r*0.42,5,col||HOT,0.85,false);
}
return m;
}
function mdlRavager(){ // 12/13 — the swarm's line animal
const m=MB();
/* Body slung between the legs, not sitting on them: the thorax rides at 3.4
while the knees reach 5.0, which is the arachnid read. Two masses, not
three spheres in a row — cephalothorax carrying the head and jaws, and a
heavier plated abdomen behind it. */
/* ONE sculpted skin from jaw to tail, not three spheres in a row. The nose
taper, the segment ridges, the tubercle field and the dorsal keel are all
displacement inside the same surface, so nothing intersects and the whole
body reads as one grown thing. */
carapace(m,-0.7,3.3,0,11.4,3.6,2.1,CHITIN,
{segs:6,ridge:0.20,bump:0.115,keel:0.30,nose:0.60,tail:0.18,
waist:0.34,waistAt:0.52,waistW:0.075,seed:1201,u:13,v:16});
/* Head capsule sits proud of the front of that skin rather than inside it. */
carapace(m,4.0,3.0,0,3.4,2.1,1.6,CHIT_D,
{segs:3,ridge:0.16,bump:0.14,keel:0.22,nose:0.40,tail:0.44,seed:83,u:10,v:7});
eyeCluster(m,5.0,3.3,0,0.66,2,HOT);
/* Six legs, fanned. The stagger in reach/knee stops them marching in
parallel, which is what made the old boxes read as a table. */
for(let k=0;k<3;k++) for(const sd of [-1,1])
broodLeg(m, 1.9-k*2.2, 3.0, 0, sd, 3.4+k*0.20, 3.3, 2.30-k*0.16, 0.52, null,
((k+(sd<0?1:0))&1)*Math.PI, [2.35,1.85,2.40][k]);
/* Raptorial jaws — curved, opposed, and the widest thing on the animal. */
for(const sd of [-1,1]){
chitinBlade(m,4.2,2.7,sd*1.0,3.2,0.42,-sd*0.42,sd*0.30,CLAW);
chitinBlade(m,4.0,3.3,sd*0.5,2.1,0.26,-sd*0.30,sd*0.14,CLAW);
tendril(m,4.4,4.3,sd*0.8,4.2,0.20,sd,2.1,BIO_LEG); // sensory tendrils
}
/* Dorsal spines raked back along the abdomen, tallest over the shoulders. */
for(let k=0;k<3;k++)
chitinBlade(m,-0.4-k*1.7,4.4-k*0.18,0,2.3-k*0.28,0.36,0.16,Math.PI*0.94,CLAW,0.78-k*0.08);
/* Paired shoulder horns, the widest point of the carapace in plan — this is
the part that actually reads from the game's overhead camera. */
for(const sd of [-1,1]) chitinBlade(m,0.8,4.2,sd*1.7,3.0,0.34,sd*0.34,Math.PI*0.82,CLAW,0.52);
tubercles(m,1.2,3.9,0,1.6,2.4,4,0.56,CHIT_D,4404);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlAlphaRavager(){ // 13 — boss silhouette, never a scaled drone
const m=MB();
/* Same anatomy as the drone, half again the mass and carried higher: the
Alpha has to read as the SAME ANIMAL grown large, not as a different one,
so it uses the same sculpt and the same seven-jointed limbs with the
proportions pushed — deeper plates, a heavier abdomen, a taller stance. */
carapace(m,-0.9,4.4,0,17.0,5.2,3.1,CHITIN,
{segs:7,ridge:0.24,bump:0.125,keel:0.38,nose:0.58,tail:0.16,
waist:0.36,waistAt:0.54,waistW:0.070,seed:5501,u:15,v:18});
carapace(m,6.0,4.0,0,5.2,3.0,2.3,CHIT_D,
{segs:3,ridge:0.18,bump:0.15,keel:0.26,nose:0.38,tail:0.42,seed:311,u:11,v:8});
eyeCluster(m,7.6,4.5,0,1.05,3,HOT);
m.sphere(5.0,6.2,0,1.0,7,HOT,1,false); // exposed brood core
for(let k=0;k<4;k++) for(const sd of [-1,1])
broodLeg(m, 3.4-k*2.7, 4.0, 0, sd, 4.6+k*0.22, 4.4, 3.10-k*0.18, 0.78, null,
((k+(sd<0?1:0))&1)*Math.PI, [3.30,2.60,3.35,3.05][k]);
for(const sd of [-1,1]){
chitinBlade(m,6.8,3.9,sd*1.7,5.6,0.72,-sd*0.44,sd*0.28,CLAW,0.10); // oversized jaws
chitinBlade(m,6.6,4.7,sd*0.9,3.8,0.44,-sd*0.32,sd*0.13,CLAW,0.14);
tendril(m,7.0,5.9,sd*1.2,7.0,0.30,sd,3.2,BIO_LEG);
m.sphere(-1.2,7.0,sd*3.0,2.0,8,BIO_TEAM,0.9,false); // brood sacs
chitinBlade(m,1.2,5.6,sd*2.4,5.0,0.52,sd*0.30,Math.PI*0.84,CLAW,0.56);
}
/* Crown of spines, tallest over the shoulders — the boss read at 40px. */
for(let k=0;k<7;k++)
chitinBlade(m,-5.0+k*1.55,6.2,0,3.2+Math.sin(k/6*Math.PI)*2.6,0.42,0.12,Math.PI*0.96,CLAW,0.95);
tubercles(m,-4.0,5.6,0,3.4,2.8,9,0.92,CHIT_D,7);
return {hull:m.build(),tur:null,s:1.18};
}
function mdlCorvette(){ // 14 — naval light
const m=MB();
const waterline=[[-7.75,-2.18],[-6.45,-3.02],[3.65,-3.42],[7.25,-2.05],[8.25,-.72],
[8.25,.72],[7.25,2.05],[3.65,3.42],[-6.45,3.02],[-7.75,2.18]];
const deck=[[-7.05,-2.02],[-5.85,-2.62],[3.55,-2.92],[6.85,-1.72],[7.55,-.58],
[7.55,.58],[6.85,1.72],[3.55,2.92],[-5.85,2.62],[-7.05,2.02]];
m.extrude(0,0,0,waterline,2.28,MET_D); // flared displacement hull
m.extrude(0,2.28,0,deck,1.34,MET); // armoured weather deck
m.extrude(.20,3.62,0,deck.map(p=>[p[0]*.94,p[1]*.88]),.30,TEAM_A);
for(const sd of [-1,1]){
m.box(-.25,1.18,sd*3.04,10.8,1.02,.52,DARK); // continuous waterline belt
m.box(.60,2.54,sd*2.78,8.55,.42,.62,TEAM_A); // readable fleet stripe
for(let k=0;k<5;k++)
m.box(-4.10+k*2.18,1.56,sd*3.33,1.15,.36,.28,MET_L); // rub rail breaks
m.box(-5.20,1.02,sd*1.52,4.15,1.45,1.02,DARKER); // propulsion tunnel attached aft
tubeX(m,-8.08,1.12,sd*1.52,1.02,.72,.34,10,TWR_BORE); // hollow waterjet outlet
ringX(m,-8.12,1.12,sd*1.52,.68,.96,12,MET_L);
ringX(m,-8.15,1.12,sd*1.52,.25,.37,10,HOT);
m.bevelBox(1.75,3.92,sd*2.18,3.55,.68,1.15,.22,MET_D); // deck-edge equipment lockers
m.box(1.75,4.58,sd*2.18,2.75,.22,.86,TEAM_T);
}
m.bevelBox(-1.25,3.84,0,5.65,2.12,4.35,.52,MET_L); // integrated superstructure
m.extrude(-1.05,5.96,0,[[-2.15,-1.72],[1.45,-1.82],[2.42,-.86],[2.42,.86],
[1.45,1.82],[-2.15,1.72]],1.48,DARK);
m.bevelBox(-.62,7.44,0,3.25,1.08,2.62,.34,TEAM_A); // bridge roof / livery
for(const sd of [-1,1]){
m.box(1.30,6.43,sd*.91,.42,.72,1.35,GLASS); // wraparound bridge glazing
m.box(-1.08,6.84,sd*1.78,2.65,.38,.18,GLASS);
}
m.box(1.48,6.43,0,.32,.72,.62,GLASS);
ventBank(m,-3.42,4.06,0,2.25,3.35,5,MET_D,0);
kitBox(m,3.95,3.95,0,2.35,.94,2.45,MET_D,0);
for(const sd of [-1,1]) for(let k=0;k<3;k++)
m.tube(-4.65+k*.82,3.94,sd*.80,.30,.15,.48,8,DARKER); // six recessed VLS cells
sensorMast(m,-1.62,8.48,0,2.42,MET_L);
m.cyl(-1.62,9.72,0,1.72,.30,.30,12,MET_L); // search radar dish
m.box(-1.62,10.02,0,.24,.84,2.65,DARKER);
glowStrip(m,-.35,8.53,0,2.55,ENERGY,0);
m.greeble(.45,3.94,0,2.85,3.35,.34,6,MET_D,0,141);
const t=MB();
t.cyl(0,0,0,2.12,1.78,.78,12,DARKER);
turretBody(t,4.05,3.55,1.78,MET,TEAM_A);
t.bevelBox(-1.15,1.78,0,1.55,.68,2.60,.22,TEAM_T); // compact bustle marking
t.box(1.62,.62,0,1.18,1.12,1.65,DARK); // mantlet overlap
gunX(t,1.92,1.12,5.35,.40,MET_D);
t.box(-1.52,1.82,0,.82,.72,1.18,MET_L);
t.tube(-1.52,2.52,0,.31,.15,.44,8,LAMP);
t.greeble(-.38,1.83,0,1.65,2.25,.28,4,MET_D,0,151);
return {hull:m.build(),tur:t.build(),s:1.0,turH:3.8,naval:1};
}
function mdlDread(){ // 15 — naval heavy
const m=MB();
const waterline=[[-13.35,-4.15],[-11.55,-5.05],[6.55,-5.65],[11.75,-3.55],[13.35,-1.38],
[13.35,1.38],[11.75,3.55],[6.55,5.65],[-11.55,5.05],[-13.35,4.15]];
const deck=[[-12.55,-3.72],[-10.75,-4.52],[6.15,-5.05],[10.85,-3.08],[12.15,-1.12],
[12.15,1.12],[10.85,3.08],[6.15,5.05],[-10.75,4.52],[-12.55,3.72]];
m.extrude(0,0,0,waterline,3.58,MET_D); // deep dreadnought hull
m.extrude(0,3.58,0,deck,1.82,MET);
m.extrude(.25,5.40,0,deck.map(p=>[p[0]*.95,p[1]*.90]),.38,TEAM_A);
for(const sd of [-1,1]){
m.box(-.55,1.52,sd*5.05,19.2,1.18,.66,DARK); // full-length armour belt
m.box(.25,3.52,sd*4.72,16.4,.58,.82,TEAM_A); // broad faction stripe
for(let k=0;k<5;k++)
m.bevelBox(-7.85+k*3.60,2.18,sd*5.43,1.58,.62,.34,.10,MET_L);
const ez=2.48;
m.box(-9.65,1.40,sd*ez,5.25,2.05,1.42,DARKER); // one large turbine tunnel per side
tubeX(m,-13.72,1.55,sd*ez,1.28,.98,.46,10,TWR_BORE);
ringX(m,-13.76,1.55,sd*ez,.88,1.22,10,MET_L);
m.bevelBox(2.65,5.80,sd*3.75,5.25,.82,1.48,.26,MET_D); // deck-edge secondary battery deck
m.box(2.65,6.60,sd*3.75,4.15,.26,1.02,TEAM_T);
m.cyl(2.65,6.86,sd*3.75,.72,.58,.72,9,DARKER);
gunX(m,3.02,7.18,2.75,.22,MET_D,sd*3.75); // one readable secondary per beam
}
m.bevelBox(-2.45,5.78,0,9.15,3.12,7.85,.74,MET_L); // broad armoured island
m.extrude(-2.20,8.90,0,[[-3.75,-3.25],[2.55,-3.45],[4.05,-1.70],[4.05,1.70],
[2.55,3.45],[-3.75,3.25]],2.55,DARK);
m.extrude(-1.75,11.45,0,[[-2.85,-2.38],[1.85,-2.55],[3.05,-1.18],[3.05,1.18],
[1.85,2.55],[-2.85,2.38]],1.35,TEAM_A);
for(const sd of [-1,1]){
m.box(.72,9.64,sd*2.33,.48,1.02,1.55,GLASS); // bridge windows
m.box(-2.30,10.15,sd*3.32,3.35,.42,.22,GLASS);
exhaust(m,-5.70,8.92,sd*1.62,1,.48,2.20,MET_D,.96);
m.bevelBox(-7.45,5.82,sd*2.15,2.45,.78,1.58,.24,MET_D);
}
m.box(.95,9.64,0,.40,1.02,.88,GLASS);
ventBank(m,-6.45,5.82,0,3.15,5.45,7,MET_D,0);
for(let xk=0;xk<3;xk++) for(const sd of [-1,1])
m.tube(-8.75+xk*1.15,5.80,sd*1.18,.38,.19,.62,8,DARKER); // six armoured VLS cells
m.bevelBox(7.05,5.80,0,3.65,.76,5.30,.25,DARKER); // fore sensor / magazine block
m.greeble(7.05,6.58,0,2.60,4.20,.46,3,MET_D,0,161);
sensorMast(m,-2.65,12.80,0,3.20,MET_L);
m.cyl(-2.65,14.58,0,2.45,.42,.34,14,MET_L); // main air-search dish
m.box(-2.65,14.90,0,.32,1.22,3.82,DARKER);
sensorMast(m,-5.25,11.05,2.18,2.25,MET_D);
glowStrip(m,-.85,12.84,0,4.25,ENERGY,0);
m.greeble(-.30,5.80,0,5.25,5.25,.42,3,MET_D,0,171);
const t=MB();
t.cyl(0,0,0,3.65,3.05,1.12,14,DARKER);
turretBody(t,7.35,6.20,2.72,MET,TEAM_A);
t.bevelBox(-2.55,2.05,0,2.65,1.18,5.10,.34,TEAM_T); // turret bustle ownership panel
t.box(2.85,.78,0,2.20,1.72,3.80,DARK); // shared mantlet joins both barrels
for(const sd of [-1,1]){
gunX(t,3.42,1.62,9.45,.56,MET_D,sd*1.12); // true twin, not coincident geometry
t.box(-1.55,2.75,sd*2.35,2.75,.32,.54,MET_L);
}
t.bevelBox(-2.75,2.78,0,1.35,.92,2.35,.25,MET_L);
t.tube(-2.75,3.68,0,.38,.18,.56,8,LAMP);
t.greeble(-.25,2.74,0,3.65,3.72,.36,3,MET_D,0,181);
return {hull:m.build(),tur:t.build(),s:1.0,turH:5.6,naval:1};
}
function mdlBombard(){ // 16 — siege artillery
const m=MB();
runningGear(m,15.4,4.0,3.15,4.25,6,MET_D);
hullShell(m,14.8,9.2,2.35,1.45,MET,TEAM_A,0.34,0.74);
m.wedge(5.1,3.82,0,4.0,1.35,7.4,MET_L);
for(const sd of [-1,1]){
armorPlate(m,0.0,2.35,sd*4.9,11.0,2.0,0.48,0,MET_D,0);
m.box(-6.2,1.25,sd*5.35,3.2,2.5,1.15,MET_D);
m.wedge(-7.1,0,sd*5.35,3.2,1.0,3.0,TWR_PAD);
hydraulic(m,-5.2,2.7,sd*4.4,2.8,0.31,TWR_MACH,0);
kitBox(m,-2.2,3.84,sd*3.5,2.7,1.05,1.7,MET_D,0);
m.box(0.5,3.86,sd*4.15,7.5,0.50,0.70,TEAM_A);
}
ventBank(m,-5.0,3.84,0,3.8,4.8,5,MET_D,0);
exhaust(m,-7.0,3.0,0,3,0.43,2.2,MET_D,2.0);
sensorMast(m,-6.1,4.0,2.0,2.7,MET_L);
const t=MB();
turretBody(t,7.3,6.8,3.25,TWR_ARM,TWR_TEAM);
t.bevelBox(-3.0,0.9,0,2.6,2.8,5.4,0.45,TWR_COAT);
for(const sd of [-1,1]){
t.cyl(0.4,0.65,sd*2.75,1.32,1.15,1.0,10,TWR_MACH);
hydraulic(t,-1.5,0.2,sd*2.5,2.8,0.34,TWR_MACH,0);
t.box(-0.3,2.7,sd*3.35,5.2,0.52,0.42,TEAM_A);
cylX(t,0.6,2.35,sd*0.94,6.2,0.34,0.30,8,TWR_MACH,false);
}
gunX(t,1.25,1.75,15.4,0.78,TWR_MACH);
cylX(t,1.2,1.75,0,3.2,1.42,1.08,12,TWR_COAT,false);
for(const x of [7.4,12.2]) ringX(t,x,1.75,0,0.82,1.08,12,TWR_TRIM);
t.box(14.7,0.72,0,1.4,2.2,3.4,TWR_MACH);
tubeX(t,16.05,1.75,0,1.1,0.98,0.49,12,TWR_BORE);
kitBox(t,-3.2,3.35,0,2.0,1.25,4.4,MET_D,0);
return {hull:m.build(),tur:t.build(),s:1.0,turH:5.95};
}
function mdlRaptor(){ // 17 — gunship
const m=MB();
const keel=[[-6.35,-1.92],[-3.25,-2.55],[2.75,-2.72],[6.15,-1.18],[7.05,0],
[6.15,1.18],[2.75,2.72],[-3.25,2.55],[-6.35,1.92]];
const deck=[[-5.75,-1.68],[-2.8,-2.12],[2.65,-2.22],[5.55,-.92],[6.15,0],
[5.55,.92],[2.65,2.22],[-2.8,2.12],[-5.75,1.68]];
m.extrude(0,0,0,keel,1.55,MET_D);
m.extrude(0,1.55,0,deck,1.62,MET); // deep troop-carrying fuselage
m.extrude(-.25,3.17,0,deck.map(p=>[p[0]*.76,p[1]*.70]),.70,TEAM_A);
m.bevelBox(3.25,3.36,0,3.95,1.36,2.85,.46,GLASS); // armoured cockpit
m.wedge(5.30,3.80,0,1.85,.72,2.50,MET_L,0,true);
m.box(-1.20,3.90,0,4.35,.42,1.05,TEAM_T);
glowStrip(m,-.65,4.31,0,2.65,ENERGY,0);
ventBank(m,-3.55,3.86,0,2.45,1.48,4,DARK,0);
for(const sd of [-1,1]){
const wing=sd>0
? [[-3.95,1.55],[2.65,1.80],[1.25,6.05],[-4.70,5.30]]
: [[-3.95,-1.55],[-4.70,-5.30],[1.25,-6.05],[2.65,-1.80]];
m.extrude(0,1.20,0,wing,.78,MET_D); // thick gunship wing
m.box(-1.45,1.36,sd*3.45,5.35,1.12,2.55,DARK); // load-bearing engine pylon
cylX(m,-5.15,1.62,sd*3.78,5.95,1.42,1.15,11,DARK,false); // armoured turbine nacelle
ringX(m,-5.18,1.62,sd*3.78,1.03,1.48,12,MET_L);
tubeX(m,-5.67,1.62,sd*3.78,1.02,1.12,.55,12,TWR_BORE); // shadowed exhaust throat
ringX(m,-5.71,1.62,sd*3.78,.37,.58,12,HOT);
ringX(m,.35,1.62,sd*3.78,.88,1.20,12,MET_L);
m.bevelBox(-1.55,2.56,sd*3.78,2.75,.82,1.75,.28,TEAM_A); // nacelle livery fairing
ventBank(m,-1.55,3.39,sd*3.78,1.95,1.18,3,MET_D,0);
m.bevelBox(-.65,.55,sd*5.28,3.40,1.00,1.42,.28,DARKER); // attached stores pylon
for(let k=0;k<3;k++)
tubeX(m,-1.72+k*1.05,.73,sd*5.28,.85,.30,.14,7,HOT);
m.box(1.95,.48,sd*1.48,2.55,1.02,.95,DARK); // chin gun receiver
gunX(m,2.72,.82,3.55,.31,MET_D,sd*1.48);
m.box(-4.95,2.55,sd*2.25,2.25,2.80,.42,MET_L); // twin vertical tails
m.box(-4.55,4.75,sd*2.25,1.45,.38,.48,TEAM_A); // tail-tip livery
}
m.bevelBox(-5.08,1.82,0,2.75,1.62,3.80,.42,DARKER); // tail gearbox joins fins
m.box(-6.42,2.00,0,.60,1.20,2.75,MET_L); // rear cargo ramp edge
sensorMast(m,-4.20,3.38,0,1.52,MET_L);
m.greeble(-.90,3.88,0,3.05,1.10,.40,5,MET_D,0,117);
return {hull:m.build(),tur:null,s:1.0,air:1};
}
function mdlScorcher(){ // 18 — heavy flamer
const m=MB();
runningGear(m,15.2,4.0,3.25,4.25,6,MET_D);
hullShell(m,14.6,9.0,2.45,1.55,MET,TEAM_A,0.32,0.76);
m.wedge(5.0,4.02,0,4.2,1.45,7.4,MET_L);
for(const sd of [-1,1]){
armorPlate(m,0.2,2.45,sd*4.85,11.0,2.1,0.5,0,MET_D,0);
m.cyl(-4.2,4.05,sd*2.65,2.0,2.0,4.8,11,RUST);
m.ring(-4.2,5.1,sd*2.65,1.98,2.24,12,TWR_MACH);
m.ring(-4.2,7.5,sd*2.65,1.98,2.24,12,TWR_MACH);
m.box(0.5,4.04,sd*4.15,7.8,0.52,0.72,TEAM_A);
m.tube(-2.2,6.0,sd*4.2,0.38,0.20,1.5,8,TWR_BORE);
}
m.box(-4.2,8.92,0,5.4,0.65,6.2,DARKER);
ventBank(m,-6.1,4.05,0,3.0,4.8,5,MET_D,0);
exhaust(m,-7.0,3.1,0,3,0.45,2.4,MET_D,2.1);
const t=MB();
turretBody(t,6.2,5.8,3.0,TWR_ARM,TWR_TEAM);
t.bevelBox(-2.1,0.8,0,2.4,2.2,4.7,0.42,TWR_COAT);
for(const sd of [-1,1]){
t.box(-0.5,1.25,sd*2.9,4.2,1.0,0.42,TEAM_A);
hydraulic(t,-1.2,0.2,sd*2.2,2.2,0.30,TWR_MACH,0);
cylX(t,1.55,1.35,sd*1.05,5.4,0.72,0.62,10,TWR_MACH,false);
for(const x of [2.3,4.5]) ringX(t,x,1.35,sd*1.05,0.72,0.92,10,TWR_TRIM);
cylX(t,6.55,1.35,sd*1.05,1.1,0.92,1.28,10,TWR_COAT,false);
tubeX(t,7.25,1.35,sd*1.05,0.95,1.30,0.69,12,TWR_BORE);
t.sphere(8.05,1.35,sd*1.05,0.34,7,HOT,1,false);
}
t.box(-2.3,3.02,0,1.8,1.4,3.8,MET_D);
return {hull:m.build(),tur:t.build(),s:1.0,turH:6.1};
}
/* ---------------------------------------------------------------------------
FACTION CHASSIS KITS
Recolouring an identical model is not a faction. These are alternate builds
of the same three roles, so an enemy army is recognisable by SHAPE before
you read its colour:
legion — tracked, slab-armoured, blunt: the standard line, up-armoured
syndicate — hover skirts instead of treads, raked prow, exposed coils
horde — grown carapace, no plating, claws and spines
--------------------------------------------------------------------------- */
/* Nova's line tank rides on four clean electromagnetic runner pods. The
separated drive blocks, arrow hull and capacitor-fed accelerator preserve a
conventional battle-tank role without borrowing the old shared tread box. */
function mdlNovaRhino(){
const m=MB();
for(const sd of [-1,1]){
m.bevelBox(-.5,0,sd*4.0,12.2,1.7,2.15,.40,TWR_COAT);
m.bevelBox(-.3,1.7,sd*4.0,10.8,1.15,1.75,.30,TWR_ARM_D);
for(const x of [-4.0,0,4.0]){
m.cyl(x,.18,sd*4.0,.78,.68,1.05,10,TWR_MACH);
m.ring(x,1.25,sd*4.0,.55,.84,12,TWR_GLOW);
}
m.box(-.2,2.88,sd*4.9,6.8,.20,.34,sd>0?TEAM_A:TWR_GLOW);
}
m.extrude(0,2.1,0,[[-6.2,-3.15],[2.2,-3.45],[6.4,-1.55],[6.9,0],
[6.4,1.55],[2.2,3.45],[-6.2,3.15]],2.15,TWR_ARM_D);
m.extrude(-.2,4.25,0,[[-5.2,-2.45],[2.0,-2.75],[5.6,-1.15],[6.1,0],
[5.6,1.15],[2.0,2.75],[-5.2,2.45]],1.25,TWR_ARM);
m.wedge(4.2,4.6,0,3.7,1.35,5.2,MET_L,0,true);
m.bevelBox(-3.8,4.5,0,3.1,1.2,4.7,.30,TWR_COAT);
for(const sd of [-1,1]){
m.box(-1.0,5.52,sd*2.48,6.8,.22,.55,TEAM_A);
m.cyl(-4.5,5.0,sd*1.7,.62,.50,2.0,9,TWR_MACH);
m.ring(-4.5,6.72,sd*1.7,.56,.80,10,TWR_GLOW);
}
glowStrip(m,.6,5.58,0,7.4,ENERGY,0);
const t=MB();
t.cyl(-.8,0,0,3.6,3.1,1.2,14,TWR_PAD);
t.extrude(0,1.2,0,[[-3.8,-2.65],[1.7,-3.0],[3.9,-1.25],[4.3,0],
[3.9,1.25],[1.7,3.0],[-3.8,2.65]],2.2,TWR_ARM);
t.extrude(-.2,3.4,0,[[-3.0,-2.05],[1.4,-2.3],[3.2,-.9],[3.45,0],
[3.2,.9],[1.4,2.3],[-3.0,2.05]],.62,TEAM_T);
cylX(t,2.0,2.05,0,7.6,.66,.54,12,TWR_MACH,false);
for(const x of [3.0,5.2,7.4]) ringX(t,x,2.05,0,.72,1.00,12,x===7.4?TWR_GLOW:TWR_TRIM);
tubeX(t,9.45,2.05,0,.85,.76,.37,12,TWR_BORE);
for(const sd of [-1,1]){
t.bevelBox(-1.5,2.0,sd*2.65,3.6,1.4,.52,.20,TWR_ARM_D);
t.box(-1.5,3.34,sd*2.93,2.0,.18,.42,sd>0?TEAM_A:TWR_GLOW);
}
t.ring(-1.7,4.25,0,1.35,2.05,16,TWR_GLOW);
t.sphere(-1.7,4.25,0,.82,8,ENERGY,1,false);
return {hull:m.build(),tur:t.build(),s:1.0,turH:5.45,muzzle:9.8};
}
/* Ascendancy doctrine makes the same battlefield role a broad assault block:
full tracks, layered slabs, visible exhaust heat and a long recoil cannon. */
function mdlLegionRhino(){
const m=MB();
treads(m,14.0,3.25,3.15,4.25,6);
m.extrude(-.3,2.1,0,[[-7.0,-4.2],[3.3,-4.2],[6.8,-2.8],[7.2,-1.2],
[7.2,1.2],[6.8,2.8],[3.3,4.2],[-7.0,4.2]],2.7,TWR_ARM_D);
m.bevelBox(-1.0,4.8,0,11.8,2.0,6.9,.65,TWR_ARM);
m.wedge(4.6,4.85,0,4.3,1.8,7.0,TWR_ARM,0,true);
for(const sd of [-1,1]){
armorPlate(m,-.1,2.4,sd*4.55,11.8,2.1,.54,0,TWR_COAT,0);
m.box(-1.8,6.78,sd*3.1,5.8,.24,.68,TEAM_A);
m.cyl(-5.2,4.85,sd*2.55,.82,.72,2.7,10,TWR_MACH);
m.ring(-5.2,6.9,sd*2.55,.78,1.04,12,HOT);
}
ventBank(m,-4.8,6.82,0,3.6,4.8,5,TWR_MACH,0);
m.bevelBox(-6.0,3.2,0,2.0,2.8,5.4,.42,DARKER);
m.box(3.8,6.85,0,3.8,.24,2.2,TWR_TRIM);
const t=MB();
t.cyl(-1.0,0,0,4.1,3.65,1.25,14,TWR_PAD);
t.bevelBox(-.8,1.25,0,8.6,3.2,6.9,.80,TWR_ARM_D);
t.bevelBox(-.4,4.45,0,7.5,1.0,5.8,.34,TWR_ARM);
for(const sd of [-1,1]){
t.bevelBox(-1.8,1.8,sd*3.2,4.2,2.1,.58,.24,TWR_COAT);
t.box(-1.8,3.82,sd*3.51,2.5,.20,.54,sd>0?TEAM_A:HOT);
hydraulic(t,-2.2,.55,sd*2.55,2.4,.28,TWR_MACH,0);
}
gunX(t,2.7,2.3,9.2,.72,TWR_MACH);
cylX(t,2.0,2.3,0,3.3,1.28,1.02,12,TWR_COAT,false);
for(const x of [5.5,8.7]) ringX(t,x,2.3,0,.82,1.12,12,x>8?HOT:TWR_TRIM);
t.box(10.5,1.35,0,1.4,2.2,3.6,TWR_MACH);
tubeX(t,11.8,2.3,0,.92,.84,.42,12,TWR_BORE);
t.bevelBox(-4.1,1.4,0,2.5,2.6,5.0,.42,DARKER);
return {hull:m.build(),tur:t.build(),s:1.0,turH:6.0,muzzle:12.2};
}
/* A role-specific Syndicate skimmer. Three exposed lift banks and a forked
coil weapon replace both tracks and a conventional gun tube. */
function mdlSyndicateRhino(){
const m=MB();
m.extrude(0,0,0,[[-6.8,-3.5],[-2.4,-4.5],[3.1,-4.2],[6.8,-1.7],[7.2,0],
[6.8,1.7],[3.1,4.2],[-2.4,4.5],[-6.8,3.5]],1.35,TWR_COAT);
for(const sd of [-1,1]) for(const x of [-4.4,0,4.4]){
m.cyl(x,.15,sd*3.55,.75,.62,1.0,10,TWR_MACH);
m.ring(x,1.18,sd*3.55,.54,.88,12,ENERGY);
m.sphere(x,1.20,sd*3.55,.42,7,TWR_GLOW,.72,false);
}
m.extrude(-.2,1.4,0,[[-6.1,-3.05],[-1.8,-3.7],[3.0,-3.25],[6.4,-1.25],[6.9,0],
[6.4,1.25],[3.0,3.25],[-1.8,3.7],[-6.1,3.05]],2.05,TWR_ARM_D);
m.extrude(.2,3.45,0,[[-4.9,-2.35],[-1.0,-2.85],[3.2,-2.45],[5.7,-.9],[6.1,0],
[5.7,.9],[3.2,2.45],[-1.0,2.85],[-4.9,2.35]],1.4,TWR_ARM);
for(const sd of [-1,1]){
m.wedge(3.9,3.55,sd*3.3,4.2,1.2,1.5,TWR_ARM,sd*.10,sd<0);
m.box(-.4,4.88,sd*2.78,6.8,.20,.52,TEAM_A);
m.bevelBox(-4.7,3.0,sd*2.1,2.4,1.4,1.5,.25,DARKER);
}
m.ring(-3.7,5.45,0,1.4,2.3,16,ENERGY);
m.box(-3.7,5.38,0,4.8,.20,.58,TEAM_T);
const t=MB();
t.cyl(-.8,0,0,3.6,3.15,1.0,14,TWR_PAD);
t.extrude(0,1.0,0,[[-3.9,-2.5],[1.6,-3.0],[4.0,-1.0],[4.4,0],
[4.0,1.0],[1.6,3.0],[-3.9,2.5]],2.1,TWR_ARM_D);
t.extrude(.1,3.1,0,[[-3.0,-1.9],[1.3,-2.3],[3.2,-.75],[3.5,0],
[3.2,.75],[1.3,2.3],[-3.0,1.9]],.65,TEAM_T);
for(const sd of [-1,1]){
const z=sd*1.35;
cylX(t,2.0,2.05,z,5.6,.64,.50,11,TWR_MACH,false);
for(const x of [2.8,4.4,6.0]) ringX(t,x,2.05,z,.70,.98,12,sd>0?TWR_GLOW:ENERGY);
tubeX(t,7.45,2.05,z,.85,.72,.35,12,TWR_BORE);
t.bevelBox(-.5,1.5,sd*2.6,4.5,1.6,.55,.22,TWR_COAT);
}
t.sphere(-2.3,3.8,0,1.5,9,TWR_GLOW,1,false);
ringX(t,-2.3,3.8,0,1.75,2.35,16,TWR_TRIM);
return {hull:m.build(),tur:t.build(),s:.98,turH:5.05,muzzle:7.8,muzzleZ:1.35};
}
function mdlSynTank(){
const m=MB();
// hover skirt: a continuous chamfered plenum where treads would be
m.extrude(0,0,0,[[-6.6,-3.8],[3.0,-4.2],[6.6,-2.0],[6.6,2.0],[3.0,4.2],[-6.6,3.8]],1.5,DARKER);
m.extrude(0,1.5,0,[[-6.2,-3.4],[2.8,-3.8],[6.2,-1.8],[6.2,1.8],[2.8,3.8],[-6.2,3.4]],1.3,MET_D);
for(const sd of [-1,1]) for(let k=0;k<4;k++) // lift coils
m.cyl(-4.2+k*3.0,0.3,sd*3.4,0.7,0.62,1.0,8,ENERGY);
m.extrude(0,2.8,0,[[-5.4,-2.8],[2.4,-3.2],[6.0,-1.2],[6.0,1.2],[2.4,3.2],[-5.4,2.8]],2.0,MET);
m.wedge(4.0,4.8,0,3.6,1.4,5.6,TEAM_A); // raked prow
m.extrude(0,4.8,0,[[-4.4,-2.2],[2.0,-2.6],[4.6,-1.0],[4.6,1.0],[2.0,2.6],[-4.4,2.2]],0.6,TEAM_T);
glowStrip(m,0,5.45,0,7.0,ENERGY,0);
for(const sd of [-1,1]){
m.cyl(-5.2,3.4,sd*2.2,0.55,0.45,2.6,8,MET_D); // vent stacks
m.tube(-5.2,6.0,sd*2.2,0.52,0.30,0.4,8,ENERGY);
}
m.bevelBox(-4.0,4.8,0,2.6,1.2,4.4,0.3,DARK);
sensorMast(m,-4.0,6.0,1.4,2.2,MET_L);
const t=MB();
t.extrude(0,0,0,[[-3.0,-2.4],[1.8,-2.8],[3.6,-1.2],[3.6,1.2],[1.8,2.8],[-3.0,2.4]],2.2,MET);
t.extrude(0,2.2,0,[[-2.4,-1.9],[1.4,-2.2],[3.0,-0.9],[3.0,0.9],[1.4,2.2],[-2.4,1.9]],0.7,TEAM_T);
// twin coil emitters instead of a barrel: energy weapon, visibly
for(const sd of [-1,1]){
t.cyl(2.0,1.1,sd*1.1,0.55,0.42,4.4,8,MET_D);
t.cyl(6.2,1.1,sd*1.1,0.62,0.30,0.9,8,ENERGY);
for(let k=0;k<3;k++) t.cyl(2.6+k*1.2,1.1,sd*1.1,0.78,0.78,0.30,8,ENERGY);
}
t.cyl(-2.4,2.9,0,1.0,0.5,1.4,9,ENERGY);
return {hull:m.build(),tur:t.build(),s:1.0,turH:5.0,muzzle:7.1,muzzleZ:1.1};
}
function bioLegBank(m,n,span,reach,y){
for(let k=0;k<n;k++) for(const sd of [-1,1]){
const x=span*(.5-k/Math.max(1,n-1)), z=sd*(reach*.58+k%2*.22);
m.sphere(x,y+.2,z,.72,7,CHIT_D,.72,false);
m.box(x,y,sd*reach,3.2,.58,.72,BIO_LEG,sd*(.16+k*.035));
m.box(x-.25,.18,sd*(reach+1.45),2.2,.48,.62,BIO_LEG,sd*(.34+k*.025));
m.sphere(x+.65,.12,sd*(reach+2.25),.44,6,CLAW,.72,false);
}
}
function bioCrown(m,x,y,count,spread,height){
for(let k=0;k<count;k++){
const q=count<2?0:k/(count-1)-.5;
m.cyl(x+q*spread,y,0,.48-Math.abs(q)*.14,.04,height*(1-Math.abs(q)*.25),6,BIO_LEG);
}
}
function mdlHordeBeast(){
const m=MB();
bioLegBank(m,4,6.6,4.8,1.55);
m.sphere(0,2.2,0,4.6,10,CHITIN,.80,false);
m.sphere(-4.4,2.6,0,3.8,9,CHIT_D,.74,false);
m.sphere(4.0,2.0,0,3.0,9,CHIT_D,.88,false);
for(const sd of [-1,1]){
m.box(6.4,2.0,sd*1.2,3.4,.66,.66,BIO_LEG,sd*.12);
m.box(8.0,1.8,sd*.7,2.0,.5,.5,CLAW,sd*.18);
}
bioCrown(m,-.2,5.5,5,6.4,3.2);
m.sphere(0,5.0,0,1.4,7,BIO_TEAM,1,false);
m.sphere(-4.4,5.0,0,1.1,7,BIO_TEAM,1,false);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlHordeSpitter(){
const m=MB(); bioLegBank(m,3,5.7,4.5,1.35);
m.sphere(-1.5,2.0,0,4.2,10,CHITIN,.76,false);
m.sphere(-4.8,2.5,0,3.8,9,CHIT_D,.92,false);
for(const sd of [-1,1]){
m.sphere(-3.8,4.8,sd*1.9,1.25,7,BIO_TEAM,.92,false);
m.box(2.6,1.2,sd*2.7,4.2,.58,.72,BIO_LEG,sd*.16);
}
bioCrown(m,-3.8,5.7,4,4.8,2.5);
const t=MB();
t.sphere(-1.0,.1,0,3.0,9,CHITIN,.72,false);
t.sphere(.8,.5,0,2.1,8,CHIT_D,.82,false);
/* A telescoping throat, not a barrel. Overlapping unequal lobes break every
straight manufactured edge while retaining a readable forward weapon. */
for(let k=0;k<5;k++){
const r=1.34-k*.17;
t.sphere(2.0+k*1.05,.48+Math.sin(k*.9)*.15,0,r,7,k&1?CHITIN:CHIT_D,.72,false);
}
t.sphere(6.6,.46,0,.82,7,LAMP,.62,false); // wet acid lumen
for(const sd of [-1,1]){
for(let k=0;k<3;k++) t.sphere(1.5+k*1.4,.1,sd*(1.18-k*.20),.48-k*.07,6,BIO_LEG,.68,false);
t.box(5.7,.12,sd*.76,2.0,.30,.30,CLAW,sd*.18); // soft mouth hook
t.sphere(-.4,2.0,sd*1.45,.72,6,BIO_TEAM,1,false);
}
return {hull:m.build(),tur:t.build(),s:1.0,turH:4.7};
}
function mdlHordeBombardier(){
const m=MB(); bioLegBank(m,4,7.0,5.2,1.45);
m.sphere(-2.5,2.6,0,5.3,11,CHIT_D,.92,false);
m.sphere(2.8,2.0,0,3.6,9,CHITIN,.76,false);
for(const sd of [-1,1]){
m.sphere(-4.2,5.9,sd*2.2,1.6,8,BIO_TEAM,1,false);
m.box(-.4,1.6,sd*4.0,6.2,.72,.82,BIO_LEG,sd*.1);
}
bioCrown(m,-2.6,7.1,7,8.0,4.2);
const t=MB();
t.sphere(-1.6,.1,0,3.4,10,CHITIN,.74,false);
t.sphere(1.0,.55,0,2.4,8,CHIT_D,.84,false);
for(let k=0;k<7;k++){
const r=1.65-k*.15;
t.sphere(2.5+k*1.25,.62+Math.sin(k*.7)*.20,0,r,8,k%2?CHITIN:CHIT_D,.72,false);
if(k<5) for(const sd of [-1,1])
t.sphere(2.6+k*1.25,.35,sd*(r*.82),.34,6,BIO_LEG,.70,false);
}
t.sphere(10.2,.62,0,.90,7,LAMP,.65,false);
for(const sd of [-1,1]) t.sphere(-1.0,2.5,sd*1.7,.82,7,BIO_TEAM,1,false);
return {hull:m.build(),tur:t.build(),s:1.06,turH:5.7};
}
function mdlHordeSupport(){
const m=MB(); bioLegBank(m,3,5.6,4.8,1.25);
m.sphere(-1.8,2.0,0,4.4,10,CHITIN,.80,false);
m.sphere(-5.0,2.8,0,4.0,10,CHIT_D,1.08,false);
for(let k=0;k<6;k++){
const a=k/6*TAU;
m.sphere(-4.8+Math.cos(a)*2.8,5.2,Math.sin(a)*2.8,1.05,7,BIO_TEAM,1,false);
}
for(const sd of [-1,1]){
m.box(1.0,1.0,sd*4.4,5.4,.56,.68,BIO_LEG,sd*.2);
m.cyl(1.8,3.5,sd*2.3,.42,.06,3.8,6,BIO_LEG);
}
bioCrown(m,-3.4,6.2,5,6.2,3.8);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlHordeFlyer(){
const m=MB();
m.sphere(-.4,.5,0,3.3,10,CHITIN,.52,false);
m.sphere(-3.4,.65,0,2.7,9,CHIT_D,.68,false);
m.sphere(2.5,.35,0,2.1,8,CHIT_D,.74,false);
for(const sd of [-1,1]){
/* Two asymmetrical membrane lobes per side read as an insect wing from
above. The perimeter is curved/segmented; there is no rectangular panel. */
const fore=[[-5,sd*1.0],[-2,sd*3.5],[2,sd*7.4],[5,sd*10.7],[3,sd*13.0],[-1,sd*11.4],[-5,sd*5.0]];
const hind=[[-5,sd*1.0],[-4,sd*4.2],[-2,sd*8.7],[1,sd*10.0],[2,sd*7.5],[-.5,sd*3.0]];
m.extrude(-.8,.28,0,fore,.22,BIO_MEM);
m.extrude(-3.8,.14,0,hind,.18,BIO_MEM);
m.box(0,.55,sd*5.7,10.0,.16,.24,BIO_LEG,sd*1.15);
m.box(-3.0,.42,sd*4.7,7.6,.15,.21,BIO_LEG,sd*1.30);
for(let k=0;k<5;k++) m.sphere(-4+k*1.65,.48,sd*(1.2+k*2.15),.38-k*.035,6,BIO_LEG,.62,false);
for(let k=0;k<3;k++) m.sphere(1.8+k*.9,.12,sd*(1.0-k*.22),.44-k*.08,6,BIO_LEG,.68,false);
m.sphere(-2.4,1.8,sd*1.65,.76,7,BIO_TEAM,1,false);
}
bioCrown(m,-3.4,2.4,3,3.6,2.3);
return {hull:m.build(),tur:null,s:1.0,air:1};
}
function mdlHordeSwimmer(){
const m=MB();
m.sphere(-1.0,1.0,0,5.2,12,CHITIN,.48,false);
m.sphere(-5.2,1.1,0,3.8,10,CHIT_D,.60,false);
m.sphere(4.0,.8,0,3.0,9,CHIT_D,.58,false);
for(const sd of [-1,1]){
m.wedge(-.8,.55,sd*5.0,9.8,.32,3.4,BIO_MEM,sd*.18,sd<0);
m.box(-5.8,.25,sd*2.5,5.0,.42,.72,BIO_LEG,sd*.25);
m.box(5.5,.35,sd*1.7,4.6,.48,.58,BIO_LEG,sd*.28);
m.sphere(-1.5,3.2,sd*2.6,1.05,7,BIO_TEAM,1,false);
}
bioCrown(m,-1.5,4.2,5,6.2,2.8);
return {hull:m.build(),tur:null,s:1.06};
}
function mdlHordeLeviathan(){
const m=MB(); bioLegBank(m,5,9.2,6.1,2.0);
m.sphere(-1.2,3.2,0,6.4,12,CHITIN,.80,false);
m.sphere(-6.2,3.8,0,5.6,11,CHIT_D,.98,false);
m.sphere(4.8,2.8,0,4.2,10,CHIT_D,.78,false);
for(const sd of [-1,1]){
m.box(7.0,2.6,sd*1.8,5.6,.9,.9,BIO_LEG,sd*.12);
m.box(9.7,2.1,sd*.9,3.2,.68,.68,CLAW,sd*.2);
m.sphere(-3.2,7.8,sd*3.1,2.2,9,BIO_TEAM,1,false);
}
bioCrown(m,-2.0,8.4,9,10.0,6.0);
return {hull:m.build(),tur:null,s:1.16};
}
/* ============================================================================
FACTION INFANTRY DOCTRINE
Only the RED and BLUE armies field humans. Nova (blue) runs the standard
rifleman; the Crimson Dominion (red) runs the same species in siege plate —
visibly heavier, shielded, slower-looking even at 15px. The Syndicate is a
CYBERNETIC force and fields no humanoids at all: its infantry slot is a
strider drone, all raked edges and hover mass, because a machine culture
sending flesh into small-arms fire would contradict everything else on its
roster. The Brood was already animals. Doctrine lives in the KIT, so both
the player's army and the AI's obey it automatically.
============================================================================ */
function mdlLegionTrooper(){ // red — breacher in siege plate
const m=MB();
const b=mfTrooper(m,{h:8.4,w:1.38,chest:MET_D,helm:MET_D,pack:MET_D});
/* full-face helm: visor slit only, plus a crest ridge */
m.box(0.72,b.sh+0.30,0,0.30,0.20,1.05,GLASS);
m.wedge(0.1,b.hd+0.05,0,1.0,0.5,0.5,TEAM_A);
/* TOWER SHIELD on the left arm — the one-item read for "more armored".
Bevelled slab, raked back, with a vision notch. */
m.bevelBox(1.15,b.hip+0.3,-1.95,0.55,4.6,2.6,0.4,MET);
m.box(1.45,b.hip+3.3,-1.95,0.2,0.7,1.2,DARKER); // vision slit
m.wedge(1.15,b.hip+4.9,-1.95,0.5,0.8,2.5,MET_L,0,true); // shield crown
/* stub carbine on the right — short, because the shield is the weapon */
const gy=b.hip+1.7;
m.bevelBox(1.5,gy,0.7,1.9,0.6,0.5,0.16,MET_D);
m.cyl(2.6,gy+0.1,0.7,0.16,0.13,1.1,6,DARKER,Math.PI/2);
m.box(1.7,gy-0.5,0.7,0.4,0.55,0.4,MET_D);
glowStrip(m,-0.6,b.sh+0.4,0,0.8,HOT,0);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlLegionPyro(){ // red — furnace trooper
const m=MB();
const b=mfTrooper(m,{h:8.2,w:1.5,chest:RUST,helm:MET_D,pack:MET_D});
m.cyl(-1.3,b.hip+0.4,0,0.85,0.85,3.6,9,RUST); // single fat boiler tank
m.cyl(-1.3,b.hip+4.0,0,0.85,0.4,0.6,9,MET_D);
m.cyl(-1.3,b.hip+2.0,0,0.9,0.9,0.26,9,MET_L); // band
m.cyl(-0.7,b.sh-0.5,0.3,0.18,0.15,2.1,5,DARKER,Math.PI/2); // hose
const gy=b.hip+1.5;
m.bevelBox(1.6,gy,0.2,2.3,0.85,0.75,0.2,MET_D); // heavy projector
m.tube(3.4,gy+0.1,0.2,0.45,0.26,1.1,8,MET_L,Math.PI/2);
m.cyl(4.4,gy+0.1,0.2,0.15,0.12,0.5,5,HOT,Math.PI/2);
m.wedge(2.4,gy+0.75,0.2,1.6,0.5,1.0,MET_D); // heat shield
glowStrip(m,3.6,gy+0.5,0.2,0.8,HOT,0);
return {hull:m.build(),tur:null,s:1.05};
}
function mdlSynStrider(){ // cybernetic — infantry-slot drone
const m=MB();
/* No box anywhere: a raked diamond chassis, a halo, and three blade legs.
The profile is one continuous line front to back — this is the "flows
cleanly to the eye" read, and it is what separates the machine culture
from the humans at a glance. */
m.extrude(0,3.4,0,[[-3.4,0],[-1.8,-1.6],[1.6,-1.9],[3.8,-0.7],[4.6,0],
[3.8,0.7],[1.6,1.9],[-1.8,1.6]],1.5,MET);
m.extrude(0.2,4.9,0,[[-2.6,0],[-1.3,-1.2],[1.3,-1.4],[3.0,-0.5],[3.5,0],
[3.0,0.5],[1.3,1.4],[-1.3,1.2]],0.8,TEAM_A);
m.wedge(3.2,5.7,0,1.6,0.6,1.2,MET_L); // brow blade
m.sphere(1.9,5.9,0,0.55,8,ENERGY,1,false); // optic
m.tube(-1.2,5.6,0,1.5,1.15,0.4,14,MET_L); // dorsal halo
m.sphere(-1.2,5.6,0,0.7,8,ENERGY,0.8,false); // core in the halo
for(const k of [0,1,2]){ // three blade legs
const a=k/3*TAU+Math.PI/6, cx=Math.cos(a), cz=Math.sin(a);
m.wedge(cx*1.6,1.8,cz*1.6,0.5,2.2,0.9,MET_D,a);
m.cyl(cx*2.3,0.2,cz*2.3,0.34,0.05,1.6,5,MET_L); // blade tip
}
for(const sd of [-1,1])
m.wedge(-2.6,4.0,sd*1.1,2.2,0.5,0.7,TEAM_A,sd*0.35,sd<0); // swept vanes
thrusterBell(m,-3.4,3.9,0,0.5,1.2,MET_L); // rear lifter
m.cyl(0.8,4.6,0,0.2,0.16,2.2,5,DARKER,Math.PI/2); // underslung emitter
m.cyl(3.0,4.6,0,0.3,0.14,0.5,6,ENERGY,Math.PI/2);
return {hull:m.build(),tur:null,s:0.95};
}
function mdlSynIncinerator(){ // cybernetic — plasma scorcher drone
const m=MB();
m.extrude(0,3.2,0,[[-3.8,0],[-2.0,-2.0],[2.2,-2.2],[4.2,0],[2.2,2.2],[-2.0,2.0]],1.7,MET);
m.extrude(-0.2,4.9,0,[[-2.8,0],[-1.4,-1.5],[1.6,-1.7],[3.1,0],[1.6,1.7],[-1.4,1.5]],0.8,TEAM_A);
m.tube(0.2,5.8,0,1.8,1.4,0.5,14,MET_L); // containment ring
m.sphere(0.2,5.8,0,0.9,9,HOT,0.8,false); // plasma core
for(const sd of [-1,1]){
m.wedge(-2.8,4.2,sd*1.3,2.6,0.6,0.8,MET_D,sd*0.3,sd<0);
thrusterBell(m,-3.8,3.6,sd*0.9,0.42,1.0,MET_L);
}
m.cyl(3.4,4.0,0,0.5,0.24,1.6,7,MET_L,Math.PI/2); // projector throat
m.cyl(4.8,4.0,0,0.20,0.16,0.6,5,HOT,Math.PI/2);
for(const k of [0,1,2]){
const a=k/3*TAU+Math.PI/6, cx=Math.cos(a), cz=Math.sin(a);
m.wedge(cx*1.7,1.7,cz*1.7,0.55,2.0,1.0,MET_D,a);
}
return {hull:m.build(),tur:null,s:1.0};
}
const FAC_KIT={
nova:{1:mdlNovaRhino},
legion:{0:mdlLegionTrooper,9:mdlLegionPyro,1:mdlLegionRhino},
syndicate:{0:mdlSynStrider,9:mdlSynIncinerator,1:mdlSyndicateRhino,2:mdlSynTank,6:mdlSynTank},
/* Every type an old save, factory, airfield, harbor or transit gate can place
for the Brood resolves to grown tissue. There is deliberately no shared
mechanical fallback hidden under an organic decoration layer.
THIS TABLE IS NOW A FALLBACK, NOT THE ROSTER.
It mapped 28 slots onto nine builders called with NO arguments, so twelve
of them were not merely similar — mdlHordeSpitter served slots 1, 2, 7,
10, 20 and 21 and mdlHordeBombardier served 3, 6, 16, 22, 26 and 27, each
rendering ONE shared vertex buffer. A tier-1 line brawler and a tier-3
siege caster were the same animal.
Those twelve are now split into twelve builders in models-units-brood.js
and registered in UNIT_MDL_BROOD, which mergeFactionUnitKits() copies over
this table at init — so for slots 1,2,3,6,7,10,12,13,16,20,21,22,26,27 and
30..32 the entry below never runs. They stay here on purpose: they are the
only thing standing between a failed load of that file and the Brood
fielding somebody else's tanks. Edit the split units THERE, not here; a
forward reference to them from this literal would be a ReferenceError,
because in dev the manifest loads each src file as its own script and this
one executes first. */
horde:{
0:mdlHordeBeast,1:mdlHordeSpitter,2:mdlHordeSpitter,3:mdlHordeBombardier,
4:mdlHordeLeviathan,5:mdlHordeFlyer,6:mdlHordeBombardier,7:mdlHordeSpitter,
8:mdlHordeLeviathan,9:mdlHordeBeast,10:mdlHordeSpitter,11:mdlHordeSupport,
12:mdlRavager,13:mdlAlphaRavager,14:mdlHordeSwimmer,15:mdlHordeSwimmer,
16:mdlHordeBombardier,17:mdlHordeFlyer,18:mdlHordeLeviathan,19:mdlHordeSupport,
20:mdlHordeSpitter,21:mdlHordeSpitter,22:mdlHordeBombardier,23:mdlHordeSupport,
24:mdlHordeSupport,25:mdlHordeFlyer,26:mdlHordeBombardier,27:mdlHordeBombardier
},
};
const FAC_MESH={}; // kit -> {type -> {hull,tur,...}}
/* A faction replacement cannot sensibly turn artillery, aircraft and support
vehicles into the same hover tank or beetle. These light doctrine shells
sit over any role chassis that does NOT have a bespoke replacement: the role
remains readable, while armour profile, propulsion language and biology make
the owner readable too. They are geometry (not a colour wash), because the
user's complaint is still valid in greyscale and at minimap-adjacent zoom. */
function mdlNovaDoctrine(air){
const m=MB();
if(air){
for(const sd of [-1,1]){
m.box(-.4,.52,sd*4.45,10.2,.42,1.05,ENERGY,sd*.06);
for(const x of [-3.0,.2,3.2]){
m.ring(x,.78,sd*4.45,.62,1.08,12,ENERGY);
m.sphere(x,.82,sd*4.45,.50,7,ENERGY,.74,false);
}
m.wedge(2.3,.68,sd*5.25,5.5,.38,1.25,TEAM_T,sd*.1,sd<0);
}
m.sphere(-2.8,1.28,0,1.25,9,ENERGY,1,false);
}else{
for(const sd of [-1,1]){
m.box(-.3,2.45,sd*4.65,10.4,.50,1.24,ENERGY);
for(const x of [-3.0,0,3.0]){
m.ring(x,2.72,sd*4.65,.66,1.16,12,ENERGY);
m.sphere(x,2.76,sd*4.65,.52,7,ENERGY,.76,false);
}
m.wedge(3.8,2.15,sd*5.0,3.8,.45,1.35,TEAM_T,0,sd<0);
}
m.ring(-2.9,5.1,0,1.28,2.12,16,ENERGY);
m.sphere(-2.9,5.1,0,1.05,9,ENERGY,1,false);
}
return m.build();
}
function mdlLegionDoctrine(air){
const m=MB();
if(air){
for(const sd of [-1,1]){
m.wedge(-0.4,0.5,sd*5.0,8.8,0.75,2.2,TWR_ARM_D,0,sd<0);
m.box(-0.6,1.28,sd*5.75,5.8,0.26,1.45,TEAM_A);
m.bevelBox(-2.7,0.1,sd*3.3,2.9,1.7,2.0,.28,TWR_COAT);
m.box(-4.2,1.82,sd*3.3,1.2,.22,.72,HOT);
}
m.wedge(3.8,0.45,0,4.4,1.0,3.0,TWR_ARM,0,false);
}else{
for(const sd of [-1,1]){
m.bevelBox(-0.6,1.0,sd*5.2,10.8,2.5,1.35,.30,TWR_ARM_D);
m.box(-0.2,3.55,sd*5.85,6.8,.24,1.55,TEAM_A);
m.wedge(4.8,1.05,sd*5.2,2.8,2.35,1.35,TWR_ARM,0,sd<0);
m.bevelBox(-4.9,3.2,sd*3.25,1.35,3.6,1.25,.24,TWR_COAT);
m.box(-4.9,6.84,sd*3.25,.72,.28,.72,HOT);
}
m.bevelBox(-3.2,4.5,0,3.4,1.0,5.0,.22,TWR_ARM_D);
}
return m.build();
}
function mdlSyndicateDoctrine(air){
const m=MB();
if(air){
for(const sd of [-1,1]){
m.wedge(-0.5,0.35,sd*5.35,9.8,.52,2.0,TWR_COAT,sd*.10,sd<0);
m.box(0,0.91,sd*6.15,5.6,.18,.72,TEAM_T);
m.cyl(-2.7,.30,sd*4.35,.72,.62,.46,10,TWR_MACH);
m.ring(-2.7,.78,sd*4.35,.76,1.08,12,ENERGY);
}
m.wedge(4.2,.4,0,4.8,.72,2.6,TWR_ARM,0,false);
}else{
/* The side plena hide the shared running gear at command zoom; exposed
lift coils and a raked fork carry the Coalition's contact-sheet design. */
for(const sd of [-1,1]){
m.bevelBox(-.4,.18,sd*4.85,11.4,1.75,1.65,.34,TWR_COAT);
m.box(-.2,1.96,sd*5.05,7.2,.20,1.24,TEAM_T);
for(const x of [-3.6,0,3.6]){
m.cyl(x,.16,sd*5.0,.62,.56,.48,10,TWR_MACH);
m.ring(x,.66,sd*5.0,.68,1.02,12,ENERGY);
}
m.wedge(4.9,1.1,sd*4.7,3.1,2.1,1.5,TWR_ARM,0,sd<0);
}
m.ring(-2.7,3.8,0,1.2,2.05,16,ENERGY);
m.box(-2.7,3.75,0,4.8,.22,.62,TEAM_T);
}
return m.build();
}
const FAC_DOCTRINE_MDL={
nova:{ground:()=>mdlNovaDoctrine(false),air:()=>mdlNovaDoctrine(true)},
legion:{ground:()=>mdlLegionDoctrine(false),air:()=>mdlLegionDoctrine(true)},
syndicate:{ground:()=>mdlSyndicateDoctrine(false),air:()=>mdlSyndicateDoctrine(true)},
};
const FAC_DOCTRINE_MESH={};
/* THE FACTION KITS WERE THREE UNITS DEEP.
Buildings were 27/27 for every army, but FAC_KIT — the UNIT overrides — held
three entries for the Dominion and five for the Coalition against a 33-slot
roster. Every other chassis fell through to the Nova hull with a thin
doctrine shell over it, which is exactly what "still using the blue faction's
models" looks like from the outside. The per-faction unit kits live in their
own files and are merged here rather than at their own top level, because
FAC_KIT is declared in this file and they load after it. */
function mergeFactionUnitKits(){
const add=(kit,map)=>{ if(!map||!FAC_KIT[kit]) return 0;
let n=0; for(const k in map){ FAC_KIT[kit][k]=map[k]; n++; } return n; };
const n=[
add('legion', typeof UNIT_MDL_LEGION !=='undefined'?UNIT_MDL_LEGION :null),
add('syndicate',typeof UNIT_MDL_SYNDICATE!=='undefined'?UNIT_MDL_SYNDICATE:null),
add('nova', typeof UNIT_MDL_NOVA !=='undefined'?UNIT_MDL_NOVA :null),
add('horde', typeof UNIT_MDL_BROOD !=='undefined'?UNIT_MDL_BROOD :null)
];
return n;
}
/* The four kits keep their bespoke packs in their own tables; this is the only
place that needs to read across all four, so it asks rather than importing a
registry that does not exist. Asset maps are a bounded visual pilot, not a
switch that turns every generated template into live battle art. A pack must
opt in with a named token before the query may unwrap and reupload its mesh.
That keeps an unfinished map set from silently changing a whole faction. */
function mfPackMaps(kit,slot){
try{
const T=kit==='nova'?(typeof TFC_NOVA_BESPOKE_PACKS!=='undefined'&&TFC_NOVA_BESPOKE_PACKS)
:kit==='legion'?(typeof DOM_LEGION_BESPOKE_PACKS!=='undefined'&&DOM_LEGION_BESPOKE_PACKS)
:kit==='horde'?(typeof BRD_BESPOKE_PACKS!=='undefined'&&BRD_BESPOKE_PACKS)
:(typeof COA_SYN_BESPOKE_PACKS!=='undefined'&&COA_SYN_BESPOKE_PACKS);
const p=T&&T[slot];
if(!p||!p.maps||!p.assetSkin) return null;
const q=new URLSearchParams(location.search).get('assetskin');
return (q==='1'||q===p.assetSkin)?p.maps:null;
}catch(e){ return null; }
}
function initFactionKits(){
mergeFactionUnitKits();
for(const k in FAC_KIT){
FAC_MESH[k]={};
const cache={};
for(const ty in FAC_KIT[k]){
const fn=FAC_KIT[k][ty];
if(!cache[fn.name]){
const g=fn();
cache[fn.name]={hull:new InstMesh(gl,g.hull,900), tur:g.tur?new InstMesh(gl,g.tur,900):null,
s:g.s||1, turH:g.turH||0, muzzle:g.muzzle||0, muzzleZ:g.muzzleZ||0};
/* Per-asset baked maps, when this slot declares a triplet AND the flag
is on. Async and self-cancelling: if any of the three fails to decode
the mesh simply stays on the shared atlas, which is why this can be
attempted for any pack without checking what exists on disk. */
if(typeof mfAssetSkinEnabled==='function'&&mfAssetSkinEnabled()){
const maps=(typeof mfPackMaps==='function')?mfPackMaps(k,ty):null;
if(maps&&typeof mfAssetSkin==='function') mfAssetSkin(gl,cache[fn.name].hull,maps);
}
}
FAC_MESH[k][ty]=cache[fn.name];
}
}
for(const k in FAC_DOCTRINE_MDL){
FAC_DOCTRINE_MESH[k]={
ground:new InstMesh(gl,FAC_DOCTRINE_MDL[k].ground(),1600),
air:new InstMesh(gl,FAC_DOCTRINE_MDL[k].air(),800)
};
}
}
function mdlConstructor(){ // 19 — engineer
const m=MB();
/* The engineer is the only unarmed thing the player drives around a battle,
so it has to look like PLANT — a works vehicle — rather than an unarmed
tank. Four bare wheels under a bevelled box was not carrying that: no
suspension, no bed, no load, and the "arm" was a stick with a ball on it.
Rebuilt around what a builder actually is: a flat load bed with cargo
strapped to it, a proper wheeled chassis with hubs and arches, and a
two-stage telescopic boom with a fabricator head at the end. */
m.bevelBox(0,1.9,0,8.2,2.4,5.2,0.45,MET); // chassis rail
m.wedge(3.6,4.3,0,2.6,1.0,4.6,MET_L); // sloped nose
/* LOAD BED: a recessed inner extrusion with a lip, which is what makes a
flatbed read as a bed rather than as the top of a box. */
const BB=[[-4.0,-2.4],[1.2,-2.4],[1.2,2.4],[-4.0,2.4]];
m.extrude(0,4.3,0,BB,0.35,MET_D);
m.extrude(0,4.65,0,BB.map(q=>[q[0]*0.90,q[1]*0.84]),0.22,DARKER);
for(const sd of [-1,1]) m.box(-1.4,4.65,sd*2.3,5.2,0.55,0.22,MET_L); // bed rails
for(const sd of [-1,1]){
for(const fx of [2.5,-2.5]){
m.cyl(fx,1.0,sd*3.0,1.55,1.55,1.5,10,RUBBER,Math.PI/2); // wheel
m.cyl(fx,1.0,sd*3.5,0.62,0.62,0.5,8,MET_L,Math.PI/2); // hub
hydraulic(m,fx,2.2,sd*2.6,1.1,0.22,MET_D,0); // suspension
m.wedge(fx,3.3,sd*2.9,3.4,0.9,1.5,MET_D); // wheel arch
}
m.bevelBox(0.6,4.9,sd*2.0,3.0,1.1,0.9,0.22,TEAM_A); // side livery
}
/* TELESCOPIC BOOM: shoulder, lower stage, upper stage stepped in, then the
fabricator head. The step is what says telescopic. */
m.bevelBox(1.7,4.7,0,2.0,2.0,2.2,0.32,MET_D); // turntable
m.cyl(1.7,6.7,0,0.90,0.80,0.6,10,MET_L);
m.box(2.5,7.0,0,3.0,0.85,0.85,MET_L,0.34); // lower stage
m.box(4.4,8.0,0,3.0,0.62,0.62,MET_D,0.34); // upper stage
m.bevelBox(6.2,8.8,0,1.3,1.0,1.1,0.24,MET_D); // head housing
m.cyl(6.9,8.8,0,0.85,0.42,0.9,10,ENERGY,Math.PI/2); // fabricator emitter
hydraulic(m,2.4,5.5,0,2.6,0.28,MET_L,0);
/* Cargo actually strapped to the bed, and a rotating beacon — the two things
that read "works vehicle" from any angle. */
kitBox(m,-2.4,4.87,0.9,2.2,1.3,2.6,MET_D,0);
kitBox(m,-3.4,4.87,-1.4,1.5,0.9,1.6,RUST,0.3);
stowage(m,-0.4,4.87,-1.8,2.4,1.0,RUST,0,41);
m.cyl(-3.9,4.9,1.9,0.26,0.20,1.9,6,DARKER);
m.cyl(-3.9,6.8,1.9,0.40,0.34,0.42,8,LAMP); // beacon
glowStrip(m,-1.2,4.92,0,3.8,ENERGY,0);
deckCrown(m,-1.0,4.88,0,7.0,5.2,MET_D,TEAM_T);
return {hull:m.build(),tur:null,s:1.0};
}
const UNIT_MDL=[mdlStriker,mdlRhino,mdlGoliath,mdlThumper,mdlCommander,mdlWasp,mdlLongbow,
mdlHornet,mdlTitan,mdlPyro,mdlVulture,mdlBulwark,mdlRavager,mdlAlphaRavager,mdlCorvette,
mdlDread,mdlBombard,mdlRaptor,mdlScorcher,mdlConstructor,
/* second wave — indices 20..27 */
mdlReaper,mdlCinder,mdlLancer,mdlResonator,mdlWarden,mdlKestrel,mdlBasilisk,mdlHarbinger,
/* faction heroes — 28..30 */
mdlPraetor,mdlArchon,mdlBroodmother];
/* WHAT A MODEL IS MADE OF, answered by the model tables themselves.
Salvage needs to know whether the thing that just died was grown or built,
and the only honest source is the SAME resolution the renderer uses:
kit override first, base roster second. Keeping this here - beside FAC_KIT
and UNIT_MDL - means editing a faction's kit automatically updates what its
dead drop; a separate list in sim.js would drift the first time a slot was
reassigned, and the failure (a strider drone bleeding, a rifleman shedding
plate) would be invisible until someone happened to reclaim the wrong pile. */
const HUMAN_MDL=new Set([
mdlStriker,mdlPyro, // the base humans (Nova fields them)
mdlLegionTrooper,mdlLegionPyro, // Dominion siege infantry - heavier, still people
]);
const ORGANIC_MDL=new Set([
...HUMAN_MDL,
mdlRavager,mdlAlphaRavager,mdlBroodmother, // wildlife and the Sovereign
mdlHordeBeast,mdlHordeSpitter,mdlHordeBombardier,mdlHordeLeviathan,
mdlHordeFlyer,mdlHordeSupport,mdlHordeSwimmer,
]);
function unitModelOrganic(kit,type){
const fn=(FAC_KIT[kit]&&FAC_KIT[kit][type])||UNIT_MDL[type];
return ORGANIC_MDL.has(fn)?1:0;
}
/* A person, specifically - the distinction dropRemains needs: fallen soldiers
leave no salvage pile at all, while a grown warbeast of any size renders
into biomass. */
function unitModelHuman(kit,type){
const fn=(FAC_KIT[kit]&&FAC_KIT[kit][type])||UNIT_MDL[type];
return HUMAN_MDL.has(fn)?1:0;
}
/* ---------------------------------------------------------------------------
BUILDINGS — authored to their real footprint so they sit inside the square
build plates rather than floating on top of them.
--------------------------------------------------------------------------- */
const BLD_GEO={}, BLD_MESH={}, BLD_FACTION_MESH={};
/* Shared tower foundations. Contact-sheet structures need a common construction
language without becoming palette-swapped copies: this handles only the
footprint, bearing reveal and sparse status strips. Each weapon/function
still owns its silhouette above the bearing. */
function towerOct(r){
return [[-r*.55,-r],[r*.55,-r],[r,-r*.55],[r,r*.55],
[r*.55,r],[-r*.55,r],[-r,r*.55],[-r,-r*.55]];
}
function towerPad(m,r,tier,bearing){
tier=tier||1;
m.extrude(0,0,0,towerOct(r),3,TWR_PAD);
m.extrude(0,3,0,towerOct(r*.86),5,TWR_ARM_D);
m.extrude(0,8,0,towerOct(r*.70),3,TWR_ARM);
for(const s of [-1,1]){
m.bevelBox(s*r*.68,3,0,r*.22,4.4,r*.44,.5,TWR_COAT);
m.box(s*r*.68,7.4,0,r*.08,.22,r*.24,s>0?TWR_GLOW:TWR_TEAM);
}
if(tier>=2) for(const s of [-1,1]){
m.bevelBox(0,5,s*r*.72,r*.42,3.4,r*.18,.4,TWR_ARM_D);
m.box(0,8.4,s*r*.72,r*.24,.22,r*.06,TWR_GLOW);
}
if(tier>=3) for(const a of [0,Math.PI/2,Math.PI,Math.PI*1.5]){
const x=Math.cos(a)*r*.69,z=Math.sin(a)*r*.69;
m.cyl(x,8,z,r*.075,r*.06,2.8,8,TWR_MACH);
}
if(bearing){
m.cyl(0,10.7,0,r*.52,r*.52,1.0,16,TWR_COAT);
m.cyl(0,11.7,0,r*.44,r*.40,.8,16,TWR_TRIM);
}
return m;
}
/* Nova economy/production architecture uses the same quiet physical finishes
as the rebuilt defense kit, but not its octagonal weapon silhouette. A
shallow bevel, dark reveal and sparse pair of status rails make each model
feel planted and give baked AO somewhere useful to collect. Keeping this
shared layer restrained is important: function-specific machinery above it
should identify a building before colour or surface noise does. */
function novaServicePad(m,w,d,h,quiet){
h=h||2.4;
m.bevelBox(0,0,0,w,h,d,Math.min(1.35,w*.04,d*.04),TWR_PAD);
m.bevelBox(0,h,0,w-2.2,1.0,d-2.2,.45,NOVA_DECK);
/* Status rails must stay short. w*.30 on the Command HQ (87) wrote a
26-unit TWR_GLOW bar — the glowing artifact, not a lamp. HQ passes
quiet so restoring TWR_GLOW points cannot revive that rail. */
const mark=quiet?TWR_TRIM:TWR_TEAM;
m.box(0,h+.98,-d*.43,Math.min(w*.30,8.0),.20,.34,mark);
m.box(0,h+.98, d*.43,Math.min(w*.18,6.2),.20,.34,quiet?TWR_TRIM:TWR_GLOW);
return h+1.0;
}
function mdlMex(){
const m=MB();
const y=novaServicePad(m,33,33,2.6,true);
/* A four-way ore intake is legible from any camera facing. The old derrick
was four identical thin posts around a box, so command view reduced it to
visual grit and never communicated extraction. */
m.cyl(0,y,0,9.4,8.2,4.0,12,NOVA_MEX_CORE);
m.cyl(0,y+4.0,0,7.5,6.0,3.0,12,NOVA_MEX_ARM);
m.cyl(0,y+7.0,0,4.2,3.5,12.0,12,NOVA_MEX_CORE);
m.tube(0,y+18.8,0,3.6,2.0,1.3,12,TWR_BORE);
for(let k=0;k<4;k++){
const a=k/4*TAU,x=Math.cos(a)*11.2,z=Math.sin(a)*11.2;
m.bevelBox(x,y+.2,z,9.2,4.2,6.0,.75,NOVA_MEX_CORE,a);
m.cyl(x,y+4.4,z,2.6,2.1,3.0,10,NOVA_MEX_TRIM);
m.box(x,y+7.3,z,2.3,.24,2.3,NOVA_MEX_CORE,a);
m.box(Math.cos(a)*6.7,y+2.7,Math.sin(a)*6.7,8.2,1.2,1.6,NOVA_MEX_ARM,a);
}
ringX(m,0,y+12.2,0,4.3,5.1,14,NOVA_MEX_TRIM);
/* A 1.15 emissive.light sphere sat past the 0.936 bloom thresh and became
the white disc on every occupied node after deposit sprites were cut. */
m.sphere(0,y+13.0,0,1.15,8,NOVA_MEX_CORE,1,false);
return m.build();
}
function mdlPgen(){
const m=MB();
const y=novaServicePad(m,37,29,2.5);
/* Twin shielded reactor cans and one exposed capacitor give this a broad,
low energy-plant silhouette without repeating access panels on every
face. Material changes, not tiny geometry, carry the PBR detail. */
for(const s of [-1,1]){
m.cyl(s*6.6,y,0,5.6,5.2,12.0,12,NOVA_REACTOR_CORE);
m.cyl(s*6.6,y+12.0,0,5.25,4.3,2.2,12,NOVA_REACTOR_ARM);
m.cyl(s*6.6,y+4.8,0,5.75,5.75,.8,12,NOVA_REACTOR_TRIM);
m.box(s*6.6,y+9.0,-5.25,5.6,1.0,.38,s<0?NOVA_REACTOR_ARM:NOVA_REACTOR_GLOW);
}
m.cyl(0,y+2.0,0,3.2,2.5,15.0,12,NOVA_REACTOR_CORE);
m.sphere(0,y+17.5,0,3.2,10,NOVA_REACTOR_GLOW,1,false);
ringX(m,0,y+17.5,0,4.1,4.8,14,NOVA_REACTOR_TRIM);
for(const s of [-1,1]){
m.bevelBox(s*15.0,y,0,4.6,8.0,18.0,.7,NOVA_REACTOR_CORE);
/* Keep the long radiator axis on Z. Rotating this bank made its 13-unit
depth become width and silently overhang the 38-unit build plot. */
ventBank(m,s*15.0,y+8.0,0,3.0,13.0,4,TWR_MACH,0);
}
return m.build();
}
function mdlFac(){
const m=MB();
const y=novaServicePad(m,63,49,3.0);
/* The production lane is a single large read: calm shed, dark open bay and
a control tower. Roof panels and two vent cassettes replace the old
saw-tooth/greeble field, cutting both noise and vertices. */
m.bevelBox(-7.0,y,0,43.0,17.0,42.0,2.2,NOVA_FACTORY_ARM);
m.bevelBox(-7.0,y+17.0,0,40.0,2.0,39.0,.8,NOVA_FACTORY_ROOF);
roofEdge(m,-7.0,y+19.0,0,40.0,39.0,1.0,NOVA_FACTORY_CORE,NOVA_FACTORY_ROOF);
for(const s of [-1,1]){
m.bevelBox(-7.0,y+20.8,s*10.0,15.0,2.7,8.0,.6,NOVA_FACTORY_CORE);
ventBank(m,-7.0,y+23.5,s*10.0,10.5,5.5,4,TWR_MACH,0);
}
/* Deep assembly opening on +X, framed by blue identification and guide
lights. The dark surface is deliberately behind the wall plane. */
m.box(14.55,y+2.0,0,.7,12.5,22.0,TWR_BORE);
for(const s of [-1,1]){
m.bevelBox(15.3,y+1.0,s*12.0,2.6,15.0,3.2,.45,NOVA_FACTORY_CORE);
m.box(15.8,y+6.0,s*10.15,.35,7.5,.35,TWR_GLOW);
}
m.bevelBox(15.3,y+13.5,0,2.6,3.2,27.0,.5,NOVA_FACTORY_ARM);
m.box(15.8,y+16.4,0,.35,.26,16.0,TWR_TEAM);
/* Offset command/assembly tower keeps the footprint directional. */
m.bevelBox(23.0,y,0,15.0,24.0,28.0,1.6,NOVA_FACTORY_CORE);
m.bevelBox(23.0,y+24.0,0,16.5,2.0,29.5,.7,NOVA_FACTORY_ROOF);
for(const s of [-1,1]) m.box(23.0,y+15.0,s*14.1,10.0,5.0,.55,NOVA_FACTORY_GLASS);
m.box(15.45,y+15.0,0,.5,5.0,20.0,NOVA_FACTORY_GLASS);
sensorMast(m,25.0,y+26.0,-8.5,7.0,TWR_MACH);
for(const s of [-1,1]) exhaust(m,-25.5,y+15.0,s*13.0,1,1.4,10.0,TWR_MACH,2.0);
return m.build();
}
function mdlTurretBase(tier){
const m=MB();
tier=tier||1;
const oct=r=>[[-r*.55,-r],[r*.55,-r],[r,-r*.55],[r,r*.55],
[r*.55,r],[-r*.55,r],[-r,r*.55],[-r,-r*.55]];
m.extrude(0,0,0,oct(13),3,NOVA_SENTINEL_CORE); // quiet coated foundation
m.extrude(0,3,0,oct(11.4),6,NOVA_SENTINEL_CORE); // armoured lower plinth
m.extrude(0,9,0,oct(9.7),3,NOVA_SENTINEL_ARM); // recessed upper tier
for(const sx of [-1,1]) for(const sz of [-1,1]){
m.bevelBox(sx*9.3,3,sz*9.3,4.2,4.6,4.2,.65,NOVA_SENTINEL_CORE); // anchor housings
m.cyl(sx*9.3,7.72,sz*9.3,.28,.28,.22,8,HQ_LAMP);
}
for(const s of [-1,1]){
m.box(0,11.75,s*8.5,7.4,.25,.42,TWR_TEAM); // sparse faction livery
m.box(s*8.5,11.75,0,.42,.25,7.4,NOVA_SENTINEL_GLOW); // mount status guides
}
if(tier>=2) for(const s of [-1,1]){
m.bevelBox(s*10.1,7.0,0,3.3,3.6,7.4,.45,NOVA_SENTINEL_CORE); // Mk2 side power pods
m.box(s*10.1,10.6,0,1.1,.22,4.5,NOVA_SENTINEL_GLOW);
}
if(tier>=3) for(const s of [-1,1]){
m.cyl(0,8.8,s*9.7,1.75,1.45,3.1,10,NOVA_SENTINEL_TRIM); // Mk3 reserve capacitors
m.box(0,11.75,s*9.7,2.3,.24,.8,TWR_GLOW);
}
m.cyl(0,12,0,8.25,8.25,1.15,16,NOVA_SENTINEL_CORE); // azimuth shadow gap
m.cyl(0,13.15,0,7.15,6.65,.85,16,NOVA_SENTINEL_TRIM); // y=14 bearing surface
return m.build();
}
function mdlTurretGun(tier){
const m=MB();
tier=tier||1;
m.cyl(0,0,0,6.5,6.1,2.7,16,NOVA_SENTINEL_TRIM); // rotating race
m.cyl(0,2.7,0,5.9,5.4,1.0,16,NOVA_SENTINEL_TRIM);
m.bevelBox(-1.4,3.4,0,13.8,6.2,11.4,1.05,NOVA_SENTINEL_CORE); // receiver
m.bevelBox(-2.6,9.6,0,6.4,1.7,5.8,.52,NOVA_SENTINEL_ARM); // quiet armour roof
m.box(-2.6,11.3,0,1.35,.18,4.2,TWR_TEAM); // narrow livery inset
m.bevelBox(-7.1,4.2,0,4.8,5.4,9.2,.75,NOVA_SENTINEL_CORE); // capacitor bustle
for(const s of [-1,1]){
m.bevelBox(-3.3,5.0,s*6.15,5.8,3.2,1.65,.35,TWR_COAT); // cooling cheeks
m.box(-3.3,8.2,s*6.2,3.5,.26,1.75,NOVA_SENTINEL_GLOW);
m.cyl(2.2,2.8,s*4.4,.66,.5,4.6,9,NOVA_SENTINEL_TRIM); // elevation rams
m.box(8.6,6.9,s*2.55,4.8,1.45,1.0,NOVA_SENTINEL_CORE); // compact barrel braces
m.box(8.5,9.35,s*1.45,3.8,.28,.36,NOVA_SENTINEL_GLOW); // short recoil guides
}
m.bevelBox(4.35,4.8,0,4.5,6.0,8.0,.75,NOVA_SENTINEL_ARM); // mantlet
cylX(m,4.6,8.0,0,10.7,1.30,1.02,12,NOVA_SENTINEL_TRIM,false);
for(const x of [6.2,9.2,12.2]) cylX(m,x,8.0,0,.75,1.68,1.68,12,NOVA_SENTINEL_TRIM);
m.bevelBox(15.2,6.0,0,3.15,4.0,4.8,.52,NOVA_SENTINEL_CORE); // muzzle shroud
tubeX(m,15.9,8.0,0,1.15,1.58,.80,12,TWR_BORE); // bore ends at x=17.05
m.cyl(-5.7,7.55,4.7,.22,.22,.28,8,HQ_LAMP); // service indicator
if(tier>=2) for(const s of [-1,1]){
m.bevelBox(-7.4,7.2,s*5.0,4.2,3.8,1.25,.34,TWR_ARM); // Mk2 capacitor cassettes
m.box(-7.4,10.8,s*5.05,2.5,.25,1.35,TWR_GLOW);
}
if(tier>=3){
m.bevelBox(-8.0,10.2,0,4.8,3.3,7.2,.55,TWR_COAT); // Mk3 reactor crown
m.box(-8.0,13.35,0,1.2,.24,3.6,TWR_TEAM);
for(const s of [-1,1]){
m.box(11.0,10.5,s*3.15,8.0,.72,.76,TWR_ARM_D); // heavy outer accelerator rails
m.box(11.0,11.24,s*3.15,5.4,.20,.34,TWR_GLOW);
}
}
return m.build();
}
function mdlBunkerBase(tier){
const m=MB(); tier=tier||1;
towerPad(m,20,tier,false);
/* Low, broad casemate: the armor is calm and the dark mechanism is confined
to service recesses instead of coating the entire structure. */
m.extrude(0,11,0,[[-15,-14],[12,-14],[17,-8],[17,8],[12,14],[-15,14],[-18,0]],5,NOVA_BUNKER_ARM);
for(const s of [-1,1]){
m.bevelBox(-11,11,s*14.2,8.5,5.0,2.3,.45,NOVA_BUNKER_CORE);
m.box(-11,15.8,s*14.4,5.3,.25,2.45,s>0?TWR_TEAM:TWR_GLOW);
m.bevelBox(11,11,s*12.7,6.0,4.2,5.0,.65,NOVA_BUNKER_CORE);
}
ventBank(m,-15.4,15.9,0,1.0,13,5,TWR_MACH,Math.PI/2);
if(tier>=2) for(const s of [-1,1]){
m.bevelBox(-2,15.8,s*11.2,10,2.0,3.2,.45,TWR_ARM_D);
m.box(-2,17.7,s*11.25,6.5,.24,1.1,NOVA_BUNKER_GLOW);
}
if(tier>=3){
m.bevelBox(-12,16,0,5.5,4.3,11,.6,NOVA_BUNKER_CORE);
m.box(-12,20.1,0,1.2,.22,6.4,TWR_TEAM);
}
m.cyl(0,16,0,9.2,9.2,1.1,16,NOVA_BUNKER_CORE);
m.cyl(0,17.1,0,8.0,7.4,.9,16,NOVA_BUNKER_TRIM); // y=18 turret bearing
return m.build();
}
function mdlBunkerGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,7.4,6.9,2.6,16,NOVA_BUNKER_TRIM);
m.bevelBox(-1.4,2.6,0,15.5,6.5,14.2,1.1,NOVA_BUNKER_CORE);
m.bevelBox(-5.8,4.3,0,6.5,5.2,11.8,.8,NOVA_BUNKER_CORE);
m.bevelBox(-1.4,9.1,0,7.8,1.8,8.2,.5,NOVA_BUNKER_ARM);
m.box(-1.4,10.85,0,1.4,.18,5.2,TWR_TEAM);
for(const s of [-1,1]){
m.bevelBox(5.0,5.0,s*3.35,5.8,4.2,2.2,.45,NOVA_BUNKER_ARM);
gunX(m,6.8,7.1,14.8,.78,NOVA_BUNKER_TRIM,s*3.35); // twin recessed cannon bores
m.box(-3.6,7.6,s*7.5,5.4,2.6,1.3,NOVA_BUNKER_CORE);
m.box(-3.6,10.1,s*7.55,3.0,.22,1.4,NOVA_BUNKER_GLOW);
}
if(tier>=2) for(const s of [-1,1]){
m.bevelBox(-8.0,7.0,s*5.6,4.0,3.2,2.0,.4,TWR_ARM);
m.box(-8.0,10.1,s*5.65,2.2,.20,2.1,TWR_GLOW);
}
if(tier>=3){
m.bevelBox(-7.0,9.4,0,5.0,3.0,8.5,.5,TWR_COAT);
sensorMast(m,-8.0,12.4,0,4.5,TWR_MACH);
}
return m.build();
}
function mdlRailBase(tier){
const m=MB(); tier=tier||1;
const oct=r=>[[-r*.55,-r],[r*.55,-r],[r,-r*.55],[r,r*.55],
[r*.55,r],[-r*.55,r],[-r,r*.55],[-r,-r*.55]];
m.extrude(0,0,0,oct(21),4,NOVA_RAIL_CORE); // compact quiet foundation
m.extrude(0,4,0,oct(18.2),8,NOVA_RAIL_CORE); // heavy lower armour
m.extrude(0,12,0,oct(15.2),5,NOVA_RAIL_ARM); // inset second tier
for(let k=0;k<4;k++){
const a=k/4*TAU, x=Math.cos(a)*15.6, z=Math.sin(a)*15.6;
m.bevelBox(x,4,z,7.2,7.6,9.2,.75,NOVA_RAIL_CORE); // radial capacitor pod
m.cyl(x,11.6,z,2.4,1.9,3.2,10,NOVA_RAIL_TRIM);
m.box(x,14.6,z,3.2,.28,1.0,k%2?NOVA_RAIL_GLOW:TWR_TEAM,a);// restrained status strip
}
for(const s of [-1,1]){
m.box(0,16.7,s*11.8,10,.26,.46,TWR_TEAM);
m.box(s*11.8,16.7,0,.46,.26,10,NOVA_RAIL_GLOW);
}
m.cyl(0,17,0,12.5,12.5,2.0,18,NOVA_RAIL_CORE); // recessed rotation race
m.cyl(0,19,0,11.0,9.7,2.0,18,NOVA_RAIL_TRIM);
if(tier>=2) for(const s of [-1,1]) m.cyl(s*16.4,4,0,2.15,1.75,8.0,10,NOVA_RAIL_TRIM);
if(tier>=3) m.box(0,16.9,0,14,.22,.5,TWR_TEAM);
m.cyl(0,21,0,9.8,9.0,1.0,18,NOVA_RAIL_TRIM); // y=22 bearing surface
return m.build();
}
function mdlRailGun(tier){
const m=MB();
m.cyl(0,0,0,8.7,8.0,3.2,18,NOVA_RAIL_TRIM); // azimuth drum
m.cyl(0,3.2,0,7.7,7.1,1.2,18,NOVA_RAIL_TRIM);
m.bevelBox(-2.4,4.0,0,18.0,8.0,15.6,1.35,NOVA_RAIL_CORE); // armoured receiver
m.bevelBox(-4.2,12.0,0,7.2,2.2,8.4,.62,NOVA_RAIL_ARM); // quiet armour roof
m.box(-4.2,14.2,0,1.55,.18,5.8,TWR_TEAM); // narrow livery inset
m.bevelBox(-10.0,4.7,0,5.8,7.0,12.8,.9,NOVA_RAIL_CORE); // reactor bustle
for(const s of [-1,1]){
m.bevelBox(-8.2,7.0,s*7.15,4.8,4.2,1.9,.4,TWR_ARM); // capacitor cheeks
m.box(-8.2,10.4,s*7.2,2.9,.30,2.0,NOVA_RAIL_GLOW);
m.cyl(1.2,3.0,s*6.2,.82,.58,5.4,10,NOVA_RAIL_TRIM); // elevation pistons
m.bevelBox(13.2,7.55,s*2.9,20.0,2.8,1.85,.38,NOVA_RAIL_TRIM); // twin accelerator rails
}
m.bevelBox(4.5,5.7,0,5.4,6.8,10.2,.85,NOVA_RAIL_ARM); // reinforced mantlet
cylX(m,4.0,9.0,0,19.1,1.48,1.12,14,NOVA_RAIL_TRIM,false); // open central penetrator
for(const x of [6.8,12.5,18.2]){
m.bevelBox(x,6.45,0,1.35,5.15,8.5,.34,TWR_ARM_D); // heavy barrel braces
cylX(m,x-.32,9.0,0,.65,2.12,2.12,14,NOVA_RAIL_GLOW); // energized coil faces
}
for(const s of [-1,1]) m.box(13.2,10.85,s*1.45,17.4,.30,.38,NOVA_RAIL_GLOW);
m.bevelBox(23.15,6.35,0,3.0,5.3,8.1,.55,NOVA_RAIL_CORE); // rectangular muzzle casing
tubeX(m,23.1,9.0,0,1.72,2.38,1.02,14,TWR_BORE); // bore ends at x=24.82
m.cyl(-10.8,12.3,4.8,.22,.22,.28,8,HQ_LAMP);
if((tier||1)>=2) for(const s of [-1,1]) m.box(-8.2,12.0,s*7.2,2.2,.24,1.6,TWR_GLOW);
if((tier||1)>=3) cylX(m,4.0,6.4,0,17.0,.7,.5,10,NOVA_RAIL_GLOW,false);
return m.build();
}
function mdlSgen(tier){
const m=MB(); tier=tier||1;
towerPad(m,16,tier,false);
m.cyl(0,11,0,8.8,7.2,7.0,14,TWR_ARM);
m.cyl(0,18,0,6.9,5.5,3.0,14,TWR_MACH);
for(let k=0;k<4;k++){
const a=k/4*TAU+Math.PI/4,x=Math.cos(a)*10.8,z=Math.sin(a)*10.8;
m.bevelBox(x,10.0,z,4.2,7.5,4.2,.6,TWR_COAT,a);
m.cyl(x,17.5,z,1.35,.85,9.0+(tier-1)*1.5,8,TWR_MACH);
m.box(x,25.6+(tier-1)*1.5,z,1.2,.26,1.2,TWR_GLOW,a);
}
m.sphere(0,21.0,0,5.3+(tier-1)*.55,12,TWR_GLOW,.9,false);
ringX(m,0,21.0,0,7.0+(tier-1)*1.1,8.3+(tier-1)*1.1,18,TWR_TRIM);
if(tier>=2) ringX(m,0,21.0,0,9.2,9.8,18,TWR_GLOW);
if(tier>=3){
m.cyl(0,27.5,0,1.2,.6,6.5,8,TWR_MACH);
m.sphere(0,34.0,0,1.8,9,TWR_GLOW,1,false);
}
m.box(0,18.4,7.2,5.5,.24,.8,TWR_TEAM);
return m.build();
}
function mdlTgate(){
const m=MB();
m.box(0,0,0,72,5,60,CONC_D);
for(const s of [-1,1]){ // gantry arches
m.box(0,5,s*24,60,6,10,MET);
m.box(-26,11,s*24,8,30,10,MET_D);
m.box(26,11,s*24,8,30,10,MET_D);
}
m.box(0,41,0,66,7,58,MET); // overhead crane deck
m.box(0,5,0,44,3,34,DARKER); // build pad
m.cyl(0,8,0,16,15,4,14,ENERGY);
for(const s of [-1,1]) m.cyl(s*30,53.15,0,.36,.36,.28,8,HQ_LAMP);
/* A single identification rail on each crane face is enough for ownership
at command zoom; colouring the whole gantry would bury its steel finish. */
for(const s of [-1,1]) m.box(0,46.8,s*28.8,34,.28,.36,TWR_TEAM);
return m.build();
}
function mdlNest(){
/* Nest is Brood-owned at runtime (bldFactionKey forces horde). This Nova
fallback used to be a chitin mound — the exact cross-faction leak the
kit resolver exists to prevent if a preview or old save ever asks the
Frontline catalogue for `nest`. A containment bunker keeps the slot
readable as Nova without growing tissue. */
const m=MB();
const y=novaServicePad(m,41,35,2.4);
m.bevelBox(0,y,0,28.0,8.5,24.0,1.4,NOVA_BUNKER_ARM);
m.bevelBox(0,y+8.5,0,24.0,1.6,20.0,.55,NOVA_BUNKER_TRIM);
roofEdge(m,0,y+10.1,0,24.0,20.0,.7,NOVA_BUNKER_CORE,TWR_TRIM);
m.cyl(0,y+10.1,0,7.2,6.0,6.5,12,NOVA_BUNKER_CORE);
m.cyl(0,y+16.6,0,5.4,3.2,2.2,12,NOVA_BUNKER_TRIM);
m.tube(0,y+18.8,0,3.4,1.6,1.4,10,TWR_BORE);
m.ring(0,y+20.1,0,2.2,3.6,14,NOVA_BUNKER_GLOW);
for(const s of [-1,1]){
m.bevelBox(s*11.5,y+1.0,0,6.0,7.2,8.5,.7,NOVA_BUNKER_ARM);
m.box(s*11.5,y+8.15,0,3.4,.22,2.2,s>0?TWR_TEAM:TWR_GLOW);
sensorMast(m,s*11.5,y+8.4,s*6.2,5.2,NOVA_BUNKER_ARM);
}
return m.build();
}
function mdlHarbor(){
const m=MB();
const y=novaServicePad(m,73,43,2.5);
/* A U-shaped drydock survives the top-down camera much better than the old
shed plus flat rectangle. The empty dark slip is the focal shape; the
service block and crane make its launch direction (+X) unambiguous. */
m.bevelBox(-20.0,y,0,29.0,15.0,36.0,1.6,NOVA_HARBOR_ARM);
m.bevelBox(-20.0,y+15.0,0,27.0,1.8,34.0,.65,NOVA_HARBOR_DECK);
roofEdge(m,-20.0,y+16.8,0,27.0,34.0,.9,NOVA_HARBOR_CORE,NOVA_HARBOR_DECK);
m.bevelBox(14.0,y+.1,0,39.0,1.5,28.0,.45,TWR_BORE); // recessed ship cradle
for(const s of [-1,1]){
m.bevelBox(12.0,y+1.0,s*16.8,43.0,6.0,6.0,.8,NOVA_HARBOR_CORE);
m.box(12.0,y+6.9,s*16.8,31.0,.24,2.0,s>0?TWR_GLOW:TWR_TEAM);
for(const x of [-4,10,24]) m.cyl(x,y+7.0,s*16.8,1.2,.85,3.6,8,NOVA_HARBOR_ARM);
}
/* Twin launch rails are intentionally long, simple emissive strokes. */
for(const s of [-1,1]){
m.box(14.0,y+1.65,s*5.8,37.0,.45,.85,NOVA_UPLINK_GLOW);
m.box(14.0,y+1.55,s*7.2,37.0,.65,.65,NOVA_HARBOR_CORE);
}
m.bevelBox(-29.0,y+16.8,-9.5,10.0,13.0,12.0,1.0,NOVA_HARBOR_CORE);
m.bevelBox(-29.0,y+29.8,-9.5,11.0,2.0,13.0,.5,NOVA_HARBOR_DECK);
m.box(-23.7,y+23.2,-9.5,.5,5.5,8.5,NOVA_HARBOR_GLASS);
sensorMast(m,-29.0,y+31.8,-9.5,6.5,NOVA_HARBOR_ARM);
m.box(-5.0,y+18.6,15.4,11.0,.24,.45,TWR_TEAM);
return m.build();
}
function mdlBastionBase(tier){
const m=MB(); tier=tier||1;
towerPad(m,21,tier,false);
m.extrude(0,11,0,towerOct(17.2),6,TWR_ARM_D);
m.extrude(0,17,0,towerOct(14.4),4,TWR_ARM);
for(const a of [0,Math.PI/2,Math.PI,Math.PI*1.5]){
const x=Math.cos(a)*15.0,z=Math.sin(a)*15.0;
m.bevelBox(x,12,z,6.8,7.4,8.2,.7,TWR_COAT,a);
m.box(x,19.2,z,3.4,.25,1.0,a===0?TWR_TEAM:TWR_GLOW,a);
}
if(tier>=2) for(const s of [-1,1]){
m.cyl(-7.5,18,s*10.2,2.4,2.0,4.1,10,TWR_MACH);
m.box(-7.5,22.0,s*10.2,3.1,.24,1.0,TWR_GLOW);
}
if(tier>=3) m.bevelBox(-10.5,18,0,5.5,4.2,11.0,.6,TWR_ARM);
m.cyl(0,20.6,0,12.2,12.2,1.2,18,TWR_COAT);
m.cyl(0,21.8,0,10.8,9.8,1.2,18,TWR_TRIM); // y=23 bearing
return m.build();
}
function mdlBastionGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,9.2,8.5,3.0,18,TWR_MACH);
m.bevelBox(-2.5,3.0,0,19.0,8.8,17.2,1.35,TWR_ARM_D);
m.bevelBox(-8.8,4.3,0,6.3,7.0,13.5,.9,TWR_COAT); // shell hoist
for(const s of [-1,1]){
m.cyl(-7.8,5.0,s*7.8,2.7,2.4,5.2,10,TWR_MACH); // magazine drums
m.box(-7.8,10.0,s*7.8,3.2,.25,1.2,TWR_GLOW);
m.cyl(2.5,2.8,s*6.3,.9,.65,5.0,10,TWR_MACH); // elevation rams
}
m.bevelBox(4.0,5.0,0,7.0,7.0,12.5,.9,TWR_ARM); // mantlet
/* A short, unmistakably broad mortar with an actual inner tube. The old gun
was a capped pencil and read as a generic cannon from command view. */
cylX(m,4.6,9.5,0,11.0,3.45,2.75,14,TWR_MACH,false);
for(const x of [6.3,10.6]) cylX(m,x,9.5,0,.75,4.05,4.05,14,TWR_TRIM);
m.bevelBox(14.2,6.0,0,3.2,7.0,8.8,.55,TWR_ARM_D);
tubeX(m,14.3,9.5,0,2.0,4.15,2.45,14,TWR_BORE);
m.bevelBox(-3.0,11.8,0,8.5,2.1,9.0,.55,TWR_ARM);
m.box(-3.0,13.8,0,1.5,.22,5.5,TWR_TEAM);
if(tier>=2) for(const s of [-1,1]){
m.bevelBox(-10.4,9.3,s*5.6,4.1,4.2,2.0,.4,TWR_ARM);
m.box(-10.4,13.3,s*5.65,2.4,.22,2.1,TWR_GLOW);
}
if(tier>=3){
m.bevelBox(-9.0,12.0,0,5.5,3.4,10.2,.6,TWR_COAT);
sensorMast(m,-10.2,15.4,0,5.2,TWR_MACH);
}
return m.build();
}
function mdlTechlab(){
const m=MB();
const y=novaServicePad(m,47,39,2.5);
/* The old hemisphere dominated every material underneath it. A stepped
data vault with a small, exposed analysis core keeps the science identity
while leaving broad wall/roof planes for the architectural PBR kit. */
/* Research is a Nova landmark, not a recoloured utility shed. Its unique
semantic selectors keep armour, recesses and roof response separated even
before its authored BaseAO/NRE/mask pack is baked. */
m.bevelBox(-3.0,y,0,34.0,13.0,29.0,1.8,NOVA_RESEARCH_ARM);
m.bevelBox(-3.0,y+13.0,0,31.0,1.8,26.0,.7,NOVA_RESEARCH_ROOF);
roofEdge(m,-3.0,y+14.8,0,31.0,26.0,.85,NOVA_RESEARCH_CORE,TWR_TRIM);
for(const s of [-1,1]){
m.bevelBox(-15.5,y+3.0,s*14.8,6.5,12.0,7.0,.9,NOVA_RESEARCH_CORE);
m.box(-15.5,y+14.7,s*14.8,3.4,.24,3.5,s>0?TWR_GLOW:TWR_TEAM);
}
m.cyl(7.0,y+14.9,0,7.2,6.2,3.0,14,NOVA_RESEARCH_ARM);
m.cyl(7.0,y+17.9,0,4.8,3.5,4.0,12,NOVA_RESEARCH_CORE);
m.sphere(7.0,y+22.8,0,3.5,11,TWR_GLOW,1,false);
ringX(m,7.0,y+22.8,0,5.0,5.8,16,TWR_TRIM);
m.bevelBox(17.0,y,0,10.0,10.0,18.0,1.0,NOVA_RESEARCH_CORE);
for(const s of [-1,1]) m.box(17.0,y+6.0,s*9.1,6.0,2.6,.42,NOVA_RESEARCH_GLASS);
m.cyl(-8.5,y+16.6,0,1.0,.7,8.5,8,TWR_MACH);
ringX(m,-8.5,y+25.1,0,3.5,5.4,16,TWR_ARM);
ringX(m,-8.45,y+25.1,0,.9,1.5,12,TWR_GLOW);
return m.build();
}
function mdlAA(tier){
const m=MB(); tier=tier||1;
const oct=r=>[[-r*.55,-r],[r*.55,-r],[r,-r*.55],[r,r*.55],
[r*.55,r],[-r*.55,r],[-r,r*.55],[-r,-r*.55]];
m.extrude(0,0,0,oct(15),3,TWR_PAD); // compact stabilised pad
m.extrude(0,3,0,oct(12.5),5,TWR_ARM_D); // faceted armour skirt
m.extrude(0,8,0,oct(10.2),2,TWR_ARM); // upper equipment tier
for(const s of [-1,1]){
m.bevelBox(s*10.6,3,0,4.2,4.0,8.2,.55,TWR_COAT); // power housings
m.box(s*10.6,7.0,0,1.3,.24,4.4,s>0?TWR_GLOW:TWR_TEAM);
}
if(tier>=2) for(const s of [-1,1]){
m.cyl(s*11.4,8.2,0,1.55,1.25,2.6,10,TWR_MACH);
m.box(s*11.4,10.9,0,1.0,.20,3.0,TWR_GLOW);
}
if(tier>=3){
m.box(0,10.35,0,1.05,.18,7.2,TWR_TEAM);
m.sphere(0,12.2,8.4,1.3,8,GLASS,.72,true);
}
m.cyl(0,9.55,0,7.6,7.0,.6,14,TWR_COAT); // azimuth shadow gap
m.cyl(0,10.15,0,6.8,6.25,.85,14,TWR_TRIM); // y=11 bearing surface
return m.build();
}
function mdlAAGun(tier){
const m=MB();
m.cyl(0,0,0,6.5,6.0,2.2,14,TWR_MACH); // rotating race
m.cyl(0,2.2,0,5.9,5.35,.9,14,TWR_TRIM);
m.bevelBox(-1.0,3.0,0,10.8,5.0,10.0,.82,TWR_ARM_D); // gun director body
m.bevelBox(-3.0,8.0,0,5.8,1.7,6.2,.42,TWR_ARM); // quiet IFF roof
m.box(-3.0,9.7,0,1.15,.16,4.3,TWR_TEAM); // narrow team identifier
for(const s of [-1,1]){
m.bevelBox(-2.6,4.2,s*5.35,4.8,3.0,1.45,.32,TWR_COAT); // feed boxes
m.box(-2.6,7.2,s*5.4,2.8,.24,1.55,TWR_GLOW); // ammunition status
m.box(7.0,5.9,s*2.4,8.2,1.0,.72,TWR_MACH); // barrel cradle
gunX(m,1.8,7.0,12.8,.44,TWR_MACH,s*2.4); // separated autocannons
}
m.bevelBox(2.0,4.8,0,3.6,4.2,7.0,.55,TWR_ARM); // armoured mantlet
m.cyl(-4.5,9.7,0,1.15,.82,2.8,10,TWR_MACH); // radar pedestal
m.sphere(-4.5,12.5,0,1.65,10,GLASS,.72,true); // tracking radar dome
m.cyl(-4.5,9.55,3.8,.20,.20,.24,8,HQ_LAMP); // acquisition lamp
if((tier||1)>=2) m.box(0,9.2,0,8.4,.18,1.0,TWR_GLOW);
if((tier||1)>=3) for(const s of [-1,1]) gunX(m,1.8,5.2,11.2,.36,TWR_MACH,s*4.4);
return m.build();
}
function mdlAirfield(){
const m=MB();
const y=novaServicePad(m,91,41,2.0);
/* Broad uninterrupted deck bands read as a runway at gameplay zoom. Seven
raised centreline blocks used to turn it into a ladder and added collision-
looking clutter despite being purely decorative. */
m.bevelBox(4.0,y,0,74.0,1.2,25.0,.45,NOVA_AIR_DECK);
m.box(4.0,y+1.2,0,68.0,.20,1.2,TWR_TEAM);
for(const s of [-1,1]){
m.box(5.0,y+1.22,s*10.5,70.0,.20,.42,TWR_GLOW);
for(const x of [-24,0,24,38]) m.cyl(x,y+1.32,s*10.5,.22,.22,.20,8,HQ_LAMP);
}
/* One deep hangar and a distinct glazed flight-control tower form an
asymmetrical silhouette while preserving most of the deck as negative
space. */
m.bevelBox(-30.5,y,-10.0,28.0,14.0,18.0,1.5,NOVA_AIR_ARM);
m.bevelBox(-30.5,y+14.0,-10.0,29.5,2.0,19.5,.7,NOVA_AIR_ROOF);
roofEdge(m,-30.5,y+16.0,-10.0,29.5,19.5,.8,NOVA_AIR_DECK,NOVA_AIR_ROOF);
m.box(-15.9,y+2.0,-10.0,.6,10.0,13.0,TWR_BORE);
m.box(-15.5,y+12.0,-10.0,.45,.25,10.0,TWR_TEAM);
m.bevelBox(34.0,y,13.0,12.0,20.0,13.0,1.0,NOVA_AIR_DECK);
m.bevelBox(34.0,y+20.0,13.0,15.0,5.5,15.0,.8,NOVA_AIR_ARM);
for(const s of [-1,1]) m.box(34.0,y+21.0,13.0+s*7.35,10.0,3.2,.45,NOVA_AIR_GLASS);
m.box(26.7,y+21.0,13.0,.45,3.2,10.0,NOVA_AIR_GLASS);
sensorMast(m,34.0,y+25.5,13.0,7.0,TWR_MACH);
return m.build();
}
function mdlUplink(tier){
const m=MB(); tier=tier||1;
towerPad(m,15,tier,false);
m.bevelBox(-4.2,11,0,12,9,18,.9,NOVA_UPLINK_CORE); // electronics bunker
m.bevelBox(-4.2,20,0,9.5,2.0,13,.5,NOVA_UPLINK_ARM);
m.box(-4.2,21.9,0,1.3,.20,8.0,TWR_TEAM);
for(const s of [-1,1]){
m.cyl(5.0,11,s*5.0,1.1,.72,19+(tier-1)*2.0,7,NOVA_UPLINK_TRIM);
m.box(5.0,29+(tier-1)*2.0,s*5.0,1.0,.22,1.0,NOVA_UPLINK_GLOW);
}
m.cyl(5.0,11,0,2.0,1.55,16+(tier-1)*2.0,10,NOVA_UPLINK_CORE);
m.bevelBox(5.0,27+(tier-1)*2.0,0,8.2,4.6,6.2,.7,NOVA_UPLINK_CORE);
/* The contact-sheet Targeting Array is defined by its dish, so the dish is
larger than the mast and visibly hollow even in the 32 px build thumbnail. */
ringX(m,5.0,35+(tier-1)*2.0,0,5.4+(tier-1)*.8,8.7+(tier-1)*1.0,20,NOVA_UPLINK_ARM);
ringX(m,5.05,35+(tier-1)*2.0,0,1.1,2.0,14,NOVA_UPLINK_GLOW);
cylX(m,5.0,35+(tier-1)*2.0,0,3.4,1.0,.6,10,NOVA_UPLINK_TRIM,false);
if(tier>=2) for(const s of [-1,1]) sensorMast(m,-8.5,22,s*5.0,8,NOVA_UPLINK_TRIM);
if(tier>=3) m.sphere(8.6,35+(tier-1)*2.0,0,1.6,10,NOVA_UPLINK_GLOW,1,false);
return m.build();
}
/* ============================================================================
HEADQUARTERS
----------------------------------------------------------------------------
The first version was the carrier hull parked on the ground: a single
extruded arrowhead in vehicle plating. From a top-down command camera that
reads as a dark blob, because a building seen from above is almost entirely
ROOF, and an arrowhead has no roof plan to read.
This is authored as architecture instead. A stepped mass — apron, main
block, inset roof deck — with four corner buttress towers that give the
silhouette its square footprint, a command tower at the back, and a landing
pad at the front. Everything is laid out on the plan the player is actually
looking down at: pad, walkway, vent banks, mast. Wall and roof use the
architectural materials, whose texture frequency is a third of vehicle
plating, so a facade shows two or three real bays instead of dissolving.
============================================================================ */
function mdlHQ(){
const m=MB();
const y=novaServicePad(m,87,73,3.2,true);
/* Three large masses replace the former collection of corner towers,
greebles and transformer boxes. The HQ remains the broadest Nova
silhouette while now fitting its real 88x74 collision plot. */
/* High-value command deck: the HQ is the player's visual anchor and needs
to separate from grass at a glance. Dark material is reserved for seams,
vents and the open bay, never the whole roof. */
m.bevelBox(-5.0,y,0,68.0,18.0,55.0,2.8,NOVA_HQ_ARM);
m.bevelBox(-5.0,y+18.0,0,61.0,3.0,49.0,1.0,NOVA_HQ_ROOF);
roofEdge(m,-5.0,y+21.0,0,61.0,49.0,1.2,NOVA_HQ_STRUCT,NOVA_HQ_ROOF);
for(const s of [-1,1]){
m.bevelBox(-33.5,y,s*25.5,14.0,23.0,17.0,1.5,NOVA_HQ_STRUCT);
m.bevelBox(-33.5,y+23.0,s*25.5,15.5,1.8,18.5,.65,NOVA_HQ_ROOF);
m.box(-33.5,y+15.0,s*34.2,8.0,4.5,.45,NOVA_HQ_GLASS);
m.box(-33.5,y+24.65,s*25.5,2.4,.16,2.4,TWR_COAT);
/* Both wings get a point lamp. TWR_TEAM here was a dark socket after
glowOk, and a cyan team bevel before it. */
m.cyl(-33.5,y+24.85,s*25.5,.44,.44,.30,8,HQ_LAMP);
}
/* Raised command block and glazing band anchor the rear half. */
m.bevelBox(-23.0,y+21.0,0,25.0,17.0,31.0,2.0,NOVA_HQ_ARM);
m.bevelBox(-23.0,y+38.0,0,27.0,2.2,33.0,.8,NOVA_HQ_ROOF);
roofEdge(m,-23.0,y+40.2,0,27.0,33.0,.9,NOVA_HQ_STRUCT,NOVA_HQ_ROOF);
for(const s of [-1,1]) m.box(-23.0,y+31.0,s*15.7,18.0,5.0,.50,NOVA_HQ_GLASS);
m.box(-10.2,y+31.0,0,.50,5.0,23.0,NOVA_HQ_GLASS);
ventBank(m,-23.0,y+42.1,0,10.0,10.0,4,TWR_MACH,0);
sensorMast(m,-31.0,y+42.0,-8.0,9.0,TWR_MACH);
for(const s of [-1,1]) m.cyl(-23.0+s*11.2,y+40.85,s*14.6,.38,.38,.26,8,HQ_LAMP);
for(const s of [-1,1]) m.cyl(-5.0+s*28.0,y+22.15,s*22.6,.34,.34,.22,8,HQ_LAMP);
/* The front deck stays intentionally open. Two rings and cross hairs are
enough to read as a landing pad without a field of lamps. TWR_TEAM on
the inner ring was Nova livery — the cyan crush at HIGH. */
m.cyl(17.0,y+21.0,0,15.0,15.0,1.0,20,NOVA_HQ_ROOF);
m.ring(17.0,y+22.0,0,12.7,14.6,22,NOVA_HQ_STRUCT);
m.ring(17.0,y+22.1,0,9.2,10.4,22,TWR_TRIM);
m.box(17.0,y+22.18,0,16.4,.16,1.15,TWR_TRIM);
m.box(17.0,y+22.18,0,1.15,.16,16.4,TWR_TRIM);
m.cyl(17.0,y+22.32,0,.62,.62,.28,10,HQ_LAMP);
for(const s of [-1,1]){
m.cyl(17.0+s*7.4,y+22.30,0,.38,.38,.22,8,HQ_LAMP);
m.cyl(17.0,y+22.30,s*7.4,.38,.38,.22,8,HQ_LAMP);
}
/* Recessed deployment bay and ramp make +X the readable front. */
m.box(29.3,y+2.0,0,.65,13.0,27.0,TWR_BORE);
for(const s of [-1,1]){
m.bevelBox(30.3,y+1.0,s*14.5,3.0,16.0,3.2,.45,TWR_ARM_D);
/* Door lamps, not 8-unit glow slabs. The old strip bloomed into a
cyan column at the bay cheek. */
m.cyl(30.8,y+10.2,s*12.7,.22,.22,.28,8,HQ_LAMP);
m.cyl(30.8,y+4.4,s*12.7,.22,.22,.28,8,TWR_TRIM);
}
m.bevelBox(30.3,y+14.0,0,3.0,4.0,32.0,.55,TWR_ARM);
m.box(30.8,y+17.8,0,.30,.24,22.0,TWR_TRIM);
m.bevelBox(36.5,y-.3,0,13.0,1.4,27.0,.4,TWR_PAD);
return m.build();
}
/* ---------------------------------------------------------------------------
ORBITAL DROPSHIP — a spacecraft, not a building
The pre-deployment carrier was borrowing the DEPLOYED HQ's mesh, which is
why it read as a bunker sliding around the map instead of the ship its own
icon promises. This is a proper hull: long spine, swept lifting body, engine
nacelles with lit bells, cargo pods slung underneath, and gear that only
comes down at touchdown. It flies nose-first and banks into its turns.
--------------------------------------------------------------------------- */
function mdlDropship(){
const m=MB();
/* One load-bearing lifting body. The previous craft intentionally left
daylight around its nacelles, but on a phone those islands read as broken
model pieces. The shoulders now include the engines inside the same plan
silhouette; only retractable landing gear is a separate draw. */
const hull=[[-54,-7],[-42,-14],[-19,-21],[12,-20],[38,-12],[56,0],[38,12],[12,20],[-19,21],[-42,14],[-54,7]];
const upper=[[-46,-5],[-31,-11],[-8,-14],[20,-13],[41,-7],[50,0],[41,7],[20,13],[-8,14],[-31,11],[-46,5]];
const crown=[[-33,-4],[-18,-8],[10,-9],[31,-6],[43,0],[31,6],[10,9],[-18,8],[-33,4]];
m.extrude(0,1,0,hull,5.8,NOVA_MET); // continuous light steel keel
m.extrude(0,6.8,0,upper,4.8,MET_L); // bright lifting-body shoulder
m.extrude(6,11.6,0,crown,2.0,MET_L); // command deck
m.extrude(5,13.7,0,crown.map(p=>[p[0]*.82,p[1]*.72]),.42,TEAM_A);
/* The old bridge was still a half-height sphere parked on the hull. From the
command camera it read as a flattened bubble, not pressure glazing. Build
the canopy as a raked recessed volume with an armoured sill and mullions;
every glass edge now physically disappears into metal. */
m.wedge(32.5,13.9,0,13.8,2.0,21.5,NOVA_CARB,Math.PI/2,true); // armoured tub
m.wedge(32.8,15.1,0,11.3,4.7,18.2,DROP_CANOPY_GLASS,Math.PI/2,true); // faceted pressure canopy
for(const sd of [-1,1]){
m.box(32.0,15.0,sd*5.45,18.0,1.15,.85,NOVA_MET); // side sill
m.box(30.0,18.7,sd*4.6,1.05,.78,8.6,NOVA_MET); // rear frame
}
m.box(37.2,17.0,0,1.05,.72,10.4,NOVA_MET); // forward frame
m.wedge(43.0,13.7,0,11.8,3.0,12.0,NOVA_MET,Math.PI/2,true); // nose closure
for(const sd of [-1,1]){
/* One continuous side sponson replaces the old stack of shoulder, pod,
wedge, cylinder and loose nozzle. It overlaps the fuselage by a full
eight units and terminates in one integrated cruise bell. The four VTOL
units attach to the sockets below as genuinely animated sub-meshes. */
m.bevelBox(-9,5.8,sd*10.4,55,7.2,8.8,1.15,NOVA_MET);
m.wedge(15,11.4,sd*11.0,27,4.8,8.6,NOVA_MET,0,sd<0);
m.box(-7,12.1,sd*10.2,34,.9,2.1,NOVA_CARB);
m.bevelBox(-34,7.0,sd*10.5,19,8.2,9.2,1.15,MET_D);
tubeX(m,-45.2,8.4,sd*10.5,6.2,4.8,3.6,16,DARKER);
tubeX(m,-49.4,8.4,sd*10.5,3.2,3.5,1.7,16,ENERGY);
m.box(-6,13.1,sd*14.6,28,.34,1.25,TEAM_A);
for(const x of [-15,16]){
m.bevelBox(x,4.8,sd*13.2,11.8,5.4,7.8,.85,NOVA_CARB); // VTOL load-bearing socket
m.box(x,10.0,sd*13.2,7.2,.34,5.0,TWR_MACH); // visible rotation bearing
}
}
/* Cargo keel and recessed lift apertures make the eventual base deployment
mechanically plausible without reading as additional loose vehicles. */
m.bevelBox(4,.1,0,48,3.8,16,1.2,DARKER);
m.box(20,-1.85,0,14,.5,12,ENERGY);
for(const x of [-17,12])for(const sd of [-1,1]){
m.bevelBox(x,1.0,sd*9.7,10.5,4.0,7.8,.85,NOVA_CARB); // turbine fairing
m.cyl(x,1.8,sd*10.4,4.0,3.6,1.0,16,TWR_MACH);
m.ring(x,2.85,sd*10.4,3.2,4.5,18,ENERGY);
}
glowStrip(m,3,14.25,0,43,ENERGY,0);
sensorMast(m,-28,14.0,0,5.5,MET_L);
return m.build();
}
/* Four tilting ducted-lift units. The hinge is evaluated by the existing GPU
bone path: high-altitude cruise rotates the ducts aft; descent brings them
upright. This restores the original lander's transforming-VTOL idea without
another CPU animation system or four extra per-frame model rebuilds. */
function mdlDropVtol(){
const m=MB(),hinge=m.joint([0,8,0],null,[0,0,1],0,1.04,0);
m.bone(hinge);
/* An open rectangular duct reads at phone zoom and leaves the fan visible;
a solid cylinder looked like another disconnected barrel. */
m.bevelBox(0,3.2,-5.0,14.2,8.4,2.2,.65,NOVA_MET);
m.bevelBox(0,3.2, 5.0,14.2,8.4,2.2,.65,NOVA_MET);
m.bevelBox(-6.0,3.2,0,2.2,8.4,8.0,.65,NOVA_MET);
m.bevelBox( 6.0,3.2,0,2.2,8.4,8.0,.65,NOVA_MET);
m.ring(0,11.7,0,4.2,6.5,20,TWR_MACH);
m.ring(0,2.8,0,4.6,6.1,20,DARKER);
m.box(0,8.0,-6.2,8.0,.34,.75,TEAM_A);
m.box(0,8.0, 6.2,8.0,.34,.75,TEAM_A);
m.box(-6.9,7.4,0,.45,2.4,5.2,ENERGY);
m.box( 6.9,7.4,0,.45,2.4,5.2,ENERGY);
m.bone(null);
return m.build();
}
/* Visible rotor, separate so its yaw can spin continuously instead of merely
oscillating with the tilt hinge. It is only shown while the duct is near
vertical; in cruise the fan is edge-on and the aft bells carry the thrust. */
function mdlDropRotor(){
const m=MB();
m.cyl(0,0,0,1.15,1.15,1.0,12,TWR_MACH,true);
for(let k=0;k<4;k++)m.box(3.4,.28,0,6.0,.34,1.05,TWR_TRIM,k*Math.PI*.5);
m.ring(0,.72,0,5.7,6.25,20,ENERGY);
return m.build();
}
/* Landing gear, drawn only once the ship is on the deck. */
function mdlDropGear(){
const m=MB();
/* Two continuous landing skids replace four isolated black feet. Each skid
overlaps the belly fairing for most of its length, while paired oleos make
the load path visible at the near-vertical mobile camera. */
for(const sd of [-1,1]){
const z=sd*10.2;
m.bevelBox(-2,.05,z,51,1.7,4.8,.6,DARKER);
m.bevelBox(-2,1.55,z,45,2.0,4.2,.6,NOVA_CARB);
for(const x of [-18,14]){
m.wedge(x,3.35,z,8.6,4.7,4.3,MET_D,0,sd<0);
m.cyl(x,2.15,z,1.45,1.2,4.7,8,TWR_MACH);
m.box(x,7.65,z,7.2,1.35,4.6,NOVA_MET);
}
}
return m.build();
}
/* A deployer is the first full-screen faction silhouette a player sees. Reusing
the Nova carrier for every army made the later faction geometry feel like a
skin swap before the match had even begun. These craft share no hull: the
Legion lands a slab-armoured assault brick, the Syndicate rides a thin hover
delta, and the Brood arrives inside a living drop-organism. */
function mdlLegionDropship(){
const m=MB();
m.extrude(-2,0,0,[[-45,-17],[-35,-24],[27,-24],[44,-14],[50,0],[44,14],[27,24],[-35,24],[-45,17]],13,TWR_ARM_D);
m.bevelBox(-8,13,0,68,12,34,2.2,TWR_ARM);
m.wedge(34,20,0,26,8,25,TWR_COAT); // breaching prow
m.bevelBox(25,28,0,18,7,18,1.4,TWR_ARM);
m.box(35,31,0,1.2,3.4,12,GLASS);
for(const sd of [-1,1]){
m.bevelBox(-2,8,sd*24,52,15,12,1.8,TWR_COAT); // troop cassettes
m.box(2,23.2,sd*24,29,.35,10,TEAM_A);
m.wedge(19,13,sd*24,18,12,12,TWR_ARM,0,sd<0);
m.bevelBox(-34,12,sd*21,18,18,15,1.5,TWR_ARM_D);
for(const z of [sd*17,sd*25]){
m.cyl(-45,12,z,5.0,4.2,6.0,12,TWR_MACH);
m.cyl(-51.2,12,z,3.8,2.8,2.2,12,HOT);
}
}
m.bevelBox(-38,23,0,18,12,20,1.6,TWR_COAT); // armoured reactor keep
m.bevelBox(-11,-7,0,55,6,28,1.4,TWR_ARM_D); // siege landing keel
m.box(16,-10.2,0,22,.28,16,HOT); // hot deploy-ramp seam
for(let k=-1;k<=1;k++){
m.tube(-48,29,k*6.2,1.35,.78,5.0,9,TWR_BORE,0);
m.box(-42,33,k*6.2,10,.5,2.1,HOT);
}
sensorMast(m,-14,35,0,9,TWR_MACH); // assault command mast
return m.build();
}
function mdlLegionDropGear(){
const m=MB();
for(const sd of [-1,1]) for(const f of [-1,1]){
const x=f*30,z=sd*23;
m.bevelBox(x,0,z,14,3.0,10,.75,TWR_PAD);
m.cyl(x,3,z,2.4,1.8,11,9,TWR_MACH);
m.box(x,12,z,4.2,8,4.2,TWR_ARM_D);
}
return m.build();
}
function mdlSyndicateDropship(){
const m=MB();
/* A shallow manta plan keeps the machine visibly agile even when stationary;
the separated lift rings replace landing struts entirely. */
m.extrude(2,1,0,[[-38,-8],[-20,-16],[10,-28],[43,-11],[52,0],[43,11],[10,28],[-20,16],[-38,8]],6,TWR_COAT);
m.extrude(5,7,0,[[-31,-5],[-13,-10],[21,-16],[43,-7],[49,0],[43,7],[21,16],[-13,10],[-31,5]],4,TEAM_A);
m.wedge(31,12,0,28,5,14,TWR_ARM,0,false);
m.box(43,15,0,.8,2.5,8,GLASS);
for(const sd of [-1,1]){
m.wedge(1,8,sd*19,44,3,13,TWR_ARM_D,sd*.08,sd<0);
m.box(3,11.2,sd*23,25,.28,2.0,TEAM_T,sd*.08);
for(const x of [-22,0,22]){
m.cyl(x,2.4,sd*18,2.2,1.8,1.0,14,TWR_MACH);
m.ring(x,3.6,sd*18,2.5,4.4,18,ENERGY);
}
}
m.ring(-22,12,0,3.6,6.8,20,ENERGY);
m.sphere(-22,12,0,3.2,10,GLASS,.92,false);
m.ring(7,3,0,8.0,11.8,22,ENERGY); // active teleport keel
m.box(13,1.6,0,22,.45,15,GLASS); // luminous cargo aperture
for(const sd of [-1,1]) m.tube(-38,7,sd*7,2.2,1.2,2.2,12,ENERGY,0);
return m.build();
}
function mdlHordeDropship(){
const m=MB();
/* No pad, plate, glass, bolt, nozzle or manufactured edge: this is an animal
that survives atmospheric entry and disgorges a hive, not a ship wearing a
biological decal. BIO_MEM/BIO_LEG also opt into the spring shader. */
m.sphere(-4,7,0,18,16,CHITIN,.62,false); // central thorax
m.sphere(-27,7,0,14,14,CHIT_D,.78,false); // brood abdomen
m.sphere(18,8,0,11,13,CHITIN,.72,false); // head shield
m.sphere(4,1,0,11,12,BIO_MEM,.44,false); // hanging brood womb
m.ring(4,0,0,5.8,9.2,16,BIO_TEAM);
for(const sd of [-1,1]){
m.sphere(-19,12,sd*10,7,10,BIO_TEAM,.72,false); // luminous brood sacs
/* Three separated, tapered petals leave daylight between their outlines.
That negative space and the beaded veins make them read as living wings
rather than the former single aircraft-like planar panel. */
/* Overlapping flattened lobes produce scalloped, curved membranes with no
planar face large enough to be mistaken for a metal wing. */
for(let k=0;k<8;k++){
const q=k/7,r=4.8-q*2.5;
m.sphere(-15+q*25,8.2+Math.sin(q*Math.PI)*2.0,sd*(5+q*40),r,8,BIO_MEM,.20,false);
m.sphere(-15+q*25,9.0,sd*(5+q*40),.88-q*.42,7,BIO_LEG,.56,false);
}
for(let k=0;k<7;k++){
const q=k/6,r=4.2-q*2.3;
m.sphere(-23+q*17,6.7+Math.sin(q*Math.PI)*1.5,sd*(5+q*31),r,8,BIO_MEM,.20,false);
m.sphere(-23+q*17,7.5,sd*(5+q*31),.76-q*.34,7,BIO_LEG,.56,false);
}
for(let k=0;k<6;k++){
const q=k/5,r=3.7-q*2.0;
m.sphere(-29+q*11,5.5+Math.sin(q*Math.PI),sd*(4+q*25),r,8,BIO_MEM,.20,false);
m.sphere(-29+q*11,6.2,sd*(4+q*25),.66-q*.28,6,BIO_LEG,.56,false);
}
for(let k=0;k<6;k++) m.sphere(8+k*2.2,7.5-k*.18,sd*(7-k*.8),1.0-k*.10,7,k<4?BIO_LEG:CLAW,.62,false);
m.sphere(24,12,sd*5,2.2,8,LAMP,1,false); // compound eye
for(let k=0;k<3;k++){
for(let j=0;j<5;j++)
m.sphere(-7-k*6-j*2.3,4-j*1.25,sd*(8+k*3+j*.8),.82-j*.09,6,BIO_LEG,.64,false);
}
}
for(let k=0;k<7;k++){
const a=-1.1+k*.36;
m.cyl(-20-k*1.2,18+Math.sin(a)*2,Math.cos(a)*5,.78,.03,7+k*.6,7,CLAW);
}
for(const sd of [-1,1]) for(let k=0;k<4;k++)
m.sphere(4-k*4.3,1.5-k*.9,sd*(3+k*2.2),1.15-k*.12,7,BIO_LEG,.60,false);
return m.build();
}
function mdlHordeDropGear(){
const m=MB();
for(const sd of [-1,1]) for(const f of [-1,1]){
const x=f*20,z=sd*12;
for(let j=0;j<5;j++)
m.sphere(x+f*j*2.2,7-j*1.55,z+sd*j*1.7,1.15-j*.12,7,BIO_LEG,.68,false);
m.sphere(x+f*9,.5,z+sd*8,2.1,7,CLAW,.7,false);
}
return m.build();
}
const DROP_MDL={
nova:mdlDropship,
legion:mdlLegionDropship,
syndicate:mdlSyndicateDropship,
horde:mdlHordeDropship
};
const DROP_GEAR_MDL={nova:mdlDropGear,legion:mdlLegionDropGear,horde:mdlHordeDropGear};
const DROP_VTOL_MDL={nova:mdlDropVtol};
const DROP_ROTOR_MDL={nova:mdlDropRotor};
/* Profiles keep exhaust/hover cues attached to authored hardpoints. Aliases
consolidate Brood/Swarm/Infestation into one biology without presenting
neutral wildlife as a fifth art faction. */
const DROP_PROFILE={
/* The command camera views the lander through terrain shadow and UI bloom.
A deeper blue multiplication looked nearly black there, so this is a
light steel-blue hull tint; dedicated team panels still carry the banner. */
nova:{scale:1.08,team:[112,124,136],glow:[142,228,255],ring:[170,224,255],eng:[[-49,-10],[-49,10]],vtol:[[-15,-15],[-15,15],[16,-15],[16,15]],lights:[[42,0],[15,-15],[15,15],[-30,0]]},
legion:{scale:1.12,team:[255,120,90],glow:[255,116,62],ring:[255,142,82],eng:[[-51,-25],[-51,-17],[-51,17],[-51,25]],lights:[[39,0],[15,-25],[15,25],[-35,0]]},
syndicate:{scale:1.02,team:[150,235,95],glow:[138,255,112],ring:[152,255,126],eng:[[-38,-7],[-38,7],[-22,-18],[-22,18]],lights:[[45,0],[12,-24],[12,24],[-27,0]],hover:true},
horde:{scale:1.04,team:[186,120,255],glow:[188,92,255],ring:[140,238,92],eng:[[-27,-10],[-27,10],[-16,-15],[-16,15]],lights:[[23,0],[-5,-20],[-5,20],[-25,0]],bio:true}
};
const DROP_ALIASES={
terran:'nova',frontline:'nova',federation:'nova',
ascendancy:'legion',red:'legion',
machine:'syndicate',coalition:'syndicate',
brood:'horde',swarm:'horde',infestation:'horde'
};
const DROP_MESH={};
function dropFactionKey(k){
k=String(k||'nova').trim().toLowerCase();
if(DROP_MDL[k]) return k;
if(DROP_ALIASES[k]) return DROP_ALIASES[k];
/* Resolve full lore/doctrine labels as well as compact simulation IDs. Keep
Machine Ascendancy ahead of the generic Ascendancy alias. */
if(k.includes('machine ascendancy')||k.includes('syndicate')||k.includes('coalition')) return 'syndicate';
if(k.includes('infestation')||k.includes('brood')||k.includes('swarm')) return 'horde';
if(k.includes('terran frontline')||k.includes('nova federation')||k.includes('frontline')||k.includes('federation')) return 'nova';
if(k.includes('red ascendancy')||k.includes('bloodward')||k.includes('legion')||k.includes('ascendancy')) return 'legion';
return 'nova';
}
function mdlHellstormBase(tier){
const m=MB(); tier=tier||1;
towerPad(m,17,tier,false);
m.cyl(0,11,0,9.2,8.0,5.8,14,TWR_ARM_D);
for(const s of [-1,1]){
m.bevelBox(-3.0,12.0,s*11.5,7.0,8.0,3.5,.55,TWR_COAT); // ammunition boxes
m.box(-3.0,19.8,s*11.55,4.2,.25,3.6,TWR_GLOW);
}
m.cyl(0,16.8,0,8.1,8.1,.7,16,TWR_COAT);
m.cyl(0,17.5,0,7.2,6.7,.5,16,TWR_TRIM); // y=18 tracking bearing
return m.build();
}
function mdlHellstormGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,6.8,6.2,2.0,16,TWR_MACH);
m.bevelBox(-1.0,2.0,0,15.5,5.5,13.5,.9,TWR_ARM);
m.bevelBox(-6.3,1.0,0,6.4,7.5,11.0,.7,TWR_COAT); // rotating feed housing
/* Horizontal rotary cluster with open bores. Barrel count grows by tier, but
each tube remains thick enough to survive normal phone zoom. Keeping this
assembly separate is required: projectiles track through a full turn and
the authored barrels must follow the same azimuth. */
const count=tier===1?4:tier===2?6:8;
for(let k=0;k<count;k++){
const a=k/count*TAU, z=Math.cos(a)*2.7, y=7.0+Math.sin(a)*2.7;
gunX(m,2.5,y,13.5,.45,TWR_MACH,z);
}
cylX(m,1.2,7.0,0,5.0,3.8,3.6,12,TWR_COAT,false);
m.bevelBox(-1.0,9.6,0,8.0,1.8,7.5,.45,TWR_ARM_D);
m.box(-1.0,11.3,0,1.3,.20,4.8,TWR_TEAM);
if(tier>=3) sensorMast(m,-6.8,6.8,0,6.0,TWR_MACH);
return m.build();
}
function mdlArc(tier){
const m=MB(); tier=tier||1;
towerPad(m,16,tier,false);
m.cyl(0,11,0,9.0,7.3,8.0,14,TWR_ARM);
m.cyl(0,19,0,6.5,5.5,4.0,14,TWR_MACH);
for(let k=0;k<3+(tier-1);k++){
const y=23+k*4.0, ro=6.6-k*.72;
m.cyl(0,y,0,ro,ro*.80,2.0,14,k%2?TWR_TRIM:TWR_MACH);
m.cyl(0,y+2.0,0,ro*.80,ro*.72,2.0,14,TWR_COAT);
}
const crown=31+(tier-1)*4;
m.cyl(0,23,0,1.35,.72,crown-20,9,TWR_MACH);
m.sphere(0,crown+2.0,0,4.7+(tier-1)*.65,12,TWR_GLOW,1,false);
ringX(m,0,crown+2.0,0,5.3+(tier-1)*.65,6.1+(tier-1)*.65,18,TWR_TRIM);
for(let k=0;k<3;k++){
const a=k/3*TAU,x=Math.cos(a)*10.8,z=Math.sin(a)*10.8;
m.cyl(x,11,z,1.35,.72,crown-15,7,TWR_COAT);
m.box(x,crown-4,z,1.0,.24,1.0,k===0?TWR_TEAM:TWR_GLOW,a);
}
if(tier>=3) for(const a of [0,Math.PI/2,Math.PI,Math.PI*1.5]){
m.cyl(Math.cos(a)*7.6,crown-2,Math.sin(a)*7.6,.55,.18,7.5,7,TWR_MACH);
}
return m.build();
}
function mdlNovaBase(tier){
const m=MB(); tier=tier||1;
/* r=24.5 keeps the complete base inside Nova's asymmetric 58x50 plot. The
previous r=27 pad silently occupied four units of neighbouring ground. */
towerPad(m,24.5,tier,false);
m.extrude(0,11,0,towerOct(20.0),6.5,TWR_ARM_D);
m.extrude(0,17.5,0,towerOct(16.5),3.2,TWR_ARM);
for(const s of [-1,1]){
m.bevelBox(-9.0,13.0,s*17.0,10.0,7.0,4.0,.6,TWR_COAT);
m.box(-9.0,19.8,s*17.05,5.5,.24,4.1,s>0?TWR_TEAM:TWR_GLOW);
}
m.bevelBox(14.5,11.0,0,9.0,8.0,16.0,.8,TWR_COAT); // fixed fire-control bunker
m.box(14.5,18.8,0,1.3,.22,9.0,TWR_TEAM);
m.cyl(0,20.4,0,11.0,11.0,.8,18,TWR_COAT);
m.cyl(0,21.2,0,9.8,9.0,.8,18,TWR_TRIM); // y=22 tracking bearing
return m.build();
}
function mdlNovaGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,8.8,8.2,2.2,18,TWR_MACH);
m.bevelBox(-1.5,2.2,0,21.0,7.0,17.0,1.0,TWR_ARM_D);
m.bevelBox(-5.5,8.8,0,11.0,2.0,14.0,.55,TWR_ARM);
const rows=tier===1?2:tier===2?3:4;
for(let r=0;r<rows;r++) for(const s of [-1,1]){
const y=4.8+r*4.2,z=s*4.3;
m.bevelBox(5.2,y-2.0,z,4.2,4.0,5.2,.5,TWR_MACH);
tubeX(m,6.6,y,z,3.0,2.0,1.22,10,TWR_BORE); // open tracking launch cell
m.box(2.9,y+1.85,z,4.0,.20,2.6,r===0&&s>0?TWR_TEAM:TWR_GLOW);
}
m.bevelBox(-9.0,3.0,0,5.0,7.0,13.0,.7,TWR_COAT); // reload mechanism
if(tier>=2) sensorMast(m,-8.5,10.0,-4.5,5.0,TWR_MACH);
if(tier>=3) m.box(-5.5,20.0,0,1.2,.22,7.0,TWR_TEAM);
return m.build();
}
/* Contact-sheet additions that have no legacy placeholder. These are live
structures, not gallery props, and therefore share the exact runtime model
contract (base plus optional tracking assembly) used by every other tower. */
function mdlMiningLaserBase(tier){
const m=MB(); tier=tier||1;
towerPad(m,18,tier,false);
m.extrude(0,11,0,towerOct(14.2),3.6,TWR_ARM_D);
for(const s of [-1,1]){
m.bevelBox(-5.0,11,s*11.8,8.0,6.0,3.8,.55,TWR_COAT);
m.box(-5.0,16.8,s*11.85,4.5,.24,4.0,s>0?TWR_GLOW:TWR_TEAM);
}
m.cyl(0,14.2,0,9.4,9.4,1.0,16,TWR_COAT);
m.cyl(0,15.2,0,8.2,7.5,.8,16,TWR_TRIM); // y=16 bearing
return m.build();
}
function mdlMiningLaserGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,7.6,7.0,2.5,16,TWR_MACH);
m.bevelBox(-2.5,2.5,0,17.0,7.4,14.0,1.15,TWR_ARM_D);
m.bevelBox(-8.7,4.1,0,5.8,6.5,11.8,.8,TWR_COAT);
for(const s of [-1,1]){
m.cyl(-6.8,6.1,s*6.5,2.3,2.0,4.4,10,TWR_MACH);
m.box(-6.8,10.3,s*6.55,2.8,.22,2.0,TWR_GLOW);
m.box(10.0,7.1,s*2.8,12.0,1.1,.78,TWR_MACH);
}
m.bevelBox(4.2,5.0,0,6.5,6.4,10.5,.8,TWR_ARM);
cylX(m,4.5,8.2,0,17.0,1.8,1.35,14,TWR_MACH,false);
for(const x of [7.0,12.0,17.0]){
cylX(m,x,8.2,0,.72,2.6,2.6,14,TWR_TRIM);
m.box(x,10.8,0,.7,.20,3.0,TWR_GLOW);
}
m.bevelBox(20.0,5.4,0,3.5,5.6,7.2,.5,TWR_ARM_D);
tubeX(m,20.4,8.2,0,2.0,2.8,1.15,14,TWR_BORE); // optical emitter throat
m.bevelBox(-3.0,9.9,0,8.0,1.8,8.0,.45,TWR_ARM);
m.box(-3.0,11.6,0,1.3,.20,4.8,TWR_TEAM);
if(tier>=2) for(const s of [-1,1]) m.bevelBox(-9.8,8.4,s*4.5,4.0,3.3,2.0,.4,TWR_ARM);
if(tier>=3) sensorMast(m,-10.5,11.7,0,5.0,TWR_MACH);
return m.build();
}
function mdlMissileBastionBase(tier){
const m=MB(); tier=tier||1;
towerPad(m,22,tier,false);
m.extrude(0,11,0,towerOct(17.8),5.0,TWR_ARM_D);
for(const s of [-1,1]){
m.bevelBox(-2.0,11.0,s*15.7,10.0,6.5,3.0,.5,TWR_COAT);
m.box(-2.0,17.3,s*15.75,6.0,.22,3.1,s>0?TWR_TEAM:TWR_GLOW);
}
m.cyl(0,16.0,0,11.8,11.8,1.0,18,TWR_COAT);
m.cyl(0,17.0,0,10.5,9.6,1.0,18,TWR_TRIM); // y=18 tracking bearing
return m.build();
}
function mdlMissileBastionGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,9.7,9.1,2.2,18,TWR_MACH);
m.bevelBox(-3.0,2.2,0,25.0,9.0,28.0,1.4,TWR_ARM);
m.bevelBox(-8.5,5.2,0,9.0,6.2,23.0,.9,TWR_COAT); // rotating feed / fire-control block
const rows=tier===1?2:tier===2?3:4, cols=2;
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){
const y=6.2+r*4.0,z=(c-.5)*8.2;
tubeX(m,7.6,y,z,2.4,2.05,1.30,10,TWR_BORE); // visible recessed missile cell
m.bevelBox(5.8,y-2.2,z,4.0,4.4,5.2,.55,TWR_MACH);
}
m.bevelBox(-3.0,11.2+(tier-1)*2.0,0,16.0,2.0,18.0,.55,TWR_ARM_D);
m.box(-3.0,13.1+(tier-1)*2.0,0,1.5,.22,10.0,TWR_TEAM);
if(tier>=2) sensorMast(m,-11.0,11.2,0,6.5,TWR_MACH);
if(tier>=3) ringX(m,-11.0,18.2,0,1.8,2.7,14,TWR_GLOW);
return m.build();
}
function mdlPlasmaCharger(tier){
const m=MB(); tier=tier||1;
towerPad(m,20,tier,false);
m.extrude(0,11,0,towerOct(16.0),6.0,TWR_ARM_D);
m.cyl(0,17,0,11.6,9.8,4.5,16,TWR_ARM);
for(let k=0;k<4;k++){
const a=k/4*TAU+Math.PI/4,x=Math.cos(a)*12.4,z=Math.sin(a)*12.4;
m.bevelBox(x,14.0,z,5.0,10.0,5.0,.7,TWR_COAT,a);
m.cyl(x,23.5,z,1.45,.72,7.0+(tier-1)*1.4,8,TWR_MACH);
m.box(x,30.2+(tier-1)*1.4,z,1.2,.24,1.2,TWR_GLOW,a);
}
m.cyl(0,21.5,0,7.5,5.6,6.0,16,TWR_MACH);
m.sphere(0,29.0+(tier-1)*1.5,0,6.0+(tier-1)*1.0,14,TWR_GLOW,1,false);
for(let k=0;k<2+(tier-1);k++){
const y=25.5+k*3.2, r=8.2+k*.45;
m.ring(0,y,0,r-.6,r,20,k%2?TWR_TRIM:TWR_MACH);
}
if(tier>=3){
ringX(m,0,30.0,0,8.2,9.2,20,TWR_TRIM);
m.cyl(0,36.0,0,1.1,.5,5.5,8,TWR_MACH);
}
m.box(0,21.2,9.6,5.0,.24,.9,TWR_TEAM);
return m.build();
}
/* SEAFORT used to draw the land Concussion Mortar. A floating dual-purpose
battery has a hull, a pontoon deck and a surface+AA mount — none of which
a poured oct pad can fake. 48x48 plot. Bearing at y=16. */
function mdlSeafortBase(tier){
const m=MB(); tier=tier||1;
const hull=[[-20,-16],[-12,-21],[12,-21],[20,-16],[20,16],[12,21],[-12,21],[-20,16]];
m.extrude(0,0,0,hull,3.2,NOVA_HARBOR_CORE);
m.extrude(0,3.2,0,hull.map(p=>[p[0]*.92,p[1]*.90]),1.4,NOVA_HARBOR_DECK);
for(const s of [-1,1]){
m.bevelBox(-6.0,0,s*18.2,26,2.6,4.0,.5,NOVA_HARBOR_ARM);
m.box(-6.0,2.62,s*18.3,16,.20,1.5,s>0?TWR_TEAM:TWR_GLOW);
m.cyl(-15.0,0,s*14.2,1.55,1.35,3.2,8,TWR_MACH);
}
m.bevelBox(-7.0,4.6,0,16,8.2,18,.9,TWR_ARM_D);
m.extrude(0,12.8,0,towerOct(11.2),2.2,TWR_ARM);
m.cyl(0,14.6,0,9.4,9.4,.8,16,TWR_COAT);
m.cyl(0,15.4,0,8.2,7.4,.6,16,TWR_TRIM); // y=16 bearing
m.bevelBox(14.5,4.6,-8.0,10,6.8,10,.7,TWR_COAT); // AA director
m.sphere(14.5,13.0,-8.0,2.35,10,GLASS,.72,true);
if(tier>=2) sensorMast(m,-15.5,4.6,10.0,6.4,TWR_MACH);
if(tier>=3) m.box(0,13.0,0,1.2,.20,8.0,TWR_TEAM);
return m.build();
}
function mdlSeafortGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,7.2,6.6,2.2,16,TWR_MACH);
m.bevelBox(-2.2,2.2,0,15.5,6.6,13.5,1.0,TWR_ARM_D);
cylX(m,3.2,6.2,0,11.5,2.25,1.75,12,TWR_MACH,false); // surface battery
for(const x of [6.4,10.8]) cylX(m,x,6.2,0,.7,2.75,2.75,12,TWR_TRIM);
m.bevelBox(13.6,4.4,0,2.8,5.6,7.2,.45,TWR_ARM);
tubeX(m,14.2,6.2,0,1.7,2.55,1.25,12,TWR_BORE);
for(const s of [-1,1]){ // twin AA
m.box(5.2,9.2,s*3.15,8.4,.72,.55,TWR_MACH);
gunX(m,2.4,9.2,10.5,.36,TWR_MACH,s*3.15);
}
m.box(-2.6,8.7,0,1.15,.20,5.8,TWR_TEAM);
if(tier>=2) for(const s of [-1,1]) m.bevelBox(-6.4,4.0,s*5.0,4.0,3.1,2.0,.35,TWR_ARM);
if(tier>=3) sensorMast(m,-7.0,8.6,0,4.0,TWR_MACH);
return m.build();
}
/* STORMCALLER used the NOVA silo mesh. A banked-charge volley battery is a
capacitor farm with a 16-cell rack, not an orbital superweapon. 46x46. */
function mdlStormcallerBase(tier){
const m=MB(); tier=tier||1;
towerPad(m,20,tier,false);
m.extrude(0,11,0,towerOct(15.2),4.0,TWR_ARM_D);
for(const s of [-1,1]){
m.bevelBox(-1.5,11,s*13.4,18,6.8,3.8,.5,TWR_COAT);
for(const x of [-6,0,6]){
m.cyl(x,17.8,s*13.4,1.28,1.05,3.0+(tier-1)*.7,8,TWR_MACH);
m.tube(x,20.7+(tier-1)*.7,s*13.4,1.08,.52,.5,8,TWR_BORE);
m.ring(x,21.3+(tier-1)*.7,s*13.4,.58,1.0,10,TWR_GLOW);
}
m.box(-1.5,17.7,s*13.5,10,.20,2.1,s>0?TWR_TEAM:TWR_GLOW);
}
m.cyl(0,15.0,0,8.4,7.6,2.0,16,TWR_MACH);
m.sphere(0,18.2,0,3.4+(tier-1)*.35,12,TWR_GLOW,1,false); // banked charge
m.cyl(0,20.4,0,9.0,9.0,.8,16,TWR_COAT);
m.cyl(0,21.2,0,7.8,7.0,.8,16,TWR_TRIM); // y=22 bearing
return m.build();
}
function mdlStormcallerGun(tier){
const m=MB(); tier=tier||1;
m.cyl(0,0,0,7.4,6.8,2.0,16,TWR_MACH);
m.bevelBox(-2.4,2.0,0,17,7.2,15.5,.95,TWR_ARM_D);
for(let r=0;r<4;r++) for(let c=0;c<4;c++){
const y=4.15+(r-1.5)*2.0,z=(c-1.5)*3.05;
m.bevelBox(4.0,y-1.05,z,3.2,2.1,2.3,.26,TWR_MACH);
tubeX(m,5.4,y,z,2.15,1.08,.68,8,TWR_BORE); // 16-cell volley rack
}
m.bevelBox(-8.0,3.0,0,4.8,6.0,11.5,.55,TWR_COAT);
if(tier>=2) sensorMast(m,-8.8,9.0,0,4.4,TWR_MACH);
if(tier>=3) m.box(-3.0,9.4,0,1.15,.20,7.6,TWR_TEAM);
return m.build();
}
function mdlWall(){
const m=MB();
m.box(0,0,0,28,3,20,CONC_D);
m.box(0,3,0,26,13,15,CONC); // rampart body
m.box(0,16,0,27,3,17,CONC_D); // parapet cap
for(const s of [-1,1]) m.box(0,19,s*7,26,3,3,CONC); // crenellation
for(const f of [-1,1]) m.box(f*12,3,0,4,17,17,MET_D); // buttress posts
m.box(0,21.8,0,11,.24,1.0,TWR_TEAM); // restrained ownership rail
return m.build();
}
function mdlGate(){
const m=MB();
m.box(0,0,0,28,3,20,CONC_D);
for(const f of [-1,1]) m.box(f*11,3,0,6,20,18,CONC); // gate towers
m.box(0,19,0,26,4,17,CONC_D);
m.box(0,3,0,16,15,4,ENERGY); // energy curtain
for(const f of [-1,1]) m.cyl(f*11,24.62,0,.32,.32,.24,8,HQ_LAMP);
m.box(0,22.8,0,9,.24,1.0,TWR_TEAM); // gatehouse livery, not curtain tint
return m.build();
}
function mdlGeo(){
const m=MB();
const y=novaServicePad(m,37,31,2.5);
/* The geothermal plant is a pressure vessel, not a second small reactor:
broad condenser wings and a hollow central steam throat distinguish it
from the Pgen before colour or animation is considered. */
m.cyl(0,y,0,8.5,7.7,8.0,12,NOVA_GEO_CORE);
m.cyl(0,y+8.0,0,6.7,5.0,9.0,12,NOVA_GEO_ARM);
m.cyl(0,y+17.0,0,5.1,4.2,3.0,12,NOVA_GEO_TRIM);
m.tube(0,y+19.8,0,4.3,2.4,1.4,12,TWR_BORE);
for(const s of [-1,1]){
m.bevelBox(s*13.3,y,0,7.0,12.0,18.0,.9,NOVA_GEO_CORE);
ventBank(m,s*13.3,y+12.0,0,4.2,13.0,4,NOVA_GEO_TRIM,0);
m.box(s*13.3,y+12.2,-9.1,3.6,.22,.45,s>0?NOVA_GEO_GLOW:TWR_TEAM);
m.box(s*8.8,y+5.5,0,5.5,1.3,1.8,NOVA_GEO_ARM);
}
ringX(m,0,y+12.5,0,5.4,6.0,14,NOVA_GEO_TRIM);
m.sphere(0,y+15.0,0,1.7,9,NOVA_GEO_GLOW,1,false);
return m.build();
}
function mdlSilo(){
const m=MB();
m.box(0,0,0,26,3,26,NOVA_SILO_CORE);
for(const [dx,dz,r] of [[-5,-5,7],[6,-4,6],[0,7,6.5]])
m.cyl(dx,3,dz,r,r*0.94,20,10,NOVA_SILO_ARM);
for(const [dx,dz,r] of [[-5,-5,7],[6,-4,6],[0,7,6.5]]){
m.cyl(dx,23,dz,r*0.96,r*0.5,4,10,NOVA_SILO_ROOF);
m.cyl(dx,11,dz,r*1.04,r*1.04,1.4,10,NOVA_SILO_CORE); // banding
}
m.box(11,3,0,5,9,5,NOVA_SILO_CORE);
m.box(13.55,7.0,0,.24,4.0,3.0,TWR_TEAM); // control-house faction plate
m.box(13.72,11.1,0,.18,.22,2.2,NOVA_SILO_GLOW); // silo status line
return m.build();
}
function mdlFab(){
const m=MB();
const y=novaServicePad(m,37,29,2.5);
/* A compact conversion line: feed hoppers on the rear, one calm processing
hall, and an unmistakable energized output stack. Rusted duplicate drums
made the former asset look abandoned rather than high-tech. */
m.bevelBox(-5.0,y,0,22.0,14.0,23.0,1.5,NOVA_FAB_ARM);
m.bevelBox(-5.0,y+14.0,0,20.0,1.8,21.0,.65,NOVA_FAB_ROOF);
roofEdge(m,-5.0,y+15.8,0,20.0,21.0,.75,NOVA_FAB_CORE,NOVA_FAB_ROOF);
for(const s of [-1,1]){
m.cyl(-14.0,y,s*7.0,3.2,2.7,10.0,10,NOVA_FAB_CORE);
m.cyl(-14.0,y+10.0,s*7.0,2.7,1.8,2.0,10,NOVA_FAB_ARM);
m.box(-11.0,y+5.5,s*7.0,4.5,1.2,1.4,NOVA_FAB_CORE);
}
m.cyl(11.0,y,0,5.0,4.4,19.0,12,NOVA_FAB_CORE);
m.cyl(11.0,y+5.0,0,5.2,5.2,.8,12,NOVA_FAB_ROOF);
m.cyl(11.0,y+19.0,0,4.5,3.1,3.4,12,NOVA_FAB_ARM);
m.tube(11.0,y+22.3,0,3.2,1.7,1.0,12,TWR_BORE);
for(const s of [-1,1]) m.box(11.0,y+12.0,s*4.5,3.8,.24,.45,s>0?TWR_GLOW:TWR_TEAM);
m.box(-5.0,y+8.0,11.65,13.0,2.5,.4,NOVA_FAB_GLASS);
m.box(2.0,y+3.0,0,8.0,1.4,1.8,TWR_GLOW);
return m.build();
}
const BLD_MDL={
mex:mdlMex, pgen:mdlPgen, fac:mdlFac, turret:mdlTurretBase, bunker:mdlBunkerBase, sgen:mdlSgen,
tgate:mdlTgate, nest:mdlNest, harbor:mdlHarbor, bastion:mdlBastionBase,
techlab:mdlTechlab, aatower:mdlAA, airfield:mdlAirfield, uplink:mdlUplink,
hq:mdlHQ, hellstorm:mdlHellstormBase, arc:mdlArc, rail:mdlRailBase, nova:mdlNovaBase, wall:mdlWall,
minelaser:mdlMiningLaserBase, missilebastion:mdlMissileBastionBase, plasma:mdlPlasmaCharger,
gate:mdlGate, geo:mdlGeo, silo:mdlSilo, fab:mdlFab,
seafort:mdlSeafortBase, stormcaller:mdlStormcallerBase,
};
const BLD_TUR_MDL={
turret:mdlTurretGun, bunker:mdlBunkerGun, bastion:mdlBastionGun,
aatower:mdlAAGun, hellstorm:mdlHellstormGun, rail:mdlRailGun,
nova:mdlNovaGun, minelaser:mdlMiningLaserGun, missilebastion:mdlMissileBastionGun,
seafort:mdlSeafortGun, stormcaller:mdlStormcallerGun
};
/* Sentinel is the production template for visual Mk1/Mk2/Mk3 geometry. Other
faction/weapon families are catalogued in design/tower-factions and can join
this registry without changing the renderer again. */
const BLD_TIER_MDL={
turret:[1,2,3].map(tier=>({base:()=>mdlTurretBase(tier),tur:()=>mdlTurretGun(tier)})),
bunker:[1,2,3].map(tier=>({base:()=>mdlBunkerBase(tier),tur:()=>mdlBunkerGun(tier)})),
bastion:[1,2,3].map(tier=>({base:()=>mdlBastionBase(tier),tur:()=>mdlBastionGun(tier)})),
sgen:[1,2,3].map(tier=>({base:()=>mdlSgen(tier)})),
uplink:[1,2,3].map(tier=>({base:()=>mdlUplink(tier)})),
hellstorm:[1,2,3].map(tier=>({base:()=>mdlHellstormBase(tier),tur:()=>mdlHellstormGun(tier)})),
arc:[1,2,3].map(tier=>({base:()=>mdlArc(tier)})),
nova:[1,2,3].map(tier=>({base:()=>mdlNovaBase(tier),tur:()=>mdlNovaGun(tier)})),
minelaser:[1,2,3].map(tier=>({base:()=>mdlMiningLaserBase(tier),tur:()=>mdlMiningLaserGun(tier)})),
missilebastion:[1,2,3].map(tier=>({base:()=>mdlMissileBastionBase(tier),tur:()=>mdlMissileBastionGun(tier)})),
plasma:[1,2,3].map(tier=>({base:()=>mdlPlasmaCharger(tier)})),
aatower:[1,2,3].map(tier=>({base:()=>mdlAA(tier),tur:()=>mdlAAGun(tier)})),
rail:[1,2,3].map(tier=>({base:()=>mdlRailBase(tier),tur:()=>mdlRailGun(tier)})),
seafort:[1,2,3].map(tier=>({base:()=>mdlSeafortBase(tier),tur:()=>mdlSeafortGun(tier)})),
stormcaller:[1,2,3].map(tier=>({base:()=>mdlStormcallerBase(tier),tur:()=>mdlStormcallerGun(tier)}))
};
/* Height at which each turret meets its mount, in the model's own units. */
const BLD_TUR_H={ turret:14, bunker:18, bastion:23, aatower:11, hellstorm:18,
rail:22, nova:22, minelaser:16, missilebastion:18, seafort:16, stormcaller:22 };
/* Weapon assemblies are deliberately oversized for command-view readability;
their bases retain the real collision footprint while the moving mass reads
clearly at phone scale. Keep projectile/beam muzzle offsets in sim.js in
step with these values. */
const BLD_TUR_S={ turret:1.20, bunker:1.12, bastion:1.14, aatower:1.18, hellstorm:1,
rail:1.15, nova:1, minelaser:1.14, missilebastion:1, seafort:1.10, stormcaller:1.06 };
/* ---------------------------------------------------------------------------
WORLD OBJECTS — ruins, scenery, resources, effects.
--------------------------------------------------------------------------- */
function mdlCityTower(){ return loadWorldModel('mdlCityTower'); }
function mdlCityDome(){ return loadWorldModel('mdlCityDome'); }
function mdlCityHall(){ return loadWorldModel('mdlCityHall'); }
function mdlCityTank(){ return loadWorldModel('mdlCityTank'); }
function mdlRock(){ return loadWorldModel('mdlRock'); }
function mdlTree(){ return loadWorldModel('mdlTree'); }
/* Region flora. One imported broadleaf was the "one tree everywhere" tell.
These stay procedural and cheap so MEDIUM can instance fewer of the same
silhouette instead of a second atlas. Authored near-neutral; instance tint
carries the kit colour. */
function mdlTreePine(){
const m=MB();
m.mat(MAT.EARTH);
m.cyl(0,0,0,1.15,0.72,8.2,6,C(72,56,40));
m.mat(MAT.DENSE_LEAF);
m.cyl(0,6.2,0,6.4,0.18,8.0,7,C(46,72,48),false);
m.cyl(0,10.4,0,4.7,0.14,7.0,7,C(40,68,44),false);
m.cyl(0,15.0,0,2.9,0.04,5.8,7,C(36,64,40),false);
return m.build();
}
function mdlTreePalm(){
const m=MB();
m.mat(MAT.EARTH);
m.cyl(0,0,0,0.92,0.52,14.2,6,C(118,92,58));
m.mat(MAT.LEAF);
for(let k=0;k<6;k++){
const a=k/6*TAU;
m.box(Math.cos(a)*4.3,15.4,Math.sin(a)*4.3,7.6,0.34,1.35,C(70,110,48),a);
}
return m.build();
}
function mdlTreeDead(){
const m=MB();
m.mat(MAT.EARTH);
m.cyl(0,0,0,1.02,0.44,12.4,6,C(58,48,40));
m.box(2.3,9.2,0,5.6,0.68,0.68,C(64,52,42),0.42);
m.box(-1.7,11.1,1.2,4.3,0.52,0.52,C(60,50,40),-0.72);
return m.build();
}
function mdlTreeSpore(){
const m=MB();
m.mat(MAT.BROOD_CHITIN);
m.cyl(0,0,0,1.45,0.88,10.2,7,C(88,48,72));
m.sphere(0,12.1,0,4.3,8,C(160,70,140),0.72);
m.mat(MAT.BROOD_SLIME);
m.sphere(0,13.3,0,2.35,7,C(210,90,170),0.80);
return m.build();
}
function mdlBush(){
const m=MB();
m.mat(MAT.LEAF);
m.sphere(0,1.55,0,2.75,7,C(62,96,44),0.72);
m.sphere(1.55,1.15,0.55,1.75,6,C(70,104,48),0.70);
m.sphere(-1.25,1.05,-0.75,1.55,6,C(54,88,40),0.70);
return m.build();
}
function mdlRockIce(){
const m=MB();
m.mat(MAT.ICE_PACK);
m.sphere(0,2.15,0,4.15,7,C(200,220,236),0.55);
m.box(-1.15,0,1.35,3.4,2.75,2.15,C(170,190,210),0.38);
return m.build();
}
function mdlRockSlag(){
const m=MB();
m.mat(MAT.BASALT_CRUST);
m.sphere(0,2.0,0,4.0,7,C(52,44,42),0.50);
m.box(1.35,0,-0.75,3.15,2.35,2.55,C(40,36,34),0.48);
m.mat(MAT.BLAST_SLAG);
m.box(0,2.35,0,1.55,0.48,1.55,C(80,48,36),0.18);
return m.build();
}
/* A crystal is not four cones. Real mineral habit is a CLUSTER: hexagonal
prisms sharing a root, leaning apart, each ending in a pointed
termination - and the faces are FLAT, because flat facets are what catch
the light as a unit and flash when the camera orbits. Every face here is
its own plane with its own normal (no smoothing), the shafts kink at a
mid-ring the way natural columns do, and the whole cluster wears the CRYST
material so the shader's transmission/fresnel path lights it from inside.
Authored near-white: the instance tint carries the deposit band colour
(cyan/green/pink), and a saturated authored hue would fight it. */
function crystalSpike(m,bx,bz,r,h,lx,lz,ph,BASE,MID,TIP){
const seg=6, mh=h*0.60, A=[], B=[];
const N=(a,b,c)=>{ const ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2],
vx=c[0]-a[0],vy=c[1]-a[1],vz=c[2]-a[2];
let nx=uy*vz-uz*vy,ny=uz*vx-ux*vz,nz=ux*vy-uy*vx;
const l=Math.hypot(nx,ny,nz)||1; return [nx/l,ny/l,nz/l]; };
for(let k=0;k<seg;k++){
const a=ph+k/seg*TAU;
A.push([bx+Math.cos(a)*r, -1.5, bz+Math.sin(a)*r]);
/* The mid ring twists 0.12 rad and steps toward the lean: the kink that
separates "grown column" from "extruded prop". */
B.push([bx+lx*0.55+Math.cos(a+0.12)*r*0.74, mh, bz+lz*0.55+Math.sin(a+0.12)*r*0.74]);
}
const apex=[bx+lx,h,bz+lz];
for(let k=0;k<seg;k++){
const j=(k+1)%seg;
m.quad(A[k],A[j],B[j],B[k],(k%3)?((k%2)?MID:BASE):C(160,190,210));
const n=N(B[k],apex,B[j]);
m.tri(B[k],apex,B[j],n[0],n[1],n[2],(k%2)?TIP:MID);
}
}
function mdlCrystal(){
const m=MB(); m.mat(MAT.CRYST);
const BASE=C(120,148,168), MID=C(196,222,238), TIP=C(255,255,255);
crystalSpike(m, 0.0, 0.0, 3.1, 17.5, 1.7, 0.9, 0.30, BASE,MID,TIP);
crystalSpike(m, 3.7, 1.1, 2.1, 11.0, 2.3, 1.1, 1.70, BASE,MID,TIP);
crystalSpike(m,-3.1, 2.3, 1.8, 9.5, -2.1, 1.5, 0.90, BASE,MID,TIP);
crystalSpike(m,-1.9,-3.3, 1.6, 8.0, -1.1,-2.3, 2.40, BASE,MID,TIP);
crystalSpike(m, 2.3,-2.9, 1.3, 6.5, 1.5,-1.7, 4.00, BASE,MID,TIP);
crystalSpike(m,-4.3,-0.7, 1.1, 5.0, -2.5,-0.5, 5.20, BASE,MID,TIP);
return m.build();
}
function mdlDeposit(){
/* Shards only. The two STONE cylinders (r 32 / r 17) were the dark
disk under every crystal node — same hole the terrain scar painted.
Veins mark the bed; FX.crystal owns the outcrop. */
const m=MB();
m.mat(MAT.CRYST);
const BASE=C(150,178,198), MID=C(214,236,248), TIP=C(255,255,255);
for(let k=0;k<6;k++){
const a=k/6*TAU, d=6+(k%2)*4;
crystalSpike(m, Math.cos(a)*d, Math.sin(a)*d, 3.0-(k%3)*0.6, 9.5+(k%3)*3.5,
Math.cos(a)*2.0, Math.sin(a)*2.0, k*1.13, BASE,MID,TIP);
}
crystalSpike(m, 0,0,3.8,14.5,0.5,-0.4,0.55,BASE,MID,TIP);
return m.build();
}
function mdlGeyser(){
/* Natural vent. Two swaps made the phone-shot hatch:
1) mdlGeyser() → loadWorldModel('mdlGeyser') (12-gon CONC frustum);
today's civic ROOF/TRIM remap painted its +Y as a metal iris.
2) hud.js still stamped sprites.geyser (dark ellipses + 5 rim arcs)
on top in 3D. A sphere pile from look-down is the same cone.
Rock spires + offset steam — STONE, not CRYST, so it is not a mass node. */
const m=MB();
const ROCK=C(118,108,92), ASH=C(86,80,70), RIM=C(148,138,118), GLOW=C(210,236,248);
m.mat(MAT.STONE);
/* Irregular boulders only. crystalSpike facets read as iris plates from
look-down — that is the hatch again. */
m.sphere(0,2.8,0,6.2,7,ROCK,0.48);
m.sphere(-5.6,2.2,3.8,4.4,6,ASH,0.55);
m.sphere(5.2,2.0,-4.2,4.1,6,RIM,0.52);
m.sphere(-3.4,1.8,-5.0,3.6,6,ASH,0.60);
m.sphere(4.8,1.6,4.6,3.4,6,ROCK,0.58);
m.sphere(1.2,2.4,5.8,3.0,6,RIM,0.62);
m.sphere(-0.4,11.5,0.6,3.2,7,ROCK,0.78);
m.sphere(0.8,16.8,-0.4,2.6,7,ASH,0.82);
m.sphere(-0.5,21.4,0.8,2.0,7,RIM,0.80);
m.mat(MAT.CRYST);
m.sphere(0.2,24.6,0.3,3.2,7,GLOW,0.88);
m.sphere(-0.8,27.8,0.7,2.4,6,GLOW,1.02);
m.sphere(0.9,30.2,-0.3,1.7,6,GLOW,0.72);
return m.build();
}
function mdlCrate(){ return loadWorldModel('mdlCrate'); }
function mdlWreck(){ return loadWorldModel('mdlWreck'); }
function mdlBerm(){ return loadWorldModel('mdlBerm'); }
/* Effect geometry — all real solids, no camera-facing quads anywhere. */
function mdlShell(){ return MB().sphere(0,0,0,1,10,C(255,255,255),1,false).build(); }
function mdlBolt(){ return MB().cyl(0,-0.5,0,0.32,0.10,1.6,6,C(255,255,255)).build(); }
function mdlShard(){ return MB().box(0,-0.5,0,1,1,1,C(255,255,255)).build(); }
function mdlBeamSeg(){ // unit cylinder along +Y, scaled in the shader
return MB().cyl(0,0,0,1,1,1,7,C(255,255,255),false).build();
}
function mdlCone(){ return MB().cyl(0,0,0,1,0.06,1,10,C(255,255,255),false).build(); }
function mdlRing(){ return MB().ring(0,0,0,0.82,1,30,C(255,255,255)).build(); }
function mdlDisc(){ return MB().ring(0,0,0,0,1,26,C(255,255,255)).build(); }
/* ---------------------------------------------------------------------------
FLASHLIGHT KIT
The old headlamp was a camera-facing ribbon: a thin streak that never read
as light FALLING ON THE GROUND. What sells a flashlight in a top-down view
is the lit floor, not the beam in the air — a wedge of brightness widening
from the lamp across the terrain, ending in a pool. Both meshes lie flat on
the ground and draw in the additive pass, so they brighten whatever they
cover; the depth test stops them painting across rocks and walls, which
doubles as free occlusion.
Gradients are baked as constant-colour bands: under additive blending black
adds nothing, so a band fading toward black IS the soft edge — no texture.
Instance scale = reach along +X, the cross-axis lane = cone width, so the
beam keeps a fixed angle at any length, exactly as a real cone does.
--------------------------------------------------------------------------- */
function mdlLightWedge(){
const m=MB();
const bands=[[0.03,0.28,1.00],[0.28,0.56,0.62],[0.56,0.82,0.30],[0.82,1.00,0.10]];
for(const [x0,x1,v] of bands){
const z0=x0*0.5, z1=x1*0.5; // half-angle baked: width tracks length
const cz0=z0*0.42, cz1=z1*0.42; // bright core strip
m.quad([x0,0,-cz0],[x1,0,-cz1],[x1,0,cz1],[x0,0,cz0],[v,v,v]);
const e=v*0.32; // soft flanks
m.quad([x0,0,-z0],[x1,0,-z1],[x1,0,-cz1],[x0,0,-cz0],[e,e,e]);
m.quad([x0,0,cz0],[x1,0,cz1],[x1,0,z1],[x0,0,z0],[e,e,e]);
}
return m.build();
}
/* Radial pool: bright core falling off to nothing. Serves as the beam's end
splash and as machine underglow — the hull sitting in its own worklight. */
function mdlLightPool(){
const m=MB();
const steps=[[0,0.34,1.00],[0.34,0.60,0.66],[0.60,0.84,0.32],[0.84,1.0,0.09]];
for(const [r0,r1,v] of steps) m.ring(0,0,0,r0,r1,26,[v,v,v]);
return m.build();
}
/* ---------------------------------------------------------------------------
SHADOW DECAL
The single largest difference between this and the reference art was not
geometry or texture — it was that nothing cast a shadow, so every building
and vehicle looked pasted onto the ground rather than standing on it. This
is the receiver: a flat disc whose vertex colour runs from dark at the core
to white at the rim. Drawn through the unlit program with a MULTIPLY blend,
white leaves the ground untouched and dark multiplies it down, so the rings
read as one soft-edged shadow rather than a stack of decals.
--------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
MODULE MARK — the field badge a crafted module puts on the machine carrying
it. A ring with a floating core, tinted to the module. Its whole job is to be
noticed when it stops being there: when a module wears out, the mark simply
is not drawn, and the player sees that a boost is gone rather than reading
it in a menu later.
--------------------------------------------------------------------------- */
function mdlModMark(){
const m=MB();
m.ring(0,0,0,0.62,1.0,18,C(255,255,255));
m.ring(0,0.05,0,1.12,1.30,18,C(255,255,255));
m.sphere(0,0.34,0,0.34,8,C(255,255,255),1,false);
for(let k=0;k<3;k++){
const a=k/3*TAU;
m.box(Math.cos(a)*0.86,0.02,Math.sin(a)*0.86,0.30,0.10,0.16,C(255,255,255),a);
}
return m.build();
}
function mdlShadow(){
const m=MB();
const steps=[[0,0.40,0.42],[0.40,0.58,0.55],[0.58,0.74,0.68],[0.74,0.88,0.82],[0.88,1.0,0.94]];
for(const [r0,r1,v] of steps) m.ring(0,0,0,r0,r1,26,[v,v,v]);
return m.build();
}
function mdlPlate(){ // flat unit square on the ground
const m=MB();
m.quad([-0.5,0,-0.5],[0.5,0,-0.5],[0.5,0,0.5],[-0.5,0,0.5],C(255,255,255)); // faces up
return m.build();
}
/* A unit-LENGTH, hairline-WIDTH strip. Instances carry a single uniform scale,
so a thin line can't be made by squashing a square — it needs its own mesh
with the thinness baked in. Everything rectilinear on the ground (build-zone
borders, range squares, guides) is drawn from this. */
function mdlLine(){
const m=MB();
m.quad([-0.5,0,-0.5],[0.5,0,-0.5],[0.5,0,0.5],[-0.5,0,0.5],C(255,255,255)); // 1 x 1; width comes from the instance
return m.build();
}
/* ---------------------------------------------------------------------------
Upload everything once, at boot. Caps are sized to what can realistically be
on screen at once, not to MAXU — culling keeps the instance streams small.
--------------------------------------------------------------------------- */
const FX={};
/* ============================================================================
SECOND-WAVE CHASSIS
Each of these exists to make a role readable at a glance. A crowd-control
launcher has to look like a launcher from directly overhead, an anti-tank
gun has to read as one long barrel, and a hero has to be unmistakable in a
press of two hundred units.
============================================================================ */
/* THESE SIX SHARED ONE HEXAGON.
Reaper, Cinder, Lancer, Resonator, Warden and Harbinger were all a 6-point
extrude whose profiles differed by under 0.6 world units — the same plan-view
shape in six colours. Top-down, the hull footprint IS the silhouette, so at
40px of screen they were the same vehicle. Each now gets a genuinely
different outline: octagon, drum, arrowhead, spade. */
function mdlReaper(){ // saturation launcher — wide octagonal carrier
const m=MB();
runningGear(m,12,3.0,2.6,3.2,5,MET_D,1);
m.extrude(0,3.4,0,[[-6.2,-5.0],[-3.6,-6.3],[3.6,-6.3],[6.2,-5.0],
[6.2,5.0],[3.6,6.3],[-3.6,6.3],[-6.2,5.0]],2.6,MET);
/* The hull was a bare octagonal slab with a box cradle on it — correct
silhouette, no fabrication. A launcher is a BOX OF TUBES on a frame, so
the frame has to be visible: a recessed deck, a cradle that is a trapezoid
with end cheeks rather than a slab, elevation rams under it, and the tubes
bedded into a real backplate instead of floating. */
m.extrude(0,6.0,0,[[-5.4,-4.3],[-3.1,-5.4],[3.1,-5.4],[5.4,-4.3],
[5.4,4.3],[3.1,5.4],[-3.1,5.4],[-5.4,4.3]],0.6,MET_D);
m.wedge(4.4,6.0,0,2.4,0.9,7.6,MET_L); // glacis
/* CRADLE: trapezoid body, cheek plates at each end, and the whole thing
pitched up so it reads as elevated rather than as a lid. */
m.extrude(-0.6,6.6,0,[[-4.2,-3.4],[3.4,-2.9],[4.2,-1.5],[4.2,1.5],
[3.4,2.9],[-4.2,3.4]],2.6,TEAM_A);
for(const sd of [-1,1]){
m.bevelBox(-0.6,6.6,sd*3.3,8.0,2.7,0.7,0.24,MET_D); // cheek plates
m.cyl(-3.0,6.2,sd*2.4,0.42,0.34,2.0,7,MET_L); // elevation ram
m.bevelBox(2.6,6.4,sd*4.5,3.0,1.3,1.1,0.26,MET_D); // sponson pods
m.cyl(5.0,6.5,sd*3.0,0.30,0.26,1.1,6,MET_L); // tie-downs
}
m.box(-4.6,7.0,0,1.0,2.4,6.2,MET_D); // backplate the tubes bed into
for(let r=0;r<3;r++) for(const sd of [-1,1]){
m.tube(1.2+r*0.2,7.4+r*1.5,sd*(1.1+r*0.9),0.85,0.55,5.6,8,DARKER,0);
m.ring(4.0+r*0.2,7.4+r*1.5,sd*(1.1+r*0.9),0.58,0.90,8,MET_L); // muzzle collars
}
m.bevelBox(-4.4,7.6,0,2.4,2.0,4.6,0.32,MET_D); // reload gantry
ventBank(m,-3.0,6.62,0,2.4,3.0,4,MET_D,0);
stowage(m,-1.2,6.62,4.0,3.0,1.1,RUST,0,53);
exhaust(m,-5.6,4.8,0,2,0.40,2.0,MET_D,2.0);
glowStrip(m,-0.6,9.9,0,6.4,HOT,0);
deckCrown(m,0,6.02,0,12.4,12.6,MET_D,TEAM_T);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlCinder(){ // incendiary sprayer — tanks and nozzle
const m=MB();
runningGear(m,11,2.8,2.4,3.0,5,MET_D,1);
/* Squared-off tanker plan, and the fuel drums below now break its outline
rather than hiding inside a hex the same width as everyone else's. */
m.extrude(0,3.2,0,[[-5.8,-4.4],[4.2,-4.4],[5.4,-2.2],[5.4,2.2],[4.2,4.4],[-5.8,4.4]],2.4,MET);
/* The drums were doing all the work and the body under them was bare. A
flame vehicle is PLUMBING: tanks need end caps, bands and a cradle; the
nozzle needs a pump, a feed line running back to the tanks, and a heat
shield. Every one of those also breaks the silhouette. */
m.extrude(0,5.6,0,[[-5.2,-3.7],[3.8,-3.7],[4.8,-1.9],[4.8,1.9],[3.8,3.7],[-5.2,3.7]],0.5,MET_D);
m.wedge(4.2,5.6,0,2.2,0.9,6.4,MET_L); // glacis
for(const sd of [-1,1]){
m.cyl(-2.6,5.9,sd*2.2,1.9,1.9,5.4,10,RUST,Math.PI/2); // fuel drum
m.cyl(-5.3,5.9,sd*2.2,2.0,1.6,0.5,10,MET_D,Math.PI/2); // end caps
m.cyl( 0.1,5.9,sd*2.2,1.6,2.0,0.5,10,MET_D,Math.PI/2);
for(let k=0;k<2;k++) m.cyl(-4.0+k*2.4,5.9,sd*2.2,2.02,2.02,0.30,10,MET_L,Math.PI/2); // bands
m.bevelBox(-2.6,4.9,sd*2.2,5.6,1.0,1.5,0.24,MET_D); // tank cradle
m.cyl(1.4,6.6,sd*1.5,0.26,0.24,4.6,6,DARKER,Math.PI/2); // feed line to the pump
}
m.bevelBox(2.6,5.9,0,3.2,1.9,2.6,0.36,MET_D); // pump housing
m.cyl(2.6,7.8,0,0.75,0.60,0.9,9,MET_L); // pressure vessel
m.bevelBox(4.8,6.1,0,1.5,1.3,1.9,0.28,MET_L); // nozzle mount
m.tube(6.4,6.1,0,0.9,0.55,3.2,8,HOT,Math.PI/2); // nozzle
m.wedge(5.6,7.1,0,1.8,0.7,2.6,MET_D); // heat shield over the nozzle
m.bevelBox(-5.4,6.1,0,1.5,1.5,4.2,0.3,DARKER);
ventBank(m,-1.0,5.68,0,2.0,2.2,3,MET_D,0);
exhaust(m,-5.6,4.6,0,2,0.36,1.8,MET_D,1.8);
glowStrip(m,-2.6,8.4,0,4.6,HOT,0);
deckCrown(m,-0.2,5.62,0,11.2,8.8,MET_D,TEAM_T);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlLancer(){ // gauss rifle — one very long barrel
const m=MB();
runningGear(m,12,2.8,2.4,3.2,5,MET_D,0);
m.extrude(0,3.2,0,[[-6.6,-1.9],[1.8,-2.5],[7.4,-1.0],[8.8,0],
[7.4,1.0],[1.8,2.5],[-6.6,1.9]],2.2,MET);
m.bevelBox(-1.0,5.4,0,6.0,2.4,4.4,0.5,TEAM_A);
m.cyl(4.0,6.4,0,0.75,0.62,13.0,10,MET_D,Math.PI/2); // rail
for(let k=0;k<4;k++) m.tube(5.0+k*2.6,6.4,0,1.05,0.78,0.55,10,ENERGY,Math.PI/2); // coils
m.box(-4.4,6.2,0,2.4,2.6,3.6,DARKER); // capacitor bank
m.wedge(4.6,5.42,0,3.6,1.1,3.2,MET_L); // nose spine
for(const sd of [-1,1]) m.bevelBox(-1.6,4.9,sd*2.1,7.0,1.3,0.9,.24,MET_D); // side rails
exhaust(m,-6.2,4.9,0,2,.40,2.0,MET_D,1.8);
glowStrip(m,-4.4,7.6,0,3.0,ENERGY,0);
deckCrown(m,-0.4,5.42,0,12.0,5.0,MET_D,TEAM_T);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlResonator(){ // sonic — dish and ribs
const m=MB();
/* The circular plan is the right call and it stays — nothing else in the
roster is round from above, so it is identifiable in one glance. What was
missing is everything that makes a round thing look ENGINEERED: the drum
had no rim, no ribs, no seam, so it read as a barrel someone left on a
tank. Chamfered rim, radial ribs, and an emitter stack that is obviously a
resonator rather than a turret. */
runningGear(m,11,2.8,2.4,3.0,5,MET_D,1);
m.cyl(0,3.2,0,4.7,4.4,1.9,16,MET); // drum body
m.cyl(0,5.1,0,4.4,4.0,0.55,16,MET_L); // chamfered rim
m.cyl(0,5.65,0,3.7,3.7,0.30,16,MET_D); // recessed deck
/* RADIAL RIBS. Eight spokes turn a smooth cylinder into a fabricated housing,
and from overhead they are what breaks the circle up. */
for(let k=0;k<8;k++){
const a=k/8*TAU;
m.box(Math.cos(a)*3.0,5.66,Math.sin(a)*3.0,2.6,0.34,0.52,MET_L,a);
}
m.box(-4.4,3.2,0,2.4,2.2,5.8,MET_D); // rear counterweight
armorPlate(m,-4.4,4.2,0,4.6,1.2,0.38,0,TEAM_A,0);
/* EMITTER STACK — three rings on a short column, widest at the base, so the
shape reads as a horn projecting upward. */
m.cyl(1.4,5.95,0,2.2,1.7,1.1,14,MET_D);
for(let k=0;k<3;k++) m.ring(1.4,7.05+k*0.52,0,1.9-k*0.46,2.5-k*0.46,18,k?ENERGY:MET_L);
m.cyl(1.4,8.7,0,0.9,0.5,0.7,12,ENERGY);
for(const sd of [-1,1]){
m.bevelBox(-3.0,5.9,sd*1.9,2.6,1.5,1.3,0.30,TEAM_A); // capacitor pods
m.box(-3.0,7.4,sd*1.6,2.4,0.42,0.42,ENERGY);
m.cyl(3.6,5.5,sd*2.4,0.30,0.26,1.1,6,MET_D); // tie-downs
}
ventBank(m,-2.0,5.68,2.4,2.0,1.6,3,MET_D,0);
glowStrip(m,0,5.98,0,3.2,ENERGY,0);
return {hull:m.build(),tur:null,s:1.0};
}
function mdlWarden(){ // field repair — crane and drum
const m=MB();
/* A support vehicle has to read as a TOOL, not as a gun with the barrel taken
off. It was a plain hex slab with a drum on top; the drum was the only
thing saying "repair", and from overhead a drum is a circle like any other.
Rebuilt around the working end: a hose reel with visible spooled line, a
folding gantry that reaches OVER the thing being repaired, and hazard
chevrons on the deck. Hard-surface throughout — bevels, plate breaks and an
inner extrusion — because it is a machine. */
runningGear(m,10,2.6,2.2,2.8,4,MET_D,0);
const HB=[[-4.8,-2.9],[-2.4,-3.3],[2.6,-3.1],[4.5,-1.6],[4.5,1.6],[2.6,3.1],[-2.4,3.3],[-4.8,2.9]];
m.extrude(0,3.0,0,HB,2.0,MET);
m.extrude(0,5.0,0,HB.map(q=>[q[0]*0.82,q[1]*0.82]),0.7,MET_D); // recessed deck
armorPlate(m,-0.4,5.1,0,4.4,1.1,0.36,0,TEAM_A,0);
/* HOSE REEL. Two cheeks with a spooled drum between them reads as a reel at a
glance; a bare cylinder does not. */
for(const sd of [-1,1]) m.cyl(-1.4,5.7,sd*1.9,2.0,2.0,0.42,12,MET_L,Math.PI/2);
for(let k=0;k<5;k++) m.cyl(-1.4,5.7,-1.5+k*0.75,1.62-k*0.06,1.62-k*0.06,0.62,10,DARKER,Math.PI/2);
m.tube(-1.4,7.5,0,1.5,0.9,0.5,12,ENERGY); // emitter head
/* FOLDING GANTRY — the arm reaches forward and DOWN over the casualty, which
is the shape that says "working on something" rather than "aiming". */
m.bevelBox(2.2,5.6,0,1.7,1.9,2.0,0.32,MET_D); // shoulder
m.cyl(2.2,7.4,0,0.40,0.36,1.5,7,MET_L);
m.box(3.8,8.5,0,3.8,0.62,0.62,MET_L); // upper boom
m.box(5.6,7.6,0,0.58,2.0,0.58,MET_D); // drop link
m.cyl(5.6,6.5,0,0.85,0.55,0.8,9,ENERGY); // repair emitter
hydraulic(m,2.9,5.9,0,2.4,0.26,MET_L,0);
/* Hazard chevrons on the rear deck — the visual language of a service
vehicle, and it lands where the top-down camera actually looks. */
for(let k=0;k<3;k++) m.box(-3.4+k*0.9,5.74,0,0.44,0.14,4.2,k&1?MET_D:LAMP,0.42);
ventBank(m,-4.0,5.72,0,1.6,3.0,3,MET_D,0);
stowage(m,0.8,5.72,2.3,2.4,1.0,RUST,0,23);
glowStrip(m,2.2,6.66,0,2.6,ENERGY,0);
deckCrown(m,0,5.72,0,8.6,6.2,MET_D,TEAM_T);
return {hull:m.build(),tur:null,s:0.95};
}
function mdlKestrel(){ // scout aircraft — thin delta
const m=MB();
const keel=[[-7.25,-1.30],[-4.25,-1.72],[2.35,-2.05],[6.45,-.82],[7.65,0],
[6.45,.82],[2.35,2.05],[-4.25,1.72],[-7.25,1.30]];
const spine=[[-6.30,-1.02],[-3.65,-1.30],[2.40,-1.48],[5.85,-.58],[6.65,0],
[5.85,.58],[2.40,1.48],[-3.65,1.30],[-6.30,1.02]];
m.extrude(0,0,0,keel,1.12,MET_D); // shallow sensor-aircraft keel
m.extrude(0,1.12,0,spine,1.18,MET);
m.extrude(.30,2.30,0,spine.map(p=>[p[0]*.76,p[1]*.65]),.48,TEAM_A);
m.bevelBox(3.60,2.46,0,2.85,.88,1.72,.32,GLASS); // low-profile sensor canopy
m.wedge(5.43,2.68,0,1.48,.44,1.48,MET_L,0,true);
m.box(-.72,2.80,0,4.25,.28,.76,TEAM_T); // dorsal livery rail
glowStrip(m,-.15,3.07,0,2.60,ENERGY,0);
for(const sd of [-1,1]){
const wing=sd>0
? [[-4.65,1.05],[2.70,1.28],[.55,6.10],[-5.45,5.02]]
: [[-4.65,-1.05],[-5.45,-5.02],[.55,-6.10],[2.70,-1.28]];
const panel=sd>0
? [[-3.72,1.55],[1.72,1.60],[.35,5.02],[-4.28,4.42]]
: [[-3.72,-1.55],[-4.28,-4.42],[.35,-5.02],[1.72,-1.60]];
m.extrude(0,.64,0,wing,.60,MET_D); // continuous delta wing
m.extrude(0,1.24,0,panel,.23,TEAM_A); // team panel follows silhouette
m.box(-3.55,1.26,sd*4.78,3.35,.55,1.10,DARK); // attached wing spar housing
m.bevelBox(-3.90,1.80,sd*4.78,2.35,.62,.88,.22,MET_L);
m.box(-4.60,2.36,sd*4.78,2.15,1.55,.34,TEAM_A); // canted-looking wingtip fin
m.box(-4.18,3.64,sd*4.78,1.25,.26,.42,LAMP); // navigation lamp on fin
m.box(-4.90,.78,sd*1.14,3.05,.88,1.32,DARKER); // twin micro-turbines merge into tail
cylX(m,-7.12,.92,sd*1.14,3.42,.72,.61,10,DARK,false);
tubeX(m,-7.66,.92,sd*1.14,.88,.58,.27,10,TWR_BORE); // real hollow exhaust
ringX(m,-7.70,.92,sd*1.14,.52,.78,12,MET_L);
ringX(m,-7.73,.92,sd*1.14,.20,.30,10,HOT);
ringX(m,-3.74,.92,sd*1.14,.44,.65,10,MET_L); // intake rim
m.box(3.72,.42,sd*.83,2.15,.72,.64,DARK); // attached nose-gun receiver
gunX(m,4.45,.68,2.58,.22,MET_D,sd*.83);
m.bevelBox(-.95,.38,sd*4.92,2.50,.76,.92,.22,DARKER); // sensor / countermeasure pod
for(let k=0;k<3;k++)
m.box(-1.72+k*.78,1.15,sd*4.92,.42,.30,.48,MET_L);
}
m.bevelBox(-5.48,1.62,0,2.55,1.02,2.45,.28,DARKER); // tail join prevents floating fins
ventBank(m,-2.62,2.80,0,1.95,1.10,4,MET_D,0);
m.greeble(.05,2.80,0,2.55,.82,.28,5,MET_D,0,251);
sensorMast(m,-4.48,2.60,0,1.12,MET_L);
return {hull:m.build(),tur:null,s:0.92};
}
function mdlBasilisk(){ // heavy siege tank, turreted
const m=MB();
/* THE DETAIL BUDGET RAN BACKWARDS. This is a tier-3 experimental at 260 mass
and 1050 energy, and its hull was five primitive calls — against roughly
twenty on the Goliath, a tier-2 costing a quarter as much. A player who
pays four times the price should not receive a model with a quarter of the
surface break-up. Brought up to the Goliath's vocabulary and past it:
that is what "experimental" has to look like. */
runningGear(m,17,4.4,3.6,4.6,7,MET_D,2);
m.extrude(0,4.2,0,[[-8.4,-4.8],[4.6,-5.4],[7.4,-2.6],[7.4,2.6],[4.6,5.4],[-8.4,4.8]],3.4,MET);
m.wedge(6.4,7.6,0,4.6,2.2,9.2,MET_L); // heavy glacis
for(const sd of [-1,1]){
armorPlate(m,0.5,7.6,sd*4.6,9.0,2.0,0.7,0,TEAM_A,0);
m.bevelBox(3.2,8.0,sd*3.8,5.0,1.6,2.2,.35,MET_D); // sponson blisters
m.cyl(6.8,7.9,sd*3.2,.32,.28,1.2,6,MET_L); // tow points
}
ventBank(m,-5.4,7.62,0,4.8,6.8,7,MET_D,0);
exhaust(m,-7.8,6.2,0,3,.52,2.8,MET_D,2.6);
stowage(m,-2.2,7.62,4.2,6.6,2.0,RUST,0,29);
sensorMast(m,-6.8,7.9,2.6,3.4,MET_L);
deckCrown(m,0,7.66,0,14.0,9.4,MET_D,TEAM_T);
m.bevelBox(-6.0,7.8,0,3.6,2.4,7.0,0.6,DARK);
return {hull:m.build(), tur:(()=>{
const t=MB();
t.bevelBox(-0.6,0,0,7.4,3.4,6.4,0.8,TEAM_A);
t.cyl(3.0,1.6,0,0.95,0.80,11.0,10,MET_D,Math.PI/2);
t.tube(13.4,1.6,0,1.25,0.90,1.0,10,DARKER,Math.PI/2); // muzzle brake
t.box(-3.6,2.6,0,2.4,1.6,4.0,MET_L);
return t.build();
})(), s:1.05, turH:7.6};
}
function mdlHarbinger(){ // long-range cluster artillery
const m=MB();
runningGear(m,16,4.0,3.2,4.2,6,MET_D,2);
m.extrude(0,3.8,0,[[-9.0,-3.2],[-6.2,-5.6],[4.2,-5.6],[7.2,-2.2],
[7.2,2.2],[4.2,5.6],[-6.2,5.6],[-9.0,3.2]],3.0,MET);
m.bevelBox(-1.0,6.8,0,9.0,2.6,8.0,0.7,TEAM_A);
for(let r=0;r<2;r++) for(let c=-1;c<=1;c++)
m.tube(0.0,8.4+r*2.0,c*2.6,1.15,0.78,7.2,8,DARKER,0);
for(const sd of [-1,1]){
m.box(-6.0,5.4,sd*4.6,3.0,1.4,1.4,MET_L); // outriggers
m.bevelBox(-6.2,4.9,sd*5.0,2.4,1.2,1.0,.28,MET_D); // outrigger feet
armorPlate(m,0.0,6.4,sd*5.2,10.0,2.2,0.6,0,TEAM_A,0); // flank skirts
m.cyl(5.6,6.5,sd*2.8,.34,.30,1.2,6,MET_L); // tow points
}
ventBank(m,-4.6,6.84,0,4.2,6.0,6,MET_D,0);
exhaust(m,-7.4,5.6,0,3,.48,2.6,MET_D,2.4);
stowage(m,-2.0,6.84,3.9,5.6,1.8,RUST,0,17);
glowStrip(m,-1.0,8.2,0,7.0,HOT,0);
deckCrown(m,-0.4,6.82,0,16.0,11.0,MET_D,TEAM_T);
return {hull:m.build(),tur:null,s:1.05};
}
/* ---- FACTION HEROES ------------------------------------------------------- */
function mdlPraetor(){ // Legion — a walking siege battery
const m=MB();
for(const sd of [-1,1]){
m.bevelBox(-1.2,0,sd*4.4,7.0,2.2,4.0,0.6,SERVO);
m.extrude(-0.4,2.2,sd*4.4,[[-2.0,-2.1],[2.0,-2.1],[2.4,0],[2.0,2.1],[-2.0,2.1],[-2.4,0]],5.6,SERVO);
hydraulic(m,-2.0,2.2,sd*4.4,5.2,0.52,SERVO,0);
m.bevelBox(0.2,7.8,sd*4.4,4.8,3.0,4.8,0.7,SERVO);
m.box(0.2,10.8,sd*4.4,4.2,3.2,4.2,SERVO);
}
m.extrude(0,13.6,0,[[-5.4,-5.8],[3.2,-6.2],[5.8,-3.0],[5.8,3.0],[3.2,6.2],[-5.4,5.8]],6.4,MET);
m.extrude(0,20.0,0,[[-4.4,-4.8],[2.6,-5.2],[4.8,-2.4],[4.8,2.4],[2.6,5.2],[-4.4,4.8]],2.4,TEAM_A);
for(const sd of [-1,1]){
m.bevelBox(0.2,19.0,sd*7.4,5.4,4.4,4.4,0.8,TEAM_A); // pauldrons
armorPlate(m,0.2,23.4,sd*7.4,5.0,1.8,0.6,0,MET_D,0);
for(let r=0;r<2;r++) m.tube(-1.0,21.0+r*1.8,sd*7.4,1.0,0.68,5.4,8,DARKER,0); // shoulder mortars
}
cylX(m,4.6,16.4,0,12.0,1.15,0.95,10,MET_D,false); // siege cannon
tubeX(m,16.0,16.4,0,1.2,1.5,1.05,10,DARKER); // open muzzle collar
m.bevelBox(-4.6,20.2,0,4.0,3.2,7.0,0.7,DARK);
m.bevelBox(2.6,24.6,0,4.0,3.0,5.0,0.6,TEAM_A); // head
m.box(4.6,25.6,0,0.6,1.2,3.4,HOT);
glowStrip(m,0,22.6,0,7.0,HOT,0);
return {hull:m.build(),tur:null,s:1.10};
}
function mdlArchon(){ // Syndicate — hovering, shielded
const m=MB();
/* NINE PRIMITIVES, and one of them was wrong in a way no screenshot catches:
the plenum skirt was an OPEN cyl(), so from any angle below the horizon the
Broker had a black hole where its underside should be — cyl() only caps
when its ninth argument is truthy, and this one passed nothing. The rest
was a slab, a glass ball, three flat rings and two wedges that read as
signboards, on the faction hero of the cybernetic army.
Rebuilt as Syndicate hardware. Three things do the work: a CLOSED plenum
ring carrying six discrete lift pods, so the round base reads as engineered
repetition instead of as a disc; a chassis with a real plate break and
applique armour over it; and the sonic weapon actually PRESENT at the nose,
because TYPES gives this thing 70 damage on a 0.55 cooldown and the old
model had nowhere for it to come from. */
/* ---- PLENUM. Solid and chamfered, not a bucket: an open ring is a well the
camera looks straight down into, and the old skirt was worse than that
because cyl() left it uncapped at both ends. --------------------------- */
chamferCyl(m,0,0.35,0,5.4,1.75,10,MET_D,MET_L); // lift drum
m.ring(0,0.30,0,1.6,4.6,10,ENERGY); // under-wash, seen at low angles
/* SIX LIFT PODS. Repetition at a fixed angular interval is what makes a round
thing read as machined; a smooth ring reads as a coin. */
for(let k=0;k<6;k++){
const a=k/6*TAU, cx=Math.cos(a)*4.9, cz=Math.sin(a)*4.9;
m.bevelBox(cx,1.05,cz,2.5,1.35,2.1,0.34,MET,a);
m.box(cx,0.72,cz,1.7,0.34,1.3,HOT,a); // pod throat, underslung
}
/* ---- CHASSIS. A DART in plan, not an octagon. This camera looks almost
straight down, so PLAN VIEW IS THE SILHOUETTE, and a hero that cannot state
which way it faces from directly above is unreadable exactly when it
matters. Prow at +X, shoulders at the back, two extrusions stepped so there
is a real plate break all the way round.
The body stays BRIGHT (MET / MET_L) and only the plinth and the recesses go
dark: the reference chassis in this file read the way they do because of
that value split, and a hull painted MET_D everywhere loses its own panel
breaks before the tint is even applied. ------------------------------- */
const LO=[[-5.0,-3.4],[-2.8,-4.3],[1.4,-3.9],[5.4,-1.5],[7.0,0],
[5.4,1.5],[1.4,3.9],[-2.8,4.3],[-5.0,3.4]];
m.extrude(0,2.30,0,LO,2.2,MET_D); // dark plinth
m.extrude(0,4.50,0,LO.map(p=>[p[0]*0.93,p[1]*0.90]),1.5,MET);
m.extrude(0,6.00,0,LO.map(p=>[p[0]*0.80,p[1]*0.72]),0.55,MET_L);
/* THE LIVERY IS ONE PANEL, and it is the one the camera looks straight at:
the whole upper deck. Scattering five thin stripes over a hull reads as
pinstriping at 60px; a single top-facing plate reads as an army. */
m.extrude(0,6.55,0,LO.map(p=>[p[0]*0.66,p[1]*0.58]),0.34,TEAM_A);
for(const sd of [-1,1]){
armorPlate(m,-0.4,2.7,sd*4.3,6.6,1.8,0.42,0,MET_D,0); // applique belt
m.box(0.2,4.44,sd*3.55,7.0,0.42,0.70,TEAM_B); // flank accent
insetPanel(m,-2.6,6.06,sd*2.2,3.0,1.6,0.28,MET_D,0); // recessed deck panel
m.wedge(3.8,6.02,sd*1.7,3.0,0.8,2.2,MET_L,0,sd<0); // prow shoulder fairings
}
ventBank(m,-3.6,6.06,0,2.0,3.2,4,MET_D,0);
glacisX(m,4.8,6.02,0,3.2,0.9,3.2,MET_L); // raked nose deck
/* ---- SONIC PROJECTORS. Two horns carried OUTSIDE the prow and projecting
past it, so the weapon is part of the OUTLINE rather than a detail sitting
on top of it. A sound weapon wants a flared throat, not gunX's bore. --- */
for(const sd of [-1,1]){
m.bevelBox(4.0,3.2,sd*3.1,3.6,2.4,2.4,0.42,MET); // trunnion housing
cylX(m,4.6,4.4,sd*3.1,3.8,0.62,1.45,8,MET_L,false); // flared horn
ringX(m,8.2,4.4,sd*3.1,0.44,1.54,8,ENERGY); // emitter face
hydraulic(m,2.6,3.2,sd*3.1,1.8,0.26,MET_L,0); // elevation ram
m.box(2.6,5.66,sd*3.1,2.8,0.34,1.8,TEAM_B); // feed cover livery
}
/* ---- CORTEX. A naked glass ball is a marble. Set the glass INSIDE a
bevelled cowl so only a band of it shows and the sensor reads as armoured
hardware carrying an eye. -------------------------------------------- */
m.bevelBox(-0.6,6.89,0,4.4,1.7,4.0,0.55,MET); // cortex housing
m.sphere(-0.6,8.05,0,1.6,8,GLASS,0.62,true); // glazed dome
m.bevelBox(-0.6,8.55,0,3.4,0.65,3.2,0.40,MET_L); // cowl cap over it
m.box(1.2,8.05,0,0.55,0.5,1.9,LAMP); // forward eye slit
/* ---- DORSAL VANES. Swept delta fins, root fairing to tip cap, raked BACK so
they point away from the prow and reinforce the facing. --------------- */
for(const sd of [-1,1]){
m.extrude(0,6.30,sd*2.9,[[-4.6,-0.36],[-1.2,-0.36],[0.9,0.36],[-3.4,0.36]],3.0,MET);
m.box(-2.6,9.30,sd*2.9,4.0,0.34,0.80,MET_L); // vane cap
m.wedge(-0.4,6.30,sd*2.9,2.0,1.7,0.82,MET_L,0,sd<0); // root fairing
glowStrip(m,-2.6,7.20,sd*3.35,3.2,ENERGY,0); // coil conduit
}
m.bevelBox(-4.9,6.06,0,2.6,2.4,4.2,0.5,MET_D); // reactor bustle
exhaust(m,-6.0,6.06,0,2,0.44,1.5,MET_D,2.0);
return {hull:m.build(),tur:null,s:1.06};
}
function mdlBroodmother(){ // Horde — a walking hive
const m=MB();
/* The progenitor. Same kit as the drone and the Alpha, at hive scale — the
player has to recognise it instantly as the thing all the others came out
of, and that only works if it is built from the same anatomy rather than
from a different set of shapes that happen to be purple. Eight legs, a
swollen egg-bearing abdomen, and a crown. */
carapace(m,-1.2,6.2,0,22.0,6.6,4.2,CHITIN,
{segs:8,ridge:0.26,bump:0.13,keel:0.44,nose:0.52,tail:0.12,
waist:0.38,waistAt:0.56,waistW:0.065,seed:9091,u:18,v:22});
carapace(m,8.0,5.9,0,6.4,3.8,3.0,CHIT_D,
{segs:3,ridge:0.20,bump:0.16,keel:0.30,nose:0.36,tail:0.42,seed:404,u:12,v:9});
eyeCluster(m,10.0,6.5,0,1.35,3,LAMP);
for(let k=0;k<4;k++) for(const sd of [-1,1])
broodLeg(m, 4.0-k*3.0, 5.6, 0, sd, 5.8+k*0.26, 5.6, 3.90-k*0.20, 0.96, null,
((k+(sd<0?1:0))&1)*Math.PI, [4.05,3.15,4.10,3.70][k]);
for(const sd of [-1,1]){
chitinBlade(m,9.0,5.6,sd*2.0,7.2,0.90,-sd*0.46,sd*0.26,CLAW,0.08); // hive jaws
chitinBlade(m,8.8,6.6,sd*1.1,4.8,0.54,-sd*0.34,sd*0.12,CLAW,0.12);
tendril(m,9.2,7.9,sd*1.5,9.5,0.38,sd,4.2,BIO_LEG);
tendril(m,7.4,8.4,sd*2.4,7.6,0.30,sd,3.4,BIO_LEG);
/* Brood-lumen lobes — the egg sacs. Lit tissue, so they carry the faction
colour and glow through the translucency term. */
m.sphere(-7.0,8.0,sd*2.7,2.3,9,BIO_TEAM,0.74,false);
m.sphere(-3.8,8.6,sd*2.9,1.6,8,BIO_TEAM,0.74,false);
chitinBlade(m,1.6,7.6,sd*3.0,6.4,0.62,sd*0.28,Math.PI*0.84,CLAW,0.60);
}
/* Dorsal crown, and a ring of spiracle vents around the abdomen. */
for(let k=0;k<8;k++)
chitinBlade(m,-6.4+k*1.75,8.4,0,3.4+Math.sin(k/7*Math.PI)*3.0,0.50,0.10,Math.PI*0.96,CLAW,1.00);
for(let k=0;k<6;k++){
const a=k/6*TAU;
m.tube(-6.0+Math.cos(a)*3.4,8.9,Math.sin(a)*3.4,0.78,0.40,0.66,8,CHIT_D);
}
tubercles(m,-5.4,7.8,0,4.4,3.6,12,1.15,CHIT_D,61);
return {hull:m.build(),tur:null,s:1.16};
}
/* Build an instancing registry from a faction model map. A faction may replace
only the silhouettes it owns; bldMeshFor() falls back to Nova geometry for
old saves or intentionally shared structures. A complete single-mesh weapon
omits a turret map, preventing a legacy Nova barrel from being drawn over it. */
function initBldMeshSet(modelMap,turMap,tierMap,turH,turS,cap){
const out={}; cap=cap||260;
for(const k in BLD_MDL){
if(!modelMap||typeof modelMap[k]!=='function') continue;
const tiers=tierMap&&tierMap[k];
const variants=tiers?tiers.map(V=>({
base:new InstMesh(gl,V.base(),cap),tur:V.tur?new InstMesh(gl,V.tur(),cap):null
})):null;
out[k]=variants?{base:variants[0].base,tur:variants[0].tur,variants}: {
base:new InstMesh(gl,modelMap[k](),cap),
tur:turMap&&turMap[k]?new InstMesh(gl,turMap[k](),cap):null,
variants:null
};
out[k].turH=turH&&turH[k]!=null?turH[k]:(BLD_TUR_H[k]||14);
out[k].turS=turS&&turS[k]!=null?turS[k]:(BLD_TUR_S[k]||1);
}
return out;
}
/* THE PREVIEWS DISAGREED WITH THE BATTLEFIELD.
Every faction has a complete bespoke building kit (27/27) and its own unit
chassis, and the battlefield resolves both correctly. But three surfaces went
straight to the base maps instead: the build-menu and intel "LIVE 3D MODEL"
thumbnails, the main-menu model carousel, and the placement ghost. A Brood
player therefore designed their base out of a Nova catalogue, watched a Nova
ghost slide across the ground, and only saw grown tissue after the structure
finished. These two are the one seam every preview should ask. */
function factionKitKey(raw){
const r=String(raw||'').trim().toLowerCase();
if(!r) return (typeof playerKitKey==='function')?playerKitKey():'nova';
if(r==='nova'||r==='legion'||r==='syndicate'||r==='horde') return r;
if(typeof FACTIONS!=='undefined'&&FACTIONS[r]&&FACTIONS[r].kit) return FACTIONS[r].kit;
/* Saves and authored missions predate the short runtime keys. Resolve their
doctrinal names here so a Coalition building can never fall through into
another faction's already-resident GPU registry. */
if(r.includes('machine')||r.includes('syndicate')||r.includes('coalition')) return 'syndicate';
if(r.includes('brood')||r.includes('swarm')||r.includes('infestation')||r.includes('horde')) return 'horde';
if(r.includes('dominion')||r.includes('red ascendancy')||r.includes('legion')) return 'legion';
if(r.includes('frontline')||r.includes('federation')||r.includes('terran')) return 'nova';
/* Preserve an unknown key so strict preview callers can reject it. Turning
malformed data into Nova would silently reintroduce the cross-faction art
leak this resolver exists to expose. Battlefield callers still retain
their explicit legacy fallback in bldMeshFor(). */
return r;
}
/* FACTION MODEL OWNERSHIP IS CLOSED, NOT A FALLBACK CHAIN.
The shared TYPES table is a save-compatible rules vocabulary; it is not an
art catalogue. Every live faction model must therefore exist in that
faction's exact kit. Slots 12/13/30/31 are Brood organisms and may never
leak through UNIT_MESH as a blue, red or green substitute. */
function factionUnitModelAllowed(ty,kit){
const k=factionKitKey(kit||((typeof playerKitKey==='function')?playerKitKey():'nova'));
return !!(typeof FAC_KIT!=='undefined'&&FAC_KIT[k]&&typeof FAC_KIT[k][ty]==='function');
}
function factionUnitMeshFor(ty,kit){
const k=factionKitKey(kit||((typeof playerKitKey==='function')?playerKitKey():'nova'));
if(!factionUnitModelAllowed(ty,k)) return null;
return (typeof FAC_MESH!=='undefined'&&FAC_MESH[k]&&FAC_MESH[k][ty])||null;
}
function factionUnitGeo(ty,kit,strict){
const k=factionKitKey(kit||((typeof playerKitKey==='function')?playerKitKey():'nova'));
const fn=(typeof FAC_KIT!=='undefined'&&FAC_KIT[k]&&FAC_KIT[k][ty])||null;
if(fn){ try{ return fn(); }catch(e){} }
/* Returning the base registry here was the leak: Nova was exempt from the
old strict check, so Blue slot 12 or 30 resolved to a Ravager or Brood
Sovereign. Missing faction art must be visible as missing, never disguised
as another faction's successfully rendered model. */
return null;
}
function factionBldMdlSet(kit,strict){
const k=factionKitKey(kit||((typeof playerKitKey==='function')?playerKitKey():'nova'));
if(k==='legion'&&typeof BLD_MDL_LEGION!=='undefined')
return {mdl:BLD_MDL_LEGION,tur:BLD_TUR_MDL_LEGION,h:BLD_TUR_H_LEGION,sc:BLD_TUR_S_LEGION};
if(k==='syndicate'&&typeof BLD_MDL_MACHINE!=='undefined')
return {mdl:BLD_MDL_MACHINE,tur:BLD_TUR_MDL_MACHINE,h:BLD_TUR_H_MACHINE,sc:BLD_TUR_S_MACHINE};
if(k==='horde'&&typeof BLD_MDL_INFESTATION!=='undefined')
return {mdl:BLD_MDL_INFESTATION,tur:BLD_TUR_MDL_INFESTATION,h:BLD_TUR_H_INFESTATION,sc:BLD_TUR_S_INFESTATION};
if(strict&&k!=='nova') return null;
return {mdl:BLD_MDL,tur:BLD_TUR_MDL,h:BLD_TUR_H,sc:BLD_TUR_S};
}
function bldFactionKey(B){
/* Live ownership is authoritative. Older sessions could persist the wrong
`fac` value while the opponent picker was still resolving, and the old
early return below made that stale Brood tag permanently override a
Coalition player's structures. Team 0/1 now follow the currently selected
armies; the saved tag remains only for neutral/replay objects. */
if(!B||B.team===0) return factionKitKey((typeof playerKitKey==='function')?playerKitKey():'nova');
if(B.team===1){
const f=(typeof AI!=='undefined'&&AI&&AI.fac)||(B&&B.fac)||'legion';
return factionKitKey(f);
}
if(B.team===2||B.type==='nest') return 'horde';
return factionKitKey(B.fac||'nova');
}
/* Alternate architecture kits are large enough that uploading all three to
the GPU on the main menu wastes both start-up time and mobile memory. A
skirmish uses one enemy faction, so create that registry the first time it
is requested and keep it for the rest of the session. The JavaScript model
factories remain cheap; only WebGL vertex/index buffers are deferred. */
function ensureBldFactionMeshes(fac){
fac=factionKitKey(fac);
if(!fac||fac==='nova') return BLD_MESH;
if(BLD_FACTION_MESH[fac]) return BLD_FACTION_MESH[fac];
let set=null;
if(fac==='legion'&&typeof BLD_MDL_LEGION!=='undefined')
set=initBldMeshSet(BLD_MDL_LEGION,
typeof BLD_TUR_MDL_LEGION!=='undefined'?BLD_TUR_MDL_LEGION:null,
typeof BLD_TIER_MDL_LEGION!=='undefined'?BLD_TIER_MDL_LEGION:null,
typeof BLD_TUR_H_LEGION!=='undefined'?BLD_TUR_H_LEGION:null,
typeof BLD_TUR_S_LEGION!=='undefined'?BLD_TUR_S_LEGION:null,260);
else if(fac==='syndicate'&&typeof BLD_MDL_MACHINE!=='undefined')
set=initBldMeshSet(BLD_MDL_MACHINE,
typeof BLD_TUR_MDL_MACHINE!=='undefined'?BLD_TUR_MDL_MACHINE:null,
typeof BLD_TIER_MDL_MACHINE!=='undefined'?BLD_TIER_MDL_MACHINE:null,
typeof BLD_TUR_H_MACHINE!=='undefined'?BLD_TUR_H_MACHINE:null,
typeof BLD_TUR_S_MACHINE!=='undefined'?BLD_TUR_S_MACHINE:null,260);
else if(fac==='horde'&&typeof BLD_MDL_INFESTATION!=='undefined')
set=initBldMeshSet(BLD_MDL_INFESTATION,
typeof BLD_TUR_MDL_INFESTATION!=='undefined'?BLD_TUR_MDL_INFESTATION:null,
typeof BLD_TIER_MDL_INFESTATION!=='undefined'?BLD_TIER_MDL_INFESTATION:null,
typeof BLD_TUR_H_INFESTATION!=='undefined'?BLD_TUR_H_INFESTATION:null,
typeof BLD_TUR_S_INFESTATION!=='undefined'?BLD_TUR_S_INFESTATION:null,260);
if(set) BLD_FACTION_MESH[fac]=set;
return set;
}
function bldMeshFor(B){
const fac=bldFactionKey(B),set=ensureBldFactionMeshes(fac);
const type=B.type;
/* A missing non-Nova mesh is a visible content error, not permission to put
a blue building in another faction. Complete-kit audits gate releases;
returning null here keeps a regression honest in the live game too. */
if(fac!=='nova')return (set&&set[type])||null;
return BLD_MESH[type]||null;
}
/* Imported geometry (Blender exports, VRoid soldiers) lands here, keyed by
UNIT_MDL slot. assets/data/meshes.js assigns into it at load; initModels
prefers an imported body over the procedural builder, so hand-written and
imported models are interchangeable per unit with no other change. */
const MF_BLENDER_GEO={};
function initModels(){
for(let t=0;t<UNIT_MDL.length;t++){
const ext=MF_BLENDER_GEO[t];
const g=ext?{hull:ext,tur:null,s:1}:UNIT_MDL[t]();
UNIT_GEO[t]=g;
UNIT_MESH[t]={
hull:new InstMesh(gl,g.hull, t===12||t===13?4200:1400),
tur: g.tur? new InstMesh(gl,g.tur, 1400):null,
s:g.s||1, turH:g.turH||0, air:g.air||0, muzzle:g.muzzle||0, muzzleZ:g.muzzleZ||0
};
}
Object.assign(BLD_MESH,initBldMeshSet(BLD_MDL,BLD_TUR_MDL,BLD_TIER_MDL,BLD_TUR_H,BLD_TUR_S,260));
FX.cityT =new InstMesh(gl,mdlCityTower(),420);
FX.cityD =new InstMesh(gl,mdlCityDome(),420);
FX.cityH =new InstMesh(gl,mdlCityHall(),160);
FX.cityK =new InstMesh(gl,mdlCityTank(),160);
FX.rock =new InstMesh(gl,mdlRock(),900);
FX.tree =new InstMesh(gl,mdlTree(),900);
FX.treePine=new InstMesh(gl,mdlTreePine(),700);
FX.treePalm=new InstMesh(gl,mdlTreePalm(),500);
FX.treeDead=new InstMesh(gl,mdlTreeDead(),500);
FX.treeSpore=new InstMesh(gl,mdlTreeSpore(),600);
FX.bush =new InstMesh(gl,mdlBush(),800);
FX.rockIce=new InstMesh(gl,mdlRockIce(),700);
FX.rockSlag=new InstMesh(gl,mdlRockSlag(),700);
FX.crystal=new InstMesh(gl,mdlCrystal(),220);
FX.dep =new InstMesh(gl,mdlDeposit(),120);
FX.geyser=new InstMesh(gl,mdlGeyser(),80);
FX.crate =new InstMesh(gl,mdlCrate(),60);
for(const k in DROP_MDL){
DROP_MESH[k]={
body:new InstMesh(gl,DROP_MDL[k](),4),
gear:DROP_GEAR_MDL[k]?new InstMesh(gl,DROP_GEAR_MDL[k](),4):null,
vtol:DROP_VTOL_MDL[k]?new InstMesh(gl,DROP_VTOL_MDL[k](),16):null,
rotor:DROP_ROTOR_MDL[k]?new InstMesh(gl,DROP_ROTOR_MDL[k](),16):null
};
}
/* Compatibility aliases keep old capture/release tooling working; live
rendering selects through DROP_MESH and never assumes the Nova body. */
FX.drop=DROP_MESH.nova.body;
FX.dropGear=DROP_MESH.nova.gear;
FX.wreck =new InstMesh(gl,mdlWreck(),480);
FX.berm =new InstMesh(gl,mdlBerm(),420);
FX.shell =new InstMesh(gl,mdlShell(),1400);
FX.bolt =new InstMesh(gl,mdlBolt(),3000);
FX.shard =new InstMesh(gl,mdlShard(),5000);
FX.beam =new InstMesh(gl,mdlBeamSeg(),1200);
FX.cone =new InstMesh(gl,mdlCone(),700);
FX.ring =new InstMesh(gl,mdlRing(),900);
FX.disc =new InstMesh(gl,mdlDisc(),900);
initFactionKits();
FX.plate =new InstMesh(gl,mdlPlate(),2600);
FX.shadow=new InstMesh(gl,mdlShadow(),4000);
FX.modMark=new InstMesh(gl,mdlModMark(),64);
FX.line =new InstMesh(gl,mdlLine(),9000);
FX.wedge =new InstMesh(gl,mdlLightWedge(),200); // flashlight cones on the ground
FX.pool =new InstMesh(gl,mdlLightPool(),480); // beam splashes + machine underglow
}
|