火力规则:添加武器命中率因素
This commit is contained in:
@@ -25,6 +25,9 @@ public class Weapon {
|
||||
//武器数量
|
||||
private Integer number;
|
||||
|
||||
//武器命中率
|
||||
private Double hitRate;
|
||||
|
||||
//目标id
|
||||
private String targetId;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,20 @@
|
||||
- 导弹发射车优先绑定蓝方导弹能力目标
|
||||
- 当优先池不足时自动回退到地面池/全目标池,保证大部分武器有目标。
|
||||
|
||||
### 2.7 阵位规则参数(新增)
|
||||
### 2.7 命中率与动态火力参数(新增)
|
||||
- `hitRateCsv`:业务可配置红方武器命中率,格式 `武器名=0.72,武器名2=0.55`。
|
||||
- `defaultHitRateFallback`:未命中 `hitRateCsv` 且武器未携带 `hitRate` 时的兜底命中率。
|
||||
- `desiredKillProbability`:目标毁伤置信度(例如 `0.9`)。
|
||||
- `offsetCsvByWeapon`:显式 offset(最高优先级),格式 `武器名=1,武器名2=2`。
|
||||
- `enableDynamicMultiRedPerBlue`:是否按命中率动态决定“一个蓝方目标需要几把红方武器”。
|
||||
- `minRedWeaponsPerBlueTarget` / `maxRedWeaponsPerBlueTargetCap`:每个蓝方目标的最小/最大红方分配数。
|
||||
|
||||
优先级说明(重要):
|
||||
- 若 `offsetCsvByWeapon` 命中某武器,则直接使用显式 offset,不再使用命中率推导 offset。
|
||||
- 未配置显式 offset 时,规则按命中率与 `desiredKillProbability` 自动推导所需火力数量。
|
||||
- 同一蓝方 `equipmentId` 可能被多个红方武器绑定(不是固定 2 把),数量由命中率动态计算并受 cap 限制。
|
||||
|
||||
### 2.8 阵位规则参数(新增)
|
||||
- `enablePositionRules`:阵位规则总开关。
|
||||
- 阵位输入来源:`blueTask.warZoneLocation` 与 `blueTask.defZoneLocation`(各 4 个经纬点)。
|
||||
- `fireUnitSpacingMeters`:防区/作战区点位间距(米),例如 `100` 代表约每 100 米一个火力单元。
|
||||
@@ -120,7 +133,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 2.8 航迹规则参数(新增)
|
||||
### 2.9 航迹规则参数(新增)
|
||||
- `enableTrajectoryRules`:航迹规则总开关。
|
||||
- `strategyMode`:`auto/shortest/flank/interfere`。
|
||||
- `auto`:智能选择策略。
|
||||
@@ -149,10 +162,12 @@
|
||||
- 主决策在 `红方武器自适应装配规则`:调用 `configureRedWeaponsByBlue(...)`,按“映射配置”添加武器。
|
||||
- 导弹增强在 `导弹联动增强规则`:调用 `applyMissileLinkage(...)`,受开关和阈值控制。
|
||||
- 全组件数量匹配在 `全组件数量匹配规则`:按红方 `targetId` 绑定蓝方装备,覆盖非导弹组件 `componentParams[0].number`;找不到组件/targetId 允许跳过。
|
||||
- 命中率驱动数量在 `命中率规则-动态数量与offset`:按 `hitRate` 与目标毁伤概率推导火力数量;显式 offset 配置优先。
|
||||
- 任务命名在 `任务自动匹配规则`:调用 `assignTaskNameByRedWeapons(...)`,按红方最终武器自动生成任务名和 `dataType`。
|
||||
- 炮类约束:命中炮类条件时,炮类武器只保留 `炮弹` 组件,单位 `范围米`。
|
||||
- `targetId` 绑定:在装配后自动执行,尽量为红方武器绑定蓝方 `equipmentId`,允许少量空值冗余。
|
||||
- `targetId` 绑定:在装配后自动执行,按命中率动态给蓝目标分配多个红方武器(受上下限约束),允许少量空值冗余。
|
||||
- 阵位部署:按多边形区域和武器类型自动赋位,保证防区火力覆盖。
|
||||
- 射程合理性在 `射程合理性校验规则`:基于蓝/红武器坐标计算距离,自动避免“射程不足却打击”的不合理情况(可自动调参)。
|
||||
- 航迹生成:根据蓝方 `trackPoints` 生成红方 `trackPoints`,点数与蓝方一致,支持三套策略和智能选择。
|
||||
|
||||
## 3.1 任务名称自动匹配(新增)
|
||||
|
||||
@@ -149,6 +149,18 @@ function Map buildBusinessConfig() {
|
||||
cfg.put("blueMissileRangeDefault", 220); // 蓝方导弹范围默认值
|
||||
cfg.put("minBlueMissileCountForLinkage", 1); // 联动触发门槛
|
||||
|
||||
// ---------- 命中率与动态火力(可改) ----------
|
||||
cfg.put("hitRateCsv", "防空导弹武器=0.72,火力打击无人机=0.62,武装直升机=0.68,反坦克火箭=0.55,反坦克导弹系统=0.78,迫榴炮=0.45,车载迫击炮=0.43,导弹发射车=0.82");
|
||||
cfg.put("defaultHitRateFallback", "0.6"); // 兜底命中率
|
||||
cfg.put("desiredKillProbability", "0.9"); // 期望毁伤置信度
|
||||
cfg.put("offsetCsvByWeapon", ""); // 显式offset(武器=offset)优先,例:反坦克火箭=1,武装直升机=2
|
||||
cfg.put("enableDynamicMultiRedPerBlue", Boolean.TRUE); // 按命中率动态决定每个蓝目标需要几个红武器
|
||||
cfg.put("minRedWeaponsPerBlueTarget", 1); // 每个蓝目标最少分配红武器数量
|
||||
cfg.put("maxRedWeaponsPerBlueTargetCap", 3); // 每个蓝目标最多分配红武器数量上限
|
||||
cfg.put("enableRangeSanityCheck", Boolean.TRUE); // 距离-射程合理性校验开关
|
||||
cfg.put("enableAutoRangeRecommend", Boolean.TRUE); // 射程不足时自动调参
|
||||
cfg.put("rangeSafetyMarginMeters", 50); // 射程安全裕量
|
||||
|
||||
// ---------- 全组件数量匹配(可改) ----------
|
||||
// 逻辑:按红方武器 targetId 找到对应蓝方装备(equipmentId),然后覆盖所有“非导弹组件”的数量
|
||||
cfg.put("enableComponentQuantityMatch", Boolean.TRUE);
|
||||
@@ -238,6 +250,17 @@ then
|
||||
applyAllComponentQuantities($fact, cfg);
|
||||
end
|
||||
|
||||
//-------------------------------------------------------------------------------
|
||||
rule "命中率规则-动态数量与offset"
|
||||
agenda-group "打击任务"
|
||||
salience 52
|
||||
when
|
||||
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
||||
then
|
||||
Map cfg = buildBusinessConfig();
|
||||
applyHitRateDrivenOffsets($fact, cfg);
|
||||
end
|
||||
|
||||
//-------------------------------------------------------------------------------
|
||||
rule "任务自动匹配规则"
|
||||
agenda-group "打击任务"
|
||||
@@ -273,9 +296,20 @@ then
|
||||
end
|
||||
|
||||
//-------------------------------------------------------------------------------
|
||||
rule "航迹规则-生成红方航迹"
|
||||
rule "射程合理性校验规则"
|
||||
agenda-group "打击任务"
|
||||
salience 47
|
||||
when
|
||||
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
||||
then
|
||||
Map cfg = buildBusinessConfig();
|
||||
applyRangeSanityAndRecommend($fact, cfg);
|
||||
end
|
||||
|
||||
//-------------------------------------------------------------------------------
|
||||
rule "航迹规则-生成红方航迹"
|
||||
agenda-group "打击任务"
|
||||
salience 46
|
||||
when
|
||||
// 根据蓝方 trackPoints 生成红方 trackPoints,点数保持一致
|
||||
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
||||
@@ -390,6 +424,9 @@ function void configureRedWeaponsByBlue(
|
||||
limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault"));
|
||||
}
|
||||
|
||||
// 写入红方武器默认命中率(若入参已给 hitRate,则不覆盖)
|
||||
applyDefaultHitRateIfAbsent(redWeapons, cfg);
|
||||
|
||||
// 自动绑定红方武器 targetId(来源:蓝方 equipmentId)
|
||||
bindTargetIdsForRedWeapons(redWeapons, blueWeapons, cfg);
|
||||
}
|
||||
@@ -541,25 +578,72 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
|
||||
}
|
||||
|
||||
Map pools = extractBlueTargetPools(blueWeapons);
|
||||
Map cursor = new java.util.HashMap();
|
||||
Map blueById = indexBlueWeaponsById(blueWeapons);
|
||||
Map assignedCountByBlueId = new java.util.HashMap();
|
||||
Map survivalByBlueId = initBlueSurvivalMap(blueById);
|
||||
double desiredKill = normalizeProbability(readDoubleCfg(cfg, "desiredKillProbability", 0.9d), 0.9d);
|
||||
int cap = readIntCfg(cfg, "maxRedWeaponsPerBlueTargetCap", 3);
|
||||
int minPerBlue = readIntCfg(cfg, "minRedWeaponsPerBlueTarget", 1);
|
||||
if (cap <= 0) {
|
||||
cap = 1;
|
||||
}
|
||||
if (minPerBlue < 0) {
|
||||
minPerBlue = 0;
|
||||
}
|
||||
if (minPerBlue > cap) {
|
||||
minPerBlue = cap;
|
||||
}
|
||||
|
||||
int total = redWeapons.size();
|
||||
int bound = 0;
|
||||
|
||||
// 第一轮:按武器类别优先匹配
|
||||
// 第一轮:先满足每个蓝目标的最小分配(在cap内)
|
||||
if (minPerBlue > 0) {
|
||||
List allBlueIds = (List) pools.get("all");
|
||||
if (allBlueIds != null && !allBlueIds.isEmpty()) {
|
||||
for (Object idObj : allBlueIds) {
|
||||
String blueId = String.valueOf(idObj);
|
||||
int need = minPerBlue;
|
||||
while (need > 0) {
|
||||
Weapon candidate = pickUnboundRedWeaponForBlue(blueId, redWeapons);
|
||||
if (candidate == null) {
|
||||
break;
|
||||
}
|
||||
candidate.setTargetId(blueId);
|
||||
bound++;
|
||||
incrementAssignedCount(assignedCountByBlueId, blueId);
|
||||
double p = resolveWeaponHitRate(candidate, cfg);
|
||||
updateBlueSurvival(survivalByBlueId, blueId, p);
|
||||
need--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第二轮:按命中率动态分配,优先填补“尚未达到期望毁伤概率”的蓝目标
|
||||
for (Object obj : redWeapons) {
|
||||
Weapon redWeapon = (Weapon) obj;
|
||||
if (redWeapon == null) {
|
||||
continue;
|
||||
}
|
||||
if (!isBlank(redWeapon.getTargetId())) {
|
||||
bound++;
|
||||
continue;
|
||||
}
|
||||
String poolKey = inferBluePoolKeyForRedWeapon(redWeapon);
|
||||
String targetId = pickTargetIdFromPools(pools, cursor, poolKey);
|
||||
String targetId = pickBlueTargetByNeed(
|
||||
pools,
|
||||
poolKey,
|
||||
assignedCountByBlueId,
|
||||
survivalByBlueId,
|
||||
desiredKill,
|
||||
cap
|
||||
);
|
||||
if (!isBlank(targetId)) {
|
||||
redWeapon.setTargetId(targetId);
|
||||
bound++;
|
||||
incrementAssignedCount(assignedCountByBlueId, targetId);
|
||||
double p = resolveWeaponHitRate(redWeapon, cfg);
|
||||
updateBlueSurvival(survivalByBlueId, targetId, p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +654,7 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二轮:若绑定率不足,回退到全目标池尽量补齐(仍允许复用目标)
|
||||
// 第三轮:若绑定率不足,回退全目标池补齐(仍受cap约束)
|
||||
for (Object obj : redWeapons) {
|
||||
if (total > 0 && ((double) bound / (double) total) >= minRatio) {
|
||||
break;
|
||||
@@ -579,28 +663,418 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
|
||||
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
|
||||
continue;
|
||||
}
|
||||
String targetId = pickTargetIdFromPools(pools, cursor, "all");
|
||||
String targetId = pickBlueTargetByNeed(
|
||||
pools,
|
||||
"all",
|
||||
assignedCountByBlueId,
|
||||
survivalByBlueId,
|
||||
desiredKill,
|
||||
cap
|
||||
);
|
||||
if (!isBlank(targetId)) {
|
||||
redWeapon.setTargetId(targetId);
|
||||
bound++;
|
||||
incrementAssignedCount(assignedCountByBlueId, targetId);
|
||||
double p = resolveWeaponHitRate(redWeapon, cfg);
|
||||
updateBlueSurvival(survivalByBlueId, targetId, p);
|
||||
}
|
||||
}
|
||||
|
||||
// 第三轮:若不允许空 targetId,最后强制从 all 池补齐(尽力而为)
|
||||
// 第四轮:若不允许空 targetId,最后尽力补齐
|
||||
if (!allowReserveWithoutTarget) {
|
||||
for (Object obj : redWeapons) {
|
||||
Weapon redWeapon = (Weapon) obj;
|
||||
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
|
||||
continue;
|
||||
}
|
||||
String targetId = pickTargetIdFromPools(pools, cursor, "all");
|
||||
String targetId = pickBlueTargetByNeed(
|
||||
pools,
|
||||
"all",
|
||||
assignedCountByBlueId,
|
||||
survivalByBlueId,
|
||||
desiredKill,
|
||||
cap
|
||||
);
|
||||
if (!isBlank(targetId)) {
|
||||
redWeapon.setTargetId(targetId);
|
||||
incrementAssignedCount(assignedCountByBlueId, targetId);
|
||||
double p = resolveWeaponHitRate(redWeapon, cfg);
|
||||
updateBlueSurvival(survivalByBlueId, targetId, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function void applyHitRateDrivenOffsets(FactTask fact, Map cfg) {
|
||||
if (fact == null || fact.getRedTask() == null) {
|
||||
return;
|
||||
}
|
||||
List redWeapons = fact.getRedTask().getTaskWeapons();
|
||||
if (redWeapons == null || redWeapons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
applyDefaultHitRateIfAbsent(redWeapons, cfg);
|
||||
Map explicitOffset = parseNameIntCsv((String) cfg.get("offsetCsvByWeapon"));
|
||||
double desiredKill = normalizeProbability(readDoubleCfg(cfg, "desiredKillProbability", 0.9d), 0.9d);
|
||||
for (Object obj : redWeapons) {
|
||||
Weapon redWeapon = (Weapon) obj;
|
||||
if (redWeapon == null) {
|
||||
continue;
|
||||
}
|
||||
int base = redWeapon.getNumber() == null || redWeapon.getNumber() <= 0 ? 1 : redWeapon.getNumber().intValue();
|
||||
Integer explicit = (Integer) explicitOffset.get(redWeapon.getName());
|
||||
int offset = 0;
|
||||
if (explicit != null) {
|
||||
offset = explicit.intValue();
|
||||
} else {
|
||||
double pHit = resolveWeaponHitRate(redWeapon, cfg);
|
||||
int required = computeRequiredShots(pHit, desiredKill, base);
|
||||
offset = required - base;
|
||||
}
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
redWeapon.setNumber(base + offset);
|
||||
}
|
||||
}
|
||||
|
||||
function void applyRangeSanityAndRecommend(FactTask fact, Map cfg) {
|
||||
if (!readBooleanCfg(cfg, "enableRangeSanityCheck", true)) {
|
||||
return;
|
||||
}
|
||||
if (fact == null || fact.getBlueTask() == null || fact.getRedTask() == null) {
|
||||
return;
|
||||
}
|
||||
List blueWeapons = fact.getBlueTask().getTaskWeapons();
|
||||
List redWeapons = fact.getRedTask().getTaskWeapons();
|
||||
if (blueWeapons == null || blueWeapons.isEmpty() || redWeapons == null || redWeapons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map blueById = indexBlueWeaponsById(blueWeapons);
|
||||
int margin = readIntCfg(cfg, "rangeSafetyMarginMeters", 50);
|
||||
boolean autoAdjust = readBooleanCfg(cfg, "enableAutoRangeRecommend", true);
|
||||
for (Object obj : redWeapons) {
|
||||
Weapon redWeapon = (Weapon) obj;
|
||||
if (redWeapon == null || isBlank(redWeapon.getTargetId())) {
|
||||
continue;
|
||||
}
|
||||
Weapon blueWeapon = (Weapon) blueById.get(redWeapon.getTargetId());
|
||||
if (blueWeapon == null) {
|
||||
continue;
|
||||
}
|
||||
Coordinate rc = redWeapon.getCoordinate();
|
||||
Coordinate bc = blueWeapon.getCoordinate();
|
||||
if (!isCoordinateUsable(rc) || !isCoordinateUsable(bc)) {
|
||||
continue;
|
||||
}
|
||||
int currentRange = readRangeMetersFromWeapon(redWeapon);
|
||||
if (currentRange <= 0) {
|
||||
continue;
|
||||
}
|
||||
double d = approxDistanceMeters(
|
||||
rc.getLongitude().doubleValue(),
|
||||
rc.getLatitude().doubleValue(),
|
||||
bc.getLongitude().doubleValue(),
|
||||
bc.getLatitude().doubleValue()
|
||||
);
|
||||
int minRequired = (int) Math.ceil(d) + margin;
|
||||
if (currentRange >= minRequired) {
|
||||
continue;
|
||||
}
|
||||
if (autoAdjust) {
|
||||
setWeaponFirstRangeParamAtLeast(redWeapon, minRequired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function void applyDefaultHitRateIfAbsent(List redWeapons, Map cfg) {
|
||||
if (redWeapons == null || redWeapons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map hitRateMap = parseNameDoubleCsv((String) cfg.get("hitRateCsv"));
|
||||
double fallback = normalizeProbability(readDoubleCfg(cfg, "defaultHitRateFallback", 0.6d), 0.6d);
|
||||
for (Object obj : redWeapons) {
|
||||
Weapon redWeapon = (Weapon) obj;
|
||||
if (redWeapon == null) {
|
||||
continue;
|
||||
}
|
||||
if (redWeapon.getHitRate() != null && redWeapon.getHitRate().doubleValue() > 0.0d) {
|
||||
continue;
|
||||
}
|
||||
Double p = (Double) hitRateMap.get(redWeapon.getName());
|
||||
if (p == null) {
|
||||
p = fallback;
|
||||
}
|
||||
redWeapon.setHitRate(normalizeProbability(p.doubleValue(), fallback));
|
||||
}
|
||||
}
|
||||
|
||||
function double resolveWeaponHitRate(Weapon redWeapon, Map cfg) {
|
||||
if (redWeapon != null && redWeapon.getHitRate() != null) {
|
||||
double p = redWeapon.getHitRate().doubleValue();
|
||||
return normalizeProbability(p, 0.6d);
|
||||
}
|
||||
Map hitRateMap = parseNameDoubleCsv((String) cfg.get("hitRateCsv"));
|
||||
Double fromCfg = redWeapon == null ? null : (Double) hitRateMap.get(redWeapon.getName());
|
||||
if (fromCfg != null) {
|
||||
return normalizeProbability(fromCfg.doubleValue(), 0.6d);
|
||||
}
|
||||
return normalizeProbability(readDoubleCfg(cfg, "defaultHitRateFallback", 0.6d), 0.6d);
|
||||
}
|
||||
|
||||
function int computeRequiredShots(double pHit, double pKill, int fallback) {
|
||||
double p = normalizeProbability(pHit, 0.6d);
|
||||
double pk = normalizeProbability(pKill, 0.9d);
|
||||
if (p >= 0.999d) {
|
||||
return 1;
|
||||
}
|
||||
if (p <= 0.0001d) {
|
||||
return fallback <= 0 ? 1 : fallback;
|
||||
}
|
||||
double up = Math.log(1.0d - pk);
|
||||
double down = Math.log(1.0d - p);
|
||||
if (down == 0.0d) {
|
||||
return fallback <= 0 ? 1 : fallback;
|
||||
}
|
||||
int n = (int) Math.ceil(up / down);
|
||||
if (n <= 0) {
|
||||
n = 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function Map parseNameDoubleCsv(String csv) {
|
||||
Map result = new java.util.HashMap();
|
||||
if (csv == null || csv.trim().equals("")) {
|
||||
return result;
|
||||
}
|
||||
String[] items = csv.split(",");
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
String one = items[i];
|
||||
if (one == null) {
|
||||
continue;
|
||||
}
|
||||
String text = one.trim();
|
||||
if (text.equals("")) {
|
||||
continue;
|
||||
}
|
||||
int idx = text.indexOf("=");
|
||||
if (idx <= 0 || idx >= text.length() - 1) {
|
||||
continue;
|
||||
}
|
||||
String key = text.substring(0, idx).trim();
|
||||
String val = text.substring(idx + 1).trim();
|
||||
if (key.equals("") || val.equals("")) {
|
||||
continue;
|
||||
}
|
||||
result.put(key, Double.valueOf(parseDoubleSafe(val, 0.0d)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function Map parseNameIntCsv(String csv) {
|
||||
Map result = new java.util.HashMap();
|
||||
if (csv == null || csv.trim().equals("")) {
|
||||
return result;
|
||||
}
|
||||
String[] items = csv.split(",");
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
String one = items[i];
|
||||
if (one == null) {
|
||||
continue;
|
||||
}
|
||||
String text = one.trim();
|
||||
if (text.equals("")) {
|
||||
continue;
|
||||
}
|
||||
int idx = text.indexOf("=");
|
||||
if (idx <= 0 || idx >= text.length() - 1) {
|
||||
continue;
|
||||
}
|
||||
String key = text.substring(0, idx).trim();
|
||||
String val = text.substring(idx + 1).trim();
|
||||
if (key.equals("") || val.equals("")) {
|
||||
continue;
|
||||
}
|
||||
result.put(key, Integer.valueOf(parseIntSafe(val, 0)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function double normalizeProbability(double p, double fallback) {
|
||||
if (Double.isNaN(p) || Double.isInfinite(p)) {
|
||||
p = fallback;
|
||||
}
|
||||
if (p <= 0.0d) {
|
||||
p = fallback;
|
||||
}
|
||||
if (p <= 0.0d) {
|
||||
p = 0.0001d;
|
||||
}
|
||||
if (p >= 1.0d) {
|
||||
p = 0.999d;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function Map indexBlueWeaponsById(List blueWeapons) {
|
||||
Map result = new java.util.HashMap();
|
||||
if (blueWeapons == null) {
|
||||
return result;
|
||||
}
|
||||
for (Object obj : blueWeapons) {
|
||||
Weapon w = (Weapon) obj;
|
||||
if (w == null || isBlank(w.getEquipmentId())) {
|
||||
continue;
|
||||
}
|
||||
result.put(w.getEquipmentId(), w);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function Map initBlueSurvivalMap(Map blueById) {
|
||||
Map result = new java.util.HashMap();
|
||||
if (blueById == null) {
|
||||
return result;
|
||||
}
|
||||
for (Object key : blueById.keySet()) {
|
||||
result.put(key, Double.valueOf(1.0d));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function void updateBlueSurvival(Map survivalByBlueId, String blueId, double pHit) {
|
||||
if (survivalByBlueId == null || isBlank(blueId)) {
|
||||
return;
|
||||
}
|
||||
Double survival = (Double) survivalByBlueId.get(blueId);
|
||||
if (survival == null) {
|
||||
survival = Double.valueOf(1.0d);
|
||||
}
|
||||
double s = survival.doubleValue();
|
||||
double p = normalizeProbability(pHit, 0.6d);
|
||||
s = s * (1.0d - p);
|
||||
if (s < 0.0d) {
|
||||
s = 0.0d;
|
||||
}
|
||||
survivalByBlueId.put(blueId, Double.valueOf(s));
|
||||
}
|
||||
|
||||
function void incrementAssignedCount(Map assignedCountByBlueId, String blueId) {
|
||||
Integer old = (Integer) assignedCountByBlueId.get(blueId);
|
||||
int now = old == null ? 1 : old.intValue() + 1;
|
||||
assignedCountByBlueId.put(blueId, Integer.valueOf(now));
|
||||
}
|
||||
|
||||
function Weapon pickUnboundRedWeaponForBlue(String blueId, List redWeapons) {
|
||||
if (redWeapons == null || redWeapons.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (Object obj : redWeapons) {
|
||||
Weapon w = (Weapon) obj;
|
||||
if (w == null || !isBlank(w.getTargetId())) {
|
||||
continue;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function String pickBlueTargetByNeed(Map pools, String preferredKey, Map assignedCountByBlueId, Map survivalByBlueId, double desiredKill, int cap) {
|
||||
String best = pickBestBlueIdFromPool((List) pools.get(preferredKey), assignedCountByBlueId, survivalByBlueId, desiredKill, cap);
|
||||
if (!isBlank(best)) {
|
||||
return best;
|
||||
}
|
||||
if (!"ground".equals(preferredKey)) {
|
||||
String fromGround = pickBestBlueIdFromPool((List) pools.get("ground"), assignedCountByBlueId, survivalByBlueId, desiredKill, cap);
|
||||
if (!isBlank(fromGround)) {
|
||||
return fromGround;
|
||||
}
|
||||
}
|
||||
return pickBestBlueIdFromPool((List) pools.get("all"), assignedCountByBlueId, survivalByBlueId, desiredKill, cap);
|
||||
}
|
||||
|
||||
function String pickBestBlueIdFromPool(List ids, Map assignedCountByBlueId, Map survivalByBlueId, double desiredKill, int cap) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String bestId = null;
|
||||
double bestNeed = -9999.0d;
|
||||
int bestAssigned = Integer.MAX_VALUE;
|
||||
for (Object obj : ids) {
|
||||
String id = String.valueOf(obj);
|
||||
if (isBlank(id)) {
|
||||
continue;
|
||||
}
|
||||
Integer assignedObj = (Integer) assignedCountByBlueId.get(id);
|
||||
int assigned = assignedObj == null ? 0 : assignedObj.intValue();
|
||||
if (assigned >= cap) {
|
||||
continue;
|
||||
}
|
||||
Double survivalObj = (Double) survivalByBlueId.get(id);
|
||||
double survival = survivalObj == null ? 1.0d : survivalObj.doubleValue();
|
||||
double achieved = 1.0d - survival;
|
||||
double need = desiredKill - achieved;
|
||||
if (need > bestNeed || (Math.abs(need - bestNeed) < 1e-9 && assigned < bestAssigned)) {
|
||||
bestNeed = need;
|
||||
bestAssigned = assigned;
|
||||
bestId = id;
|
||||
}
|
||||
}
|
||||
return bestId;
|
||||
}
|
||||
|
||||
function boolean isCoordinateUsable(Coordinate c) {
|
||||
return c != null && c.getLongitude() != null && c.getLatitude() != null;
|
||||
}
|
||||
|
||||
function int readRangeMetersFromWeapon(Weapon weapon) {
|
||||
if (weapon == null || weapon.getComponents() == null) {
|
||||
return -1;
|
||||
}
|
||||
for (Object obj : weapon.getComponents()) {
|
||||
SubComponents comp = (SubComponents) obj;
|
||||
if (comp == null || comp.getComponentParams() == null || comp.getComponentParams().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ComponentParam first = (ComponentParam) comp.getComponentParams().get(0);
|
||||
if (first == null || first.getAttExplain() == null) {
|
||||
continue;
|
||||
}
|
||||
String explain = first.getAttExplain();
|
||||
if (!(explain.contains("范围") || explain.contains("射程"))) {
|
||||
continue;
|
||||
}
|
||||
return parseIntSafe(first.getAttDefaultValue(), -1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function void setWeaponFirstRangeParamAtLeast(Weapon weapon, int minRange) {
|
||||
if (weapon == null || weapon.getComponents() == null || minRange <= 0) {
|
||||
return;
|
||||
}
|
||||
for (Object obj : weapon.getComponents()) {
|
||||
SubComponents comp = (SubComponents) obj;
|
||||
if (comp == null || comp.getComponentParams() == null || comp.getComponentParams().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ComponentParam first = (ComponentParam) comp.getComponentParams().get(0);
|
||||
if (first == null || first.getAttExplain() == null) {
|
||||
continue;
|
||||
}
|
||||
String explain = first.getAttExplain();
|
||||
if (!(explain.contains("范围") || explain.contains("射程"))) {
|
||||
continue;
|
||||
}
|
||||
int current = parseIntSafe(first.getAttDefaultValue(), 0);
|
||||
if (current < minRange) {
|
||||
first.setAttDefaultValue(String.valueOf(minRange));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------
|
||||
// component 映射解析 + 全组件数量覆盖
|
||||
function Map parseDeviceNameMapping(String csv) {
|
||||
@@ -1653,6 +2127,17 @@ function int parseIntSafe(String text, int fallback) {
|
||||
}
|
||||
}
|
||||
|
||||
function double parseDoubleSafe(String text, double fallback) {
|
||||
if (text == null || text.equals("")) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(text.trim());
|
||||
} catch (Exception ex) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function int readIntCfg(Map cfg, String key, int fallback) {
|
||||
if (cfg == null || key == null) {
|
||||
return fallback;
|
||||
|
||||
Reference in New Issue
Block a user