diff --git a/auto-solution-admin/src/main/java/com/solution/web/controller/behaviour/BehaviortreeController.java b/auto-solution-admin/src/main/java/com/solution/web/controller/behaviour/BehaviortreeController.java index be0323a..c664e37 100644 --- a/auto-solution-admin/src/main/java/com/solution/web/controller/behaviour/BehaviortreeController.java +++ b/auto-solution-admin/src/main/java/com/solution/web/controller/behaviour/BehaviortreeController.java @@ -97,6 +97,19 @@ public class BehaviortreeController extends BaseController return toAjax(behaviortreeProcessor.create(behaviortree)); } + /** + * 复制行为树 + */ + @ApiOperation("复制行为树") + @PreAuthorize("@ss.hasPermi('system:behaviortree:add')") + @Log(title = "行为树主", businessType = BusinessType.INSERT) + @PostMapping("/copy") + public AjaxResult copy(@RequestBody Behaviortree behaviortree) + { + //return toAjax(behaviortreeService.copy(behaviortree)); + return toAjax(behaviortreeProcessor.copy(behaviortree)); + } + /** * 修改行为树主 */ diff --git a/auto-solution-admin/src/main/java/com/solution/web/core/BehaviortreeProcessor.java b/auto-solution-admin/src/main/java/com/solution/web/core/BehaviortreeProcessor.java index 2c41d49..32185af 100644 --- a/auto-solution-admin/src/main/java/com/solution/web/core/BehaviortreeProcessor.java +++ b/auto-solution-admin/src/main/java/com/solution/web/core/BehaviortreeProcessor.java @@ -8,8 +8,10 @@ package com.solution.web.core; * that was distributed with this source code. */ +import cn.hutool.core.util.ObjectUtil; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.solution.common.constant.ExceptionConstants; import com.solution.system.domain.*; import com.solution.system.service.IBehaviortreeService; import com.solution.system.service.INodeconnectionService; @@ -20,6 +22,7 @@ import com.solution.web.core.graph.GraphEdge; import com.solution.web.core.graph.GraphNode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.ArrayList; @@ -56,6 +59,7 @@ public class BehaviortreeProcessor { return result; } + @Transactional(rollbackFor = Exception.class) public int update(Behaviortree behaviortree) { int result = behaviortreeService.updateBehaviortree(behaviortree); @@ -74,7 +78,7 @@ public class BehaviortreeProcessor { } private void processGraph(Behaviortree behaviortree) { - + //代码丢失 原:libertyspy 改:MHW Graph graph = null; try { graph = objectMapper.readValue(behaviortree.getXmlContent(), Graph.class); @@ -223,4 +227,23 @@ public class BehaviortreeProcessor { } + /** + * 复制行为树 + * @param behaviortree + * @return + */ + public int copy(Behaviortree behaviortree) { + if(ObjectUtil.isEmpty(behaviortree)){ + throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION); + } + String name = behaviortree.getName(); + String newName = name + "_" + behaviortree.getId(); + + String englishName = behaviortree.getEnglishName(); + String newEnglishName = englishName + "_" + behaviortree.getId(); + behaviortree.setEnglishName(newEnglishName); + behaviortree.setName(newName); + + return this.create(behaviortree); + } } diff --git a/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java b/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java index 18be66b..4927258 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java @@ -20,16 +20,21 @@ 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.FireRuleSceneGroupNodeVO; 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 com.solution.rule.domain.ultimately.vo.FireRuleWingmanDataVO; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -231,6 +236,241 @@ public final class FireRuleRedWeaponOutputFillHelper { } } + /** + * 编组规则:按红方装备列表分组,写入 {@link FireRuleTrackParamVO#getGroups}(JSON 键 Groups), + * groupType=addGroup,drawName=领队装备名+后缀;僚机 wingmanData 中 name 为红方 equipmentId。 + * 多蓝方任务循环时与航迹一致:在已有 Groups 上追加,且 group id 与已有项冲突时自动加后缀。 + */ + @SuppressWarnings("rawtypes") + public static void fillTrackParamGroups(FireRuleOutputVO out, FireRuleTaskInputDTO blueTask, Map params) { + if (out == null || blueTask == null) { + return; + } + boolean enabled = Boolean.parseBoolean(String.valueOf(params.getOrDefault("groupRuleEnabled", true))); + if (!enabled) { + return; + } + List reds = out.getRedWeapons(); + if (reds == null || reds.isEmpty()) { + return; + } + ensureTrackParam(out); + + String dirMode = str(params, "trackPointDirectionMode", "head2next"); + double fallbackBrg = readDouble(params, "trackFallbackBearingDeg", readDouble(params, "fallbackBearingDeg", 0d)); + double mainBearing = computeTrackBearingDeg(blueTask, dirMode, fallbackBrg); + + String mode = str(params, "groupFormationMode", "onePerRed"); + int clusterSize = readInt(params, "groupClusterSize", 3); + if (clusterSize < 1) { + clusterSize = 1; + } + int minWing = readInt(params, "groupMinMembersForWingman", 2); + + List> buckets = splitRedsIntoGroups(reds, mode, clusterSize); + List batch = new ArrayList<>(); + String blueSeg = sanitizeRouteIdSegment(nz(blueTask.getId())); + int gidx = 0; + for (List bucket : buckets) { + if (bucket == null || bucket.isEmpty()) { + continue; + } + FireRuleRedWeaponEquipmentVO leader = pickGroupLeader(bucket, params); + if (leader == null) { + continue; + } + FireRuleSceneGroupNodeVO node = new FireRuleSceneGroupNodeVO(); + String baseId = "group_" + blueSeg + "_" + gidx; + node.setId(ensureUniqueGroupId(out.getTrackParam(), baseId)); + String suffix = str(params, "groupDrawNameSuffix", "编组"); + boolean withIndex = Boolean.parseBoolean(String.valueOf(params.getOrDefault("groupDrawNameWithIndex", false))); + String drawName = nz(leader.getName()) + suffix; + if (withIndex) { + drawName += (gidx + 1); + } + node.setDrawName(drawName); + node.setGroupType("addGroup"); + node.setName("addGroup"); + node.setLeader(nz(leader.getEquipmentId())); + node.setSort(System.currentTimeMillis() + gidx); + node.setWingmanData(buildWingmanData(bucket, leader, mainBearing, params, minWing)); + batch.add(node); + gidx++; + } + mergeGroupsIntoTrackParam(out.getTrackParam(), batch); + } + + private static List> splitRedsIntoGroups( + List reds, String mode, int clusterSize) { + List clean = new ArrayList<>(); + for (FireRuleRedWeaponEquipmentVO r : reds) { + if (r != null) { + clean.add(r); + } + } + List> out = new ArrayList<>(); + if (clean.isEmpty()) { + return out; + } + if ("singleGroup".equalsIgnoreCase(mode)) { + out.add(clean); + return out; + } + if ("clusterByCount".equalsIgnoreCase(mode)) { + for (int i = 0; i < clean.size(); i += clusterSize) { + int end = Math.min(i + clusterSize, clean.size()); + out.add(new ArrayList<>(clean.subList(i, end))); + } + return out; + } + for (FireRuleRedWeaponEquipmentVO r : clean) { + out.add(Collections.singletonList(r)); + } + return out; + } + + private static FireRuleRedWeaponEquipmentVO pickGroupLeader(List bucket, Map params) { + if (bucket == null || bucket.isEmpty()) { + return null; + } + String mode = str(params, "groupLeaderPickMode", "byHitRateThenId"); + FireRuleRedWeaponEquipmentVO best = bucket.get(0); + for (int i = 1; i < bucket.size(); i++) { + FireRuleRedWeaponEquipmentVO cur = bucket.get(i); + if ("byId".equalsIgnoreCase(mode)) { + if (compareEquipmentId(cur, best) < 0) { + best = cur; + } + } else { + if (isBetterLeaderByHitRateThenId(cur, best)) { + best = cur; + } + } + } + return best; + } + + private static boolean isBetterLeaderByHitRateThenId(FireRuleRedWeaponEquipmentVO cand, FireRuleRedWeaponEquipmentVO inc) { + double rc = leaderRad(cand); + double ri = leaderRad(inc); + int c = Double.compare(rc, ri); + if (c > 0) { + return true; + } + if (c < 0) { + return false; + } + return compareEquipmentId(cand, inc) < 0; + } + + private static double leaderRad(FireRuleRedWeaponEquipmentVO w) { + if (w == null || w.getSuccessTargetRad() == null) { + return -1d; + } + return w.getSuccessTargetRad().doubleValue(); + } + + private static int compareEquipmentId(FireRuleRedWeaponEquipmentVO a, FireRuleRedWeaponEquipmentVO b) { + String sa = a != null ? nz(a.getEquipmentId()) : ""; + String sb = b != null ? nz(b.getEquipmentId()) : ""; + return sa.compareTo(sb); + } + + private static List buildWingmanData( + List bucket, + FireRuleRedWeaponEquipmentVO leader, + double mainBearing, + Map params, + int minMembersForWingman) { + if (bucket == null || leader == null || bucket.size() < minMembersForWingman) { + return Collections.emptyList(); + } + double distBase = readDouble(params, "wingmanDistanceBaseMeters", 100); + double distStep = readDouble(params, "wingmanDistanceStepMeters", 50); + double angBase = readDouble(params, "wingmanAngleBaseDeg", 50); + double angStep = readDouble(params, "wingmanAngleStepDeg", 15); + double altBase = readDouble(params, "wingmanAltBaseMeters", 40); + double altScale = readDouble(params, "wingmanAltScale", 1.0); + + List list = new ArrayList<>(); + int wmIdx = 0; + for (FireRuleRedWeaponEquipmentVO r : bucket) { + if (r == null || r == leader) { + continue; + } + FireRuleWingmanDataVO w = new FireRuleWingmanDataVO(); + w.setKey(wmIdx); + w.setName(nz(r.getEquipmentId())); + w.setDistance((int) Math.round(distBase + wmIdx * distStep)); + double deg = normDeg(mainBearing + angBase + wmIdx * angStep); + w.setAngle(String.format(Locale.US, "%.1f", deg)); + Double lh = extractFirstPlatformHeight(leader); + Double wh = extractFirstPlatformHeight(r); + double delta = 0d; + if (lh != null && wh != null) { + delta = wh - lh; + } + w.setAlt((int) Math.round(altBase + altScale * delta)); + list.add(w); + wmIdx++; + } + return list; + } + + private static Double extractFirstPlatformHeight(FireRuleRedWeaponEquipmentVO red) { + if (red == null || red.getSubComponents() == null || red.getSubComponents().getPlatform() == null) { + return null; + } + for (FireRuleRedPlatformSlotVO pl : red.getSubComponents().getPlatform()) { + if (pl == null || pl.getPositions() == null || pl.getPositions().size() < 3) { + continue; + } + return pl.getPositions().get(2); + } + return null; + } + + private static double normDeg(double deg) { + double a = deg % 360d; + if (a < 0) { + a += 360d; + } + return a; + } + + private static String ensureUniqueGroupId(FireRuleTrackParamVO tp, String baseId) { + Set seen = new HashSet<>(); + if (tp.getGroups() != null) { + for (FireRuleSceneGroupNodeVO g : tp.getGroups()) { + if (g != null && !isBlank(g.getId())) { + seen.add(g.getId()); + } + } + } + if (!seen.contains(baseId)) { + return baseId; + } + int k = 1; + while (seen.contains(baseId + "_" + k)) { + k++; + } + return baseId + "_" + k; + } + + private static void mergeGroupsIntoTrackParam(FireRuleTrackParamVO tp, List batch) { + if (tp == null || batch == null || batch.isEmpty()) { + return; + } + List cur = tp.getGroups(); + if (cur == null) { + cur = new ArrayList<>(); + } else { + cur = new ArrayList<>(cur); + } + cur.addAll(batch); + tp.setGroups(cur); + } + private static void ensureTrackParam(FireRuleOutputVO out) { if (out.getTrackParam() == null) { out.setTrackParam(new FireRuleTrackParamVO()); diff --git a/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java b/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java index 4074a9f..59b3ecc 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java @@ -245,6 +245,22 @@ public final class RuleFunction { FireRuleRedWeaponOutputFillHelper.fillTrackParamAndBindMoveRoute(out, fact.getTask(), p); } + /** + * 编组规则:填充 {@link com.solution.rule.domain.ultimately.vo.FireRuleTrackParamVO#getGroups}(groupType=addGroup)与僚机 wingmanData。 + */ + @SuppressWarnings("rawtypes") + public static void groupFormation(DroolsFact fact, Map globalParams) { + if (fact == null || fact.getTask() == null || fact.getFireRuleOutputVO() == null) { + return; + } + Map p = castParams(globalParams); + boolean enabled = Boolean.parseBoolean(String.valueOf(p.getOrDefault("groupRuleEnabled", true))); + if (!enabled) { + return; + } + FireRuleRedWeaponOutputFillHelper.fillTrackParamGroups(fact.getFireRuleOutputVO(), fact.getTask(), p); + } + @SuppressWarnings("unchecked") private static Map castParams(Map raw) { return raw == null ? new java.util.HashMap<>() : (Map) raw; diff --git a/auto-solution-rule/src/main/resources/rules/rule.drl b/auto-solution-rule/src/main/resources/rules/rule.drl index b059f40..f30613c 100644 --- a/auto-solution-rule/src/main/resources/rules/rule.drl +++ b/auto-solution-rule/src/main/resources/rules/rule.drl @@ -7,6 +7,7 @@ import static com.solution.rule.utils.RuleFunction.equipmentRule; import static com.solution.rule.utils.RuleFunction.target; import static com.solution.rule.utils.RuleFunction.position; import static com.solution.rule.utils.RuleFunction.trackRoute; +import static com.solution.rule.utils.RuleFunction.groupFormation; global java.util.Map globalParams; @@ -176,6 +177,29 @@ function Map buildParam(){ param.put("trackJamWobbleMeters", 400); param.put("trackJamSegments", 4); + // ===================== 编组规则参数(写入 TrackParam.Groups + wingmanData) ===================== + // groupRuleEnabled:是否生成编组节点。 + param.put("groupRuleEnabled", true); + // groupDrawNameSuffix:drawName = 领队装备 Name + 此后缀(默认「编组」) + param.put("groupDrawNameSuffix", "编组"); + // groupDrawNameWithIndex:是否在 drawName 末尾追加序号(避免多组同名),如 J15编组1 + param.put("groupDrawNameWithIndex", false); + // groupFormationMode:onePerRed(每件红装一组) / clusterByCount(按固定人数分组) / singleGroup(全部一组) + param.put("groupFormationMode", "onePerRed"); + // groupClusterSize:clusterByCount 模式下每组人数上限(>=1) + param.put("groupClusterSize", 3); + // groupLeaderPickMode:byHitRateThenId(命中率高优先,平手 equipmentId 小) / byId(equipmentId 字典序最小) + param.put("groupLeaderPickMode", "byHitRateThenId"); + // groupMinMembersForWingman:组内人数达到该值才生成 wingmanData(否则仅 leader 壳,僚机列表为空) + param.put("groupMinMembersForWingman", 2); + // wingman 几何参数(相对蓝方主航向 mainBearing 叠加) + param.put("wingmanDistanceBaseMeters", 100); + param.put("wingmanDistanceStepMeters", 50); + param.put("wingmanAngleBaseDeg", 50); + param.put("wingmanAngleStepDeg", 15); + param.put("wingmanAltBaseMeters", 40); + param.put("wingmanAltScale", 1.0); + return param; } @@ -218,3 +242,13 @@ then globalParams.putAll(buildParam()); trackRoute($fact, globalParams); end + +rule "编组匹配" +salience 60 +when + $fact : DroolsFact(task != null) +then + // 显式编组规则:填充 TrackParam.Groups(groupType=addGroup)与 wingmanData + globalParams.putAll(buildParam()); + groupFormation($fact, globalParams); +end diff --git a/docker-compose.yml b/docker-compose.yml index 287a58f..f51a847 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: mysql: - image: mysql:8.0 + image: autosolution-mysql-preloaded:latest container_name: autosolution-mysql restart: unless-stopped environment: @@ -27,7 +27,7 @@ services: test: [ "CMD-SHELL", - "mysql -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD:-root} -Nse \"SELECT 1 FROM information_schema.tables WHERE table_schema='$${MYSQL_DATABASE:-autosolution_db}' AND table_name='sys_config' LIMIT 1\" | grep -q 1", + "mysql -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD -Nse 'SELECT 1 FROM information_schema.tables WHERE table_schema=\"$MYSQL_DATABASE\" AND table_name=\"sys_config\" LIMIT 1' | grep -q 1", ] interval: 10s timeout: 5s diff --git a/modeler/src/views/decision/designer/trees-card.vue b/modeler/src/views/decision/designer/trees-card.vue index 6354bb1..2cced9e 100644 --- a/modeler/src/views/decision/designer/trees-card.vue +++ b/modeler/src/views/decision/designer/trees-card.vue @@ -29,6 +29,12 @@ + + + import { defineComponent, onMounted, ref } from 'vue'; -import { CheckOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue'; +import { CheckOutlined, CopyOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue'; import type { BehaviorTree, BehaviorTreeRequest } from './tree'; -import { deleteOneTreeById, findTreesByQuery } from './api'; +import { copyTree, deleteOneTreeById, findTreesByQuery } from './api'; import { substring } from '@/utils/strings'; export default defineComponent({ emits: ['select-tree', 'create-tree'], components: { CheckOutlined, + CopyOutlined, PlusOutlined, DeleteOutlined, EditFilled, @@ -95,6 +102,14 @@ export default defineComponent({ }); }; + const handleCopy = (item: BehaviorTree) => { + copyTree({ id: item.id }).then(r => { + if (r.code === 200) { + loadTress(); + } + }); + }; + const columns = [ { title: '名称', @@ -133,6 +148,7 @@ export default defineComponent({ handleSelect, handleChange, handleDelete, + handleCopy, }; }, }); diff --git a/modeler/src/views/decision/graph/canvas.ts b/modeler/src/views/decision/graph/canvas.ts index 43b94fd..f737652 100644 --- a/modeler/src/views/decision/graph/canvas.ts +++ b/modeler/src/views/decision/graph/canvas.ts @@ -104,6 +104,7 @@ export const createGraphConnectingAttributes = (): Partial => { export const createGraphCanvas = (container: HTMLDivElement, readonly: boolean = false): Graph => { const graph = new Graph({ container: container, + autoResize: true, grid: { size: 20, visible: true,