火力规则:装备匹配规则实现,目标规则实现,阵位规则、航迹规则【初版】

This commit is contained in:
MHW
2026-04-09 10:22:53 +08:00
parent 2fafd931cc
commit 6add28fdfb
11 changed files with 967 additions and 32 deletions

View File

@@ -45,6 +45,8 @@ public class FireRuleExecuteTargetItemVO {
private String cruiseRouteId;
private String moveRouteId;
private List<FireRuleCruiseRouteOffsetItemVO> cruiseRouteOffset;
private String fireType;

View File

@@ -1,42 +1,45 @@
package com.solution.rule.domain.ultimately.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 火力规则输出根文档(与 {@code 火力规则输出.json} 字段一一对应)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FireRuleOutputVO {
/**
* 来源场景文件名
*/
private String sourceFile;
/**
* 火力规则任务列表(可含防区等输出字段)
*/
private List<FireRuleTaskInputVO> fireRuleInputs;
/**
* 场景任务节点列表
*/
@JsonProperty("Tasks")
private List<FireRuleSceneTaskNodeVO> tasks;
@JsonProperty("TrackParam")
private FireRuleTrackParamVO trackParam;
/**
* 编组列表
*/
@JsonProperty("Groups")
private List<FireRuleSceneGroupNodeVO> groups;
@JsonIgnore
public List<FireRuleSceneGroupNodeVO> getGroups() {
return trackParam == null ? null : trackParam.getGroups();
}
/**
* 红方装备列表
*/
private List<FireRuleRedWeaponEquipmentVO> redWeapons;
@JsonIgnore
public void setGroups(List<FireRuleSceneGroupNodeVO> groups) {
if (trackParam == null) {
trackParam = new FireRuleTrackParamVO();
}
trackParam.setGroups(groups);
}
@JsonIgnore
public List<FireRuleRedWeaponEquipmentVO> getRedWeapons() {
return trackParam == null ? null : trackParam.getRedWeapons();
}
@JsonIgnore
public void setRedWeapons(List<FireRuleRedWeaponEquipmentVO> redWeapons) {
if (trackParam == null) {
trackParam = new FireRuleTrackParamVO();
}
trackParam.setRedWeapons(redWeapons);
}
}

View File

@@ -6,9 +6,6 @@ import lombok.Data;
import java.util.Map;
/**
* 火力规则输出中 redWeapons 数组的单项(红方装备/平台)
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FireRuleRedWeaponEquipmentVO {
@@ -23,15 +20,10 @@ public class FireRuleRedWeaponEquipmentVO {
private String platformType;
private Boolean isStrikeTarget;
private Boolean isReconTarget;
private Boolean isInterferenceTarget;
private Boolean isDefendImportantPlace;
//命中率
private Double successTargetRad;
private String groupType;
@JsonProperty("EquipmentID")

View File

@@ -0,0 +1,17 @@
package com.solution.rule.domain.ultimately.vo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FireRuleRouteTrackPointVO {
private String index;
private String longitude;
private String latitude;
private String height;
private String speed;
private String psia;
private Integer time;
private String active;
}

View File

@@ -0,0 +1,41 @@
package com.solution.rule.domain.ultimately.vo;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FireRuleTrackParamVO {
private static final ObjectMapper MAPPER = new ObjectMapper();
private Map<String, FireRuleTrackRouteVO> routeMap = new LinkedHashMap<>();
@JsonProperty("Groups")
private List<FireRuleSceneGroupNodeVO> groups;
@JsonProperty("redWeapons")
private List<FireRuleRedWeaponEquipmentVO> redWeapons;
@JsonAnySetter
public void putRoute(String key, JsonNode value) {
if ("Groups".equals(key) || "redWeapons".equals(key) || value == null) {
return;
}
routeMap.put(key, MAPPER.convertValue(value, FireRuleTrackRouteVO.class));
}
@JsonAnyGetter
public Map<String, FireRuleTrackRouteVO> getRouteMap() {
return routeMap;
}
}

View File

@@ -0,0 +1,28 @@
package com.solution.rule.domain.ultimately.vo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FireRuleTrackRouteVO {
private String name;
@JsonProperty("StartTime")
private Integer startTime;
@JsonProperty("EndTime")
private Integer endTime;
@JsonProperty("TrackType")
private String trackType;
@JsonProperty("HeightType")
private String heightType;
private String seaType;
@JsonProperty("TrackPoints")
private List<FireRuleRouteTrackPointVO> trackPoints;
@JsonProperty("Color")
private String color;
@JsonProperty("PointCount")
private Integer pointCount;
}

View File

@@ -18,8 +18,12 @@ 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.FireRuleOutputVO;
import com.solution.rule.domain.ultimately.vo.FireRuleRouteTrackPointVO;
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskNodeVO;
import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskPayloadVO;
import com.solution.rule.domain.ultimately.vo.FireRuleTrackParamVO;
import com.solution.rule.domain.ultimately.vo.FireRuleTrackRouteVO;
import java.util.ArrayList;
import java.util.Collections;
@@ -170,6 +174,399 @@ public final class FireRuleRedWeaponOutputFillHelper {
}
}
/**
* 航迹规则:按蓝方 trackPoints 生成 {@link FireRuleTrackParamVO#routeMap} 条目JSON 顶层 key=航迹 id
* 并将同一 id 写入对应红方任务 {@code execute[0].targetList[*].moveRouteId}。多蓝方任务循环时 merge不覆盖已有 id。
*/
@SuppressWarnings("rawtypes")
public static void fillTrackParamAndBindMoveRoute(FireRuleOutputVO out, FireRuleTaskInputDTO blueTask, Map params) {
if (out == null || blueTask == null || out.getTasks() == null || out.getTasks().isEmpty()) {
return;
}
ensureTrackParam(out);
List<FireRuleSceneTaskNodeVO> tasks = out.getTasks();
List<FireRuleRedWeaponEquipmentVO> reds = out.getRedWeapons();
if (reds == null) {
reds = Collections.emptyList();
}
List<Coord> warPoly = toWarZonePolygon(blueTask.getWarZoneLocation());
Coord blueAnchor = computeBlueAnchor(blueTask);
Coord clampAnchor = resolveClampAnchor(blueAnchor, warPoly);
boolean zoneClamp = Boolean.parseBoolean(String.valueOf(params.getOrDefault("enableTrackWarZoneClamp", true)));
String dirMode = str(params, "trackPointDirectionMode", "head2next");
double fallbackBrg = readDouble(params, "trackFallbackBearingDeg", readDouble(params, "fallbackBearingDeg", 0d));
double mainBearing = computeTrackBearingDeg(blueTask, dirMode, fallbackBrg);
String algo = str(params, "trackRouteAlgorithm", "followBlue");
String nameSuffix = str(params, "trackRouteNameSuffix", "航迹");
String groundType = str(params, "trackGroundTrackType", "routeLineGround");
String blueIdSeg = sanitizeRouteIdSegment(nz(blueTask.getId()));
for (int i = 0; i < tasks.size(); i++) {
FireRuleSceneTaskNodeVO node = tasks.get(i);
if (node == null) {
continue;
}
FireRuleRedWeaponEquipmentVO red = i < reds.size() ? reds.get(i) : null;
List<TrackNode> nodes = extractSortedBlueTrackNodes(blueTask);
if (nodes.isEmpty()) {
continue;
}
List<TrackNode> transformed = applyTrackRouteAlgorithm(nodes, algo, params, mainBearing, i);
int extraMax = readInt(params, "trackExtraNodesMax", 0);
if (extraMax > 0) {
transformed = injectExtraTrackNodes(transformed, extraMax);
}
if (zoneClamp && warPoly.size() >= 3) {
transformed = clampTrackNodesToWarZone(transformed, warPoly, clampAnchor);
}
boolean air = resolveAirTrack(blueTask, red, params);
String routeName = nz(node.getDrawName()) + nameSuffix;
String routeId = buildUniqueRouteId(out.getTrackParam().getRouteMap(), blueIdSeg, sanitizeRouteIdSegment(nz(node.getId())), i);
FireRuleTrackRouteVO route = buildTrackRouteVo(transformed, routeName, air, groundType);
out.getTrackParam().getRouteMap().put(routeId, route);
bindMoveRouteIdToFirstExecute(node, routeId);
}
}
private static void ensureTrackParam(FireRuleOutputVO out) {
if (out.getTrackParam() == null) {
out.setTrackParam(new FireRuleTrackParamVO());
}
}
private static String buildUniqueRouteId(Map<String, FireRuleTrackRouteVO> existing, String blueSeg, String nodeSeg, int index) {
String base = "route_" + blueSeg + "_" + nodeSeg + "_" + index;
if (existing == null || !existing.containsKey(base)) {
return base;
}
int k = 1;
while (existing.containsKey(base + "_" + k)) {
k++;
}
return base + "_" + k;
}
private static void bindMoveRouteIdToFirstExecute(FireRuleSceneTaskNodeVO node, String routeId) {
if (node == null || isBlank(routeId)) {
return;
}
FireRuleSceneTaskPayloadVO payload = node.getTask();
if (payload == null || payload.getExecute() == null || payload.getExecute().isEmpty()) {
return;
}
FireRuleExecuteBlockVO block = payload.getExecute().get(0);
if (block == null || block.getTargetList() == null) {
return;
}
for (FireRuleExecuteTargetItemVO item : block.getTargetList()) {
if (item != null) {
item.setMoveRouteId(routeId);
}
}
}
private static FireRuleTrackRouteVO buildTrackRouteVo(List<TrackNode> nodes, String name, boolean air, String groundType) {
FireRuleTrackRouteVO vo = new FireRuleTrackRouteVO();
vo.setName(name);
vo.setStartTime(null);
vo.setEndTime(null);
vo.setTrackType(air ? "routeLineAir" : groundType);
vo.setHeightType("msl");
vo.setSeaType("seaLevel");
List<FireRuleRouteTrackPointVO> pts = new ArrayList<>(nodes.size());
for (int j = 0; j < nodes.size(); j++) {
TrackNode tn = nodes.get(j);
FireRuleRouteTrackPointVO p = new FireRuleRouteTrackPointVO();
p.setIndex(String.valueOf(j + 1));
p.setLongitude(formatCoordNumber(tn.coord.lon));
p.setLatitude(formatCoordNumber(tn.coord.lat));
p.setHeight(tn.coord.height != null ? formatCoordNumber(tn.coord.height) : "0");
p.setSpeed(tn.speed != null ? formatCoordNumber(tn.speed) : null);
pts.add(p);
}
vo.setTrackPoints(pts);
vo.setPointCount(pts.size());
return vo;
}
private static String formatCoordNumber(double v) {
return String.valueOf(v);
}
private static boolean resolveAirTrack(FireRuleTaskInputDTO blue, FireRuleRedWeaponEquipmentVO red, Map params) {
String dt = blue != null ? nz(blue.getDataType()) : "";
if (csvContainsAny(str(params, "trackAirDataTypeCsv", ""), dt, false)) {
return true;
}
String blob = nz(blue != null ? blue.getDrawName() : "");
if (red != null) {
blob += " " + nz(red.getName()) + " " + nz(red.getPlatformType());
}
return csvContainsAny(str(params, "trackAirKeywordsCsv", ""), blob, false);
}
private static boolean csvContainsAny(String csv, String text, boolean tokenAsEquals) {
if (isBlank(csv) || text == null) {
return false;
}
String t = text.toLowerCase();
for (String part : csv.split(",")) {
String k = part.trim();
if (k.isEmpty()) {
continue;
}
if (tokenAsEquals) {
if (t.equalsIgnoreCase(k)) {
return true;
}
} else if (text.contains(k) || t.contains(k.toLowerCase())) {
return true;
}
}
return false;
}
private static String sanitizeRouteIdSegment(String raw) {
if (raw == null || raw.isEmpty()) {
return "x";
}
String s = raw.replaceAll("[^a-zA-Z0-9_-]", "_");
return s.isEmpty() ? "x" : s;
}
private static class TrackNode {
Coord coord;
Double speed;
TrackNode(Coord coord, Double speed) {
this.coord = coord;
this.speed = speed;
}
}
private static List<TrackNode> extractSortedBlueTrackNodes(FireRuleTaskInputDTO blueTask) {
List<TrackNode> list = new ArrayList<>();
if (blueTask == null || blueTask.getTrackPoints() == null) {
return list;
}
List<FireRuleTrackPointDTO> pts = new ArrayList<>(blueTask.getTrackPoints());
pts.sort((a, b) -> {
int ia = a != null && a.getIndex() != null ? a.getIndex() : 0;
int ib = b != null && b.getIndex() != null ? b.getIndex() : 0;
return Integer.compare(ia, ib);
});
for (FireRuleTrackPointDTO p : pts) {
if (p == null || p.getLongitude() == null || p.getLatitude() == null) {
continue;
}
list.add(new TrackNode(new Coord(p.getLongitude(), p.getLatitude(), p.getHeight()), p.getSpeed()));
}
return list;
}
private static List<TrackNode> applyTrackRouteAlgorithm(List<TrackNode> nodes, String algo, Map params, double mainBearing, int taskIndex) {
if (nodes == null || nodes.isEmpty()) {
return nodes;
}
String a = algo == null ? "followBlue" : algo.trim();
if ("shortestPath".equalsIgnoreCase(a)) {
return applyShortestPath(nodes, params);
}
if ("flank".equalsIgnoreCase(a)) {
return applyFlank(nodes, params, taskIndex);
}
if ("jam".equalsIgnoreCase(a)) {
return applyJam(nodes, params, mainBearing);
}
return copyTrackNodes(nodes);
}
private static List<TrackNode> copyTrackNodes(List<TrackNode> nodes) {
List<TrackNode> out = new ArrayList<>(nodes.size());
for (TrackNode n : nodes) {
out.add(new TrackNode(new Coord(n.coord.lon, n.coord.lat, n.coord.height), n.speed));
}
return out;
}
private static List<TrackNode> applyShortestPath(List<TrackNode> nodes, Map params) {
int segN = readInt(params, "trackShortPathSegments", 3);
if (segN < 1) {
segN = 1;
}
List<TrackNode> out = new ArrayList<>();
for (int i = 0; i < nodes.size(); i++) {
if (i == 0) {
out.add(copyNode(nodes.get(i)));
continue;
}
Coord a = nodes.get(i - 1).coord;
Coord b = nodes.get(i).coord;
Double spPrev = nodes.get(i - 1).speed;
Double spNext = nodes.get(i).speed;
for (int s = 1; s < segN; s++) {
double t = (double) s / (double) segN;
double lon = a.lon + (b.lon - a.lon) * t;
double lat = a.lat + (b.lat - a.lat) * t;
Double h = null;
if (a.height != null && b.height != null) {
h = a.height + (b.height - a.height) * t;
} else if (b.height != null) {
h = b.height;
} else {
h = a.height;
}
Double spd = null;
if (spPrev != null && spNext != null) {
spd = spPrev + (spNext - spPrev) * t;
} else if (spNext != null) {
spd = spNext;
} else {
spd = spPrev;
}
out.add(new TrackNode(new Coord(lon, lat, h), spd));
}
out.add(copyNode(nodes.get(i)));
}
return out;
}
private static TrackNode copyNode(TrackNode n) {
return new TrackNode(new Coord(n.coord.lon, n.coord.lat, n.coord.height), n.speed);
}
private static List<TrackNode> applyFlank(List<TrackNode> nodes, Map params, int taskIndex) {
double offsetM = readDouble(params, "trackFlankOffsetMeters", 800d);
String sideMode = str(params, "trackFlankSideMode", "alternate");
List<TrackNode> out = new ArrayList<>(nodes.size());
for (int i = 0; i < nodes.size(); i++) {
Coord c = nodes.get(i).coord;
double brg;
if (i < nodes.size() - 1) {
Coord nxt = nodes.get(i + 1).coord;
brg = bearingDeg(c.lon, c.lat, nxt.lon, nxt.lat);
} else if (i > 0) {
Coord prev = nodes.get(i - 1).coord;
brg = bearingDeg(prev.lon, prev.lat, c.lon, c.lat);
} else {
brg = 0d;
}
int sign = flankSign(sideMode, i, taskIndex);
Coord moved = moveByMeters(c, sign * offsetM, brg + 90d);
out.add(new TrackNode(moved, nodes.get(i).speed));
}
return out;
}
private static int flankSign(String mode, int pointIndex, int taskIndex) {
if ("left".equalsIgnoreCase(mode)) {
return 1;
}
if ("right".equalsIgnoreCase(mode)) {
return -1;
}
return (pointIndex + taskIndex) % 2 == 0 ? 1 : -1;
}
private static List<TrackNode> applyJam(List<TrackNode> nodes, Map params, double mainBearing) {
double wobble = readDouble(params, "trackJamWobbleMeters", 400d);
double periods = readDouble(params, "trackJamSegments", 4d);
if (periods < 0.5d) {
periods = 0.5d;
}
int n = nodes.size();
List<TrackNode> out = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
double frac = n <= 1 ? 0d : (double) i / (double) (n - 1);
double lateral = wobble * Math.sin(2d * Math.PI * periods * frac);
Coord c = moveByMeters(nodes.get(i).coord, lateral, mainBearing + 90d);
out.add(new TrackNode(c, nodes.get(i).speed));
}
return out;
}
private static List<TrackNode> injectExtraTrackNodes(List<TrackNode> nodes, int extraMax) {
if (nodes == null || nodes.size() < 2 || extraMax <= 0) {
return nodes;
}
int segs = nodes.size() - 1;
List<Integer> perSeg = new ArrayList<>(Collections.nCopies(segs, 0));
int base = extraMax / segs;
int rem = extraMax % segs;
for (int s = 0; s < segs; s++) {
perSeg.set(s, base + (s < rem ? 1 : 0));
}
List<TrackNode> out = new ArrayList<>();
for (int i = 0; i < nodes.size(); i++) {
out.add(copyNode(nodes.get(i)));
if (i >= nodes.size() - 1) {
break;
}
int ins = perSeg.get(i);
Coord a = nodes.get(i).coord;
Coord b = nodes.get(i + 1).coord;
Double sa = nodes.get(i).speed;
Double sb = nodes.get(i + 1).speed;
for (int k = 1; k <= ins; k++) {
double t = (double) k / (double) (ins + 1);
double lon = a.lon + (b.lon - a.lon) * t;
double lat = a.lat + (b.lat - a.lat) * t;
Double h = null;
if (a.height != null && b.height != null) {
h = a.height + (b.height - a.height) * t;
} else if (b.height != null) {
h = b.height;
} else {
h = a.height;
}
Double spd = null;
if (sa != null && sb != null) {
spd = sa + (sb - sa) * t;
} else if (sb != null) {
spd = sb;
} else {
spd = sa;
}
out.add(new TrackNode(new Coord(lon, lat, h), spd));
}
}
return out;
}
private static List<TrackNode> clampTrackNodesToWarZone(List<TrackNode> nodes, List<Coord> poly, Coord anchor) {
List<TrackNode> out = new ArrayList<>(nodes.size());
for (TrackNode n : nodes) {
Coord c = n.coord;
if (!isPointInPolygon(c, poly)) {
c = projectInsidePolygon(c, anchor, poly);
}
out.add(new TrackNode(c, n.speed));
}
return out;
}
private static Coord resolveClampAnchor(Coord blueAnchor, List<Coord> poly) {
if (poly.size() >= 3) {
if (blueAnchor != null && isPointInPolygon(blueAnchor, poly)) {
return blueAnchor;
}
return polygonCentroid(poly);
}
return blueAnchor != null ? blueAnchor : new Coord(0d, 0d, null);
}
private static Coord polygonCentroid(List<Coord> poly) {
double sx = 0, sy = 0;
for (Coord c : poly) {
sx += c.lon;
sy += c.lat;
}
int n = poly.size();
return new Coord(sx / n, sy / n, null);
}
/**
* 目标分配:为每个红方 Tasks 节点写入 task.execute[0].targetList[*].targetId。\n
* targetId 来源:当前蓝方任务 {@link FireRuleTaskInputDTO#getTaskWeapons()} 下每条武器的 equipmentId。\n

View File

@@ -225,6 +225,26 @@ public final class RuleFunction {
FireRuleRedWeaponOutputFillHelper.fillPlatformPositions(out.getRedWeapons(), fact.getTask(), p);
}
/**
* 航迹规则:根据蓝方 trackPoints 与作战区生成 TrackParam 航迹,并绑定 execute.targetList.moveRouteId。
*/
@SuppressWarnings("rawtypes")
public static void trackRoute(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("trackRuleEnabled", true)));
if (!enabled) {
return;
}
FireRuleOutputVO out = fact.getFireRuleOutputVO();
if (out.getTasks() == null || out.getTasks().isEmpty()) {
return;
}
FireRuleRedWeaponOutputFillHelper.fillTrackParamAndBindMoveRoute(out, fact.getTask(), p);
}
@SuppressWarnings("unchecked")
private static Map<String, Object> castParams(Map raw) {
return raw == null ? new java.util.HashMap<>() : (Map<String, Object>) raw;