火力规则:装备匹配规则实现,目标规则实现,阵位规则简单实现(需要细化)

This commit is contained in:
MHW
2026-04-07 17:04:13 +08:00
parent 72f4caf555
commit 2fafd931cc
6 changed files with 777 additions and 1 deletions

View File

@@ -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;

View File

@@ -32,6 +32,11 @@ public class FireRuleTaskInputDTO {
*/ */
private String side; private String side;
/**
* 蓝方任务装备命中率
*/
private Double successTargetRad;
/** /**
* 航迹所属实体或阵营标识 * 航迹所属实体或阵营标识
*/ */

View File

@@ -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;

View File

@@ -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;
}
} }

View File

@@ -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;
} }
} }

View File

@@ -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");
// targetPickModeroundRobin(稳定轮询) / random(伪随机但同输入稳定)
param.put("targetPickMode", "roundRobin");
// minTargetsPerRed / maxTargetsPerRedCap每个红方任务最少/最多分配的目标数
param.put("minTargetsPerRed", 1);
param.put("maxTargetsPerRedCap", 3);
// radToTargetsCsvsuccessTargetRad(命中率) -> 每红装目标数 的映射(阈值:目标数),按阈值从大到小匹配
// 例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