File size: 90,082 Bytes
b6a38d7 |
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 |
const.AIDecisionThreshold = 80 -- targets/locations up to this percent of max scored target/location can be selected
const.AIPointBlankTargetMod = 50 -- targets in point-blank range get +50% score
const.AIFallbackWeight_OpenDoor = 100
const.AIFallbackWeight_ClosedDoor = 40
const.AIFallbackWeight_Window = 70
const.AIAvoidFireWeigth = -200
const.AIAvoidGasWeigth = -200
const.AIAvoidBombardEdge = 100 -- % of score retained at the border of the zone
const.AIAvoidBombardCenter = 30 -- % of score retained at the center of the zone
const.AIFriendlyFire_MaxRange = 10 * const.SlabSizeX -- max range to ally for it to be considered in danger
const.AIFriendlyFire_LOFWidth = 100*guic -- max distance from an ally to the line between position and target considered in danger
const.AIFriendlyFire_LOFConeNear = 100*guic -- same as above for cone attacks (near side of the cone, positioned at attacker)
const.AIFriendlyFire_LOFConeFar = 300*guic -- same as above for cone attacks (far side of the cone, positioned at AIFriendlyFire_MaxRange)
const.AIFriendlyFire_ScoreMod = 50 -- % of damage score evaluation remanining when an ally is in danger
const.AIShootAboveCTH = 0
local function CanReload(unit, weapon)
if not IsKindOf(weapon, "Firearm") then
return false
end
if (weapon.ammo and weapon.ammo.Amount or 0) >= weapon.MagazineSize then
return false
end
if not unit:HasAP(CombatActions["Reload"]:GetAPCost(unit)) then
return false
end
local ammo_type
if (weapon.ammo and weapon.ammo.Amount or 0) > 0 then
ammo_type = weapon.ammo.class
end
local ammo = unit:GetAvailableAmmos(weapon, ammo_type)
if not ammo or not ammo[1] then
return false
end
return true
end
function WaitIdle(unit)
while IsValidTarget(unit) and not unit:IsIdleCommand() do
WaitMsg("Idle", 200)
end
end
local remove_action_cam_actions = { Move = true, MeleeAttack = true, ThrowGrenadeA = true, ThrowGrenadeB = true , ThrowGrenadeC = true, ThrowGrenadeD = true}
function AIStartCombatAction(action_id, unit, ap, args, ...)
if not ap then
ap = CombatActions[action_id]:GetAPCost(unit, args, ...)
end
if not ap or ap < 0 or not unit:HasAP(ap, action_id) then
return false
end
if ActionCameraPlaying then
local waited
if CurrentActionCamera.wait_signal then
waited = true
WaitMsg("ActionCameraWaitSignalEnd", 2000)
end
if remove_action_cam_actions[action_id] and g_Combat and g_Combat:IsVisibleByPoVTeam(unit) and not args.reposition then
if not waited then
Sleep(500)
end
RemoveActionCamera()
end
end
if args and type(args) == "table" then
if args.target then
--HandleCameraTargetFixed(unit, args.target)
ShowBadgeOfAttacker(unit, true)
end
if args.voiceResponse then
PlayVoiceResponseGroup(unit, args.voiceResponse)
elseif unit.ai_context and unit.ai_context.movement_action then
local vr = unit.ai_context.movement_action:GetVoiceResponse()
if vr then
PlayVoiceResponseGroup(unit, vr)
end
end
end
local willBeTracked, visibleMovement
if action_id == "Move" then
willBeTracked, visibleMovement = AddToCameraTrackingBehavior(unit, args)
args.willBeTracked = willBeTracked
args.visibleMovement = visibleMovement
end
StartCombatAction(action_id, unit, ap, args, ...)
return true
end
function AIPlayCombatAction(action_id, unit, ap, args)
--[[if args and IsKindOf(args.target, "Unit") then
printf("%s (%d): %s vs %s", _InternalTranslate(unit.Name or ""), unit.handle, action_id, _InternalTranslate(args.target.Name or ""))
end--]]
if not AIStartCombatAction(action_id, unit, ap, args) then
return false
end
WaitCombatActionsPostAction(unit)
ClearAITurnContours()
StopCinematicCombatCamera()
return true
end
function AIStartChangeStance(unit, stance, target_pos)
if unit.stance == stance then
return true
end
local angle
if target_pos and target_pos:IsValid() then
angle = CalcOrientation(unit, target_pos)
end
local args = { angle = angle }
local result
if stance == "Standing" then
result = AIStartCombatAction("StanceStanding", unit, nil, args)
elseif stance == "Crouch" then
result = AIStartCombatAction("StanceCrouch", unit, nil, args)
elseif stance == "Prone" then
result = AIStartCombatAction("StanceProne", unit, nil, args)
end
return result or false
end
function AIPlayChangeStance(unit, stance, target_pos)
if not AIStartChangeStance(unit, stance, target_pos) then
return false
end
WaitCombatActionsPostAction(unit)
return true
end
MapVar("g_AIDestIndoorsCache", {})
MapVar("g_AISignatureActionModifiers", {})
function AIUpdateContext(context, unit)
unit = unit or context.unit
context.unit_pos = GetPassSlab(unit) or context.unit_pos
context.unit_stance_pos = GetPackedPosAndStance(unit) or context.unit_stance_pos
context.unit_grid_voxel = point_pack(unit:GetGridCoords())
end
function AIGetIntendedTarget(unit, context)
context = context or unit.ai_context or empty_table
local dest = context.ai_destination or GetPackedPosAndStance(unit)
return (context.dest_target or empty_table)[dest]
end
function AILockTarget(unit, context)
context = context or unit.ai_context
local target = AIGetIntendedTarget(unit, context)
if target then
context.target_locked = target
end
end
function AIGetAttackTargetingOptions(unit, context, target, action, targeting)
local body_parts
targeting = targeting or context.archetype.BaseAttackTargeting
if IsKindOf(target, "Unit") and targeting then
action = action or context.default_attack
local args = { target = target, aim = 0 }
local parts = target:GetBodyParts(context.weapon)
local valid, fallback
for _, part in ipairs(parts) do
args.target_spot_group = part.id
local results = action:GetActionResults(unit, args)
body_parts = body_parts or {}
results.chance_to_hit = results.chance_to_hit or 0
table.insert(body_parts, {id = part.id, chance = results.chance_to_hit})
if results.chance_to_hit > 0 then
fallback = fallback or {id = part.id, chance = results.chance_to_hit}
if targeting[part.id] then
valid = true
end
end
end
if not valid then
table.insert(body_parts, fallback)
end
end
return body_parts
end
function AIPlayAttacks(unit, context, dbg_action, force_or_skip_action)
-- filter enemies because they might have been killed by a teammate
if g_AIExecutionController then
g_AIExecutionController:Log("Unit %s (%d) start attack sequence", unit.unitdatadef_id, unit.handle)
end
local enemies = context.enemies
for i = #enemies, 1, -1 do
if not IsValidTarget(enemies[i]) then
table.remove(enemies, i)
end
end
local remaining_free_ap = unit.free_move_ap
unit:RemoveStatusEffect("FreeMove") -- lose any remaining free movement points, we're going to use actions now
AIUpdateContext(context, unit)
if g_AIExecutionController then
g_AIExecutionController:Log(" Num enemies: %d", #enemies)
g_AIExecutionController:Log(" Action Points: %d", unit.ActionPoints)
end
local dest = not force_or_skip_action and context.ai_destination or GetPackedPosAndStance(unit)
-- recalc target to make sure we're firing at a valid target, but prefer the already picked target if there's one
--table.insert(g_AIDamageScoreLog, string.format("[%s] AIPlayAttacks (%s)", _InternalTranslate(unit.Name or ""), context.archetype.id))
context.dest_ap[dest] = context.dest_ap[dest] or unit.ActionPoints
AIPrecalcDamageScore(context, {dest}, context.target_locked or (context.dest_target or empty_table)[dest])
-- archetype signature actions
local signature_action
if dbg_action then
context.action_states = context.action_states or {}
context.action_states[dbg_action] = {}
dbg_action:PrecalcAction(context, context.action_states[dbg_action])
if dbg_action:IsAvailable(context, context.action_states[dbg_action]) then
signature_action = dbg_action
elseif force_or_skip_action then
table.insert(failed_actions, dbg_action.BiasId or dbg_action.class)
return
end
end
if not context.reposition and not unit:HasStatusEffect("Numbness") then
signature_action = signature_action or AIChooseSignatureAction(context)
end
local default_attack = context.default_attack
local default_attack_vr = "AIAttack"
if default_attack and default_attack.FiringModeMember and default_attack.FiringModeMember == "AttackShotgun" then
default_attack_vr = "AIDoubleBarrel"
end
local voice_response = signature_action and (signature_action:GetVoiceResponse() or "") or default_attack_vr
if voice_response == "" then
voice_response = nil
end
if signature_action then
if g_AIExecutionController then
g_AIExecutionController:Log(" Signature Action: %s", signature_action:GetEditorView())
end
signature_action:OnActivate(unit)
--printf("[signature] %s (%d)", _InternalTranslate(unit.Name or ""), unit.handle)
if voice_response then
context.action_states[signature_action].args = context.action_states[signature_action].args or {}
context.action_states[signature_action].args.voiceResponse = voice_response
end
local status = signature_action:Execute(context, context.action_states[signature_action])
context.ap_after_signature = unit.ActionPoints
if status then -- support signature actions that want to restart or stop ai turn execution
return status
end
AIReloadWeapons(unit)
context.max_attacks = context.max_attacks - 1
else
if g_AIExecutionController then
g_AIExecutionController:Log(" No Signature Action chosen")
end
end
local target = (context.dest_target or empty_table)[dest]
if signature_action and (not IsValidTarget(target) or (IsKindOf(target, "Unit") and target:IsIncapacitated())) then
--table.insert(g_AIDamageScoreLog, string.format("[%s] TargetChange (%s)", _InternalTranslate(unit.Name or ""), context.archetype.TargetChangePolicy))
if context.archetype.TargetChangePolicy == "restart" then
return "restart"
end
context.dest_ap[dest] = unit.ActionPoints
context.target_locked = nil
AIPrecalcDamageScore(context, {dest})
target = context.dest_target[dest]
end
if IsValidTarget(target) then
if g_AIExecutionController then
g_AIExecutionController:Log(" Target: %s", IsKindOf(target, "Unit") and target.unitdatadef_id or target.class)
end
-- revert to basic attacks
local attacks, aim = AICalcAttacksAndAim(context, unit.ActionPoints)
if context.default_attack.id == "Bombard" and AICheckIndoors(dest) then
attacks = 0
end
local args = { target = target, voiceResponse = voice_response }
if attacks > 1 then
unit:SequentialActionsStart()
end
if g_AIExecutionController then
g_AIExecutionController:Log(" Executing %d attacks...", attacks)
end
local body_parts = AIGetAttackTargetingOptions(unit, context, target)
for i = 1, attacks do
args.aim = aim[i]
args.target_spot_group = nil
if body_parts and #body_parts > 0 then
local pick = table.weighted_rand(body_parts, "chance", InteractionRand(1000000, "Combat"))
if pick then
args.target_spot_group = pick.id
end
end
Sleep(0)
local result = AIPlayCombatAction(context.default_attack.id, unit, nil, args)
context.max_attack = context.max_attacks - 1
if g_AIExecutionController then
g_AIExecutionController:Log(" Attack %d result: %s", i, tostring(result))
end
if IsSetpiecePlaying() then
unit:SequentialActionsEnd()
return
end
AIReloadWeapons(unit)
if not result or i == attacks or not IsValidTarget(unit) or context.max_attacks <= 0 then
break
end
while IsKindOf(target, "Unit") and target:IsGettingDowned() do
WaitMsg("UnitDowned", 20)
end
if not IsValidTarget(target) or (IsKindOf(target, "Unit") and target:IsIncapacitated()) then
--table.insert(g_AIDamageScoreLog, string.format("[%s] TargetChange (%s)", _InternalTranslate(unit.Name or ""), context.archetype.TargetChangePolicy))
if context.archetype.TargetChangePolicy == "restart" then
unit:SequentialActionsEnd()
return "restart"
end
-- look for another target
context.dest_ap[dest] = unit.ActionPoints
context.target_locked = nil
AIPrecalcDamageScore(context, {dest})
target = context.dest_target[dest]
if not IsValidTarget(target) then
break
end
end
Sleep(0)
end
unit:SequentialActionsEnd()
elseif unit:HasStatusEffect("StationedMachineGun") and CombatActions.MGPack:GetUIState({unit}) == "enabled" then
unit:SequentialActionsEnd()
AIPlayCombatAction("MGPack", unit)
return "restart"
else
if g_AIExecutionController then
g_AIExecutionController:Log(" No target")
end
end
unit:SequentialActionsEnd()
while not unit:IsIdleCommand() do
WaitMsg("Idle", 50)
end
if unit.ActionPoints + remaining_free_ap == context.start_ap and not unit:HasStatusEffect("ManningEmplacement") then
-- no action was taken, use a fallback one
-- if all fails, move toward optimal loc
if context.closest_dest then
unit:GainAP(remaining_free_ap)
local dest = context.closest_dest
local x, y, z, stance_idx = stance_pos_unpack(dest)
local move_stance_idx = context.dest_combat_path[dest]
local cpath = context.combat_paths[move_stance_idx]
local pt = SnapToPassSlab(x, y, z)
local path = pt and cpath and cpath:GetCombatPathFromPos(pt)
if path then
local goto_stance = StancesList[move_stance_idx]
if goto_stance ~= unit.stance then
AIPlayChangeStance(unit, goto_stance, point(point_unpack(path[2])))
end
local goto_ap = unit.ActionPoints -- context.dest_ap[dest] --cpath.paths_ap[point_pack(x, y, z)] or 0
context.ai_destination = path[1]
AIPlayCombatAction("Move", unit, goto_ap, { goto_pos = point(point_unpack(path[1])), fallbackMove = true, goto_stance = stance_idx })
end
end
if unit:GetDist(context.unit_pos) < const.SlabSizeX / 2 then
local revert = true
if context.archetype.FallbackAction == "overwatch" then
-- try to place overwatch
revert = not AIPlaceFallbackOverwatch(unit, context)
end
if revert then
-- we're stuck somewhere and unable to move or act, revert back to being Unaware (only if no sight of any enemies)
local sight = false
for _, enemy in ipairs(context.enemies) do
sight = sight or HasVisibilityTo(unit, enemy)
end
if not sight then
table.insert(g_UnawareQueue, unit)
end
end
end
end
end
function AIPlaceFallbackOverwatch(unit, context)
if not IsKindOf(context.weapon, "Firearm") then
return false
end
if context.weapon.PreparedAttackType ~= "Overwatch" and context.weapon.PreparedAttackType ~= "Both" then
return false
end
local target_pt
local room = EnumVolumes(unit, "smallest")
if room then
-- indoors - overwatch against an open door/window or a closed one if none of them are opened
local targets = {}
room:ForEachSpawnedDoor(function(obj)
local w = (obj.pass_through_state == "open" or obj.pass_through_state == "broken") and const.AIFallbackWeight_OpenDoor or const.AIFallbackWeight_ClosedDoor
targets[#targets + 1] = { obj = obj, weight = w }
end)
room:ForEachSpawnedWindow(function(obj)
targets[#targets + 1] = { obj = obj, weight = const.AIFallbackWeight_Window }
end)
if #targets > 0 then
local target = table.weighted_rand(targets, "weight", InteractionRand(1000000, "AIDecision"))
target_pt = target.obj:GetPos()
end
elseif context.unit.last_known_enemy_pos then
target_pt = context.unit.last_known_enemy_pos
else
-- check for aware teammates that we can see
local sp = GetPackedPosAndStance(unit)
local targets = {}
for _, ally in ipairs(context.allies) do
if ally ~= context.unit and context.unit:GetDist(ally) < 12 * guim and stance_pos_visibility(sp, context.ally_pack_pos_stance[ally]) then
-- try to find a point that we can (probably) see in front of our ally
local v = Rotate(point(guim, 0, 0), ally:GetAngle())
for i = 6, 1, -1 do
local tpt = SnapToPassSlab(ally:GetPos() + SetLen(v, i*guim))
if tpt then
local x, y, z = tpt:xyz()
local tsp = stance_pos_pack(x, y, z, StancesList.Standing)
if stance_pos_visibility(sp, tsp) then
targets[#targets + 1] = tpt
break
end
end
end
end
end
if #targets == 0 then
-- target in direction of alive enemy
local revealed, all = {}, {}
for _, enemy in ipairs(context.enemies) do
if IsValidTarget(enemy) then
all[#all + 1] = enemy
if not enemy:HasStatusEffect("Hidden") then
revealed[#revealed + 1] = enemy
end
end
end
local target_units = #revealed > 0 and revealed or all
for _, enemy in ipairs(target_units) do
targets[#targets + 1] = enemy:GetPos() + Rotate(point(InteractionRand(4*guim), 0, 0, InteractionRand(360*60)))
end
end
if #targets > 0 then
target_pt = table.interaction_rand(targets, "AIDecision")
end
end
if target_pt then
local args, has_ap = AIGetAttackArgs(context, CombatActions.Overwatch, nil, "None")
if args and has_ap then
args.target_pos = target_pt
args.target = target_pt
if AIPlayCombatAction("Overwatch", context.unit, nil, args) then
PlayVoiceResponse(context.unit, "AIOverwatch")
return true
end
end
end
return false
end
function AIExecuteUnitBehavior(unit, force_or_skip_action)
if not g_Combat or not IsValid(unit) or unit:IsDead() then
return
end
if unit.ai_context.behavior then
local status = unit.ai_context.behavior:Play(unit)
if g_AIExecutionController then
g_AIExecutionController:Log(" Behavior %s for unit %s (%d) returned '%s'", unit.ai_context.behavior:GetEditorView(), unit.unitdatadef_id, unit.handle, tostring(status))
end
if status then -- support behaviors that want to restart or stop the unit's ai
return status
end
end
-- recheck unit, they could be killed or despawned during Play
if IsValid(unit) and not unit:IsDead() then
-- use the rest of the ap (if any) in signature actions and basic attacks
return AIPlayAttacks(unit, unit.ai_context, unit.ai_context.forced_signature_action, force_or_skip_action) or AITakeCover(unit)
end
end
function AITakeCover(unit, context)
local context = unit.ai_context
if unit:HasPreparedAttack() or not context or ((context.ap_after_signature or 0) <= 0) then
return
end
local cover_high, cover_low = GetCoverTypes(unit)
if not cover_high and not cover_low then
return
end
if unit.species == "Human" and unit.stance ~= "Prone" then
local context = unit.ai_context
local chance = context and context.behavior and context.behavior.TakeCoverChance or 0
if chance > 0 and (chance >= 100 or unit:Random(100) < chance) then
local dest = GetPackedPosAndStance(unit)
local enemy_visible = context.enemy_visible
local enemy_pos = context.enemy_pack_pos_stance
for _, enemy in ipairs(context.enemies) do
if (enemy_visible[enemy] and GetCoverFrom(dest, enemy_pos[enemy]) or 0) > 0 then
AIPlayCombatAction("TakeCover", unit, 0)
return
end
end
end
end
if cover_low then
AIPlayCombatAction("StanceCrouch", unit, 0)
end
end
function AIApplyActionModifiers(signature_action, unit)
for _, mod in ipairs(signature_action.WeightModifications) do
local id = mod.ActionId
if id then
local act_mods = g_AISignatureActionModifiers[id] or {}
g_AISignatureActionModifiers[id] = act_mods
local list
if mod.ApplyTo == "Self" then
list = act_mods[unit] or {}
act_mods[unit] = list
else
list = act_mods[unit.team] or {}
act_mods[unit.team] = {}
end
list[#list + 1] = { end_turn = g_Combat.current_turn + mod.Period, value = mod.Value }
end
end
end
function AIGetActionWeight(action, unit, action_state)
local w = action.Weight
local id = action.ActionId
if id and id ~= "" then
local mods = g_AISignatureActionModifiers[id] or empty_table
if mods[unit] then w = w + mods[unit].total end
if mods[unit.team] then w = w + mods[unit.team].total end
end
local score = action_state and action_state.score or 100
return MulDivRound(w, score, 100)
end
function AIGetSignatureActions(context, movement)
local actions = {}
-- if the behavior has any defined actions, pick from that list, otherwise revert to archetype's
local actions_pool = context.behavior:GetSignatureActions(context)
if not actions_pool or #actions_pool == 0 then
actions_pool = context.archetype.SignatureActions
end
local unit = context.unit
movement = movement or false
for _, action in ipairs(actions_pool) do
if (action.movement == movement) and action:MatchUnit(unit) then
actions[#actions + 1] = action
end
end
return actions
end
function AISelectAction(context, actions, base_weight, dbg_available_actions)
local available = {}
local weight = base_weight or 0
context.action_states = context.action_states or {}
for _, action in ipairs(actions) do
context.action_states[action] = {}
local weight_mod, disable, priority = AIGetBias(action.BiasId, context.unit)
disable = disable or context.disable_actions[action.BiasId or false]
if not disable then
action:PrecalcAction(context, context.action_states[action])
if action:IsAvailable(context, context.action_states[action]) then
local action_weight = MulDivRound(action.Weight, weight_mod, 100)
priority = priority or action.Priority
if dbg_available_actions then
table.insert(dbg_available_actions, { action = action, weight = action_weight, priority = priority })
end
if priority then
return action
end
available[#available + 1] = action
available[available] = action_weight
weight = weight + action_weight
elseif dbg_available_actions then
table.insert(dbg_available_actions, { action = action, weight = false })
end
end
end
if weight > 0 then
local roll = InteractionRand(weight, "AISignatureAction", context.unit)
for _, action in ipairs(available) do
local w = available[action]
if roll <= weight then
return action
end
roll = roll - weight
end
end
return available[#available]
end
function AIChooseSignatureAction(context)
local weight = context.archetype.BaseAttackWeight
context.choose_actions = { { action = false, weight = weight, priority = false } },
AIUpdateBiases()
local sig_actions = AIGetSignatureActions(context)
return AISelectAction(context, sig_actions, weight, context.choose_actions)
end
function AIChooseMovementAction(context)
local actions = AIGetSignatureActions(context, true)
AIUpdateBiases()
return AISelectAction(context, actions, context.archetype.BaseMovementWeight)
end
function AIFindDestinations(unit, context)
local pos = GetPassSlab(unit) or unit:GetPos()
local destinations, paths, dest_ap, dest_path, voxel_to_dest, closest_free_pos = AIBuildArchetypePaths(unit, pos, context)
if not closest_free_pos then
if unit.ActionPoints == 0 then
assert(not "AI try to act with 0 action points!!!")
else
print("AI can't find unit free destination prints!!!")
printf(" AP = %d", unit.ActionPoints)
printf(" Command = %s", unit.command)
printf(" Status effects: %s", table.concat(table.keys(unit.StatusEffects), ", "))
printf(" Pos: %s", tostring(unit:GetPos()))
printf(" Pass slab pos: %s", tostring(GetPassSlab(unit) or ""))
printf(" Target dummy pos %s", unit.target_dummy and tostring(unit.target_dummy:GetPos()) or "")
local o = GetOccupiedBy(unit:GetPos(), unit)
if o then
printf("Other pos %s", tostring(o:GetPos()))
printf("Other target dummy pos %s", o.target_dummy and tostring(o.target_dummy:GetPos()) or "")
printf("Other efResting=%d", o:GetEnumFlags(const.efResting))
if o.reposition_dest then
printf("Other reposition dest=%s", tostring(point(stance_pos_unpack(o.reposition_dest))))
end
end
assert(not "AI can't find unit free destination")
end
end
local crouch_idx = StancesList.Crouch
local important_dests = context.important_dests or {}
context.important_dests = important_dests
local change_stance_costs = {}
for stance_idx in ipairs(StancesList) do
change_stance_costs[stance_idx] = GetStanceToStanceAP(StancesList[stance_idx], "Crouch")
end
-- preprocess destinations to find those where we need to change stance at the dest to take cover
local low = const.CoverLow
--local high = const.CoverHigh
for i, dest in ipairs(destinations) do
local x, y, z, stance_idx = stance_pos_unpack(dest)
if stance_idx ~= crouch_idx then
local cost = change_stance_costs[stance_idx]
local ap = dest_ap[dest]
if cost and ap and ap >= cost then
local up, right, down, left = GetCover(x, y, z)
if up then
local cover_low = up == low or right == low or down == low or left == low
--local cover_high = up == high or right == high or down == high or left == high
if cover_low then --and not cover_high then
table.remove_value(important_dests, dest)
local new_dest = stance_pos_pack(x, y, z, crouch_idx)
destinations[i] = new_dest
voxel_to_dest[point_pack(x, y, z)] = new_dest
dest_ap[new_dest] = ap - cost
dest_path[new_dest] = dest_path[dest]
table.insert_unique(important_dests, new_dest)
end
end
end
end
end
context.destinations = destinations -- available destinations
context.dest_ap = dest_ap -- dest -> available ap
context.combat_paths = paths
context.dest_combat_path = dest_path -- dest -> index in context.combat_paths (to reach this dest)
context.voxel_to_dest = voxel_to_dest
context.closest_free_pos = closest_free_pos
context.all_destinations = AIEnumValidDests(context)
end
MapVar("g_BiasMarkers", false)
function AICreateContext(unit, context)
local gx, gy, gz = unit:GetGridCoords()
local weapon = unit:GetActiveWeapons()
local default_attack = unit:GetDefaultAttackAction(nil, "ungrouped", nil, "sync")
local enemies = table.icopy(GetEnemies(unit))
for _, groupname in ipairs(unit.Groups) do
local group_modifiers = gv_AITargetModifiers[groupname]
for target_group, mod in pairs(group_modifiers) do
for _, obj in ipairs(Groups[target_group]) do
if IsKindOf(obj, "Unit") then
table.insert_unique(enemies, obj)
end
end
end
end
if not g_BiasMarkers then
InitAIBiasMarkers()
end
-- fallback when our whole team doesn't have a visual on the enemy but we're still aware
if #(enemies or empty_table) == 0 then
enemies = table.ifilter(GetAllEnemyUnits(unit), function(idx, enemy) return not enemy:HasStatusEffect("Hidden") end)
end
-- special-case when having ManningEmplacement status - filter out non targetable enemies
if unit:HasStatusEffect("ManningEmplacement") then
enemies = table.ifilter(enemies, function(idx, enemy) return enemy:IsThreatened({unit}) end)
end
table.sortby_field(enemies, "handle")
local pos = GetPassSlab(unit)
if not pos then -- can happen if the unit is on impassable for some reason
--assert(false, "GetPassSlab failed for unit " .. unit.session_id)
local x, y, z = unit:GetPosXYZ()
local gx, gy, gz = WorldToVoxel(x, y, z)
if not z then
gz = nil
end
pos = point(VoxelToWorld(gx, gy, (gz)))
end
local wx, wy, wz = pos:xyz()
context = context or {}
context.unit = unit
context.unit_pos = pos
context.start_ap = unit.ActionPoints
context.archetype = unit:GetArchetype()
context.unit_grid_voxel = point_pack(gx, gy, gz)
context.unit_world_voxel = point_pack(pos)
context.unit_stance_pos = stance_pos_pack(wx, wy, wz, StancesList[unit.stance])
context.max_attacks = unit.MaxAttacks
context.dest_target = {} -- dest -> picked target (if any)
context.dest_target_score = {} -- dest -> estimated damage
context.weapon = weapon
context.default_attack = default_attack
context.default_attack_cost = default_attack:GetAPCost(unit)
context.EffectiveRange = IsKindOf(weapon, "Firearm") and weapon.WeaponRange / 2 or 1
context.ExtremeRange = IsKindOf(weapon, "Firearm") and weapon.WeaponRange or 1
context.enemies = enemies
context.enemy_visible = {} -- [enemy] -> true/false
context.enemy_visible_by_team = {} -- [enemy] -> true/false
context.enemy_pos = {}
context.enemy_grid_voxel = {}
context.enemy_pack_pos_stance = {}
context.enemy_dir = {}
context.stance_pos_to_vis_enemies = {}
context.allies = unit.team.units
context.ally_grid_voxel = {}
context.ally_pack_pos_stance = {}
context.ally_pos = {}
context.voxel_heal_target = {}
context.voxel_heal_score = {}
context.forced_signature_action = false
context.apply_bias = true
context.disable_actions = {} -- support for custom filtering for signature action selection by BiasId
NetUpdateHash("AICreateContext", unit, pos, unit.stance, context.start_ap, context.archetype.id, context.max_attacks, weapon and weapon.class, weapon and weapon.id, default_attack.id)
if unit:HasStatusEffect("Stimmed") then
context.max_attacks = context.max_attacks + 1
end
for _, action in ipairs(context.archetype.SignatureActions) do
context.can_heal = context.can_heal or IsKindOf(action, "AIActionBandage")
end
if not context.can_heal then
for _, behavior in ipairs(context.archetype.Behaviors) do
for _, action in ipairs(behavior.SignatureActions) do
context.can_heal = context.can_heal or IsKindOf(action, "AIActionBandage")
end
end
end
for i, enemy in ipairs(enemies) do
local x, y, z = enemy:GetGridCoords()
context.enemy_grid_voxel[enemy] = point_pack(x, y, z)
context.enemy_pack_pos_stance[enemy] = GetPackedPosAndStance(enemy)
local enemy_pos = GetPassSlab(enemy) or SnapToVoxel(enemy:GetPos())
context.enemy_pos[enemy] = enemy_pos
if not pos:Equal2D(enemy_pos) then
local dir = enemy_pos - pos
dir = dir:SetInvalidZ()
context.enemy_dir[enemy] = SetLen(dir, guim)
else
context.enemy_dir[enemy] = point(0, 0, guim)
end
context.enemy_visible[enemy] = HasVisibilityTo(unit, enemy)
context.enemy_visible_by_team[enemy] = HasVisibilityTo(unit.team, enemy)
end
if context.behavior then
context.behavior:EnumDestinations(unit, context)
else
AIFindDestinations(unit, context)
end
AIUpdateDestLosCache(unit, context)
for i, ally in ipairs(context.allies) do
local x, y, z = ally:GetGridCoords()
context.ally_grid_voxel[ally] = point_pack(x, y, z)
context.ally_pack_pos_stance[ally] = GetPackedPosAndStance(ally)
context.ally_pos[ally] = ally:GetPos()
end
unit.ai_context = context
return context
end
MapVar("g_AIDestEnemyLOSCache", {})
function dbgShowAIDestCache()
DbgClearVectors()
DbgClearTexts()
for dest, los in pairs(g_AIDestEnemyLOSCache) do
local x, y, z, stance_idx = stance_pos_unpack(dest)
z = z or terrain.GetHeight(x, y)
DbgAddVector(point(x, y, z), point(0, 0, guim), los and const.clrGreen or const.clrRed)
DbgAddText(StancesList[stance_idx], point(x, y, z), const.clrWhite)
end
end
function AIUpdateDestLosCache(unit, context)
assert(CurrentThread()) -- the function will sleep internally due to the amount of calculations performed
--local tStart = GetPreciseTicks()
--ic("AIUpdateDestLosCache start", #units)
local sight = unit:GetSightRadius()
local all_destinations = context.all_destinations
local enemies = context.enemies
if #enemies == 0 then return end
NetUpdateHash("AIUpdateDestLosCache_Start", GameTime(), sight, #all_destinations, hashParamTable(all_destinations), #enemies, hashParamTable(context.enemy_pack_pos_stance))
local dests
local los_cache = g_AIDestEnemyLOSCache
for _, dest in ipairs(all_destinations) do
if los_cache[dest] == nil then
if not dests then dests = {} end
dests[#dests + 1] = dest
los_cache[dest] = false
end
end
if dests then
local max_los_checks = 100
local targets = {}
local srcs = {}
local enemies_count = #enemies
local next_dest_idx = 1
local start_dest_idx = 1
local cur_enemy = 1
while true do
local ppos = context.enemy_pack_pos_stance[enemies[cur_enemy]]
local count = #targets
local last_dest_idx = Min(#dests, next_dest_idx + max_los_checks - count - 1)
for i = next_dest_idx, last_dest_idx do
count = count + 1
targets[count] = ppos
srcs[count] = dests[i]
end
next_dest_idx = last_dest_idx + 1
if next_dest_idx > #dests then
next_dest_idx = 1
cur_enemy = cur_enemy + 1
end
if count >= max_los_checks or cur_enemy > enemies_count then
local los_any, los_data = CheckLOS(targets, srcs, sight)
if los_any then
local visible_dests = 0
for i, value in ipairs(los_data) do
if value then
local dest = srcs[i]
if not los_cache[dest] then
los_cache[dest] = true
visible_dests = visible_dests + 1
end
end
end
if visible_dests >= #dests then
break
end
if cur_enemy < enemies_count or cur_enemy == enemies_count and next_dest_idx == 1 then
-- There will be more LOS checks. Remove visible destinations from dests list to not cast more lines from there
if #targets >= #dests then
for i = #dests, 1, -1 do
if los_cache[dests[i]] then
table.remove(dests, i)
if i < next_dest_idx then next_dest_idx = next_dest_idx - 1 end
end
end
elseif start_dest_idx <= last_dest_idx then
for i = last_dest_idx, start_dest_idx, -1 do
if los_cache[dests[i]] then
table.remove(dests, i)
if i < next_dest_idx then next_dest_idx = next_dest_idx - 1 end
end
end
else
for i = #dests, start_dest_idx, -1 do
if los_cache[dests[i]] then
table.remove(dests, i)
if i < next_dest_idx then next_dest_idx = next_dest_idx - 1 end
end
end
for i = last_dest_idx, 1, -1 do
if los_cache[dests[i]] then
table.remove(dests, i)
if i < next_dest_idx then next_dest_idx = next_dest_idx - 1 end
end
end
end
if #dests == 0 then
assert(#dests > 0)
break
end
end
end
if cur_enemy > enemies_count then
break
end
start_dest_idx = next_dest_idx
table.iclear(targets)
table.iclear(srcs)
if GetInGameInterfaceMode() ~= "IModeAIDebug" then
Sleep(10) --yield
end
end
end
end
NetUpdateHash("AIUpdateDestLosCache_End", GameTime())
--printf("AIUpdateDestLosCache: %d ms for %s", GetPreciseTicks() - tStart, unit.unitdatadef_id)
end
function AIHasLOSToEnemyFromDest(dest)
return not not g_AIDestEnemyLOSCache[dest]
end
function AICalcAttacksAndAim(context, ap)
local aim_cost = const.Scale.AP
if GameState.RainHeavy then
aim_cost = MulDivRound(aim_cost, 100 + const.EnvEffects.RainAimingMultiplier, 100)
end
local cost = context.default_attack_cost
local num_attacks = Min(ap / cost, context.max_attacks)
if context.force_max_aim then
num_attacks = Min(ap / (cost + aim_cost * context.weapon.MaxAimActions), context.max_attacks)
end
local remaining = ap - num_attacks * cost
local aims = {}
local attack_idx = 1
while remaining > aim_cost do
local aim = (aims[attack_idx] or 0) + 1
if aim > context.weapon.MaxAimActions then
break
end
aims[attack_idx] = aim
attack_idx = attack_idx + 1
if attack_idx > num_attacks then
attack_idx = 1
end
remaining = remaining - aim_cost
end
return num_attacks, aims
end
function AIBuildArchetypePaths(unit, pos, context)
local stationary = context.stationary
local paths = {}
local destinations, dest_path, dest_ap, voxel_to_dest = {}, {}, {}, {}
if stationary or CombatActions.Move:GetUIState{unit} ~= "enabled" then
local dest = GetPackedPosAndStance(unit)
local x, y, z = stance_pos_unpack(dest)
local voxel = point_pack(x, y, z)
destinations[1] = dest
dest_ap[dest] = unit.ActionPoints
voxel_to_dest[voxel] = dest
return destinations, paths, dest_ap, dest_path, voxel_to_dest, voxel
end
local archetype = unit:GetArchetype()
local goto_stance = archetype.MoveStance
local pref_stance = archetype.PrefStance
local move_stance_idx = StancesList[goto_stance] or 0
local pref_stance_idx = StancesList[pref_stance] or 0
local ps_ap = (unit.species == "Human") and (unit.ActionPoints - GetStanceToStanceAP(unit.stance, pref_stance)) or unit.ActionPoints
local ms_ap = (unit.species == "Human") and (unit.ActionPoints - GetStanceToStanceAP(unit.stance, goto_stance)) or unit.ActionPoints
local move_path = CombatPath:new()
move_path:RebuildPaths(unit, ms_ap, pos, goto_stance)
local dest_voxels = table.keys(move_path.destinations, true)
local pref_path
if goto_stance == pref_stance then
pref_path = move_path
else
local visited = move_path.destinations
pref_path = CombatPath:new()
pref_path:RebuildPaths(unit, ps_ap, pos, pref_stance)
for voxel in sorted_pairs(pref_path.destinations) do
if not visited[voxel] then
dest_voxels[#dest_voxels+1] = voxel
end
end
end
local important_dests = context.important_dests or {}
local min_melee_dist = 2 * const.SlabSizeX
local move_paths_ap = move_path.paths_ap
local pref_paths_ap = pref_path.paths_ap
for _, voxel in ipairs(dest_voxels) do
local x, y, z = point_unpack(voxel)
local move_ap = move_paths_ap[voxel]
local pref_ap = pref_paths_ap[voxel]
local mn_ap = move_ap and (ms_ap - move_ap) or -1
local pn_ap = pref_ap and (ps_ap - pref_ap) or -1
local dest
if pn_ap > mn_ap then
assert(pref_ap)
dest = stance_pos_pack(x, y, z, pref_stance_idx)
destinations[#destinations+1] = dest
dest_path[dest] = pref_stance_idx
dest_ap[dest] = pn_ap
elseif move_ap then
dest = stance_pos_pack(x, y, z, move_stance_idx)
destinations[#destinations+1] = dest
dest_path[dest] = move_stance_idx
dest_ap[dest] = mn_ap
else
dest = stance_pos_pack(x, y, z, StancesList[unit.stance])
assert(dest == context.unit_stance_pos)
destinations[#destinations+1] = dest
dest_path[dest] = move_stance_idx
dest_ap[dest] = unit.ActionPoints
end
voxel_to_dest[voxel] = dest
if not table.find(important_dests, dest) then
if context.EffectiveRange <= 1 then
-- make sure all potential melee positions are included in the end and not cut off by CollapsePoints
for enemy, enemy_ppos in pairs(context.enemy_pack_pos_stance) do
if stance_pos_dist(enemy_ppos, dest) < min_melee_dist then
table.insert_unique(important_dests, dest)
break
end
end
end
-- also do the same for allies, since we might wanna heal them
if context.can_heal then
for _, ally in ipairs(context.allies) do
local ppos = GetPackedPosAndStance(ally)
if stance_pos_dist(ppos, dest) < min_melee_dist then
table.insert_unique(important_dests, dest)
break
end
end
end
end
end
destinations = CollapsePoints(destinations, 1)
context.important_dests = important_dests
for _, dest in ipairs(important_dests) do
if dest_ap[dest] and CanOccupy(unit, stance_pos_unpack(dest)) then
table.insert_unique(destinations, dest)
end
end
-- filter out destinations someone already called dibs for
for _, u in ipairs(context.allies) do
if u ~= unit and u.ai_context then
local idx = table.find(destinations, u.ai_context.ai_destination)
if idx then
destinations[idx] = destinations[#destinations]
destinations[#destinations] = nil
end
end
end
paths[goto_stance] = move_path
paths[move_stance_idx] = move_path
paths[pref_stance] = pref_path
paths[pref_stance_idx] = pref_path
return destinations, paths, dest_ap, dest_path, voxel_to_dest, move_path.closest_free_pos
end
function AIScoreDest(context, policies, dest, grid_voxel, base_score, visual_voxels, score_details)
local score = 0
local x, y, z, stance_idx = stance_pos_unpack(dest)
if not grid_voxel then
local vx, vy, vz = WorldToVoxel(x, y, z)
grid_voxel = point_pack(vx, vy, vz)
end
local voxels, head = context.unit:GetVisualVoxels(point_pack(x, y, z), StancesList[stance_idx], visual_voxels)
if AreVoxelsInFireRange(voxels) then
score = const.AIAvoidFireWeigth
if score_details then
score_details[#score_details + 1] = "ADJACENT FIRE"
score_details[#score_details + 1] = const.AIAvoidFireWeigth
end
elseif g_SmokeObjs[head] then
score = const.AIAvoidFireWeigth
if score_details then
score_details[#score_details + 1] = "GASSED AREA"
score_details[#score_details + 1] = const.AIAvoidGasWeigth
end
end
for _, policy in ipairs(policies) do
local peval = policy:EvalDest(context, dest, grid_voxel)
local pscore = MulDivRound(peval or 0, policy.Weight, 100)
local failed = policy.Required and pscore == 0
score = score + pscore
if score_details then
score_details[#score_details + 1] = (failed and "[FAILED] " or "") .. policy:GetEditorView()
score_details[#score_details + 1] = pscore
end
if failed then
return 0
end
end
score = (base_score or 0) + score
-- bombard zone modifier
for _, zone in ipairs(g_Bombard) do
local dist = zone:GetDist(x, y, z)
local radius = zone.radius * const.SlabSizeX
if dist <= radius then
local mod = MulDivRound(dist, const.AIAvoidBombardEdge, radius) + MulDivRound(radius - dist, const.AIAvoidBombardCenter, radius)
local loss = MulDivRound(score, 100 - mod, 100)
if score_details and loss > 0 then
score_details[#score_details + 1] = "BOMBARD ZONE"
score_details[#score_details + 1] = -loss
end
score = Max(0, score - loss)
end
end
-- apply modifiers from bias markers at the end
if context.apply_bias then
local unit = context.unit
for _, marker in ipairs(g_BiasMarkers) do
local bias = marker:GetAIBias(unit, dest)
if bias ~= 100 then
score = MulDivRound(score, bias, 100)
if score_details then
score_details[#score_details + 1] = string.format("Bias Marker %s (%%): ", marker.ID)
score_details[#score_details + 1] = bias
end
end
end
end
return score
end
MapSlabsBBox_MaxZ = 100000
function AIEnumValidDests(context)
local unit = context.unit
local r = context.archetype.OptLocSearchRadius * const.SlabSizeX
local ux, uy, uz = point_unpack(context.unit_grid_voxel)
local px, py, pz = VoxelToWorld(ux, uy, uz)
local bbox = box(px - r, py - r, 0, px + r + 1, py + r + 1, MapSlabsBBox_MaxZ)
local dests, dest_added = {}, {}
local function push_dest(x, y, z, context, dests, dest_added, ux, uy, uz)
local gx, gy, gz = WorldToVoxel(x, y, z)
if not IsCloser(gx, gy, gz, ux, uy, uz, context.archetype.OptLocSearchRadius) then
return
end
if not CanOccupy(unit, x, y, z) then
return
end
local world_voxel = point_pack(x, y, z)
local dest = context.voxel_to_dest[world_voxel]
if not dest then
dest = stance_pos_pack(x, y, z, StancesList[context.archetype.PrefStance])
end
if not dest_added[dest] then
dests[#dests + 1] = dest
dest_added[dest] = true
end
end
ForEachPassSlab(bbox, push_dest, context, dests, dest_added, ux, uy, uz)
-- add current pos
if not dest_added[context.unit_stance_pos] then
local x, y, z = stance_pos_unpack(context.unit_stance_pos)
if CanOccupy(unit, x, y, z) then
dests[#dests + 1] = context.unit_stance_pos
dest_added[context.unit_stance_pos] = true
end
end
-- add from context.destinations
for _, dest in ipairs(context.destinations) do
if not dest_added[dest] then
dests[#dests + 1] = dest
end
end
dests = CollapsePoints(dests, 1)
for _, dest in ipairs(context.important_dests) do
table.insert_unique(dests, dest)
end
return dests
end
function AIFindOptimalLocation(context, dest_score_details)
if context.best_dest then
-- optimal location doesn't change across behaviors, no need to recalc it
return context.best_dest
end
local unit = context.unit
context.best_dests = {}
local r = context.archetype.OptLocSearchRadius * const.SlabSizeX
local ux, uy, uz = point_unpack(context.unit_grid_voxel)
local px, py, pz = VoxelToWorld(ux, uy, uz)
local bbox = box(px - r, py - r, 0, px + r + 1, py + r + 1, MapSlabsBBox_MaxZ)
context.best_score = 0
local unit_voxels = {}
local dest_scores = {}
local policies = table.ifilter(context.archetype.OptLocPolicies, function(idx, policy) return policy:MatchUnit(unit) end)
for _, dest in ipairs(context.all_destinations) do
local x, y, z = stance_pos_unpack(dest)
local gx, gy, gz = WorldToVoxel(x, y, z)
local world_voxel = point_pack(x, y, z)
local grid_voxel = point_pack(gx, gy, gz)
--eval_voxel(x, y, z, context, ux, uy, uz)
if not context.voxel_to_dest[world_voxel] then
context.voxel_to_dest[world_voxel] = dest
end
local scores
if dest_score_details then
scores = {}
dest_score_details[dest] = scores
end
table.iclear(unit_voxels)
local score = AIScoreDest(context, policies, dest, grid_voxel, 0, unit_voxels, scores)
if score > 0 then
context.best_score = Max(context.best_score, score)
local threshold = MulDivRound(context.best_score, const.AIDecisionThreshold, 100)
if score >= threshold then
dest_scores[dest] = score
context.best_dests[#context.best_dests + 1] = dest
for i = #context.best_dests, 1, -1 do
local dest = context.best_dests[i]
if dest_scores[dest] < threshold then
table.remove(context.best_dests, i)
end
end
end
end
if scores then
scores.final_score = score
end
end
-- check if a best dest candidate is on our starting voxel, default to it
for _, dest in ipairs(context.best_dests) do
if stance_pos_dist(context.unit_stance_pos, dest) == 0 then
context.best_dest = dest
end
end
if not context.best_dest and #(context.best_dests or empty_table) > 0 then
if #(context.best_dests or empty_table) > 15 then
context.collapsed = CollapsePoints(context.best_dests, 1)
else
context.collapsed = context.best_dests
end
local pf_dests = {}
for i, dest in ipairs(context.collapsed) do
local x, y, z = stance_pos_unpack(dest)
pf_dests[i] = point(x, y, z)
end
context.best_dest_path = pf.GetPosPath(unit, pf_dests)
if #(context.best_dest_path or empty_table) > 0 then
local voxel = point_pack(SnapToPassSlabXYZ(context.best_dest_path[1]))
local dest = context.voxel_to_dest[voxel]
if not dest then
-- try non-snapped
voxel = point_pack(context.best_dest_path[1])
dest = context.voxel_to_dest[voxel]
end
--assert(dest and (not dest_score_details or dest_score_details[dest]))
context.best_dest = dest
end
end
context.dest_scores = dest_scores
context.best_dest = context.best_dest or context.voxel_to_dest[context.unit_world_voxel] or context.unit_stance_pos
if context.dest_combat_path[context.best_dest] then
table.insert_unique(context.important_dests, context.best_dest)
table.insert_unique(context.destinations, context.best_dest)
end
return context.best_dest
end
function AICalcPathDistances(context)
local unit = context.unit
local path_voxels, voxel_dist, total_dist
if context.best_dest_path then
path_voxels, voxel_dist, total_dist = CalcPathVoxels(context.best_dest_path)
end
context.path_voxels = path_voxels
context.path_to_target = table.copy(path_voxels or empty_table)
context.voxel_dist = voxel_dist
context.total_dist = total_dist
-- calc distance to optimal location from each dest
if path_voxels and voxel_dist then
AICalcDistancesFromReachableLocations(context) -- will add path nodes to path_voxels and voxel_dist
else
-- no path to target, use default distances on all reachable voxels
context.dest_dist = {}
end
end
function AIGetWeaponCheckRange(unit, weapon, action)
if IsKindOf(weapon, "MeleeWeapon") then
local tiles = unit.body_type == "Large animal" and 2 or 1
local range = (2 * tiles + 1) * const.SlabSizeX / 2
return range, true
elseif IsKindOf(weapon, "Firearm") then
local max_range = weapon.WeaponRange * const.SlabSizeX
if action.AimType ~= "cone" then
max_range = 15 * max_range / 10
end
return max_range
end
end
--MapVar("g_AIDamageScoreLog", {})
function AIAllyInDanger(allies, ally_pos, pos, target, dist_near, dist_far)
local target_pos = target:GetPos()
local v = target:GetPos() - pos
local d = const.AIFriendlyFire_MaxRange
for _, ally in ipairs(allies) do
if ally:GetDist2D(pos) <= const.AIFriendlyFire_MaxRange then
local ally_pos = ally_pos and ally_pos[ally] or ally:GetPos()
local dist, x, y, z = DistSegmentToPt2D(pos, target_pos, ally_pos)
local nearest = point(x, y, z)
local d1 = pos:Dist2D(nearest)
local dist_threshold = MulDivRound(dist_near, Clamp(0, d, d - d1), d) + MulDivRound(dist_far, Clamp(0, d, d1), d)
if dist < dist_threshold then
local v1 = nearest - pos
if Dot2D(v, v1) > 0 then
return true
end
end
end
end
end
function AIPrecalcDamageScore(context, destinations, preferred_target, debug_data)
local unit = context.unit
local weapon = context.weapon
local action = CombatActions[context.override_attack_id or false] or context.default_attack
local archetype = context.archetype
local behavior = context.behavior
if not weapon or context.reposition or unit:HasStatusEffect("Burning") then
return
end
if not destinations and context.damage_score_precalced then
return
end
local action_targets = action:GetTargets({unit})
local targets = table.ifilter(action_targets, function(idx, target) return unit:IsOnEnemySide(target) end)
if #targets == 0 then
return
end
context.damage_score_precalced = true
local target_score_mod = {}
local tsr = archetype.TargetScoreRandomization
for i, target in ipairs(targets) do
target_score_mod[i] = 100 + ((tsr > 0) and unit:RandRange(-tsr, tsr) or 0)
end
context.target_score_mod = target_score_mod
local base_mod = unit[weapon.base_skill]
local cost_ap = context.override_attack_cost or context.default_attack_cost
local max_check_range, is_melee = AIGetWeaponCheckRange(unit, weapon, action)
local is_heavy = IsKindOf(weapon, "HeavyWeapon")
local hit_modifiers = Presets["ChanceToHitModifier"]["Default"]
-- stance mod
local modCrouchBonus = 0
local modProneBonus = 0
--if IsKindOf(weapon, "Firearm") then
--modCrouchBonus = hit_modifiers.AttackerStance:ResolveValue("CrouchBonus")
--modProneBonus = hit_modifiers.AttackerStance:ResolveValue("ProneBonus")
local value = GetComponentEffectValue(weapon, "AccuracyBonusProne", "bonus_cth")
if value then
modProneBonus = modProneBonus + value
end
--end
-- ground difference mod
local MinGroundDifference = hit_modifiers.GroundDifference:ResolveValue("RangeThreshold") * const.SlabSizeZ / 100
local modHighGround = hit_modifiers.GroundDifference:ResolveValue("HighGround")
local modLowGround = hit_modifiers.GroundDifference:ResolveValue("LowGround")
-- cover
local modCover = hit_modifiers.RangeAttackTargetStanceCover:ResolveValue("Cover")
local modSameTarget = hit_modifiers.SameTarget:ResolveValue("Bonus")
local target_policies = archetype.TargetingPolicies
if behavior and #(behavior.TargetingPolicies or empty_table) > 0 then
target_policies = behavior.TargetingPolicies
end
local dest_target = context.dest_target
local dest_target_score = context.dest_target_score
local dest_ap = context.dest_ap
local aim_mod = Presets.ChanceToHitModifier.Default.Aim
local dest_cth = {}
context.dest_cth = dest_cth
local lof_params
local attacker_pos = unit:GetPos()
-- script-driven modifiers (based on groups)
local target_modifiers
for _, groupname in ipairs(unit.Groups) do
local group_modifiers = gv_AITargetModifiers[groupname]
for target_group, mod in pairs(group_modifiers) do
target_modifiers = target_modifiers or {}
target_modifiers[target_group] = (target_modifiers[target_group] or 0) + mod
for _, obj in ipairs(Groups[target_group]) do
if IsKindOf(obj, "Unit") and not table.find(targets, obj) then
table.insert(targets, obj) -- make sure the target is considired regardless if it's an enemy or not
table.insert(target_score_mod, 100 + ((tsr > 0) and unit:RandRange(-tsr, tsr) or 0))
end
end
end
end
if unit:HasStatusEffect("StationedMachineGun") or unit:HasStatusEffect("ManningEmplacement") then
local ow_units = {unit}
targets = table.ifilter(targets, function(idx, target) return target:IsThreatened(ow_units, "overwatch") end)
end
if not IsValidTarget(preferred_target) or (IsKindOf(preferred_target, "Unit") and preferred_target:IsIncapacitated() or not table.find(targets, preferred_target)) then
preferred_target = nil
end
if weapon and not is_melee then
lof_params = {
obj = unit,
action_id = action.id,
weapon = weapon,
step_pos = false,
stance = false,
range = max_check_range,
prediction = true,
output_collisions = true,
}
if not destinations or #destinations > 1 then
lof_params.target_spot_group = "Torso"
end
end
--[[ local logdata = {}
if destinations then
table.insert(g_AIDamageScoreLog, logdata)
end
logdata.preferred_target = preferred_target and (IsKindOf(preferred_target, "Unit") and _InternalTranslate(preferred_target.Name or "") or preferred_target.class) or tostring(preferred_target)--]]
destinations = destinations or context.destinations
NetUpdateHash("AIPrecalcDamageScore", unit, hashParamTable(destinations), hashParamTable(targets), preferred_target)
for j, upos in ipairs(destinations) do
local ux, uy, uz, ustance_idx = stance_pos_unpack(upos)
local ustance = StancesList[ustance_idx]
uz = uz or terrain.GetHeight(ux, uy)
local ap = dest_ap[upos] or 0
local best_target, best_cth
local best_score = 0
local potential_targets, target_score, target_cth = {}, {}, {}
if weapon and ap >= cost_ap then
local pos_mod = base_mod
pos_mod = pos_mod + (ustance_idx == 2 and modCrouchBonus or ustance_idx == 3 and modProneBonus or 0)
local targets_attack_data
if not is_melee then
attacker_pos = point(ux, uy, uz)
lof_params.step_pos = point_pack(ux, uy, uz)
lof_params.stance = ustance
targets_attack_data = GetLoFData(unit, targets, lof_params)
end
for k, target in ipairs(targets) do
local tpos = GetPackedPosAndStance(target)
local dist = stance_pos_dist(upos, tpos)
if dist <= (max_check_range or dist) and (is_melee or targets_attack_data[k] and not targets_attack_data[k].stuck) then
local tx, ty, tz, tstance_idx = stance_pos_unpack(tpos)
tz = tz or terrain.GetHeight(tx, ty)
local hit_mod = pos_mod
if not is_heavy then
hit_mod = hit_mod + (uz > tz + MinGroundDifference and modHighGround or uz < tz - MinGroundDifference and modLowGround or 0)
hit_mod = hit_mod + (unit:GetLastAttack() == target and modSameTarget or 0)
end
local target_cover = GetCoverFrom(tpos, upos)
if target_cover == const.CoverLow or target_cover == const.CoverHigh then
hit_mod = hit_mod + modCover
end
local penalty = is_heavy and 0 or (100 - weapon:GetAccuracy(dist))
local mod = hit_mod - penalty --dist_penalty
-- environmental modifiers when applicable
local apply, value, target_spot_group, action, weapon1, weapon2, lof, aim, opportunity_attack
apply, value = hit_modifiers.Darkness:CalcValue(unit, target, target_spot_group, action, weapon1, weapon2, lof, aim, opportunity_attack, attacker_pos)
if apply then
mod = mod + value
end
if not is_heavy and unit:IsPointBlankRange(target) then
mod = MulDivRound(mod, 100 + const.AIPointBlankTargetMod, 100)
end
mod = Max(0, mod)
if mod > const.AIShootAboveCTH then
-- calc base score based on cth/attacks/aiming
local base_mod = mod
local attacks, aims = AICalcAttacksAndAim(context, ap)
mod = 0
for i = 1, attacks do
local use, bonus
if (aims[i] or 0) > 0 then
use, bonus = aim_mod:CalcValue(unit, nil, nil, nil, nil, nil, nil, aims[i])
end
mod = mod + base_mod + (use and bonus or 0)
end
-- modify score by archetype-specific weight and (optional) targeting policies
mod = MulDivRound(mod, archetype.TargetBaseScore, 100)
for _, policy in ipairs(target_policies) do
local peval = policy:EvalTarget(unit, target)
mod = mod + MulDivRound(peval or 0, policy.Weight, 100)
end
if IsKindOf(target, "Unit") and (target:IsDowned() or target:IsGettingDowned()) then
mod = MulDivRound(mod, 5, 100)
end
local attack_data = targets_attack_data and targets_attack_data[k]
local ally_in_danger = attack_data and (attack_data.best_ally_hits_count or 0) > 0
if action and action.AimType == "cone" then
ally_in_danger = ally_in_danger or AIAllyInDanger(context.allies, context.ally_pos, attacker_pos, target, const.AIFriendlyFire_LOFConeNear, const.AIFriendlyFire_LOFConeFar)
else
ally_in_danger = ally_in_danger or AIAllyInDanger(context.allies, context.ally_pos, attacker_pos, target, const.AIFriendlyFire_LOFWidth, const.AIFriendlyFire_LOFWidth)
end
if ally_in_danger then
mod = MulDivRound(mod, const.AIFriendlyFire_ScoreMod, 100)
end
mod = MulDivRound(mod, target_score_mod[k], 100)
-- apply group-based modifiers
if target_modifiers and IsKindOf(target, "Unit") then
local group_mod = 0
for _, groupname in ipairs(target.Groups) do
group_mod = group_mod + (target_modifiers[groupname] or 0)
end
if group_mod > 0 then
mod = MulDivRound(mod, group_mod, 100)
end
end
--[[table.insert(logdata, {
name = IsKindOf(target, "Unit") and _InternalTranslate(target.Name or "") or target.class,
score = mod
})--]]
if mod > 0 and target == preferred_target then
best_target = target
best_score = mod
best_cth = base_mod
potential_targets = {}
break
end
best_score = Max(best_score, mod)
target_cth[target] = base_mod
target_score[target] = mod
local threshold = MulDivRound(best_score or 0, const.AIDecisionThreshold, 100)
if mod >= threshold then
potential_targets[#potential_targets + 1] = target
for i = #potential_targets, 1, -1 do
local target = potential_targets[i]
local score = target_score[target]
if score < threshold then
table.remove(potential_targets, i)
end
end
--best_target, best_score, best_cth = target, mod, base_mod
end
end
end
end
end
if #potential_targets > 0 then
local total = 0
for _, target in ipairs(potential_targets) do
local score = target_score[target]
total = total + score
if debug_data then
debug_data[target] = score
end
end
local roll = InteractionRand(total, "AIDecision")
for _, target in ipairs(potential_targets) do
local score = target_score[target]
if roll < score then
best_target = target
break
end
roll = roll - score
end
best_target = best_target or potential_targets[#potential_targets] or false
best_score = target_score[best_target] or 0
best_cth = target_cth[best_target] or 0
end
--[[
if destinations and IsKindOf(best_target, "Unit") then
if best_target == preferred_target then
printf("%s (%d) selected target (preferred): %s (score %d)", _InternalTranslate(unit.Name or ""), unit.handle, _InternalTranslate(best_target.Name or ""), best_score)
else
printf("%s (%d) selected target: %s (score %d)", _InternalTranslate(unit.Name or ""), unit.handle, _InternalTranslate(best_target.Name or ""), best_score)
printf(" potential targets:")
for _, target in ipairs(potential_targets) do
printf(" %s (score %d)", _InternalTranslate(target.Name or ""), target_score[target])
end
end
end--]]
--logdata.chosen_target = best_target and (IsKindOf(best_target, "Unit") and _InternalTranslate(best_target.Name or "") or best_target.class) or tostring(best_target)
dest_target_score[upos] = best_score
dest_target[upos] = best_target
dest_cth[upos] = best_cth
end
end
function AIScoreReachableVoxels(context, policies, opt_loc_weight, dest_score_details, cur_dest_preference)
local unit = context.unit
policies = table.ifilter(policies, function(idx, policy) return policy:MatchUnit(unit) end)
unit.ai_end_turn_search = {}
local total_dist = context.total_dist
local dest_dist = context.dest_dist or empty_table
local curr_dest = context.voxel_to_dest[context.unit_world_voxel] or context.voxel_to_dest[context.closest_free_pos] or context.unit_stance_pos
local dist = dest_dist[curr_dest] or total_dist
local score = -opt_loc_weight
if (total_dist or 0) > 0 then
score = MulDivRound(score, dist, total_dist)
end
local unit_voxels = {}
local best_end_score = curr_dest and AIScoreDest(context, policies, curr_dest, context.unit_grid_voxel, score, unit_voxels)
-- cache the best voxel on the way to optimal location to use as fallback if needed
local best_dist_score, closest_dest
local potential_dests, dest_scores = {curr_dest}, {best_end_score}
for _, dest in ipairs(context.destinations) do
total_dist = Max(total_dist or 0, dest_dist[dest] or 0)
end
for _, dest in ipairs(context.destinations) do
local score = 0
local scores
local dist = dest_dist[dest] or 100*guim
local dist_score = 0
if total_dist and total_dist > 0 then
dist_score = MulDivRound(100 - MulDivRound(100, dist, total_dist), opt_loc_weight, 100)
end
if dist_score > (best_dist_score or 0) then
best_dist_score, closest_dest = dist_score, dest
end
score = score + dist_score
if dest_score_details then
scores = { "Distance to optimal location", dist_score }
dest_score_details[dest] = scores
end
table.iclear(unit_voxels)
score = AIScoreDest(context, policies, dest, nil, score, unit_voxels, scores)
if MulDivRound(best_end_score or 0, const.AIDecisionThreshold, 100) <= score then
best_end_score = Max(score, best_end_score or 0)
local n = #potential_dests
potential_dests[n+1] = dest
dest_scores[n+1] = score
local threshold = MulDivRound(best_end_score, const.AIDecisionThreshold, 100) -- updated threshold
for i = n, 1, -1 do
if dest_scores[i] < threshold then
table.remove(dest_scores, i)
table.remove(potential_dests, i)
end
end
end
if scores then
scores.final_score = score
end
end
-- pick best_end_dest/score from potential_dests
assert(#potential_dests > 0)
context.best_end_dest = false
if cur_dest_preference == "prefer" then
if table.find(potential_dests, curr_dest) then
context.best_end_dest = curr_dest
end
elseif cur_dest_preference == "avoid" then
if #potential_dests > 1 then
table.remove_value(potential_dests, curr_dest)
end
end
NetUpdateHash("AIScoreReachableVoxels", unit, unit:GetPos(), unit.ActionPoints, context.archetype.id, #(context.destinations or ""), hashParamTable(context.destinations), #(potential_dests or ""), hashParamTable(potential_dests), cur_dest_preference)
if not context.best_end_dest then
local total = 0
for _, score in ipairs(potential_dests) do
total = total + score
end
local roll = InteractionRand(total, "AIDecision")
for i, dest in ipairs(potential_dests) do
local score = dest_scores[i]
if score <= roll then
context.best_end_dest = dest
break
end
roll = roll - score
end
context.best_end_dest = context.best_end_dest or potential_dests[#potential_dests] or curr_dest
end
context.best_end_score = best_end_score
context.closest_dest = closest_dest
return context.best_end_dest, context.best_end_score
end
function CalcPathVoxels(path)
local dist = 0
if not IsPoint(path[1]) then
local pt_path = {}
for i, ppos in ipairs(path) do
pt_path[i] = point(point_unpack(ppos))
end
path = pt_path
end
local processed_path = { path[1] }
local voxel_dist = {}
local voxels = {}
voxel_dist[point_pack(path[1])] = 0
local function push_path_segment(seg_start, seg_end, path_dist, tunnel)
local seg_dist = seg_start:Dist(seg_end)
if not tunnel and seg_dist > const.SlabSizeX/2 then
local midpt = (seg_start + seg_end) / 2
push_path_segment(seg_start, midpt, path_dist)
push_path_segment(midpt, seg_end, path_dist + seg_dist / 2)
else
processed_path[#processed_path + 1] = seg_end
local x, y, z = GetPassSlabXYZ(seg_end)
local pck_end = x and point_pack(x, y, z)
if pck_end and not voxel_dist[pck_end] then
voxel_dist[pck_end] = path_dist + seg_dist
voxels[#voxels + 1] = pck_end
--[[
local pt = point(x, y, z)
if not pt:IsValidZ() then pt = pt:SetTerrainZ() end
DbgAddVector(pt, point(0, 0, guim), const.clrGreen)
DbgAddText(tostring(path_dist + seg_dist), pt + point(0, 0, guim/2), const.clrWhite)--]]
end
end
return seg_dist
end
local dist = 0
local marker = InvalidPos()
local seg_start_idx, seg_end_idx
--DbgClearVectors()
--DbgClearTexts()
for i = 1, #path do
if not seg_start_idx then
seg_start_idx = path[i] ~= marker and i
elseif not seg_end_idx then
seg_end_idx = path[i] ~= marker and i
end
if seg_start_idx and seg_end_idx then
--[[
local pt1 = path[seg_end_idx]
local pt2 = path[seg_start_idx]
if not pt1:IsValidZ() or pt1:z() < terrain.GetHeight(pt1) + 50*guic then
pt1 = pt1:SetTerrainZ(100*guic)
end
if not pt2:IsValidZ() or pt2:z() < terrain.GetHeight(pt2) + 50*guic then
pt2 = pt2:SetTerrainZ(100*guic)
end
DbgAddVector(pt1, point(0, 0, guim), const.clrWhite)
DbgAddVector(pt2, point(0, 0, guim), const.clrWhite)
printf("seg %d: %s - %s", seg_start_idx, tostring(pt2), tostring(pt1))
DbgAddVector(pt1, pt2 - pt1, seg_end_idx > seg_start_idx + 1 and const.clrYellow or const.clrWhite)
DbgAddText(tostring(seg_start_idx), (pt1+pt2)/2, const.clrBlue)
--]]
dist = dist + push_path_segment(path[seg_start_idx], path[seg_end_idx], dist, seg_end_idx > seg_start_idx + 1)
seg_start_idx = seg_end_idx
seg_end_idx = false
end
end
return voxels, voxel_dist, dist
end
function AICalcDistancesFromReachableLocations(context)
local voxel_idx = 1
local stance = context.archetype.MoveStance
local tunnel_mask = stance == "Prone" and const.TunnelTypeWalk or -1
local processed = {}
local voxel_to_dest = context.voxel_to_dest
local path_voxels = context.path_voxels
local voxel_dist = context.voxel_dist
local dest_dist = {}
context.dest_dist = dest_dist
for voxel, dist in pairs(context.voxel_dist) do
local dest = voxel_to_dest[voxel]
if dest then
context.dest_dist[dest] = dist
end
end
--DbgClearVectors()
--DbgClearTexts()
while path_voxels[voxel_idx] do
local voxel = path_voxels[voxel_idx]
local dest = voxel_to_dest[voxel]
if not processed[voxel] then
processed[voxel] = true
local px, py, pz = point_unpack(voxel)
--[[
local pt = point(px, py, pz)
if not pt:IsValidZ() then pt = pt:SetTerrainZ() end
DbgAddVector(pt, point(0, 0, 2*guim), const.clrBlue)
DbgAddText(dest and dest_dist[dest] and tostring(dest_dist[dest]) or "n/a", pt + point(0, 0, guim), const.clrWhite)
DbgAddText(voxel_dist[voxel] and tostring(voxel_dist[voxel]) or "n/a", pt + point(0, 0, guim/2), const.clrYellow)
--]]
ForEachPassSlabStep(px, py, pz, tunnel_mask, function(x, y, z, tunnel)
local curr_voxel = point_pack(x, y, z)
local curr_dest = voxel_to_dest[curr_voxel]
if curr_dest and dest then
assert(voxel_dist[voxel])
local x2, y2, z2 = point_unpack(voxel)
local dx, dy, dz = x - x2, y - y2, (z and z2) and z - z2 or 0
local dist = voxel_dist[voxel] + sqrt(dx*dx + dy*dy + dz*dz) -- the tile is guaranteed to be reachable, so we can take linear distance
if not voxel_dist[curr_voxel] or voxel_dist[curr_voxel] > dist then
-- if the step is a tunnel, we need to check if it goes both ways to filter out shortcuts from target to current location
if not tunnel or pf.GetTunnel(tunnel.end_point, tunnel:GetPos()) then
voxel_dist[curr_voxel] = dist
dest_dist[curr_dest] = dist
end
end
path_voxels[#path_voxels + 1] = curr_voxel
if not voxel_dist[curr_voxel] then
voxel_dist[curr_voxel] = dist
end
if not dest_dist[curr_dest] then
dest_dist[curr_dest] = dist
end
end
end)
end
voxel_idx = voxel_idx + 1
end
end
function AIGetAttackArgs(context, action, target_spot_group, aim_type, override_target)
local upos = GetPackedPosAndStance(context.unit)
local target = override_target or context.dest_target[upos]
local args = { target = target, target_spot_group = target_spot_group or "Torso" }
local dest_ap
if context.ai_destination then
local u_x, u_y, u_z = stance_pos_unpack(upos)
local dest_x, dest_y, dest_z = stance_pos_unpack(context.ai_destination)
if point(u_x, u_y, u_z) ~= point(dest_x, dest_y, dest_z) then
dest_ap = context.dest_ap[context.ai_destination]
end
end
local unit_ap = dest_ap or context.unit:GetUIActionPoints()
if action.id == "Overwatch" then
local attacks, aim = context.unit:GetOverwatchAttacksAndAim(action, args, unit_ap)
args.num_attacks = attacks
args.aim_ap = aim
elseif aim_type ~= "None" then
args.aim = context.weapon.MaxAimActions
if aim_type == "Remaining AP" then
while args.aim > 0 and not context.unit:HasAP(action:GetAPCost(context.unit, args)) do
args.aim = args.aim - 1
end
end
end
local cost = action:GetAPCost(context.unit, args)
local has_ap = cost >= 0 and (unit_ap >= cost)
return args, has_ap, target
end
function AIFilterTargetPoints(unit, target_pts, min_range, max_range)
for i = #target_pts, 1, -1 do
local dist = unit:GetDist(target_pts[i])
if dist == 0 or (max_range and dist > max_range) then
table.remove(target_pts, i)
elseif min_range and min_range < max_range and dist < min_range then
table.remove(target_pts, i)
end
end
end
function AICalcAOETargetPoints(context, min_range, max_range, max_radius)
local target_pts = {}
local unit = context.unit
local enemies = context.enemies
-- add enemy positions
for i, enemy in ipairs(enemies) do
if VisibilityCheckAll(unit, enemy, nil, const.uvVisible) then
target_pts[#target_pts + 1] = context.enemy_pos[enemy]
end
end
local num_targets = #target_pts
-- add midpoints of enemy pairs
for i = 1, num_targets - 1 do
for j = i + 1, num_targets do
local pt = (target_pts[i] + target_pts[j]) / 2
if not max_radius or pt:Dist(target_pts[i]) <= max_radius then
target_pts[#target_pts + 1] = pt
end
end
end
-- add midpoints of enemy triples
for i = 1, num_targets - 2 do
for j = i + 1, num_targets - 1 do
for k = j + 1, num_targets do
local pt = (target_pts[i] + target_pts[j] + target_pts[k]) / 3
if not max_radius or pt:Dist(target_pts[i]) <= max_radius then
target_pts[#target_pts + 1] = pt
end
end
end
end
-- filter out target points not in range
AIFilterTargetPoints(unit, target_pts, min_range, max_range)
return target_pts
end
function AIPrecalcConeTargetZones(context, action_id, additional_target_pt, stance)
if context.target_locked then return {} end
local unit = context.unit
local weapon = context.weapon
local params = weapon:GetAreaAttackParams(action_id, unit)
local min_range = params.min_range * const.SlabSizeX
local max_range = params.max_range * const.SlabSizeX
local target_pts = AICalcAOETargetPoints(context, min_range, max_range)
if additional_target_pt then
target_pts[#target_pts + 1] = additional_target_pt
end
-- calc cone areas for each remaining target point
local zones = {}
local cone_angle = params.cone_angle
local targets = {}
local attack_pos = unit:GetPos() -- make sure we're using the current position in case the unit has moved
local units = table.copy(context.enemies)
table.iappend(units, GetAllAlliedUnits(unit))
local unit_sight = unit:GetSightRadius()
for zi, pt in ipairs(target_pts) do
local dir = pt - attack_pos
if dir:Len() > 0 then
local target_pos = (attack_pos + SetLen(dir, max_range)):SetTerrainZ()
local zone = {
target_pos = target_pos,
units = {},
}
zones[#zones + 1] = zone
local angle = CalcOrientation(attack_pos, pt)
local los_any, los_targets = CheckLOS(units, unit, unit:GetDist(target_pos), nil, cone_angle, angle)
if los_any then
for i, target_unit in ipairs(units) do
if los_targets[i] and IsValidTarget(target_unit) then
zone.units[#zone.units + 1] = target_unit
table.insert_unique(targets, target_unit)
end
end
end
end
end
local check_ally
if action_id == "Overwatch" then
local atk_action = context.default_attack
local aim_type = atk_action.AimType
local is_aoe = aim_type == "cone" or aim_type == "aoe" or aim_type == "parabola aoe" or aim_type == "line aoe"
check_ally = not is_aoe
end
-- filter LOS targets
local max_distance = Min(unit_sight, weapon:GetMaxRange())
local los_any, los_targets = CheckLOS(targets, unit, max_distance)
if not los_any then
for _, zone in ipairs(zones) do
table.iclear(zone.units)
end
return zones
end
for i = #targets, 1, -1 do
if not los_any or not los_targets[i] then
for _, zone in ipairs(zones) do
table.remove_value(zone.units, targets[i])
end
table.remove(targets, i)
end
end
-- check chance to hit
local targets_attack_data = GetLoFData(unit, targets, {
obj = unit,
action_id = context.default_attack.id,
weapon = weapon,
stance = unit.stance,
range = max_distance,
target_spot_group = "Torso",
prediction = true,
})
local action = CombatActions[action_id]
local args = { target_spot_group = false }
for i, attack_data in ipairs(targets_attack_data) do
local target = targets[i]
local chance_to_hit = 0
if attack_data and not attack_data.stuck then
for j, hit_info in ipairs(attack_data.lof) do
if not check_ally or hit_info.ally_hits_count == 0 then
args.target_spot_group = hit_info.target_spot_group
chance_to_hit = unit:CalcChanceToHit(target, action, args, "chance_only")
if chance_to_hit > 0 then
break
end
end
end
end
if chance_to_hit == 0 then
for _, zone in ipairs(zones) do
table.remove_value(zone.units, target)
end
end
end
return zones
end
local function IsUnitHit(hit)
if not IsKindOf(hit.obj, "Unit") then return false end
if hit.damage > 0 then return true end
for _, effect in ipairs(hit.effects) do
if effect and effect ~= "" then
return true
end
end
end
function AIPrecalcGrenadeZones(context, action_id, min_range, max_range, blast_radius, aoeType, target_pts)
if context.target_locked then return {} end
if not target_pts then
target_pts = AICalcAOETargetPoints(context, min_range, max_range, blast_radius)
else
-- make sure the target points are within the allowed range
AIFilterTargetPoints(context.unit, target_pts, min_range, max_range)
end
-- calculate parabolas and affected units to each target point
local zones = {}
local action = CombatActions[action_id]
local args = { target = false }
for i, target_pt in ipairs(target_pts) do
args.target = target_pt
local results = action:GetActionResults(context.unit, args)
local units
local trajectory = results.trajectory or empty_table
local pos = #trajectory > 0 and trajectory[#trajectory].pos or results.target_pos
if pos and (aoeType == "smoke" or aoeType == "toxicgas" or aoeType == "teargas") then
local water = terrain.IsWater(pos) and terrain.GetWaterHeight(pos)
if not (water and (not pos:IsValidZ() or water >= pos:z())) then
pos = SnapToPassSlab(pos) or pos
local dx, dy = 1, 1
for i = #trajectory - 1, 1, -1 do
local step = trajectory[i]
if step.pos:Dist2D(pos) > 0 then
local px, py = step.pos:xy()
local x, y = pos:xy()
dx = (px == x) and 1 or ((x - px) / abs(x - px))
dy = (py == y) and 1 or ((y - py) / abs(y - py))
break
end
end
local gx, gy, gz = WorldToVoxel(pos)
local smoke, blocked = PropagateSmokeInGrid(gx, gy, gz, dx, dy)
local smoke_voxels = {}
for _, wpt in pairs(smoke) do
local ppos = point_pack(WorldToVoxel(wpt))
smoke_voxels[ppos] = true
end
for _, unit in ipairs(g_Units) do
local _, head = unit:GetVisualVoxels()
if smoke_voxels[head] then
units = units or {}
table.insert(units, unit)
end
end
end
else
for _, hit in ipairs(results) do
if IsUnitHit(hit) then
units = units or {}
table.insert(units, hit.obj)
end
end
end
if units then
zones[#zones + 1] = { target_pos = target_pt, units = units }
end
end
--print("grenade targeting precalc in", GetPreciseTicks() - tstart, "ms")
return zones
end
function AIPrecalcLandmineZones(context)
if context.target_locked then return {} end
local weapon = context.weapon
if not IsKindOf(weapon, "Firearm") then
return {}
end
if not context.mine_zones then
local unit = context.unit
local sight = unit:GetSightRadius()
local max_range = Min(weapon.WeaponRange * const.SlabSizeX, sight)
local landmines = MapGet(unit, max_range, "Landmine", function(o, unit)
return o:SeenBy(unit)
end, unit)
local zones = {}
for _, mine in ipairs(landmines) do
local aoe_params = mine:GetAreaAttackParams(nil, unit, mine:GetPos())
aoe_params.prediction = true
local results = GetAreaAttackResults(aoe_params, 0)
local units
for _, hit in ipairs(results) do
if IsKindOf(hit.obj, "Unit") and hit.damage > 0 then
if not units then
units = {}
end
table.insert(units, hit.obj)
end
end
if units then
zones[#zones + 1] = { target = mine, units = units }
end
end
context.mine_zones = zones
end
return context.mine_zones
end
function AISelectHealTarget(context, dest, grid_voxel, heal_policy)
if context.voxel_heal_score[grid_voxel] then
return context.voxel_heal_target[grid_voxel], context.voxel_heal_score[grid_voxel]
end
local x, y, z = point_unpack(grid_voxel)
local best_target, best_score = false, 0
local dx, dy, dz = stance_pos_unpack(dest)
local ppos = point_pack(dx, dy, dz)
for _, ally in ipairs(context.allies) do
local hpp = MulDivRound(ally.HitPoints, 100, ally.MaxHitPoints)
local score
if hpp <= heal_policy.MaxHp and not ally:IsDead() then
local bleed = 0
if ally:HasStatusEffect("Bleeding") then
bleed = heal_policy.BleedingWeight
end
local gx, gy, gz = point_unpack(context.ally_grid_voxel[ally])
if ally == context.unit or IsMeleeRangeTarget(context.unit, ppos, nil, ally) then --(abs(x - gx) <= 1 and abs(y - gy) <= 1 and abs(z - gz) <= 1) then
score = MulDivRound(100 - hpp, heal_policy.HpWeight, 100) + bleed
end
if ally == context.unit then
score = MulDivRound(score, heal_policy.SelfHealMod, 100)
end
end
score = score or 0
if not best_score or score > best_score then
best_target, best_score = ally, score
end
end
local ap_at_dest = context.dest_ap[dest] or 0
if ap_at_dest >= CombatActions.Bandage.ActionPoints then
best_score = MulDivRound(best_score, heal_policy.CanUseMod, 100)
end
context.voxel_heal_target[grid_voxel] = best_target
context.voxel_heal_score[grid_voxel] = best_score
return best_target, best_score
end
function AIEvalStimTarget(unit, target, rules)
if target:IsDead() or target:HasStatusEffect("Stimmed") then
return 0
end
local score = 0
for _, rule in ipairs(rules) do
if table.find(target.AIKeywords or empty_table, rule.Keyword) then
score = score + rule.Weight
end
end
return score
end
local AITurnPhasePriority = {
Early = 1,
Normal = 2,
Late = 3,
}
function AIGetNextPhaseUnits(units, max)
local best_units, best_prio
for _, unit in ipairs(units) do
local behavior = unit.ai_context and unit.ai_context.behavior
if behavior then
local turn_phase = behavior:GetTurnPhase(unit)
local prio = AITurnPhasePriority[turn_phase] or 999
if not best_prio or prio < best_prio then
best_units, best_prio = {unit}, prio
elseif prio == best_prio then
best_units[#best_units + 1] = unit
end
if max and #(best_units or empty_table) >= max then
break
end
end
end
return best_units
end
function IsMeleeRangeTarget(attacker, attack_pos, attack_stance, target, target_pos, target_stance, attacker_face_angle)
if not IsValidTarget(target) then return end
if IsSittingUnit(target) then
target_pos = target_pos or target.last_visit:GetPos()
target_stance = "Crouch"
end
return IsMeleeRangeTargetC(attacker, attack_pos, attack_stance, target, target_pos, target_stance, attacker_face_angle)
end
function GetMeleeRangePositions(attacker, target, target_pos, check_occupied)
if IsSittingUnit(target) then
target_pos = target.last_visit:GetPos()
end
return GetMeleeRangePositionsC(attacker, target, target_pos, check_occupied)
end
function GetClosestMeleeRangePos(attacker, target, target_pos, check_occupied)
if IsSittingUnit(target) then
target_pos = target.last_visit:GetPos()
end
return GetClosestMeleeRangePosC(attacker, target, target_pos, check_occupied)
end
function AIRangeCheck(context, ppt1, target, ppt2, range_type, range_min, range_max)
if range_type == "Melee" then
local p1 = point_pack(VoxelToWorld(point_unpack(ppt1)))
local p2 = point_pack(VoxelToWorld(point_unpack(ppt2)))
return IsMeleeRangeTarget(context.unit, p1, context.unit.stance, target, p2, target.stance)
end
if range_type ~= "Absolute" then
-- weapon range based
assert(range_type == "Weapon")
local base_range = context.ExtremeRange
range_min = range_min and MulDivRound(range_min, base_range, 100)
range_max = range_max and MulDivRound(range_max, base_range, 100)
end
local x1, y1, z1 = point_unpack(ppt1)
local x2, y2, z2 = point_unpack(ppt2)
if (range_min or 0) > 0 and IsCloser(x1, y1, z1, x2, y2, z2, range_min) then
return false
end
if (range_max or 0) > 0 and not IsCloser(x1, y1, z1, x2, y2, z2, range_max + 1) then
return false
end
return true
end
function AIReloadWeapons(unit)
if IsMerc(unit) then return end
local firearms = select(3, unit:GetActiveWeapons("Firearm"))
table.iappend(firearms, select(3, unit:GetActiveWeapons("HeavyWeapon")))
for _, firearm in ipairs(firearms) do
if not firearm.ammo then
local ammos = unit:GetAvailableAmmos(firearm) or empty_table
local ammo
if #ammos > 0 then
ammo = ammos[1]
ammo.Amount = Max(ammo.Amount, firearm.MagazineSize)
unit:ReloadWeapon(firearm, ammo, "delay fx", "ai")
CreateFloatingText(unit, T(160472488023, "Reload"))
ObjModified(unit)
else
ammos = GetAmmosWithCaliber(firearm.Caliber, "sorted")
if #ammos > 0 then
ammo = PlaceInventoryItem(ammos[1].id)
ammo.Amount = firearm.MagazineSize
unit:ReloadWeapon(firearm, ammo, "delay fx", "ai")
CreateFloatingText(unit, T(160472488023, "Reload"))
DoneObject(ammo)
ObjModified(unit)
end
end
elseif firearm.ammo.Amount < Max(1, firearm.MagazineSize / 2) then
local ammo = firearm.ammo
ammo.Amount = firearm.MagazineSize
unit:ReloadWeapon(firearm, ammo, "delay fx", "ai")
CreateFloatingText(unit, T(160472488023, "Reload"))
ObjModified(unit)
end
end
end
function AIPickScoutLocation(unit)
local AIScoutLocationSearchRadius = 5 * guim
-- pick a new position around alive enemy randomly, prefer non-hidden enemies
local enemies = GetAllEnemyUnits(unit)
if #enemies == 0 then
return
end
local targets
local nearest, nearby = {}, {}
for _, enemy in ipairs(enemies) do
local dist = unit:GetDist(enemy)
if dist <= AIScoutLocationSearchRadius then
nearest[#nearest + 1] = enemy
targets = nearest
elseif dist <= 2*AIScoutLocationSearchRadius then
nearby[#nearby + 1] = enemy
targets = targets or nearby
end
end
targets = targets or enemies
local enemy = table.interaction_rand(enemies, "Combat")
local ux, uy, uz = enemy:GetGridCoords()
local px, py, pz = VoxelToWorld(ux, uy, uz)
local r = AIScoutLocationSearchRadius
local bbox = box(px - r, py - r, 0, px + r + 1, py + r + 1, MapSlabsBBox_MaxZ)
local dests, dest_added = {}, {}
local function push_dest(x, y, z, dests, dest_added, ux, uy, uz)
local gx, gy, gz = WorldToVoxel(x, y, z)
if not IsCloser(gx, gy, gz, ux, uy, uz, AIScoutLocationSearchRadius) then
return
end
local world_voxel = point_pack(x, y, z)
if not dest_added[world_voxel] then
dests[#dests + 1] = world_voxel
dest_added[world_voxel] = true
end
end
ForEachPassSlab(bbox, push_dest, dests, dest_added, ux, uy, uz)
if #dests > 0 then
local voxel = table.interaction_rand(dests, "Combat")
local x, y, z = point_unpack(voxel)
return point(x, y, z)
end
end
function AIUpdateScoutLocation(unit)
if not unit.last_known_enemy_pos then
return
end
local sight = unit:GetSightRadius()
if CheckLOS(unit.last_known_enemy_pos, unit, sight) then
-- scouted here, next time pick a different location if still necessary
unit.last_known_enemy_pos = nil
end
end
MapVar("g_MGPriorityAssignment", {})
function AIAssignToEmplacements(team)
local emplacements = MapGet("map", "MachineGunEmplacement")
local units = table.ifilter(team.units, function(idx, unit) return unit.CanManEmplacements end)
-- update emplacements' appeal for the team
for _, emplacement in ipairs(emplacements) do
local targets = #units > 0 and emplacement:GetEnemyUnitsInArea(units[1]) or empty_table
local appeal = MulDivRound(emplacement.appeal[team.side] or 0, Max(0, 100 - emplacement.appeal_decay), 100)
for _, enemy in ipairs(targets) do
local dist = emplacement:GetDist(enemy)
local diff = abs(dist - emplacement.appeal_optimal_dist)
appeal = appeal + Max(0, emplacement.appeal_per_target + MulDivRound(emplacement.appeal_per_meter, dist, guim))
end
emplacement.appeal[team.side] = appeal
if not SpawnedByEnabledMarker(emplacement) or not emplacement.enabled then
emplacement.appeal[team.side] = 0
end
end
if emplacements then
table.sort(emplacements, function(a, b) return a.appeal[team.side] > b.appeal[team.side] end)
end
for _, emplacement in ipairs(emplacements) do
local assigned_unit = g_Combat:GetEmplacementAssignment(emplacement)
if (emplacement.appeal[team.side] or 0) > emplacement.appeal_use_threshold then
if not emplacement.manned_by and not assigned_unit then
-- free for grabs, find a unit to man the MG
-- check priority assignment first
local gunner
for _, unit in ipairs(g_MGPriorityAssignment) do
if IsValidTarget(unit) and unit.team == team and unit.CanManEmplacements and not unit:IsIncapacitated() then
gunner = unit
break
end
end
if not gunner then
local emplacement_pos = SnapToPassSlab(emplacement:GetPosXYZ())
if emplacement_pos then
table.sort(units, function(a, b) return IsCloser(emplacement_pos, a, b) end)
local closest, closest_pf_dist
for _, u in ipairs(units) do
-- select unit closest to the emplacement (by pathfind)
local emp = g_Combat:GetEmplacementAssignment(u)
if not emp and (not closest or IsCloser(u, emplacement_pos, closest_pf_dist)) then
local has_path, path_len, closest_pos = pf.PosPathLen(u, emplacement_pos, nil, 0, 0, u, 0, nil, 0)
if has_path and closest_pos == emplacement_pos then
if not closest_pf_dist or path_len < closest_pf_dist then
closest, closest_pf_dist = u, path_len
end
end
end
end
gunner = closest
end
end
if gunner then
g_Combat:AssignEmplacement(emplacement, gunner)
end
elseif assigned_unit and assigned_unit.team == team then
if emplacement.manned_by and emplacement.manned_by ~= assigned_unit then
-- somebody else took it, clean up assignment
g_Combat:AssignEmplacement(emplacement, nil)
end
end
elseif assigned_unit and assigned_unit.team == team then
g_Combat:AssignEmplacement(emplacement, nil)
end
end
end
function AIEnemyWeaponsCombo()
local types = table.map(GetWeaponTypes(), "id")
table.insert_unique(types, "Pistol")
table.insert_unique(types, "Revolver")
table.insert_unique(types, "MeleeWeapon")
table.insert_unique(types, "Unarmed")
return types
end
function measure_func(func, num_invocations, ...)
num_invocations = num_invocations or 0
if num_invocations < 1 then
return
end
local start = GetPreciseTicks()
for i = 1, num_invocations do
func(...)
end
local elapsed_ms = GetPreciseTicks() - start
printf("%d invocations finished in %d ms for (%d ms average)", num_invocations, elapsed_ms, elapsed_ms / num_invocations)
end
DefineClass.AIBiasMarker = {
__parents = { "GridMarker" },
properties = {
{ category = "AI Bias", id = "UnitGroups", name = "UnitGroups", editor = "string_list", default = false, items = function (self) return GetUnitGroups() end },
{ category = "AI Bias", id = "Bias", editor = "number", min = 0, max = 1000, scale = "%", slider = true, default = 100, help = "modifier applied to AI evaluations of destinations inside the marker area"},
},
}
function AIBiasMarker:GetAIBias(unit, dest)
if not unit or not self:IsMarkerEnabled() then return 100 end
local x, y, z = stance_pos_unpack(dest)
z = z or terrain.GetHeight(x, y)
x, y = WorldToVoxel(x, y, z)
if not self:IsVoxelInsideArea2D(x, y) then
return 100
end
local apply_groups = g_BiasMarkers[self] or empty_table
for _, group in ipairs(unit.Groups) do
if apply_groups[group] then
return self.Bias
end
end
return 100
end
function InitAIBiasMarkers()
g_BiasMarkers = g_BiasMarkers or MapGetMarkers("GridMarker", nil, function(m) return IsKindOf(m, "AIBiasMarker") end) or false
for _, marker in ipairs(g_BiasMarkers) do
local apply_grous = {}
g_BiasMarkers[marker] = apply_grous
for _, group in ipairs(marker.UnitGroups) do
apply_grous[group] = true
end
end
end
function AICheckIndoors(dest)
if g_AIDestIndoorsCache[dest] == nil then
local x, y, z = stance_pos_unpack(dest)
local volume = EnumVolumes(point(x, y, z), "smallest")
g_AIDestIndoorsCache[dest] = not not volume
end
return g_AIDestIndoorsCache[dest]
end |