Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution
This commit is contained in:
@@ -46,6 +46,17 @@ public class Behaviortree extends BaseEntity
|
|||||||
@Excel(name = "储存行为树的节点关系图")
|
@Excel(name = "储存行为树的节点关系图")
|
||||||
private String xmlContent;
|
private String xmlContent;
|
||||||
|
|
||||||
|
@Excel(name = "平台ID")
|
||||||
|
private Integer platformId;
|
||||||
|
|
||||||
|
public Integer getPlatformId() {
|
||||||
|
return platformId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatformId(Integer platformId) {
|
||||||
|
this.platformId = platformId;
|
||||||
|
}
|
||||||
|
|
||||||
public void setId(Long id)
|
public void setId(Long id)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -118,14 +129,15 @@ public class Behaviortree extends BaseEntity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
return "Behaviortree{" +
|
||||||
.append("id", getId())
|
"id=" + id +
|
||||||
.append("name", getName())
|
", name='" + name + '\'' +
|
||||||
.append("description", getDescription())
|
", description='" + description + '\'' +
|
||||||
.append("createdAt", getCreatedAt())
|
", createdAt=" + createdAt +
|
||||||
.append("updatedAt", getUpdatedAt())
|
", updatedAt=" + updatedAt +
|
||||||
.append("englishName", getEnglishName())
|
", englishName='" + englishName + '\'' +
|
||||||
.append("xmlContent", getXmlContent())
|
", xmlContent='" + xmlContent + '\'' +
|
||||||
.toString();
|
", platformId=" + platformId +
|
||||||
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="updatedAt != null">updated_at,</if>
|
<if test="updatedAt != null">updated_at,</if>
|
||||||
<if test="englishName != null and englishName != ''">english_name,</if>
|
<if test="englishName != null and englishName != ''">english_name,</if>
|
||||||
<if test="xmlContent != null">xml_content,</if>
|
<if test="xmlContent != null">xml_content,</if>
|
||||||
|
<if test="platformId != null">platform_id,</if>
|
||||||
</trim>
|
</trim>
|
||||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
<if test="name != null and name != ''">#{name},</if>
|
<if test="name != null and name != ''">#{name},</if>
|
||||||
@@ -58,6 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="updatedAt != null">#{updatedAt},</if>
|
<if test="updatedAt != null">#{updatedAt},</if>
|
||||||
<if test="englishName != null and englishName != ''">#{englishName},</if>
|
<if test="englishName != null and englishName != ''">#{englishName},</if>
|
||||||
<if test="xmlContent != null">#{xmlContent},</if>
|
<if test="xmlContent != null">#{xmlContent},</if>
|
||||||
|
<if test="platformId != null">#{platformId},</if>
|
||||||
</trim>
|
</trim>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public class Weapon {
|
|||||||
//武器数量
|
//武器数量
|
||||||
private Integer number;
|
private Integer number;
|
||||||
|
|
||||||
|
//武器命中率
|
||||||
|
private Double hitRate;
|
||||||
|
|
||||||
//目标id
|
//目标id
|
||||||
private String targetId;
|
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`:阵位规则总开关。
|
- `enablePositionRules`:阵位规则总开关。
|
||||||
- 阵位输入来源:`blueTask.warZoneLocation` 与 `blueTask.defZoneLocation`(各 4 个经纬点)。
|
- 阵位输入来源:`blueTask.warZoneLocation` 与 `blueTask.defZoneLocation`(各 4 个经纬点)。
|
||||||
- `fireUnitSpacingMeters`:防区/作战区点位间距(米),例如 `100` 代表约每 100 米一个火力单元。
|
- `fireUnitSpacingMeters`:防区/作战区点位间距(米),例如 `100` 代表约每 100 米一个火力单元。
|
||||||
@@ -120,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.8 航迹规则参数(新增)
|
### 2.9 航迹规则参数(新增)
|
||||||
- `enableTrajectoryRules`:航迹规则总开关。
|
- `enableTrajectoryRules`:航迹规则总开关。
|
||||||
- `strategyMode`:`auto/shortest/flank/interfere`。
|
- `strategyMode`:`auto/shortest/flank/interfere`。
|
||||||
- `auto`:智能选择策略。
|
- `auto`:智能选择策略。
|
||||||
@@ -149,10 +162,12 @@
|
|||||||
- 主决策在 `红方武器自适应装配规则`:调用 `configureRedWeaponsByBlue(...)`,按“映射配置”添加武器。
|
- 主决策在 `红方武器自适应装配规则`:调用 `configureRedWeaponsByBlue(...)`,按“映射配置”添加武器。
|
||||||
- 导弹增强在 `导弹联动增强规则`:调用 `applyMissileLinkage(...)`,受开关和阈值控制。
|
- 导弹增强在 `导弹联动增强规则`:调用 `applyMissileLinkage(...)`,受开关和阈值控制。
|
||||||
- 全组件数量匹配在 `全组件数量匹配规则`:按红方 `targetId` 绑定蓝方装备,覆盖非导弹组件 `componentParams[0].number`;找不到组件/targetId 允许跳过。
|
- 全组件数量匹配在 `全组件数量匹配规则`:按红方 `targetId` 绑定蓝方装备,覆盖非导弹组件 `componentParams[0].number`;找不到组件/targetId 允许跳过。
|
||||||
|
- 命中率驱动数量在 `命中率规则-动态数量与offset`:按 `hitRate` 与目标毁伤概率推导火力数量;显式 offset 配置优先。
|
||||||
- 任务命名在 `任务自动匹配规则`:调用 `assignTaskNameByRedWeapons(...)`,按红方最终武器自动生成任务名和 `dataType`。
|
- 任务命名在 `任务自动匹配规则`:调用 `assignTaskNameByRedWeapons(...)`,按红方最终武器自动生成任务名和 `dataType`。
|
||||||
- 炮类约束:命中炮类条件时,炮类武器只保留 `炮弹` 组件,单位 `范围米`。
|
- 炮类约束:命中炮类条件时,炮类武器只保留 `炮弹` 组件,单位 `范围米`。
|
||||||
- `targetId` 绑定:在装配后自动执行,尽量为红方武器绑定蓝方 `equipmentId`,允许少量空值冗余。
|
- `targetId` 绑定:在装配后自动执行,按命中率动态给蓝目标分配多个红方武器(受上下限约束),允许少量空值冗余。
|
||||||
- 阵位部署:按多边形区域和武器类型自动赋位,保证防区火力覆盖。
|
- 阵位部署:按多边形区域和武器类型自动赋位,保证防区火力覆盖。
|
||||||
|
- 射程合理性在 `射程合理性校验规则`:基于蓝/红武器坐标计算距离,自动避免“射程不足却打击”的不合理情况(可自动调参)。
|
||||||
- 航迹生成:根据蓝方 `trackPoints` 生成红方 `trackPoints`,点数与蓝方一致,支持三套策略和智能选择。
|
- 航迹生成:根据蓝方 `trackPoints` 生成红方 `trackPoints`,点数与蓝方一致,支持三套策略和智能选择。
|
||||||
|
|
||||||
## 3.1 任务名称自动匹配(新增)
|
## 3.1 任务名称自动匹配(新增)
|
||||||
|
|||||||
@@ -149,6 +149,18 @@ function Map buildBusinessConfig() {
|
|||||||
cfg.put("blueMissileRangeDefault", 220); // 蓝方导弹范围默认值
|
cfg.put("blueMissileRangeDefault", 220); // 蓝方导弹范围默认值
|
||||||
cfg.put("minBlueMissileCountForLinkage", 1); // 联动触发门槛
|
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),然后覆盖所有“非导弹组件”的数量
|
// 逻辑:按红方武器 targetId 找到对应蓝方装备(equipmentId),然后覆盖所有“非导弹组件”的数量
|
||||||
cfg.put("enableComponentQuantityMatch", Boolean.TRUE);
|
cfg.put("enableComponentQuantityMatch", Boolean.TRUE);
|
||||||
@@ -239,6 +251,17 @@ then
|
|||||||
applyAllComponentQuantities($fact, cfg);
|
applyAllComponentQuantities($fact, cfg);
|
||||||
end
|
end
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------
|
||||||
|
rule "命中率规则-动态数量与offset"
|
||||||
|
agenda-group "打击任务"
|
||||||
|
salience 52
|
||||||
|
when
|
||||||
|
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
||||||
|
then
|
||||||
|
Map cfg = buildBusinessConfig();
|
||||||
|
applyHitRateDrivenOffsets($fact, cfg);
|
||||||
|
end
|
||||||
|
|
||||||
//-------------------------------------------------------------------------------
|
//-------------------------------------------------------------------------------
|
||||||
rule "任务自动匹配规则"
|
rule "任务自动匹配规则"
|
||||||
agenda-group "打击任务"
|
agenda-group "打击任务"
|
||||||
@@ -274,9 +297,20 @@ then
|
|||||||
end
|
end
|
||||||
|
|
||||||
//-------------------------------------------------------------------------------
|
//-------------------------------------------------------------------------------
|
||||||
rule "航迹规则-生成红方航迹"
|
rule "射程合理性校验规则"
|
||||||
agenda-group "打击任务"
|
agenda-group "打击任务"
|
||||||
salience 47
|
salience 47
|
||||||
|
when
|
||||||
|
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
||||||
|
then
|
||||||
|
Map cfg = buildBusinessConfig();
|
||||||
|
applyRangeSanityAndRecommend($fact, cfg);
|
||||||
|
end
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------
|
||||||
|
rule "航迹规则-生成红方航迹"
|
||||||
|
agenda-group "打击任务"
|
||||||
|
salience 46
|
||||||
when
|
when
|
||||||
// 根据蓝方 trackPoints 生成红方 trackPoints,点数保持一致
|
// 根据蓝方 trackPoints 生成红方 trackPoints,点数保持一致
|
||||||
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
|
||||||
@@ -391,6 +425,9 @@ function void configureRedWeaponsByBlue(
|
|||||||
limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault"));
|
limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入红方武器默认命中率(若入参已给 hitRate,则不覆盖)
|
||||||
|
applyDefaultHitRateIfAbsent(redWeapons, cfg);
|
||||||
|
|
||||||
// 自动绑定红方武器 targetId(来源:蓝方 equipmentId)
|
// 自动绑定红方武器 targetId(来源:蓝方 equipmentId)
|
||||||
bindTargetIdsForRedWeapons(redWeapons, blueWeapons, cfg);
|
bindTargetIdsForRedWeapons(redWeapons, blueWeapons, cfg);
|
||||||
}
|
}
|
||||||
@@ -542,25 +579,72 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map pools = extractBlueTargetPools(blueWeapons);
|
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 total = redWeapons.size();
|
||||||
int bound = 0;
|
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) {
|
for (Object obj : redWeapons) {
|
||||||
Weapon redWeapon = (Weapon) obj;
|
Weapon redWeapon = (Weapon) obj;
|
||||||
if (redWeapon == null) {
|
if (redWeapon == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isBlank(redWeapon.getTargetId())) {
|
if (!isBlank(redWeapon.getTargetId())) {
|
||||||
bound++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String poolKey = inferBluePoolKeyForRedWeapon(redWeapon);
|
String poolKey = inferBluePoolKeyForRedWeapon(redWeapon);
|
||||||
String targetId = pickTargetIdFromPools(pools, cursor, poolKey);
|
String targetId = pickBlueTargetByNeed(
|
||||||
|
pools,
|
||||||
|
poolKey,
|
||||||
|
assignedCountByBlueId,
|
||||||
|
survivalByBlueId,
|
||||||
|
desiredKill,
|
||||||
|
cap
|
||||||
|
);
|
||||||
if (!isBlank(targetId)) {
|
if (!isBlank(targetId)) {
|
||||||
redWeapon.setTargetId(targetId);
|
redWeapon.setTargetId(targetId);
|
||||||
bound++;
|
bound++;
|
||||||
|
incrementAssignedCount(assignedCountByBlueId, targetId);
|
||||||
|
double p = resolveWeaponHitRate(redWeapon, cfg);
|
||||||
|
updateBlueSurvival(survivalByBlueId, targetId, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +655,7 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第二轮:若绑定率不足,回退到全目标池尽量补齐(仍允许复用目标)
|
// 第三轮:若绑定率不足,回退全目标池补齐(仍受cap约束)
|
||||||
for (Object obj : redWeapons) {
|
for (Object obj : redWeapons) {
|
||||||
if (total > 0 && ((double) bound / (double) total) >= minRatio) {
|
if (total > 0 && ((double) bound / (double) total) >= minRatio) {
|
||||||
break;
|
break;
|
||||||
@@ -580,28 +664,418 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
|
|||||||
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
|
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String targetId = pickTargetIdFromPools(pools, cursor, "all");
|
String targetId = pickBlueTargetByNeed(
|
||||||
|
pools,
|
||||||
|
"all",
|
||||||
|
assignedCountByBlueId,
|
||||||
|
survivalByBlueId,
|
||||||
|
desiredKill,
|
||||||
|
cap
|
||||||
|
);
|
||||||
if (!isBlank(targetId)) {
|
if (!isBlank(targetId)) {
|
||||||
redWeapon.setTargetId(targetId);
|
redWeapon.setTargetId(targetId);
|
||||||
bound++;
|
bound++;
|
||||||
|
incrementAssignedCount(assignedCountByBlueId, targetId);
|
||||||
|
double p = resolveWeaponHitRate(redWeapon, cfg);
|
||||||
|
updateBlueSurvival(survivalByBlueId, targetId, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第三轮:若不允许空 targetId,最后强制从 all 池补齐(尽力而为)
|
// 第四轮:若不允许空 targetId,最后尽力补齐
|
||||||
if (!allowReserveWithoutTarget) {
|
if (!allowReserveWithoutTarget) {
|
||||||
for (Object obj : redWeapons) {
|
for (Object obj : redWeapons) {
|
||||||
Weapon redWeapon = (Weapon) obj;
|
Weapon redWeapon = (Weapon) obj;
|
||||||
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
|
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String targetId = pickTargetIdFromPools(pools, cursor, "all");
|
String targetId = pickBlueTargetByNeed(
|
||||||
|
pools,
|
||||||
|
"all",
|
||||||
|
assignedCountByBlueId,
|
||||||
|
survivalByBlueId,
|
||||||
|
desiredKill,
|
||||||
|
cap
|
||||||
|
);
|
||||||
if (!isBlank(targetId)) {
|
if (!isBlank(targetId)) {
|
||||||
redWeapon.setTargetId(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 映射解析 + 全组件数量覆盖
|
// component 映射解析 + 全组件数量覆盖
|
||||||
function Map parseDeviceNameMapping(String csv) {
|
function Map parseDeviceNameMapping(String csv) {
|
||||||
@@ -1654,6 +2128,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) {
|
function int readIntCfg(Map cfg, String key, int fallback) {
|
||||||
if (cfg == null || key == null) {
|
if (cfg == null || key == null) {
|
||||||
return fallback;
|
return fallback;
|
||||||
|
|||||||
@@ -1831,6 +1831,12 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-select .ant-select-clear {
|
||||||
|
color: rgb(153 168 180);
|
||||||
|
background: #475f71;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.ks-add-parameter-action{
|
.ks-add-parameter-action{
|
||||||
color: #eee;
|
color: #eee;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user