火力规则:装备匹配规则实现,目标规则实现,阵位规则简单实现(需要细化)
This commit is contained in:
@@ -30,6 +30,8 @@ public class FireRuleInputRedWeaponElementDTO {
|
|||||||
private Boolean isInterferenceTarget;
|
private Boolean isInterferenceTarget;
|
||||||
|
|
||||||
private Boolean isDefendImportantPlace;
|
private Boolean isDefendImportantPlace;
|
||||||
|
//命中率
|
||||||
|
private Double successTargetRad;
|
||||||
|
|
||||||
private String groupType;
|
private String groupType;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ public class FireRuleTaskInputDTO {
|
|||||||
*/
|
*/
|
||||||
private String side;
|
private String side;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 蓝方任务装备命中率
|
||||||
|
*/
|
||||||
|
private Double successTargetRad;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 航迹所属实体或阵营标识
|
* 航迹所属实体或阵营标识
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public class FireRuleRedWeaponEquipmentVO {
|
|||||||
private Boolean isInterferenceTarget;
|
private Boolean isInterferenceTarget;
|
||||||
|
|
||||||
private Boolean isDefendImportantPlace;
|
private Boolean isDefendImportantPlace;
|
||||||
|
//命中率
|
||||||
|
private Double successTargetRad;
|
||||||
|
|
||||||
private String groupType;
|
private String groupType;
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,32 @@ package com.solution.rule.utils;
|
|||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.solution.rule.domain.ultimately.dto.FireRuleInputRedSubComponentsDTO;
|
import com.solution.rule.domain.ultimately.dto.FireRuleInputRedSubComponentsDTO;
|
||||||
|
import com.solution.rule.domain.ultimately.dto.FireRuleCoordinateDTO;
|
||||||
|
import com.solution.rule.domain.ultimately.dto.FireRuleTrackPointDTO;
|
||||||
|
import com.solution.rule.domain.ultimately.dto.FireRuleTaskInputDTO;
|
||||||
|
import com.solution.rule.domain.ultimately.dto.FireRuleTaskWeaponDTO;
|
||||||
|
import com.solution.rule.domain.ultimately.dto.FireRuleWeaponComponentDTO;
|
||||||
|
import com.solution.rule.domain.ultimately.dto.FireRuleComponentParamDTO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleLauncherConfigurationVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleLauncherConfigurationVO;
|
||||||
|
import com.solution.rule.domain.ultimately.vo.FireRuleExecuteBlockVO;
|
||||||
|
import com.solution.rule.domain.ultimately.vo.FireRuleExecuteTargetItemVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleMissionListItemVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleMissionListItemVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleMountedWeaponRefVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleMountedWeaponRefVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleRedSubComponentsVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleRedSubComponentsVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponEquipmentVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponEquipmentVO;
|
||||||
|
import com.solution.rule.domain.ultimately.vo.FireRuleRedPlatformSlotVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponSlotVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponSlotVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskNodeVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskNodeVO;
|
||||||
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskPayloadVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskPayloadVO;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将入参中的红方装备 {@code SubComponents}(DTO)转为输出 VO,保持与原始 JSON 结构一致。
|
* 将入参中的红方装备 {@code SubComponents}(DTO)转为输出 VO,保持与原始 JSON 结构一致。
|
||||||
@@ -95,6 +108,325 @@ public final class FireRuleRedWeaponOutputFillHelper {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阵位填充:写入每个 redWeapon 的 SubComponents.platform[].positions = [lon,lat,height]。
|
||||||
|
* 混合模式:蓝方武器坐标中心为锚点 + 航迹方位偏移 + 编队偏移 + 作战区约束。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public static void fillPlatformPositions(List<FireRuleRedWeaponEquipmentVO> redWeapons, FireRuleTaskInputDTO blueTask, Map params) {
|
||||||
|
if (redWeapons == null || redWeapons.isEmpty() || blueTask == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Coord anchor = computeBlueAnchor(blueTask);
|
||||||
|
if (anchor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double bearingDeg = computeTrackBearingDeg(blueTask, str(params, "trackPointDirectionMode", "head2next"),
|
||||||
|
readDouble(params, "fallbackBearingDeg", 0d));
|
||||||
|
|
||||||
|
double minKm = readDouble(params, "deployDistanceKmMin", 8d);
|
||||||
|
double maxKm = readDouble(params, "deployDistanceKmMax", 30d);
|
||||||
|
double defKm = readDouble(params, "deployDistanceKmDefault", 15d);
|
||||||
|
String formation = str(params, "formationType", "line");
|
||||||
|
double spacingM = readDouble(params, "formationSpacingMeters", 300d);
|
||||||
|
double headingOffset = readDouble(params, "formationHeadingOffsetDeg", 15d);
|
||||||
|
double minInterM = readDouble(params, "minInterPlatformDistanceMeters", 80d);
|
||||||
|
if (spacingM < minInterM) {
|
||||||
|
spacingM = minInterM;
|
||||||
|
}
|
||||||
|
double defaultHeight = readDouble(params, "defaultDeployHeight", 30d);
|
||||||
|
double followRatio = readDouble(params, "heightFollowBlueRatio", 0d);
|
||||||
|
boolean clamp = Boolean.parseBoolean(String.valueOf(params.getOrDefault("enableWarZoneClamp", true)));
|
||||||
|
List<Coord> warZone = toWarZonePolygon(blueTask.getWarZoneLocation());
|
||||||
|
Map<String, Double> distanceByPlatform = parseDistanceByPlatformCsv(str(params, "distanceByPlatformCsv", ""));
|
||||||
|
|
||||||
|
for (FireRuleRedWeaponEquipmentVO red : redWeapons) {
|
||||||
|
if (red == null || red.getSubComponents() == null || red.getSubComponents().getPlatform() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double distKm = resolveDeployDistanceKm(red, distanceByPlatform, defKm);
|
||||||
|
distKm = clampDouble(distKm, minKm, maxKm);
|
||||||
|
|
||||||
|
List<FireRuleRedPlatformSlotVO> platforms = red.getSubComponents().getPlatform();
|
||||||
|
for (int i = 0; i < platforms.size(); i++) {
|
||||||
|
FireRuleRedPlatformSlotVO slot = platforms.get(i);
|
||||||
|
if (slot == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double extraBearing = formationBearingOffsetDeg(formation, i, headingOffset);
|
||||||
|
double extraMeters = formationOffsetMeters(formation, i, spacingM);
|
||||||
|
Coord c = moveByMeters(anchor, distKm * 1000d + extraMeters, bearingDeg + extraBearing);
|
||||||
|
|
||||||
|
if (clamp && warZone.size() >= 3 && !isPointInPolygon(c, warZone)) {
|
||||||
|
c = projectInsidePolygon(c, anchor, warZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
double h = defaultHeight;
|
||||||
|
if (anchor.height != null) {
|
||||||
|
h = defaultHeight + anchor.height * followRatio;
|
||||||
|
}
|
||||||
|
slot.setPositions(asPosition(c.lon, c.lat, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标分配:为每个红方 Tasks 节点写入 task.execute[0].targetList[*].targetId。\n
|
||||||
|
* targetId 来源:当前蓝方任务 {@link FireRuleTaskInputDTO#getTaskWeapons()} 下每条武器的 equipmentId。\n
|
||||||
|
* 允许多个红方装备指向同一个蓝方目标(不做去重占用)。\n
|
||||||
|
* 参数来自 rule.drl buildParam:\n
|
||||||
|
* - executeTypeDefault\n
|
||||||
|
* - targetPickMode: roundRobin/random\n
|
||||||
|
* - minTargetsPerRed / maxTargetsPerRedCap\n
|
||||||
|
* - radToTargetsCsv\n
|
||||||
|
* - rangeParseRegex / rangeUnit / minRangeToAllowAssignKm\n
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
public static void assignTargetsToTasks(List<FireRuleSceneTaskNodeVO> tasks,
|
||||||
|
List<FireRuleRedWeaponEquipmentVO> redWeapons,
|
||||||
|
FireRuleTaskInputDTO blueTask,
|
||||||
|
Map params) {
|
||||||
|
if (tasks == null || tasks.isEmpty() || blueTask == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> candidates = extractBlueTargetEquipmentIds(blueTask, params);
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String executeType = str(params, "executeTypeDefault", "assault");
|
||||||
|
String pickMode = str(params, "targetPickMode", "roundRobin");
|
||||||
|
|
||||||
|
for (int i = 0; i < tasks.size(); i++) {
|
||||||
|
FireRuleSceneTaskNodeVO node = tasks.get(i);
|
||||||
|
if (node == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node.getTask() == null) {
|
||||||
|
node.setTask(new FireRuleSceneTaskPayloadVO());
|
||||||
|
}
|
||||||
|
FireRuleSceneTaskPayloadVO payload = node.getTask();
|
||||||
|
Double redHitRate = resolveRedWeaponHitRate(payload.getWeaponId(), redWeapons);
|
||||||
|
int targetsPerRed = resolveTargetsPerRed(redHitRate, params);
|
||||||
|
|
||||||
|
FireRuleExecuteBlockVO block = new FireRuleExecuteBlockVO();
|
||||||
|
block.setType(executeType);
|
||||||
|
|
||||||
|
List<FireRuleExecuteTargetItemVO> targetList = new ArrayList<>();
|
||||||
|
for (int k = 0; k < targetsPerRed; k++) {
|
||||||
|
String blueEquipmentId = pickBlueTargetId(candidates, pickMode, i, k, node, blueTask);
|
||||||
|
if (isBlank(blueEquipmentId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FireRuleExecuteTargetItemVO item = new FireRuleExecuteTargetItemVO();
|
||||||
|
item.setId(UUID.randomUUID().toString());
|
||||||
|
item.setTargetId(blueEquipmentId);
|
||||||
|
// weaponId:可填红方装备的 equipmentId(若无则留空)
|
||||||
|
item.setWeaponId(payload.getWeaponId());
|
||||||
|
targetList.add(item);
|
||||||
|
}
|
||||||
|
block.setTargetList(targetList.isEmpty() ? null : targetList);
|
||||||
|
|
||||||
|
List<FireRuleExecuteBlockVO> execute = new ArrayList<>();
|
||||||
|
execute.add(block);
|
||||||
|
payload.setExecute(execute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double resolveRedWeaponHitRate(String weaponId, List<FireRuleRedWeaponEquipmentVO> redWeapons) {
|
||||||
|
if (isBlank(weaponId) || redWeapons == null || redWeapons.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (FireRuleRedWeaponEquipmentVO w : redWeapons) {
|
||||||
|
if (w != null && weaponId.equals(w.getEquipmentId())) {
|
||||||
|
return w.getSuccessTargetRad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String pickBlueTargetId(List<String> candidates, String pickMode, int taskIndex, int innerIndex,
|
||||||
|
FireRuleSceneTaskNodeVO node, FireRuleTaskInputDTO blueTask) {
|
||||||
|
if (candidates == null || candidates.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ("random".equalsIgnoreCase(pickMode)) {
|
||||||
|
// 伪随机但稳定:基于 redTask.id + blueTask.id + innerIndex 做 hash
|
||||||
|
String seed = nz(node != null ? node.getId() : "") + "|" + nz(blueTask.getId()) + "|" + innerIndex;
|
||||||
|
int h = Math.abs(seed.hashCode());
|
||||||
|
return candidates.get(h % candidates.size());
|
||||||
|
}
|
||||||
|
// 默认 roundRobin:按 Tasks 序号与目标序号轮询
|
||||||
|
int idx = Math.abs(taskIndex + innerIndex) % candidates.size();
|
||||||
|
return candidates.get(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resolveTargetsPerRed(Double successTargetRad, Map params) {
|
||||||
|
int min = readInt(params, "minTargetsPerRed", 1);
|
||||||
|
int max = readInt(params, "maxTargetsPerRedCap", 3);
|
||||||
|
if (max <= 0) {
|
||||||
|
max = 1;
|
||||||
|
}
|
||||||
|
if (min < 0) {
|
||||||
|
min = 0;
|
||||||
|
}
|
||||||
|
if (min > max) {
|
||||||
|
min = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
int byRad = parseRadToTargets(successTargetRad, str(params, "radToTargetsCsv", ""));
|
||||||
|
int v = byRad > 0 ? byRad : min;
|
||||||
|
if (v < min) {
|
||||||
|
v = min;
|
||||||
|
}
|
||||||
|
if (v > max) {
|
||||||
|
v = max;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseRadToTargets(Double rad, String csv) {
|
||||||
|
if (rad == null || csv == null || csv.trim().isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double r = rad.doubleValue();
|
||||||
|
// 允许无序输入:找所有 threshold<=r 的最大 threshold 对应的 targets
|
||||||
|
double bestTh = -1;
|
||||||
|
int bestTargets = 0;
|
||||||
|
for (String part : csv.split(",")) {
|
||||||
|
String p = part.trim();
|
||||||
|
if (p.isEmpty() || !p.contains(":")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] kv = p.split(":");
|
||||||
|
if (kv.length != 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
double th = Double.parseDouble(kv[0].trim());
|
||||||
|
int t = Integer.parseInt(kv[1].trim());
|
||||||
|
if (r >= th && th > bestTh) {
|
||||||
|
bestTh = th;
|
||||||
|
bestTargets = t;
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestTargets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> extractBlueTargetEquipmentIds(FireRuleTaskInputDTO blueTask, Map params) {
|
||||||
|
List<String> all = new ArrayList<>();
|
||||||
|
if (blueTask == null || blueTask.getTaskWeapons() == null) {
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 射程过滤参数
|
||||||
|
String regex = str(params, "rangeParseRegex", "(\\d+(?:\\.\\d+)?)");
|
||||||
|
String unit = str(params, "rangeUnit", "km");
|
||||||
|
double minKm = readDouble(params, "minRangeToAllowAssignKm", 0d);
|
||||||
|
Pattern pattern = safePattern(regex);
|
||||||
|
|
||||||
|
for (FireRuleTaskWeaponDTO w : blueTask.getTaskWeapons()) {
|
||||||
|
if (w == null || isBlank(w.getEquipmentId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Double km = tryParseRangeKm(w, pattern, unit);
|
||||||
|
if (km != null && km.doubleValue() < minKm) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
all.add(w.getEquipmentId());
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double tryParseRangeKm(FireRuleTaskWeaponDTO w, Pattern pattern, String unit) {
|
||||||
|
if (w == null || w.getComponents() == null || w.getComponents().isEmpty() || pattern == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (FireRuleWeaponComponentDTO c : w.getComponents()) {
|
||||||
|
if (c == null || c.getComponentParams() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (FireRuleComponentParamDTO p : c.getComponentParams()) {
|
||||||
|
if (p == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Double km = parseNumberToKm(p.getAttDefaultValue(), pattern, unit);
|
||||||
|
if (km != null) {
|
||||||
|
return km;
|
||||||
|
}
|
||||||
|
km = parseNumberToKm(p.getAttExplain(), pattern, unit);
|
||||||
|
if (km != null) {
|
||||||
|
return km;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double parseNumberToKm(String text, Pattern pattern, String unit) {
|
||||||
|
if (text == null || text.trim().isEmpty() || pattern == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Matcher m = pattern.matcher(text);
|
||||||
|
if (!m.find()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
double v = Double.parseDouble(m.group(1));
|
||||||
|
if ("m".equalsIgnoreCase(unit)) {
|
||||||
|
return v / 1000.0d;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern safePattern(String regex) {
|
||||||
|
try {
|
||||||
|
return Pattern.compile(regex);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double readDouble(Map params, String key, double def) {
|
||||||
|
Object v = params != null ? params.get(key) : null;
|
||||||
|
if (v == null) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
if (v instanceof Number) {
|
||||||
|
return ((Number) v).doubleValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(String.valueOf(v).trim());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readInt(Map params, String key, int def) {
|
||||||
|
Object v = params != null ? params.get(key) : null;
|
||||||
|
if (v == null) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
if (v instanceof Number) {
|
||||||
|
return ((Number) v).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(String.valueOf(v).trim());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String str(Map params, String key, String def) {
|
||||||
|
Object v = params != null ? params.get(key) : null;
|
||||||
|
return v == null ? def : String.valueOf(v);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<FireRuleMissionListItemVO> buildMissionList(FireRuleRedWeaponEquipmentVO redWeapon) {
|
private static List<FireRuleMissionListItemVO> buildMissionList(FireRuleRedWeaponEquipmentVO redWeapon) {
|
||||||
if (redWeapon == null || redWeapon.getSubComponents() == null || redWeapon.getSubComponents().getWeapon() == null) {
|
if (redWeapon == null || redWeapon.getSubComponents() == null || redWeapon.getSubComponents().getWeapon() == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -149,4 +481,210 @@ public final class FireRuleRedWeaponOutputFillHelper {
|
|||||||
private static boolean isBlank(String s) {
|
private static boolean isBlank(String s) {
|
||||||
return s == null || s.trim().isEmpty();
|
return s == null || s.trim().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------ 阵位几何辅助 ------------------
|
||||||
|
private static class Coord {
|
||||||
|
final double lon;
|
||||||
|
final double lat;
|
||||||
|
final Double height;
|
||||||
|
Coord(double lon, double lat, Double height) {
|
||||||
|
this.lon = lon;
|
||||||
|
this.lat = lat;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Coord computeBlueAnchor(FireRuleTaskInputDTO blueTask) {
|
||||||
|
if (blueTask == null || blueTask.getTaskWeapons() == null || blueTask.getTaskWeapons().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
double sumLon = 0d, sumLat = 0d, sumH = 0d;
|
||||||
|
int count = 0, hc = 0;
|
||||||
|
for (FireRuleTaskWeaponDTO w : blueTask.getTaskWeapons()) {
|
||||||
|
if (w == null || w.getCoordinate() == null || w.getCoordinate().getLongitude() == null || w.getCoordinate().getLatitude() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sumLon += w.getCoordinate().getLongitude();
|
||||||
|
sumLat += w.getCoordinate().getLatitude();
|
||||||
|
count++;
|
||||||
|
if (w.getCoordinate().getHeight() != null) {
|
||||||
|
sumH += w.getCoordinate().getHeight();
|
||||||
|
hc++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Coord(sumLon / count, sumLat / count, hc > 0 ? sumH / hc : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double computeTrackBearingDeg(FireRuleTaskInputDTO blueTask, String mode, double fallbackDeg) {
|
||||||
|
if (blueTask == null || blueTask.getTrackPoints() == null || blueTask.getTrackPoints().size() < 2) {
|
||||||
|
return fallbackDeg;
|
||||||
|
}
|
||||||
|
FireRuleTrackPointDTO a;
|
||||||
|
FireRuleTrackPointDTO b;
|
||||||
|
if ("tail2prev".equalsIgnoreCase(mode)) {
|
||||||
|
int n = blueTask.getTrackPoints().size();
|
||||||
|
a = blueTask.getTrackPoints().get(n - 2);
|
||||||
|
b = blueTask.getTrackPoints().get(n - 1);
|
||||||
|
} else {
|
||||||
|
a = blueTask.getTrackPoints().get(0);
|
||||||
|
b = blueTask.getTrackPoints().get(1);
|
||||||
|
}
|
||||||
|
if (a == null || b == null || a.getLongitude() == null || a.getLatitude() == null || b.getLongitude() == null || b.getLatitude() == null) {
|
||||||
|
return fallbackDeg;
|
||||||
|
}
|
||||||
|
return bearingDeg(a.getLongitude(), a.getLatitude(), b.getLongitude(), b.getLatitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Coord> toWarZonePolygon(List<FireRuleCoordinateDTO> warZone) {
|
||||||
|
List<Coord> list = new ArrayList<>();
|
||||||
|
if (warZone == null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
for (FireRuleCoordinateDTO c : warZone) {
|
||||||
|
if (c != null && c.getLongitude() != null && c.getLatitude() != null) {
|
||||||
|
list.add(new Coord(c.getLongitude(), c.getLatitude(), c.getHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Double> parseDistanceByPlatformCsv(String csv) {
|
||||||
|
Map<String, Double> map = new HashMap<>();
|
||||||
|
if (isBlank(csv)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
for (String part : csv.split(",")) {
|
||||||
|
String p = part.trim();
|
||||||
|
if (!p.contains(":")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] kv = p.split(":");
|
||||||
|
if (kv.length != 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
map.put(kv[0].trim(), Double.parseDouble(kv[1].trim()));
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double resolveDeployDistanceKm(FireRuleRedWeaponEquipmentVO red, Map<String, Double> byPlatform, double defKm) {
|
||||||
|
String name = red != null ? nz(red.getName()) : "";
|
||||||
|
String platform = red != null ? nz(red.getPlatformType()) : "";
|
||||||
|
for (Map.Entry<String, Double> e : byPlatform.entrySet()) {
|
||||||
|
String k = e.getKey();
|
||||||
|
if (!isBlank(k) && (name.contains(k) || platform.contains(k))) {
|
||||||
|
return e.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defKm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double formationBearingOffsetDeg(String formation, int index, double headingOffset) {
|
||||||
|
if ("line".equalsIgnoreCase(formation)) {
|
||||||
|
return 90d;
|
||||||
|
}
|
||||||
|
if ("wedge".equalsIgnoreCase(formation)) {
|
||||||
|
return (index % 2 == 0 ? 1 : -1) * headingOffset;
|
||||||
|
}
|
||||||
|
if ("circle".equalsIgnoreCase(formation)) {
|
||||||
|
return index * 45d;
|
||||||
|
}
|
||||||
|
return 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double formationOffsetMeters(String formation, int index, double spacingM) {
|
||||||
|
if ("line".equalsIgnoreCase(formation)) {
|
||||||
|
return (index - 0.5d) * spacingM;
|
||||||
|
}
|
||||||
|
if ("wedge".equalsIgnoreCase(formation)) {
|
||||||
|
return (index + 1) * spacingM * 0.5d;
|
||||||
|
}
|
||||||
|
if ("circle".equalsIgnoreCase(formation)) {
|
||||||
|
return spacingM;
|
||||||
|
}
|
||||||
|
return index * spacingM;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Coord moveByMeters(Coord anchor, double meters, double bearingDeg) {
|
||||||
|
double R = 6378137.0d;
|
||||||
|
double brng = Math.toRadians(bearingDeg);
|
||||||
|
double lat1 = Math.toRadians(anchor.lat);
|
||||||
|
double lon1 = Math.toRadians(anchor.lon);
|
||||||
|
double dR = meters / R;
|
||||||
|
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(dR) + Math.cos(lat1) * Math.sin(dR) * Math.cos(brng));
|
||||||
|
double lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dR) * Math.cos(lat1),
|
||||||
|
Math.cos(dR) - Math.sin(lat1) * Math.sin(lat2));
|
||||||
|
return new Coord(Math.toDegrees(lon2), Math.toDegrees(lat2), anchor.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double bearingDeg(double lon1, double lat1, double lon2, double lat2) {
|
||||||
|
double phi1 = Math.toRadians(lat1);
|
||||||
|
double phi2 = Math.toRadians(lat2);
|
||||||
|
double dLon = Math.toRadians(lon2 - lon1);
|
||||||
|
double y = Math.sin(dLon) * Math.cos(phi2);
|
||||||
|
double x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(dLon);
|
||||||
|
double brng = Math.toDegrees(Math.atan2(y, x));
|
||||||
|
return (brng + 360d) % 360d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPointInPolygon(Coord p, List<Coord> poly) {
|
||||||
|
boolean inside = false;
|
||||||
|
for (int i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
|
||||||
|
double xi = poly.get(i).lon, yi = poly.get(i).lat;
|
||||||
|
double xj = poly.get(j).lon, yj = poly.get(j).lat;
|
||||||
|
boolean intersect = ((yi > p.lat) != (yj > p.lat))
|
||||||
|
&& (p.lon < (xj - xi) * (p.lat - yi) / (yj - yi + 1e-12) + xi);
|
||||||
|
if (intersect) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Coord projectInsidePolygon(Coord out, Coord anchor, List<Coord> poly) {
|
||||||
|
if (isPointInPolygon(out, poly)) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// 二分逼近:沿 anchor -> out 连线回退到多边形内部
|
||||||
|
Coord in = anchor;
|
||||||
|
if (!isPointInPolygon(in, poly)) {
|
||||||
|
// anchor 不在区内时,退化为原点不处理
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
Coord lo = in;
|
||||||
|
Coord hi = out;
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
Coord mid = new Coord((lo.lon + hi.lon) / 2d, (lo.lat + hi.lat) / 2d, out.height);
|
||||||
|
if (isPointInPolygon(mid, poly)) {
|
||||||
|
lo = mid;
|
||||||
|
} else {
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Double> asPosition(double lon, double lat, double height) {
|
||||||
|
List<Double> p = new ArrayList<>(3);
|
||||||
|
p.add(lon);
|
||||||
|
p.add(lat);
|
||||||
|
p.add(height);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double clampDouble(double v, double min, double max) {
|
||||||
|
if (v < min) {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
if (v > max) {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import com.solution.rule.domain.ultimately.vo.FireRuleTaskInputVO;
|
|||||||
import com.solution.rule.domain.ultimately.vo.FireRuleTaskWeaponVO;
|
import com.solution.rule.domain.ultimately.vo.FireRuleTaskWeaponVO;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drools 规则调用的装备匹配逻辑。所有业务词均来自 globalParams(由 rule.drl 的 buildParam 注入),本类不写死中文业务词。
|
* Drools 规则调用的装备匹配逻辑。所有业务词均来自 globalParams(由 rule.drl 的 buildParam 注入),本类不写死中文业务词。
|
||||||
@@ -103,13 +105,126 @@ public final class RuleFunction {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FireRuleInputRedWeaponElementDTO chosen = pool.remove(bestIndex);
|
pool.remove(bestIndex);
|
||||||
|
|
||||||
out.setRedWeapons(convertPoolToEquipmentVoList(pool));
|
out.setRedWeapons(convertPoolToEquipmentVoList(pool));
|
||||||
// Tasks 由最终输出 redWeapons 一一生成(一个装备 -> 一个任务)
|
// Tasks 由最终输出 redWeapons 一一生成(一个装备 -> 一个任务)
|
||||||
out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons()));
|
out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标规则:显式为 Tasks 填 targetId,并按红方命中率不足时补拿装备(出池,带上限)。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public static void target(DroolsFact fact, Map globalParams){
|
||||||
|
if (fact == null || fact.getTask() == null || fact.getFireRuleOutputVO() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> p = castParams(globalParams);
|
||||||
|
FireRuleTaskInputDTO blue = fact.getTask();
|
||||||
|
FireRuleOutputVO out = fact.getFireRuleOutputVO();
|
||||||
|
List<FireRuleInputRedWeaponElementDTO> pool = fact.getRedWeapons();
|
||||||
|
if (pool == null) {
|
||||||
|
pool = new ArrayList<>();
|
||||||
|
fact.setRedWeapons(pool);
|
||||||
|
}
|
||||||
|
if (out.getRedWeapons() == null) {
|
||||||
|
out.setRedWeapons(convertPoolToEquipmentVoList(pool));
|
||||||
|
}
|
||||||
|
if (out.getTasks() == null || out.getTasks().size() != out.getRedWeapons().size()) {
|
||||||
|
out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons()));
|
||||||
|
}
|
||||||
|
|
||||||
|
double threshold = readDouble(p, "redHitRateThreshold", 0.6d);
|
||||||
|
int maxExtra = readInt(p, "maxExtraWeaponsPerTask", 2);
|
||||||
|
int maxRounds = readInt(p, "maxSupplementRounds", 5);
|
||||||
|
int extraMinScore = readInt(p, "extraPickMinScore", 1);
|
||||||
|
int weight = readInt(p, "weight", 1);
|
||||||
|
String blueBlob = buildBlueTextBlob(blue);
|
||||||
|
|
||||||
|
if (maxExtra > 0 && maxRounds > 0 && !pool.isEmpty()) {
|
||||||
|
Set<String> selectedIds = new HashSet<>();
|
||||||
|
for (FireRuleRedWeaponEquipmentVO w : out.getRedWeapons()) {
|
||||||
|
if (w != null && !isBlank(w.getEquipmentId())) {
|
||||||
|
selectedIds.add(w.getEquipmentId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int extraCount = 0;
|
||||||
|
int rounds = 0;
|
||||||
|
while (rounds < maxRounds && extraCount < maxExtra && !pool.isEmpty()) {
|
||||||
|
rounds++;
|
||||||
|
boolean needExtra = false;
|
||||||
|
for (FireRuleRedWeaponEquipmentVO w : out.getRedWeapons()) {
|
||||||
|
Double rad = w != null ? w.getSuccessTargetRad() : null;
|
||||||
|
if (rad == null || rad.doubleValue() < threshold) {
|
||||||
|
needExtra = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!needExtra) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestIdx = -1;
|
||||||
|
int bestScore = Integer.MIN_VALUE;
|
||||||
|
for (int i = 0; i < pool.size(); i++) {
|
||||||
|
FireRuleInputRedWeaponElementDTO red = pool.get(i);
|
||||||
|
if (red == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isBlank(red.getEquipmentId()) && selectedIds.contains(red.getEquipmentId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String redBlob = buildRedTextBlob(red);
|
||||||
|
int s = scoreRuleSlots(blueBlob, redBlob, p, weight)
|
||||||
|
+ scoreLegacyLayer(blueBlob, redBlob, p, weight);
|
||||||
|
if (s > bestScore) {
|
||||||
|
bestScore = s;
|
||||||
|
bestIdx = i;
|
||||||
|
} else if (s == bestScore && bestIdx >= 0) {
|
||||||
|
if (compareRedForTieBreak(pool.get(bestIdx), red, p) > 0) {
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestIdx < 0 || bestScore < extraMinScore) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
FireRuleInputRedWeaponElementDTO extra = pool.remove(bestIdx);
|
||||||
|
FireRuleRedWeaponEquipmentVO vo = toRedEquipmentVo(extra);
|
||||||
|
out.getRedWeapons().add(vo);
|
||||||
|
if (!isBlank(vo.getEquipmentId())) {
|
||||||
|
selectedIds.add(vo.getEquipmentId());
|
||||||
|
}
|
||||||
|
extraCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons()));
|
||||||
|
FireRuleRedWeaponOutputFillHelper.assignTargetsToTasks(out.getTasks(), out.getRedWeapons(), blue, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阵位规则:根据蓝方武器 coordinate + trackPoints + warZone 计算红方平台 positions。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public static void position(DroolsFact fact, Map globalParams) {
|
||||||
|
if (fact == null || fact.getTask() == null || fact.getFireRuleOutputVO() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> p = castParams(globalParams);
|
||||||
|
boolean enabled = Boolean.parseBoolean(String.valueOf(p.getOrDefault("positionRuleEnabled", true)));
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FireRuleOutputVO out = fact.getFireRuleOutputVO();
|
||||||
|
if (out.getRedWeapons() == null || out.getRedWeapons().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FireRuleRedWeaponOutputFillHelper.fillPlatformPositions(out.getRedWeapons(), fact.getTask(), p);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static Map<String, Object> castParams(Map raw) {
|
private static Map<String, Object> castParams(Map raw) {
|
||||||
return raw == null ? new java.util.HashMap<>() : (Map<String, Object>) raw;
|
return raw == null ? new java.util.HashMap<>() : (Map<String, Object>) raw;
|
||||||
@@ -277,10 +392,29 @@ public final class RuleFunction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double readDouble(Map<String, Object> p, String key, double def) {
|
||||||
|
Object v = p.get(key);
|
||||||
|
if (v == null) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
if (v instanceof Number) {
|
||||||
|
return ((Number) v).doubleValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(String.valueOf(v).trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String nz(String s) {
|
private static String nz(String s) {
|
||||||
return s == null ? "" : s;
|
return s == null ? "" : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String s) {
|
||||||
|
return s == null || s.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private static FireRuleTaskWeaponVO toTaskWeaponVo(FireRuleInputRedWeaponElementDTO r) {
|
private static FireRuleTaskWeaponVO toTaskWeaponVo(FireRuleInputRedWeaponElementDTO r) {
|
||||||
FireRuleTaskWeaponVO w = new FireRuleTaskWeaponVO();
|
FireRuleTaskWeaponVO w = new FireRuleTaskWeaponVO();
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
@@ -315,6 +449,7 @@ public final class RuleFunction {
|
|||||||
vo.setIsReconTarget(src.getIsReconTarget());
|
vo.setIsReconTarget(src.getIsReconTarget());
|
||||||
vo.setIsInterferenceTarget(src.getIsInterferenceTarget());
|
vo.setIsInterferenceTarget(src.getIsInterferenceTarget());
|
||||||
vo.setIsDefendImportantPlace(src.getIsDefendImportantPlace());
|
vo.setIsDefendImportantPlace(src.getIsDefendImportantPlace());
|
||||||
|
vo.setSuccessTargetRad(src.getSuccessTargetRad());
|
||||||
vo.setGroupType(src.getGroupType());
|
vo.setGroupType(src.getGroupType());
|
||||||
vo.setEquipmentId(src.getEquipmentId());
|
vo.setEquipmentId(src.getEquipmentId());
|
||||||
vo.setName(src.getName());
|
vo.setName(src.getName());
|
||||||
@@ -323,4 +458,5 @@ public final class RuleFunction {
|
|||||||
vo.setSubComponents(FireRuleRedWeaponOutputFillHelper.toOutputSubComponents(src.getSubComponents()));
|
vo.setSubComponents(FireRuleRedWeaponOutputFillHelper.toOutputSubComponents(src.getSubComponents()));
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.solution.rule.domain.ultimately.fact.DroolsFact;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static com.solution.rule.utils.RuleFunction.equipmentRule;
|
import static com.solution.rule.utils.RuleFunction.equipmentRule;
|
||||||
|
import static com.solution.rule.utils.RuleFunction.target;
|
||||||
|
import static com.solution.rule.utils.RuleFunction.position;
|
||||||
|
|
||||||
|
|
||||||
global java.util.Map globalParams;
|
global java.util.Map globalParams;
|
||||||
@@ -74,6 +76,77 @@ function Map buildParam(){
|
|||||||
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
|
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
|
||||||
param.put("missileScore", 1);
|
param.put("missileScore", 1);
|
||||||
|
|
||||||
|
// ===================== 目标分配参数(写入 Tasks.task.execute) =====================
|
||||||
|
// executeTypeDefault:生成 execute[0] 的类型字段
|
||||||
|
param.put("executeTypeDefault", "assault");
|
||||||
|
// targetPickMode:roundRobin(稳定轮询) / random(伪随机但同输入稳定)
|
||||||
|
param.put("targetPickMode", "roundRobin");
|
||||||
|
// minTargetsPerRed / maxTargetsPerRedCap:每个红方任务最少/最多分配的目标数
|
||||||
|
param.put("minTargetsPerRed", 1);
|
||||||
|
param.put("maxTargetsPerRedCap", 3);
|
||||||
|
// radToTargetsCsv:successTargetRad(命中率) -> 每红装目标数 的映射(阈值:目标数),按阈值从大到小匹配
|
||||||
|
// 例:0.8:1,0.5:2,0.2:3 表示 successTargetRad>=0.8 分1个;>=0.5 分2个;>=0.2 分3个
|
||||||
|
param.put("radToTargetsCsv", "0.8:1,0.5:2,0.2:3");
|
||||||
|
// rangeParseRegex:从 attDefaultValue/attExplain 中提取射程数值的正则(取第1个数字)
|
||||||
|
param.put("rangeParseRegex", "(\\\\d+(?:\\\\.\\\\d+)?)");
|
||||||
|
// rangeUnit:提取数值的单位,km/m(二选一)
|
||||||
|
param.put("rangeUnit", "km");
|
||||||
|
// minRangeToAllowAssignKm:若解析到的蓝方射程小于该值,则该蓝方装备不参与被分配(无法解析则忽略此过滤)
|
||||||
|
param.put("minRangeToAllowAssignKm", 0);
|
||||||
|
|
||||||
|
// ===================== 低命中率补拿装备参数 =====================
|
||||||
|
// redHitRateThreshold:红方装备命中率阈值(低于该值时触发补拿)
|
||||||
|
param.put("redHitRateThreshold", 0.6);
|
||||||
|
// maxExtraWeaponsPerTask:每条蓝方任务最多补拿几件红装
|
||||||
|
param.put("maxExtraWeaponsPerTask", 2);
|
||||||
|
// maxSupplementRounds:补拿循环最大轮次(防死循环)
|
||||||
|
param.put("maxSupplementRounds", 2);
|
||||||
|
// extraPickMinScore:补拿时红装最低匹配分
|
||||||
|
param.put("extraPickMinScore", 1);
|
||||||
|
|
||||||
|
// ===================== 阵位规则参数(写入 SubComponents.platform[].positions) =====================
|
||||||
|
// positionRuleEnabled:是否启用阵位规则。true=执行阵位生成;false=跳过,不改 platform.positions。
|
||||||
|
param.put("positionRuleEnabled", true);
|
||||||
|
// positionAnchorMode:锚点模式。当前实现使用 hybrid(蓝方 taskWeapons.coordinate 的中心点作为主锚点)。
|
||||||
|
param.put("positionAnchorMode", "hybrid");
|
||||||
|
// trackPointDirectionMode:航向计算模式。
|
||||||
|
// - head2next:取 trackPoints[0] -> trackPoints[1] 作为方向(默认)
|
||||||
|
// - tail2prev:取倒数第二个 -> 最后一个点作为方向
|
||||||
|
param.put("trackPointDirectionMode", "head2next");
|
||||||
|
// fallbackBearingDeg:当 trackPoints 缺失或无法计算方位时,使用该默认方位角(度,0-360)。
|
||||||
|
param.put("fallbackBearingDeg", 0);
|
||||||
|
// deployDistanceKmMin:部署距离下限(km)。最终距离不会小于该值。
|
||||||
|
param.put("deployDistanceKmMin", 8);
|
||||||
|
// deployDistanceKmMax:部署距离上限(km)。最终距离不会大于该值。
|
||||||
|
param.put("deployDistanceKmMax", 30);
|
||||||
|
// deployDistanceKmDefault:默认部署距离(km)。
|
||||||
|
// 当 distanceByPlatformCsv 未命中任何关键词时,使用该值。
|
||||||
|
param.put("deployDistanceKmDefault", 15);
|
||||||
|
// distanceByPlatformCsv:按“关键词”覆盖部署距离(km),不写死具体类型,完全由业务配置。
|
||||||
|
// 格式:关键词:距离,关键词:距离(示例:防空:18,反坦克:10,迫击炮:8)
|
||||||
|
// 匹配范围:红方装备 Name / Platform_type 文本包含关键词即命中。
|
||||||
|
// 优先级:命中后覆盖 deployDistanceKmDefault;但最终仍受 deployDistanceKmMin 与 deployDistanceKmMax 约束。
|
||||||
|
param.put("distanceByPlatformCsv", "");
|
||||||
|
// formationType:编队样式,可选 line / wedge / circle。
|
||||||
|
param.put("formationType", "line");
|
||||||
|
// formationSpacingMeters:编队间距(米),影响同一红装下 platform[] 点位离散程度。
|
||||||
|
// 说明:Java 侧会与 minInterPlatformDistanceMeters 比较,取更大值,避免平台重叠过近。
|
||||||
|
param.put("formationSpacingMeters", 300);
|
||||||
|
// formationHeadingOffsetDeg:编队相对主航向的偏转角(度),主要用于 wedge/circle 的分散方向。
|
||||||
|
param.put("formationHeadingOffsetDeg", 15);
|
||||||
|
// defaultDeployHeight:默认部署高度(米),用于 positions 第3位高度值基线。
|
||||||
|
param.put("defaultDeployHeight", 30);
|
||||||
|
// heightFollowBlueRatio:高度跟随蓝方比例(>=0)。
|
||||||
|
// 计算方式:高度 = defaultDeployHeight + 蓝方锚点平均高度 * heightFollowBlueRatio。
|
||||||
|
// 0 表示不跟随蓝方高度,仅使用默认高度。
|
||||||
|
param.put("heightFollowBlueRatio", 0.0);
|
||||||
|
// enableWarZoneClamp:是否启用作战区约束。true=超出 warZoneLocation 时回拉到区内。
|
||||||
|
param.put("enableWarZoneClamp", true);
|
||||||
|
// warZoneClampMode:作战区约束模式。当前实现使用 nearestInside(沿锚点到目标点方向二分回拉到区内)。
|
||||||
|
param.put("warZoneClampMode", "nearestInside");
|
||||||
|
// minInterPlatformDistanceMeters:平台最小间距(米)下限,用于抑制平台点位过度重叠。
|
||||||
|
param.put("minInterPlatformDistanceMeters", 80);
|
||||||
|
|
||||||
return param;
|
return param;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,3 +159,23 @@ then
|
|||||||
globalParams.putAll(buildParam());
|
globalParams.putAll(buildParam());
|
||||||
equipmentRule($fact, globalParams);
|
equipmentRule($fact, globalParams);
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule "目标匹配"
|
||||||
|
salience 90
|
||||||
|
when
|
||||||
|
$fact : DroolsFact(task != null)
|
||||||
|
then
|
||||||
|
// 显式目标分配规则:填充 Tasks.task.execute.targetList[*].targetId
|
||||||
|
globalParams.putAll(buildParam());
|
||||||
|
target($fact, globalParams);
|
||||||
|
end
|
||||||
|
|
||||||
|
rule "阵位匹配"
|
||||||
|
salience 80
|
||||||
|
when
|
||||||
|
$fact : DroolsFact(task != null)
|
||||||
|
then
|
||||||
|
// 显式阵位规则:填充 redWeapons.SubComponents.platform[].positions
|
||||||
|
globalParams.putAll(buildParam());
|
||||||
|
position($fact, globalParams);
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user