火力规则:装备匹配规则实现,目标规则实现,阵位规则简单实现(需要细化)
This commit is contained in:
@@ -30,6 +30,8 @@ public class FireRuleInputRedWeaponElementDTO {
|
||||
private Boolean isInterferenceTarget;
|
||||
|
||||
private Boolean isDefendImportantPlace;
|
||||
//命中率
|
||||
private Double successTargetRad;
|
||||
|
||||
private String groupType;
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ public class FireRuleTaskInputDTO {
|
||||
*/
|
||||
private String side;
|
||||
|
||||
/**
|
||||
* 蓝方任务装备命中率
|
||||
*/
|
||||
private Double successTargetRad;
|
||||
|
||||
/**
|
||||
* 航迹所属实体或阵营标识
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,8 @@ public class FireRuleRedWeaponEquipmentVO {
|
||||
private Boolean isInterferenceTarget;
|
||||
|
||||
private Boolean isDefendImportantPlace;
|
||||
//命中率
|
||||
private Double successTargetRad;
|
||||
|
||||
private String groupType;
|
||||
|
||||
|
||||
@@ -3,19 +3,32 @@ package com.solution.rule.utils;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.FireRuleExecuteBlockVO;
|
||||
import com.solution.rule.domain.ultimately.vo.FireRuleExecuteTargetItemVO;
|
||||
import com.solution.rule.domain.ultimately.vo.FireRuleMissionListItemVO;
|
||||
import com.solution.rule.domain.ultimately.vo.FireRuleMountedWeaponRefVO;
|
||||
import com.solution.rule.domain.ultimately.vo.FireRuleRedSubComponentsVO;
|
||||
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.FireRuleSceneTaskNodeVO;
|
||||
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskPayloadVO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 将入参中的红方装备 {@code SubComponents}(DTO)转为输出 VO,保持与原始 JSON 结构一致。
|
||||
@@ -95,6 +108,325 @@ public final class FireRuleRedWeaponOutputFillHelper {
|
||||
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) {
|
||||
if (redWeapon == null || redWeapon.getSubComponents() == null || redWeapon.getSubComponents().getWeapon() == null) {
|
||||
return null;
|
||||
@@ -149,4 +481,210 @@ public final class FireRuleRedWeaponOutputFillHelper {
|
||||
private static boolean isBlank(String s) {
|
||||
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 java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Drools 规则调用的装备匹配逻辑。所有业务词均来自 globalParams(由 rule.drl 的 buildParam 注入),本类不写死中文业务词。
|
||||
@@ -103,13 +105,126 @@ public final class RuleFunction {
|
||||
return;
|
||||
}
|
||||
|
||||
FireRuleInputRedWeaponElementDTO chosen = pool.remove(bestIndex);
|
||||
pool.remove(bestIndex);
|
||||
|
||||
out.setRedWeapons(convertPoolToEquipmentVoList(pool));
|
||||
// Tasks 由最终输出 redWeapons 一一生成(一个装备 -> 一个任务)
|
||||
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")
|
||||
private static Map<String, Object> castParams(Map 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) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static FireRuleTaskWeaponVO toTaskWeaponVo(FireRuleInputRedWeaponElementDTO r) {
|
||||
FireRuleTaskWeaponVO w = new FireRuleTaskWeaponVO();
|
||||
if (r != null) {
|
||||
@@ -315,6 +449,7 @@ public final class RuleFunction {
|
||||
vo.setIsReconTarget(src.getIsReconTarget());
|
||||
vo.setIsInterferenceTarget(src.getIsInterferenceTarget());
|
||||
vo.setIsDefendImportantPlace(src.getIsDefendImportantPlace());
|
||||
vo.setSuccessTargetRad(src.getSuccessTargetRad());
|
||||
vo.setGroupType(src.getGroupType());
|
||||
vo.setEquipmentId(src.getEquipmentId());
|
||||
vo.setName(src.getName());
|
||||
@@ -323,4 +458,5 @@ public final class RuleFunction {
|
||||
vo.setSubComponents(FireRuleRedWeaponOutputFillHelper.toOutputSubComponents(src.getSubComponents()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.solution.rule.domain.ultimately.fact.DroolsFact;
|
||||
import java.util.Map;
|
||||
|
||||
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;
|
||||
@@ -74,6 +76,77 @@ function Map buildParam(){
|
||||
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -86,3 +159,23 @@ then
|
||||
globalParams.putAll(buildParam());
|
||||
equipmentRule($fact, globalParams);
|
||||
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