From ae780eba47ede3e1efade0e024dc70711ea98e0f Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sat, 21 Dec 2024 13:06:17 -0300 Subject: [PATCH 01/16] feat: removed a bunch of little warnings from princess --- .../megamek/ai/utility/AbstractAction.java | 35 ++++++ megamek/src/megamek/ai/utility/Action.java | 34 ++++++ megamek/src/megamek/ai/utility/Agent.java | 62 +++++++++++ .../src/megamek/ai/utility/Blackboard.java | 105 ++++++++++++++++++ .../src/megamek/ai/utility/Consideration.java | 77 +++++++++++++ megamek/src/megamek/ai/utility/Curve.java | 20 ++++ .../megamek/ai/utility/DecisionContext.java | 68 ++++++++++++ .../ai/utility/DecisionScoreEvaluator.java | 93 ++++++++++++++++ .../src/megamek/ai/utility/LinearCurve.java | 32 ++++++ .../src/megamek/ai/utility/LogisticCurve.java | 37 ++++++ .../src/megamek/ai/utility/LogitCurve.java | 41 +++++++ .../megamek/ai/utility/ParabolicCurve.java | 34 ++++++ megamek/src/megamek/ai/utility/Profile.java | 47 ++++++++ .../src/megamek/client/bot/PhaseHandler.java | 74 ++++++++++++ .../megamek/client/bot/duchess/Duchess.java | 102 +++++++++++++++++ .../duchess/considerations/MyUnitArmor.java | 45 ++++++++ .../TargetWithinOptimalRange.java | 61 ++++++++++ .../considerations/TargetWithinRange.java | 54 +++++++++ .../client/ui/ai/editor/UtilityAiEditor.java | 24 ++++ .../megamek/codeUtilities/MathUtility.java | 55 +++++++++ megamek/src/megamek/common/Entity.java | 63 +++++++++-- 21 files changed, 1155 insertions(+), 8 deletions(-) create mode 100644 megamek/src/megamek/ai/utility/AbstractAction.java create mode 100644 megamek/src/megamek/ai/utility/Action.java create mode 100644 megamek/src/megamek/ai/utility/Agent.java create mode 100644 megamek/src/megamek/ai/utility/Blackboard.java create mode 100644 megamek/src/megamek/ai/utility/Consideration.java create mode 100644 megamek/src/megamek/ai/utility/Curve.java create mode 100644 megamek/src/megamek/ai/utility/DecisionContext.java create mode 100644 megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java create mode 100644 megamek/src/megamek/ai/utility/LinearCurve.java create mode 100644 megamek/src/megamek/ai/utility/LogisticCurve.java create mode 100644 megamek/src/megamek/ai/utility/LogitCurve.java create mode 100644 megamek/src/megamek/ai/utility/ParabolicCurve.java create mode 100644 megamek/src/megamek/ai/utility/Profile.java create mode 100644 megamek/src/megamek/client/bot/PhaseHandler.java create mode 100644 megamek/src/megamek/client/bot/duchess/Duchess.java create mode 100644 megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java create mode 100644 megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java create mode 100644 megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java create mode 100644 megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java diff --git a/megamek/src/megamek/ai/utility/AbstractAction.java b/megamek/src/megamek/ai/utility/AbstractAction.java new file mode 100644 index 00000000000..7edb304704a --- /dev/null +++ b/megamek/src/megamek/ai/utility/AbstractAction.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package megamek.ai.utility; + +public abstract class AbstractAction { + // Actions with target are scored per target + + private final String actionName; + private final long actionId; + + public AbstractAction(String actionName, long actionId) { + this.actionName = actionName; + this.actionId = actionId; + } + + public String getActionName() { + return actionName; + } + + public long getActionId() { + return actionId; + } +} diff --git a/megamek/src/megamek/ai/utility/Action.java b/megamek/src/megamek/ai/utility/Action.java new file mode 100644 index 00000000000..f8cd7927507 --- /dev/null +++ b/megamek/src/megamek/ai/utility/Action.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +public enum Action { + + DIRECT_ATTACK("Attack Unit"), + MOVE_TO_POSITION("Move to Position"), + INDIRECT_ATTACK("Indirect Attack"), + USE_SPECIAL_ABILITY("Use Special Ability"); + + private final String actionName; + + Action(String actionName) { + this.actionName = actionName; + } + + public String getActionName() { + return actionName; + } +} diff --git a/megamek/src/megamek/ai/utility/Agent.java b/megamek/src/megamek/ai/utility/Agent.java new file mode 100644 index 00000000000..6c625ed691a --- /dev/null +++ b/megamek/src/megamek/ai/utility/Agent.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package megamek.ai.utility; + +import megamek.client.bot.BotClient; +import megamek.client.bot.princess.Princess; +import megamek.common.IGame; + +public interface Agent { + + // IAUS - Infinite Axis Utility System + // Atomic actions + // Each has one or many considerations (axis) + // They describe why you would want to take this action + // Considerations have Input + // parameters + // Curve type + // - Linear + // - Parabolic + // - Logistic + // - Logit + // 4 parameter values + // - m, k, b, c + // Inputs are normalized + // if not normalized we can clamp between 0-1 + + // Infinite Axis utility system + // Modular influence maps + // Content tagging + + + // Agent is just a character + // They have properties + // some are defined + // some are calculated + + // Series of records + // Each is a piece of data about the world as perceived by this agent + // Each piece of data has a bunch of attributes + + // Attributes have name + // They must come from somewhere - equations? agents? + // Validations - its a range, tag, enumeration? + // + // Prefab equations - path finding, influence maps, distances, etc + + IGame getContext(); + Princess getCharacter(); + +} diff --git a/megamek/src/megamek/ai/utility/Blackboard.java b/megamek/src/megamek/ai/utility/Blackboard.java new file mode 100644 index 00000000000..37f8e8fe02c --- /dev/null +++ b/megamek/src/megamek/ai/utility/Blackboard.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +package megamek.ai.utility; + + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Blackboard is a centralized store for data that can be read/written by various AI components. + * It can hold arbitrary key-value pairs. Typically, keys might be strings (for attribute names) + * and values might be objects, numbers, or domain-specific classes. + * @author Luana Coppio + */ +public class Blackboard { + + // Internally, we can store data in a thread-safe map or a regular map if not accessed concurrently. + // For simplicity, let's use a plain HashMap here. + private final Map data = new ConcurrentHashMap<>(); + + /** + * Stores or updates a value in the blackboard under the given key. + * @param key The unique name of the attribute + * @param value The data to store + */ + public void set(String key, Object value) { + data.put(key, value); + } + + /** + * Retrieves a value associated with the given key. + * Returns null if key not found. Could also throw if that’s preferred. + * @param key The attribute name to look up + * @return The stored value, or null if not present + */ + public Object get(String key) { + return data.get(key); + } + + /** + * Convenience method for retrieving numeric data (e.g. health ratios, distances). + * Returns a default value if the attribute is missing or not a Number. + */ + public double getDouble(String key, double defaultValue) { + Object val = data.get(key); + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } + return defaultValue; + } + + /** + * Convenience method for retrieving numeric data (e.g. health ratios, distances). + * Returns a default value if the attribute is missing or not a Number. + */ + public long getLong(String key, long defaultValue) { + Object val = data.get(key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return defaultValue; + } + + /** + * Convenience method for retrieving string data. + */ + public String getString(String key, String defaultValue) { + Object val = data.get(key); + return val instanceof String ? (String)val : defaultValue; + } + + /** + * Convenience method for retrieving boolean data. + */ + public boolean getBoolean(String key, boolean defaultValue) { + Object val = data.get(key); + return val instanceof Boolean ? (boolean) val : defaultValue; + } + + /** + * Checks if the blackboard contains a certain key. + */ + public boolean contains(String key) { + return data.containsKey(key); + } + + /** + * Provides an immutable snapshot of the blackboard data. + * Might be useful for debugging or passing consistent snapshots around. + */ + public Map snapshot() { + return Map.copyOf(data); + } +} diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java new file mode 100644 index 00000000000..ee7a04bb746 --- /dev/null +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +package megamek.ai.utility; + +import java.util.HashMap; +import java.util.Map; + +public abstract class Consideration { + private Curve curve; + private Map parameters; + + protected Consideration(Curve curve) { + this(curve, new HashMap<>()); + } + + protected Consideration(Curve curve, Map parameters) { + this.curve = curve; + this.parameters = Map.copyOf(parameters); + } + + public abstract double score(DecisionContext context); + + public Curve getCurve() { + return curve; + } + + public void setCurve(Curve curve) { + this.curve = curve; + } + + public Map getParameters() { + return Map.copyOf(parameters); + } + + public void setParameters(Map parameters) { + this.parameters = Map.copyOf(parameters); + } + + protected double getDoubleParameter(String key) { + return (double) parameters.get(key); + } + + protected int getIntParameter(String key) { + return (int) parameters.get(key); + } + + protected boolean getBooleanParameter(String key) { + return (boolean) parameters.get(key); + } + + protected String getStringParameter(String key) { + return (String) parameters.get(key); + } + + protected float getFloatParameter(String key) { + return (float) parameters.get(key); + } + + protected long getLongParameter(String key) { + return (long) parameters.get(key); + } + + public double computeResponseCurve(double score) { + return curve.evaluate(score); + } +} diff --git a/megamek/src/megamek/ai/utility/Curve.java b/megamek/src/megamek/ai/utility/Curve.java new file mode 100644 index 00000000000..d7de6c92b9e --- /dev/null +++ b/megamek/src/megamek/ai/utility/Curve.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +public interface Curve { + double evaluate(double x); +} diff --git a/megamek/src/megamek/ai/utility/DecisionContext.java b/megamek/src/megamek/ai/utility/DecisionContext.java new file mode 100644 index 00000000000..f12b7592aac --- /dev/null +++ b/megamek/src/megamek/ai/utility/DecisionContext.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ +package megamek.ai.utility; + +import megamek.common.Entity; +import megamek.common.IGame; + +import java.util.Optional; + +public class DecisionContext { + + private final Agent agent; + private final IGame worldState; + + public DecisionContext(Agent agent, IGame game) { + this.agent = agent; + this.worldState = game; + } + + public IGame getWorldState() { + return worldState; + } + + public Agent getAgent() { + return agent; + } + + public Optional getTarget() { + // TODO implement this correctly + return agent.getCharacter().getEnemyEntities().stream().findAny(); + } + + public Optional getFiringUnit() { + // TODO implement this correctly + return Optional.ofNullable(agent.getCharacter().getEntityToFire(agent.getCharacter().getFireControlState())); + } + + public Optional getCurrentUnit() { + // TODO implement this correctly + return getFiringUnit(); + } + + // Decision Identifier - enum of what you are trying to do? + // Link to intelligence controller object - who is asking? + // Link to content data with parameters - what do you need? + + /* + * Example input - MyHealth + * class ConsiderationMyHealth extends Consideration { + * public float Score(DecisionContext context) { + * var intelligence = context.getIntelligence(); + * var character = intelligence.getCharacter(); + * return character.getHealth() / character.getMaxHealth(); + * } + */ + +} diff --git a/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java b/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java new file mode 100644 index 00000000000..41cfc779101 --- /dev/null +++ b/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static megamek.codeUtilities.MathUtility.clamp01; + +public class DecisionScoreEvaluator { + // Represents the decision process + // Evaluates input via considerations + // Scores it + // if selected result into a decision + + // weight - Weight goes from 1.0 to 5.0 + // it is an implicit representation of priority + // 1.0 -> basic action (search enemy) + // 2.0 -> tactical movement (move to cover, move to flank, move to attack) + // 3.0 -> Special action usage (spot for indirect fire, LAM conversion) + // 4.0 -> Emergency (move to cover, + // 5.0 -> Emergency (move away from orbital strike) + // name + // description + // notes + + // considerations + // considerations may have parameters + // Aggregate all considerations into a single score + // multiply the score by the weight + + private AbstractAction action; + private String description; + private String notes; + private double weight; + private final List considerations = new ArrayList<>(); + + public DecisionScoreEvaluator(AbstractAction action, String description, String notes, double weight) { + this(action, description, notes, weight, Collections.emptyList()); + } + + public DecisionScoreEvaluator(AbstractAction action, String description, String notes, double weight, List considerations) { + this.action = action; + this.description = description; + this.notes = notes; + this.weight = weight; + this.considerations.addAll(considerations); + } + + public double score(DecisionContext context, double bonus, double min) { + + var finalScore = bonus; + + for (var consideration : getConsiderations()) { + if ((0.0f < finalScore) || (0.0f < min)) { + break; + } + var score = consideration.score(context); + var response = consideration.computeResponseCurve(score); + + finalScore *= clamp01(response); + } + // adjustment + var modificationFactor = 1 - (1 / getConsiderations().size()); + var makeUpValue = (1 - finalScore) * modificationFactor; + finalScore = finalScore + (makeUpValue * finalScore); + + return finalScore; + } + + public List getConsiderations() { + return considerations; + } + + public DecisionScoreEvaluator addConsideration(Consideration consideration) { + considerations.add(consideration); + return this; + } +} diff --git a/megamek/src/megamek/ai/utility/LinearCurve.java b/megamek/src/megamek/ai/utility/LinearCurve.java new file mode 100644 index 00000000000..3a33182f68a --- /dev/null +++ b/megamek/src/megamek/ai/utility/LinearCurve.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import static megamek.codeUtilities.MathUtility.clamp01; + +public class LinearCurve implements Curve { + private final double m; + private final double b; + + public LinearCurve(double m, double b) { + this.m = m; + this.b = b; + } + + public double evaluate(double x) { + return clamp01(m * x + b); + } +} diff --git a/megamek/src/megamek/ai/utility/LogisticCurve.java b/megamek/src/megamek/ai/utility/LogisticCurve.java new file mode 100644 index 00000000000..cb6d36cfc14 --- /dev/null +++ b/megamek/src/megamek/ai/utility/LogisticCurve.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import static megamek.codeUtilities.MathUtility.clamp01; + +public class LogisticCurve implements Curve { + private final double m; + private final double b; + private final double k; + private final double c; + + public LogisticCurve(double m, double b, double k, double c) { + this.m = m; + this.b = b; + this.k = k; + this.c = c; + } + + public double evaluate(double x) { + return clamp01(m / (1 + Math.exp(-k * (x - b))) + c); + + } +} diff --git a/megamek/src/megamek/ai/utility/LogitCurve.java b/megamek/src/megamek/ai/utility/LogitCurve.java new file mode 100644 index 00000000000..3bb84bf8ea5 --- /dev/null +++ b/megamek/src/megamek/ai/utility/LogitCurve.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import static megamek.codeUtilities.MathUtility.clamp01; + +public class LogitCurve implements Curve { + private final double m; + private final double b; + private final double k; + private final double c; + + public LogitCurve(double m, double b, double k, double c) { + this.m = m; + this.b = b; + this.k = k; + this.c = c; + } + + public double evaluate(double x) { + // Ensure p is in the valid range + // Typically, you want 0 < p < 1 to avoid division by zero or log of zero. + if (x - c == 0) { + return 0d; + } + return clamp01(b - (1.0 / k) * Math.log((m - (x - c)) / (x - c))); + } +} diff --git a/megamek/src/megamek/ai/utility/ParabolicCurve.java b/megamek/src/megamek/ai/utility/ParabolicCurve.java new file mode 100644 index 00000000000..310f799b6c1 --- /dev/null +++ b/megamek/src/megamek/ai/utility/ParabolicCurve.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import static megamek.codeUtilities.MathUtility.clamp01; + +public class ParabolicCurve implements Curve { + private final double m; + private final double b; + private final double k; + + public ParabolicCurve(double m, double b, double k) { + this.m = m; + this.b = b; + this.k = k; + } + + public double evaluate(double x) { + return clamp01(m * Math.pow(x, 2) + k * x + b); + } +} diff --git a/megamek/src/megamek/ai/utility/Profile.java b/megamek/src/megamek/ai/utility/Profile.java new file mode 100644 index 00000000000..1e8cc4bf6ec --- /dev/null +++ b/megamek/src/megamek/ai/utility/Profile.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import java.util.List; + +public class Profile { + + private final String name; + private String description; + private final List decisionScoreEvaluators; + + public Profile(String name, String description, List decisionScoreEvaluators) { + this.name = name; + this.description = description; + this.decisionScoreEvaluators = decisionScoreEvaluators; + } + + public String getName() { + return name; + } + + public List getDecisionScoreEvaluators() { + return decisionScoreEvaluators; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/megamek/src/megamek/client/bot/PhaseHandler.java b/megamek/src/megamek/client/bot/PhaseHandler.java new file mode 100644 index 00000000000..3f306e9c58f --- /dev/null +++ b/megamek/src/megamek/client/bot/PhaseHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MekHQ. + * + * MekHQ is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MekHQ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ + +package megamek.client.bot; + +import megamek.common.Game; +import megamek.common.enums.GamePhase; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class PhaseHandler { + + private final GamePhase phase; + private final Game game; + private final Runnable executePhase; + + public PhaseHandler(GamePhase phase, Game game, Runnable executePhase) { + this.phase = phase; + this.game = game; + this.executePhase = executePhase; + } + + private boolean isPhase(GamePhase phase) { + return this.phase == phase; + } + + protected Game getGame() { + return game; + } + + public GamePhase getPhase() { + return phase; + } + + public void execute() { + if (isPhase(getGame().getPhase())) { + try { + this.executePhase.run(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof PhaseHandler that)) return false; + + return new EqualsBuilder().append(phase, that.phase).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(phase).toHashCode(); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/Duchess.java b/megamek/src/megamek/client/bot/duchess/Duchess.java new file mode 100644 index 00000000000..5c12ad12899 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/Duchess.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package megamek.client.bot.duchess; + +import megamek.client.bot.PhysicalOption; +import megamek.client.bot.princess.Princess; +import megamek.common.Coords; +import megamek.common.Entity; +import megamek.common.Minefield; +import megamek.common.MovePath; +import megamek.common.event.GamePlayerChatEvent; +import megamek.logging.MMLogger; + +import java.util.Vector; + +public class Duchess extends Princess{ + private static final MMLogger logger = MMLogger.create(Duchess.class); + + /** + * Constructor - initializes a new instance of the Princess bot. + * + * @param name The display name. + * @param host The host address to which to connect. + * @param port The port on the host where to connect. + */ + public Duchess(String name, String host, int port) { + super(name, host, port); + } + + + @Override + public void initialize() { + + } + + @Override + protected void processChat(GamePlayerChatEvent ge) { + + } + + @Override + protected void initMovement() { + + } + + @Override + protected void initFiring() { + + } + + @Override + protected MovePath calculateMoveTurn() { + return null; + } + + @Override + protected void calculateFiringTurn() { + + } + + @Override + protected void calculateDeployment() { + + } + + @Override + protected PhysicalOption calculatePhysicalTurn() { + return null; + } + + @Override + protected MovePath continueMovementFor(Entity entity) { + return null; + } + + @Override + protected Vector calculateMinefieldDeployment() { + return null; + } + + @Override + protected Vector calculateArtyAutoHitHexes() { + return null; + } + + @Override + protected void checkMorale() { + + } +} diff --git a/megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java b/megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java new file mode 100644 index 00000000000..5aea0b3ef22 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.considerations; + +import megamek.ai.utility.Consideration; +import megamek.ai.utility.Curve; +import megamek.ai.utility.DecisionContext; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if a target is an easy target. + */ +public class MyUnitArmor extends Consideration { + + + protected MyUnitArmor(Curve curve) { + super(curve); + } + + @Override + public double score(DecisionContext context) { + var currentUnit = context.getCurrentUnit(); + if (currentUnit.isEmpty()) { + return 0d; + } + + var currentEntity = currentUnit.get(); + + return clamp01(currentEntity.getArmorRemainingPercent()); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java b/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java new file mode 100644 index 00000000000..5dca0561b22 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.considerations; + +import megamek.ai.utility.Consideration; +import megamek.ai.utility.Curve; +import megamek.ai.utility.DecisionContext; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if a target is an easy target. + */ +public class TargetWithinOptimalRange extends Consideration { + + + protected TargetWithinOptimalRange(Curve curve) { + super(curve); + } + + @Override + public double score(DecisionContext context) { + var target = context.getTarget(); + var firingUnit = context.getFiringUnit(); + + if (target.isEmpty() || firingUnit.isEmpty()) { + return 0d; + } + + var targetEntity = target.get(); + var firingEntity = firingUnit.get(); + + if (!firingEntity.hasFiringSolutionFor(targetEntity.getId())) { + return 0d; + } + + var distance = firingEntity.getPosition().distance(targetEntity.getPosition()); + + var maxRange = firingEntity.getMaxWeaponRange(); + var bestRange = firingEntity.getOptimalRange(); + + if (distance <= bestRange) { + return 1d; + } + + return clamp01(1.0001d - (double) (distance - bestRange) / (maxRange - bestRange)); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java b/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java new file mode 100644 index 00000000000..42840cbdc2c --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.considerations; + +import megamek.ai.utility.Consideration; +import megamek.ai.utility.Curve; +import megamek.ai.utility.DecisionContext; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if a target is an easy target. + */ +public class TargetWithinRange extends Consideration { + + + protected TargetWithinRange(Curve curve) { + super(curve); + } + + @Override + public double score(DecisionContext context) { + var target = context.getTarget(); + var firingUnit = context.getFiringUnit(); + + if (target.isEmpty() || firingUnit.isEmpty()) { + return 0d; + } + + var targetEntity = target.get(); + var firingEntity = firingUnit.get(); + + if (!firingEntity.hasFiringSolutionFor(targetEntity.getId())) { + return 0d; + } + + var distance = firingEntity.getPosition().distance(targetEntity.getPosition()); + var maxRange = firingEntity.getMaxWeaponRange(); + return clamp01(1.00001d - (double) distance / maxRange); + } +} diff --git a/megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java b/megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java new file mode 100644 index 00000000000..c2b0e2ce201 --- /dev/null +++ b/megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.ai.editor; + +public class UtilityAiEditor { + // TODO Implement a Utility AI editor + // which allows you to create a "personality" + // Each personality has a set of Decision Score Evaluators + // Each Decision Score Evaluator has a single Action and multiple considerations + // +} diff --git a/megamek/src/megamek/codeUtilities/MathUtility.java b/megamek/src/megamek/codeUtilities/MathUtility.java index 37f8bebf610..c943684ab0b 100644 --- a/megamek/src/megamek/codeUtilities/MathUtility.java +++ b/megamek/src/megamek/codeUtilities/MathUtility.java @@ -137,4 +137,59 @@ public static long clamp(final long value, final long min, final long max) { return Math.min(Math.max(value, min), max); } // endregion Clamp + + // region Clamp01 + /** + * @param value the int value to clamp + * @return The value if it is inside the range 0-1; + * the 0 value if value is below that range and 1 value if value + * is above that range. + * clamp01(2) returns 1, + * clamp01(-1) returns 0, + * clamp01(1) returns 1. + */ + public static int clamp01(final int value) { + return Math.min(Math.max(value, 0), 1); + } + + /** + * @param value the double value to clamp + * @return The value if it is inside the range 0-1; + * the 0 value if value is below that range and 1 value if value + * is above that range. + * clamp01(0.1d) returns 0.1d, + * clamp01(-1) returns 0d, + * clamp01(1) returns 1d. + */ + public static double clamp01(final double value) { + return Math.min(Math.max(value, 0d), 1d); + } + + /** + * @param value the float value to clamp + * @return The value if it is inside the range 0-1; + * the 0 value if value is below that range and 1 value if value + * is above that range. + * clamp01(0.4f) returns 0.4f, + * clamp01(-1) returns 0f, + * clamp01(1) returns 1f. + */ + public static float clamp01(final float value) { + return Math.min(Math.max(value, 0f), 1f); + } + + /** + * @param value the long value to clamp + * @return The value if it is inside the range 0-1; + * the 0 value if value is below that range and 1 value if value + * is above that range. + * clamp01(2) returns 1, + * clamp01(-1) returns 0, + * clamp01(1) returns 1. + */ + public static long clamp01(final long value) { + return Math.min(Math.max(value, 0), 1); + } + // endregion Clamp + } diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 352aff9d367..5a5f36db644 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -32,14 +32,7 @@ import megamek.client.ui.swing.calculationReport.DummyCalculationReport; import megamek.codeUtilities.StringUtility; import megamek.common.MovePath.MoveStepType; -import megamek.common.actions.AbstractAttackAction; -import megamek.common.actions.ChargeAttackAction; -import megamek.common.actions.DfaAttackAction; -import megamek.common.actions.DisplacementAttackAction; -import megamek.common.actions.EntityAction; -import megamek.common.actions.PushAttackAction; -import megamek.common.actions.TeleMissileAttackAction; -import megamek.common.actions.WeaponAttackAction; +import megamek.common.actions.*; import megamek.common.annotations.Nullable; import megamek.common.battlevalue.BVCalculator; import megamek.common.enums.AimingMode; @@ -14445,6 +14438,60 @@ public int getMaxWeaponRange(boolean targetIsAirborne) { return maxRange; } + public int getOptimalRange() { + Map rangeDamages = new TreeMap<>(); + if ((ETYPE_MEK == getEntityType()) + || (ETYPE_INFANTRY == getEntityType()) + || (ETYPE_PROTOMEK == getEntityType())) { + // account for physical attacks. + rangeDamages.put(1, + PunchAttackAction.getDamageFor(this, PunchAttackAction.BOTH, false, false) + + KickAttackAction.getDamageFor(this, KickAttackAction.BOTH, false)); + } + + for (WeaponMounted weapon : getWeaponList()) { + if (!weapon.isReady()) { + continue; + } + + WeaponType type = weapon.getType(); + + if (isAirborne()) { + int rangeMultiplier = type.isCapital() ? 2 : 1; + rangeMultiplier *= isAirborneAeroOnGroundMap() ? 8 : 1; + + var range = WeaponType.AIRBORNE_WEAPON_RANGES[type.getMaxRange(weapon)] * rangeMultiplier; + rangeDamages.put(range, rangeDamages.getOrDefault(range, 0) + type.getDamage(range)); + } else { + var range = type.getShortRange(); + rangeDamages.put(range, rangeDamages.getOrDefault(range, 0) + type.getDamage(range)); + } + } + + var keys = new ArrayList<>(rangeDamages.keySet()); + Collections.sort(keys); + + if (keys.isEmpty()) { + return -1; + } + + if (keys.size() == 1) { + return keys.get(0); + } + + var totalDamage = rangeDamages.values().stream().mapToInt(Integer::intValue).sum(); + var halfDamage = totalDamage / 2; + var accumulator = 0; + for (int i = rangeDamages.size() -1; i > 0; i--) { + accumulator += rangeDamages.get(keys.get(i)); + if (accumulator > halfDamage / 2) { + return keys.get(i); + } + } + + return keys.get(keys.size() -1); + } + public int getHeat() { return heat; } From a7bac3610847b050fa9c13eaf7cf90a1a5348e75 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 25 Dec 2024 16:12:42 -0300 Subject: [PATCH 02/16] feat: basis for AI editor, decision, dse, etc --- .gitignore | 1 + megamek/build.gradle | 8 +- .../considerations/proto_considerations.yaml | 16 + .../data/ai/tw/decisions/proto_decisions.yaml | 26 + megamek/data/ai/tw/evaluators/proto_dse.yaml | 56 + .../data/ai/tw/profiles/proto_profiles.yaml | 0 .../i18n/megamek/client/messages.properties | 1 + .../megamek/ai/utility/AbstractAction.java | 35 - megamek/src/megamek/ai/utility/Action.java | 8 +- megamek/src/megamek/ai/utility/Agent.java | 50 +- .../src/megamek/ai/utility/Consideration.java | 62 +- megamek/src/megamek/ai/utility/Curve.java | 70 + megamek/src/megamek/ai/utility/Decision.java | 107 + .../megamek/ai/utility/DecisionContext.java | 83 +- .../src/megamek/ai/utility/DecisionMaker.java | 70 + .../ai/utility/DecisionScoreEvaluator.java | 97 +- .../src/megamek/ai/utility/DefaultCurve.java | 39 + .../src/megamek/ai/utility/Intelligence.java | 41 + .../src/megamek/ai/utility/LinearCurve.java | 43 +- .../src/megamek/ai/utility/LogisticCurve.java | 71 +- .../src/megamek/ai/utility/LogitCurve.java | 80 +- .../utility/NamedObject.java} | 10 +- .../megamek/ai/utility/ParabolicCurve.java | 59 +- megamek/src/megamek/ai/utility/Profile.java | 41 +- .../megamek/ai/utility/ScoredDecision.java | 48 + megamek/src/megamek/ai/utility/World.java | 71 + .../megamek/ai/utility/dummy/DummyAgent.java | 62 + .../megamek/ai/utility/dummy/DummyClient.java | 103 + .../megamek/client/bot/duchess/Duchess.java | 85 +- .../ai/utility/tw/TWUtilityAIRepository.java | 227 ++ .../tw}/considerations/MyUnitArmor.java | 25 +- .../tw/considerations/MyUnitUnderThreat.java | 53 + .../tw/considerations/TWConsideration.java | 42 + .../TargetWithinOptimalRange.java | 41 +- .../tw}/considerations/TargetWithinRange.java | 37 +- .../ai/utility/tw/context/TWWorld.java | 85 + .../ai/utility/tw/decision/TWDecision.java | 33 + .../tw/decision/TWDecisionContext.java | 53 + .../tw/decision/TWDecisionScoreEvaluator.java | 39 + .../tw/intelligence/SimpleIntelligence.java | 82 + .../ai/utility/tw/profile/TWProfile.java | 40 + .../megamek/client/bot/princess/Princess.java | 2 +- .../megamek/client/ui/swing/ClientGUI.java | 3 +- .../megamek/client/ui/swing/ExitsDialog.java | 4 +- .../megamek/client/ui/swing/MegaMekGUI.java | 25 +- .../ui/swing/ai/editor/AiProfileEditor.form | 295 ++ .../ui/swing/ai/editor/AiProfileEditor.java | 232 ++ .../ui/swing/ai/editor/ConsiderationPane.form | 91 + .../ui/swing/ai/editor/ConsiderationPane.java | 106 + .../client/ui/swing/ai/editor/CurvePane.form | 108 + .../client/ui/swing/ai/editor/CurvePane.java | 219 ++ .../ai/editor/DecisionScoreEvaluatorPane.form | 93 + .../ai/editor/DecisionScoreEvaluatorPane.java | 127 + .../editor/DecisionScoreEvaluatorTable.java | 66 + .../DecisionScoreEvaluatorTableModel.java | 99 + .../swing/ai/editor/ParametersTableModel.java | 115 + .../ui/swing/ai/editor/UtilityAiEditor.java | 2674 +++++++++++++++++ .../ui/swing/util/MegaMekController.java | 14 +- megamek/src/megamek/common/Configuration.java | 26 +- .../common/preference/ClientPreferences.java | 10 + megamek/src/megamek/logging/MMLogger.java | 46 + 61 files changed, 6220 insertions(+), 335 deletions(-) create mode 100644 megamek/data/ai/tw/considerations/proto_considerations.yaml create mode 100644 megamek/data/ai/tw/decisions/proto_decisions.yaml create mode 100644 megamek/data/ai/tw/evaluators/proto_dse.yaml create mode 100644 megamek/data/ai/tw/profiles/proto_profiles.yaml delete mode 100644 megamek/src/megamek/ai/utility/AbstractAction.java create mode 100644 megamek/src/megamek/ai/utility/Decision.java create mode 100644 megamek/src/megamek/ai/utility/DecisionMaker.java create mode 100644 megamek/src/megamek/ai/utility/DefaultCurve.java create mode 100644 megamek/src/megamek/ai/utility/Intelligence.java rename megamek/src/megamek/{client/ui/ai/editor/UtilityAiEditor.java => ai/utility/NamedObject.java} (64%) create mode 100644 megamek/src/megamek/ai/utility/ScoredDecision.java create mode 100644 megamek/src/megamek/ai/utility/World.java create mode 100644 megamek/src/megamek/ai/utility/dummy/DummyAgent.java create mode 100644 megamek/src/megamek/ai/utility/dummy/DummyClient.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java rename megamek/src/megamek/client/bot/duchess/{ => ai/utility/tw}/considerations/MyUnitArmor.java (60%) create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitUnderThreat.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java rename megamek/src/megamek/client/bot/duchess/{ => ai/utility/tw}/considerations/TargetWithinOptimalRange.java (51%) rename megamek/src/megamek/client/bot/duchess/{ => ai/utility/tw}/considerations/TargetWithinRange.java (51%) create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/context/TWWorld.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionContext.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/intelligence/SimpleIntelligence.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java diff --git a/.gitignore b/.gitignore index 8b829940bc7..38a55c1cc65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/megamek/.intellijPlatform /tmp/ /.metadata /.recommenders/ diff --git a/megamek/build.gradle b/megamek/build.gradle index 6d94571c683..dc909c1fe8e 100644 --- a/megamek/build.gradle +++ b/megamek/build.gradle @@ -9,7 +9,6 @@ plugins { id 'jacoco' id 'java' id 'org.ec4j.editorconfig' version '0.1.0' - } java { @@ -36,7 +35,14 @@ sourceSets { } } +repositories { + mavenCentral() + maven { url "https://www.jetbrains.com/intellij-repository/releases" } + maven { url "https://jetbrains.bintray.com/intellij-third-party-dependencies" } +} + dependencies { + implementation 'com.jetbrains.intellij.java:java-gui-forms-rt:243.22562.242' implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2' implementation 'com.formdev:flatlaf:3.5.1' diff --git a/megamek/data/ai/tw/considerations/proto_considerations.yaml b/megamek/data/ai/tw/considerations/proto_considerations.yaml new file mode 100644 index 00000000000..939fb9e1bd2 --- /dev/null +++ b/megamek/data/ai/tw/considerations/proto_considerations.yaml @@ -0,0 +1,16 @@ +--- ! +name: "MyUnitArmor" +curve: ! + m: 0.5 + b: 0.3 +parameters: + minValue: 0 + maxValue: 10 +--- ! +name: "TargetWithinOptimalRange" +curve: ! + m: 1.0 + b: 0.5 + k: 10.0 + c: 0.0 +parameters: {} \ No newline at end of file diff --git a/megamek/data/ai/tw/decisions/proto_decisions.yaml b/megamek/data/ai/tw/decisions/proto_decisions.yaml new file mode 100644 index 00000000000..ab04fef9e2b --- /dev/null +++ b/megamek/data/ai/tw/decisions/proto_decisions.yaml @@ -0,0 +1,26 @@ +--- +! +action: AttackUnit +weight: 1.0 +decisionScoreEvaluator: ! + name: "AttackEnemyInRange" + description: "Evaluate if the enemy is optimal for attack." + notes: "File for testing the load of the evaluator system." + weight: 2.5 + considerations: + - ! + name: "MyUnitArmor" + curve: ! + m: 0.5 + b: 0.3 + parameters: + minValue: 0 + maxValue: 10 + - ! + name: "TargetWithinOptimalRange" + curve: ! + m: 1.0 + b: 0.5 + k: 10.0 + c: 0.0 + parameters: {} \ No newline at end of file diff --git a/megamek/data/ai/tw/evaluators/proto_dse.yaml b/megamek/data/ai/tw/evaluators/proto_dse.yaml new file mode 100644 index 00000000000..092c3027257 --- /dev/null +++ b/megamek/data/ai/tw/evaluators/proto_dse.yaml @@ -0,0 +1,56 @@ +--- +! +name: "AttackEnemyInRange" +description: "Evaluate if the enemy is optimal for attack." +notes: "File for testing the load of the evaluator system." +considerations: + - ! + name: "MyUnitArmor" + curve: ! + m: 0.5 + b: 0.3 + parameters: + minValue: 0 + maxValue: 10 + - ! + name: "TargetWithinOptimalRange" + curve: ! + m: 1.0 + b: 0.5 + k: 10.0 + c: 0.0 + parameters: {} +--- +! +name: "Foobar" +description: "Spam Spam" +notes: "File for testing the load of the evaluator system." +weight: 2.5 +considerations: + - ! + name: "MyUnitArmor" + curve: + curveType: "LinearCurve" + m: 0.5 + b: 0.3 + parameters: + minValue: 0 + maxValue: 10 + - ! + name: "TargetWithinOptimalRange" + curve: + curveType: "LogisticCurve" + m: 1.0 + b: 0.5 + k: 10.0 + c: 0.0 + parameters: {} + - ! + name: "TargetWithinOptimalRange" + curve: + curveType: "LogisticCurve" + m: 1.0 + b: 0.5 + k: 10.0 + c: 0.0 + parameters: {} \ No newline at end of file diff --git a/megamek/data/ai/tw/profiles/proto_profiles.yaml b/megamek/data/ai/tw/profiles/proto_profiles.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 84f059eb3d4..2a0096eea7e 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4326,6 +4326,7 @@ MegaMek.ScenarioDialog.otherh=Other Human MegaMek.ScenarioDialog.bot=Princess MegaMek.ScenarioDialog.Camo=Camo MegaMek.SkinEditor.label=Skin Editor +MegaMek.AiEditor.label=AI Editor MegaMek.NoCamoBtn=No Camo MegaMek.ScenarioErrorAlert.title=Scenario Error MegaMek.ScenarioErrorAlert.message=Only one faction can be set to 'Me'. diff --git a/megamek/src/megamek/ai/utility/AbstractAction.java b/megamek/src/megamek/ai/utility/AbstractAction.java deleted file mode 100644 index 7edb304704a..00000000000 --- a/megamek/src/megamek/ai/utility/AbstractAction.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -package megamek.ai.utility; - -public abstract class AbstractAction { - // Actions with target are scored per target - - private final String actionName; - private final long actionId; - - public AbstractAction(String actionName, long actionId) { - this.actionName = actionName; - this.actionId = actionId; - } - - public String getActionName() { - return actionName; - } - - public long getActionId() { - return actionId; - } -} diff --git a/megamek/src/megamek/ai/utility/Action.java b/megamek/src/megamek/ai/utility/Action.java index f8cd7927507..71373a12196 100644 --- a/megamek/src/megamek/ai/utility/Action.java +++ b/megamek/src/megamek/ai/utility/Action.java @@ -17,10 +17,10 @@ public enum Action { - DIRECT_ATTACK("Attack Unit"), - MOVE_TO_POSITION("Move to Position"), - INDIRECT_ATTACK("Indirect Attack"), - USE_SPECIAL_ABILITY("Use Special Ability"); + AttackUnit("Attack Unit"), + MoveToPosition("Move to Position"), + IndirectAttack("Indirect Attack"), + UseSpecialAbility("Use Special Ability"); private final String actionName; diff --git a/megamek/src/megamek/ai/utility/Agent.java b/megamek/src/megamek/ai/utility/Agent.java index 6c625ed691a..f49c63ae0a1 100644 --- a/megamek/src/megamek/ai/utility/Agent.java +++ b/megamek/src/megamek/ai/utility/Agent.java @@ -14,49 +14,11 @@ package megamek.ai.utility; -import megamek.client.bot.BotClient; -import megamek.client.bot.princess.Princess; -import megamek.common.IGame; - -public interface Agent { - - // IAUS - Infinite Axis Utility System - // Atomic actions - // Each has one or many considerations (axis) - // They describe why you would want to take this action - // Considerations have Input - // parameters - // Curve type - // - Linear - // - Parabolic - // - Logistic - // - Logit - // 4 parameter values - // - m, k, b, c - // Inputs are normalized - // if not normalized we can clamp between 0-1 - - // Infinite Axis utility system - // Modular influence maps - // Content tagging - - - // Agent is just a character - // They have properties - // some are defined - // some are calculated - - // Series of records - // Each is a piece of data about the world as perceived by this agent - // Each piece of data has a bunch of attributes - - // Attributes have name - // They must come from somewhere - equations? agents? - // Validations - its a range, tag, enumeration? - // - // Prefab equations - path finding, influence maps, distances, etc - - IGame getContext(); - Princess getCharacter(); +import megamek.client.IClient; +public interface Agent { + int getId(); + World getContext(); + Intelligence getIntelligence(); + IClient getClient(); } diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java index ee7a04bb746..25b2ace43ae 100644 --- a/megamek/src/megamek/ai/utility/Consideration.java +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -13,23 +13,55 @@ */ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import megamek.client.bot.duchess.ai.utility.tw.considerations.*; + import java.util.HashMap; import java.util.Map; - -public abstract class Consideration { +import java.util.StringJoiner; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TWConsideration.class, name = "TWConsideration"), + @JsonSubTypes.Type(value = MyUnitArmor.class, name = "MyUnitArmor"), + @JsonSubTypes.Type(value = TargetWithinOptimalRange.class, name = "TargetWithinOptimalRange"), + @JsonSubTypes.Type(value = TargetWithinRange.class, name = "TargetWithinRange"), + @JsonSubTypes.Type(value = MyUnitUnderThreat.class, name = "MyUnitUnderThreat") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Consideration implements NamedObject { + @JsonProperty("name") + private String name; + @JsonProperty("curve") private Curve curve; + @JsonProperty("parameters") private Map parameters; - protected Consideration(Curve curve) { - this(curve, new HashMap<>()); + public Consideration() { + } + + public Consideration(String name) { + this.name = name; + } + + public Consideration(String name, Curve curve) { + this(name, curve, new HashMap<>()); } - protected Consideration(Curve curve, Map parameters) { + public Consideration(String name, Curve curve, Map parameters) { + this.name = name; this.curve = curve; this.parameters = Map.copyOf(parameters); } - public abstract double score(DecisionContext context); + public abstract double score(DecisionContext context); public Curve getCurve() { return curve; @@ -74,4 +106,22 @@ protected long getLongParameter(String key) { public double computeResponseCurve(double score) { return curve.evaluate(score); } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return new StringJoiner(", ", Consideration.class.getSimpleName() + "[", "]") + .add("name='" + name + "'") + .add("curve=" + curve) + .add("parameters=" + parameters) + .toString(); + } } diff --git a/megamek/src/megamek/ai/utility/Curve.java b/megamek/src/megamek/ai/utility/Curve.java index d7de6c92b9e..97f252e9eb3 100644 --- a/megamek/src/megamek/ai/utility/Curve.java +++ b/megamek/src/megamek/ai/utility/Curve.java @@ -15,6 +15,76 @@ package megamek.ai.utility; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import javax.swing.*; +import java.awt.*; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "curveType" // The YAML will have something like "curveType: LinearCurve" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = LinearCurve.class, name = "LinearCurve"), + @JsonSubTypes.Type(value = LogisticCurve.class, name = "LogisticCurve"), + @JsonSubTypes.Type(value = LogitCurve.class, name = "LogitCurve"), + @JsonSubTypes.Type(value = ParabolicCurve.class, name = "ParabolicCurve") +}) public interface Curve { double evaluate(double x); + + default void drawAxes(Graphics g, int width, int height) { + // Center lines + g.setColor(Color.LIGHT_GRAY); + g.drawLine(0, height/2, width, height/2); // X-axis + g.drawLine(width/2, 0, width/2, height); // Y-axis + + // Restore color to black + g.setColor(Color.BLACK); + } + + default void drawCurve(Graphics g, int width, int height, Color color) { + Graphics2D g2d = (Graphics2D) g; + g2d.setColor(color); + g2d.setStroke(new BasicStroke(3)); + g2d.setColor(color); + + double step = 0.001; + double xPrev = 0.0; + double yPrev = this.evaluate(xPrev); + + for (double x = step; x <= 1.0; x += step) { + double y = this.evaluate(x); + + int px1 = (int)(xPrev * width); + int py1 = (int)((1.0 - yPrev) * height); + + int px2 = (int)(x * width); + int py2 = (int)((1.0 - y) * height); + + g2d.drawLine(px1, py1, px2, py2); + + xPrev = x; + yPrev = y; + } + } + + default void setM(double m) { + // + } + + default void setB(double b) { + // + } + + default void setK(double k) { + // + } + + default void setC(double c) { + // + } } diff --git a/megamek/src/megamek/ai/utility/Decision.java b/megamek/src/megamek/ai/utility/Decision.java new file mode 100644 index 00000000000..3e9abd2784b --- /dev/null +++ b/megamek/src/megamek/ai/utility/Decision.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; + +import java.util.StringJoiner; + + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TWDecision.class, name = "TWDecision"), +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Decision implements NamedObject{ + + private Action action; + private double weight; + private DecisionScoreEvaluator decisionScoreEvaluator; + private transient double score; + private transient DecisionContext decisionContext; + + public Decision() { + } + + public Decision(Action action, double weight, DecisionScoreEvaluator decisionScoreEvaluator) { + this.action = action; + this.weight = weight; + this.decisionScoreEvaluator = decisionScoreEvaluator; + } + + @Override + public String getName() { + return (action.name() + "::" + decisionScoreEvaluator.getName()).replace(" ", "_").toUpperCase(); + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public double getWeight() { + return weight; + } + + public void setWeight(double weight) { + this.weight = weight; + } + + public DecisionScoreEvaluator getDecisionScoreEvaluator() { + return decisionScoreEvaluator; + } + + public void setDecisionScoreEvaluator(DecisionScoreEvaluator decisionScoreEvaluator) { + this.decisionScoreEvaluator = decisionScoreEvaluator; + } + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } + + public DecisionContext getDecisionContext() { + return decisionContext; + } + + public void setDecisionContext(DecisionContext decisionContext) { + this.decisionContext = decisionContext; + } + + @Override + public String toString() { + return new StringJoiner(", ", Decision.class.getSimpleName() + "[", "]") + .add("action=" + action) + .add("weight=" + weight) + .add("score=" + score) + .add("decisionScoreEvaluator=" + decisionScoreEvaluator) + .add("decisionContext=" + decisionContext) + .toString(); + } +} diff --git a/megamek/src/megamek/ai/utility/DecisionContext.java b/megamek/src/megamek/ai/utility/DecisionContext.java index f12b7592aac..97a98a85747 100644 --- a/megamek/src/megamek/ai/utility/DecisionContext.java +++ b/megamek/src/megamek/ai/utility/DecisionContext.java @@ -13,56 +13,75 @@ */ package megamek.ai.utility; -import megamek.common.Entity; -import megamek.common.IGame; +import java.util.*; -import java.util.Optional; -public class DecisionContext { +public abstract class DecisionContext { - private final Agent agent; - private final IGame worldState; + private final Agent agent; + private final World world; + private final IN_GAME_OBJECT currentUnit; + private final List targetUnits; + private final Map damageCache; + private final static int DAMAGE_CACHE_SIZE = 10_000; - public DecisionContext(Agent agent, IGame game) { + public DecisionContext(Agent agent, World world) { + this(agent, world, null, Collections.emptyList()); + } + + public DecisionContext(Agent agent, World world, IN_GAME_OBJECT currentUnit) { + this(agent, world, currentUnit, Collections.emptyList()); + } + + public DecisionContext(Agent agent, World world, IN_GAME_OBJECT currentUnit, List targetUnits) { this.agent = agent; - this.worldState = game; + this.world = world; + this.currentUnit = currentUnit; + this.targetUnits = targetUnits; + this.damageCache = new LinkedHashMap<>(32, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > DAMAGE_CACHE_SIZE; + } + }; } - public IGame getWorldState() { - return worldState; + public World getWorld() { + return world; } - public Agent getAgent() { + public Agent getAgent() { return agent; } - public Optional getTarget() { - // TODO implement this correctly - return agent.getCharacter().getEnemyEntities().stream().findAny(); + public List getTargets() { + return targetUnits; } - public Optional getFiringUnit() { - // TODO implement this correctly - return Optional.ofNullable(agent.getCharacter().getEntityToFire(agent.getCharacter().getFireControlState())); + public Optional getCurrentUnit() { + return Optional.ofNullable(currentUnit); } - public Optional getCurrentUnit() { - // TODO implement this correctly - return getFiringUnit(); + public List getEnemyUnits() { + return world.getEnemyUnits(); } - // Decision Identifier - enum of what you are trying to do? - // Link to intelligence controller object - who is asking? - // Link to content data with parameters - what do you need? + public double getUnitMaxDamageAtRange(TARGETABLE unit, int enemyRange) { + String cacheKey = unit.hashCode() + "-" + enemyRange; + if (damageCache.containsKey(cacheKey)) { + return damageCache.get(cacheKey); + } - /* - * Example input - MyHealth - * class ConsiderationMyHealth extends Consideration { - * public float Score(DecisionContext context) { - * var intelligence = context.getIntelligence(); - * var character = intelligence.getCharacter(); - * return character.getHealth() / character.getMaxHealth(); - * } - */ + double maxDamage = calculateUnitMaxDamageAtRange(unit, enemyRange); + damageCache.put(cacheKey, maxDamage); + return maxDamage; + } + + public abstract double calculateUnitMaxDamageAtRange(TARGETABLE unit, int enemyRange); + + public void clearCaches() { + damageCache.clear(); + } + public abstract double getBonusFactor(DecisionContext lastContext); } diff --git a/megamek/src/megamek/ai/utility/DecisionMaker.java b/megamek/src/megamek/ai/utility/DecisionMaker.java new file mode 100644 index 00000000000..0c2a3c401f2 --- /dev/null +++ b/megamek/src/megamek/ai/utility/DecisionMaker.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import java.util.*; + +public interface DecisionMaker { + + default Optional> pickOne(PriorityQueue> scoredDecisions) { + if (scoredDecisions.isEmpty()) { + return Optional.empty(); + } + + if (scoredDecisions.size() == 1) { + return Optional.of(scoredDecisions.poll().getDecisionScoreEvaluator()); + } + + var decisionScoreEvaluators = new ArrayList>(); + if (getTopN() > 0) { + for (int i = 0; i < getTopN(); i++) { + var scoredDecision = Optional.ofNullable(scoredDecisions.poll()); + if (scoredDecision.isEmpty()) { + break; + } + decisionScoreEvaluators.add(scoredDecision.get().getDecisionScoreEvaluator()); + } + Collections.shuffle(decisionScoreEvaluators); + return Optional.of(decisionScoreEvaluators.get(0)); + } else { + scoredDecisions.stream().map(ScoredDecision::getDecisionScoreEvaluator).forEach(decisionScoreEvaluators::add); + } + + Collections.shuffle(decisionScoreEvaluators); + return Optional.of(decisionScoreEvaluators.get(0)); + } + + default int getTopN() { + return 1; + }; + + default void scoreAllDecisions(List> decisions, DecisionContext lastContext) { + + double cutoff = 0.0d; + for (var decision : decisions) { + double bonus = decision.getDecisionContext().getBonusFactor(lastContext); + if (bonus < cutoff) { + continue; + } + var decisionScoreEvaluator = decision.getDecisionScoreEvaluator(); + var score = decisionScoreEvaluator.score(decision.getDecisionContext(), getBonusFactor(decision), 0.0d); + decision.setScore(score); + } + } + + double getBonusFactor(Decision scoreEvaluator); + +} diff --git a/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java b/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java index 41cfc779101..0067cff7e16 100644 --- a/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java +++ b/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java @@ -15,55 +15,51 @@ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import static megamek.codeUtilities.MathUtility.clamp01; -public class DecisionScoreEvaluator { - // Represents the decision process - // Evaluates input via considerations - // Scores it - // if selected result into a decision - - // weight - Weight goes from 1.0 to 5.0 - // it is an implicit representation of priority - // 1.0 -> basic action (search enemy) - // 2.0 -> tactical movement (move to cover, move to flank, move to attack) - // 3.0 -> Special action usage (spot for indirect fire, LAM conversion) - // 4.0 -> Emergency (move to cover, - // 5.0 -> Emergency (move away from orbital strike) - // name - // description - // notes - - // considerations - // considerations may have parameters - // Aggregate all considerations into a single score - // multiply the score by the weight - - private AbstractAction action; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TWDecisionScoreEvaluator.class, name = "TWDecisionScoreEvaluator"), +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DecisionScoreEvaluator implements NamedObject { + private String name; private String description; private String notes; - private double weight; - private final List considerations = new ArrayList<>(); + private final List> considerations = new ArrayList<>(); + + public DecisionScoreEvaluator() { + // no-args constructor for Jackson + } - public DecisionScoreEvaluator(AbstractAction action, String description, String notes, double weight) { - this(action, description, notes, weight, Collections.emptyList()); + public DecisionScoreEvaluator(String name, String description, String notes) { + this(name, description, notes, Collections.emptyList()); } - public DecisionScoreEvaluator(AbstractAction action, String description, String notes, double weight, List considerations) { - this.action = action; + public DecisionScoreEvaluator(String name, String description, String notes, List> considerations) { + this.name = name; this.description = description; this.notes = notes; - this.weight = weight; this.considerations.addAll(considerations); } - public double score(DecisionContext context, double bonus, double min) { - + public double score(DecisionContext context, double bonus, double min) { var finalScore = bonus; + var considerationSize = getConsiderations().size(); for (var consideration : getConsiderations()) { if ((0.0f < finalScore) || (0.0f < min)) { @@ -75,19 +71,50 @@ public double score(DecisionContext context, double bonus, double min) { finalScore *= clamp01(response); } // adjustment - var modificationFactor = 1 - (1 / getConsiderations().size()); + var modificationFactor = 1 - (1 / considerationSize); var makeUpValue = (1 - finalScore) * modificationFactor; finalScore = finalScore + (makeUpValue * finalScore); return finalScore; } - public List getConsiderations() { + + public List> getConsiderations() { return considerations; } - public DecisionScoreEvaluator addConsideration(Consideration consideration) { + public DecisionScoreEvaluator addConsideration(Consideration consideration) { considerations.add(consideration); return this; } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } } diff --git a/megamek/src/megamek/ai/utility/DefaultCurve.java b/megamek/src/megamek/ai/utility/DefaultCurve.java new file mode 100644 index 00000000000..2cd896e5b9b --- /dev/null +++ b/megamek/src/megamek/ai/utility/DefaultCurve.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + + +public enum DefaultCurve { + + LinearIncreasing(new LinearCurve(1.0, 0)), + LinearDecreasing(new LinearCurve(-1.0, 1)), + ParabolicPositive(new ParabolicCurve(4.0, 0.5, 1.0)), + ParabolicNegative(new ParabolicCurve(-4.0, 0.5, 0.0)), + LogisticIncreasing(new LogisticCurve(1.0, 0.5, 10.0, 0.0)), + LogisticDecreasing(new LogisticCurve(1.0, 0.5, -10.0, 0.0)), + LogitIncreasing(new LogitCurve(1.0, 0.5, -15.0, 0.0)), + LogitDecreasing(new LogitCurve(1.0, 0.5, 15.0, 0.0)); + + private final Curve curve; + + DefaultCurve(Curve curve) { + this.curve = curve; + } + + public Curve getCurve() { + return curve; + } +} diff --git a/megamek/src/megamek/ai/utility/Intelligence.java b/megamek/src/megamek/ai/utility/Intelligence.java new file mode 100644 index 00000000000..fa4b49342fe --- /dev/null +++ b/megamek/src/megamek/ai/utility/Intelligence.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import java.util.List; +import java.util.Optional; +import java.util.PriorityQueue; + +public interface Intelligence { + + void update(Intelligence intelligence); + void addDecisionScoreEvaluator(Decision scoreEvaluator); + List> getDecisions(); + DecisionMaker getDecisionMaker(); + double getBonusFactor(Decision scoreEvaluator); + + default void clearDecisionScoreEvaluators() { + getDecisions().clear(); + } + + default void scoreAllDecisions(DecisionContext context) { + getDecisionMaker().scoreAllDecisions(getDecisions(), context); + } + + default Optional> pickOne(PriorityQueue> scoredDecisions) { + return getDecisionMaker().pickOne(scoredDecisions); + } +} diff --git a/megamek/src/megamek/ai/utility/LinearCurve.java b/megamek/src/megamek/ai/utility/LinearCurve.java index 3a33182f68a..fe9d844a18a 100644 --- a/megamek/src/megamek/ai/utility/LinearCurve.java +++ b/megamek/src/megamek/ai/utility/LinearCurve.java @@ -15,13 +15,24 @@ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.awt.*; +import java.util.StringJoiner; + import static megamek.codeUtilities.MathUtility.clamp01; +@JsonTypeName("LinearCurve") public class LinearCurve implements Curve { - private final double m; - private final double b; + private double m; + private double b; - public LinearCurve(double m, double b) { + @JsonCreator + public LinearCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b) { this.m = m; this.b = b; } @@ -29,4 +40,30 @@ public LinearCurve(double m, double b) { public double evaluate(double x) { return clamp01(m * x + b); } + + public double getM() { + return m; + } + + public double getB() { + return b; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public String toString() { + return new StringJoiner(", ", LinearCurve.class.getSimpleName() + "[", "]") + .add("m=" + m) + .add("b=" + b) + .toString(); + } } diff --git a/megamek/src/megamek/ai/utility/LogisticCurve.java b/megamek/src/megamek/ai/utility/LogisticCurve.java index cb6d36cfc14..749d5bcbeb1 100644 --- a/megamek/src/megamek/ai/utility/LogisticCurve.java +++ b/megamek/src/megamek/ai/utility/LogisticCurve.java @@ -15,15 +15,29 @@ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import java.awt.*; +import java.util.StringJoiner; + import static megamek.codeUtilities.MathUtility.clamp01; +@JsonTypeName("LogisticCurve") public class LogisticCurve implements Curve { - private final double m; - private final double b; - private final double k; - private final double c; + private double m; + private double b; + private double k; + private double c; - public LogisticCurve(double m, double b, double k, double c) { + @JsonCreator + public LogisticCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k, + @JsonProperty("c") double c) { this.m = m; this.b = b; this.k = k; @@ -31,7 +45,52 @@ public LogisticCurve(double m, double b, double k, double c) { } public double evaluate(double x) { - return clamp01(m / (1 + Math.exp(-k * (x - b))) + c); + return clamp01(m * (1 / (1 + Math.exp(-k * (x - b)))) + c); + } + + public double getM() { + return m; + } + + public double getB() { + return b; + } + + public double getK() { + return k; + } + + public double getC() { + return c; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public void setC(double c) { + this.c = c; + } + @Override + public String toString() { + return new StringJoiner(", ", LogisticCurve.class.getSimpleName() + "[", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .add("c=" + c) + .toString(); } } diff --git a/megamek/src/megamek/ai/utility/LogitCurve.java b/megamek/src/megamek/ai/utility/LogitCurve.java index 3bb84bf8ea5..f70cb08658d 100644 --- a/megamek/src/megamek/ai/utility/LogitCurve.java +++ b/megamek/src/megamek/ai/utility/LogitCurve.java @@ -15,15 +15,28 @@ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import java.util.StringJoiner; + import static megamek.codeUtilities.MathUtility.clamp01; +@JsonTypeName("LogitCurve") public class LogitCurve implements Curve { - private final double m; - private final double b; - private final double k; - private final double c; + private double m; + private double b; + private double k; + private double c; - public LogitCurve(double m, double b, double k, double c) { + @JsonCreator + public LogitCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k, + @JsonProperty("k") double c) { this.m = m; this.b = b; this.k = k; @@ -31,11 +44,58 @@ public LogitCurve(double m, double b, double k, double c) { } public double evaluate(double x) { - // Ensure p is in the valid range - // Typically, you want 0 < p < 1 to avoid division by zero or log of zero. - if (x - c == 0) { - return 0d; + if (x <= c) { + x = c + 0.0001; } - return clamp01(b - (1.0 / k) * Math.log((m - (x - c)) / (x - c))); + if (x >= m + c) { + x = m + c - 0.0001; + } + return clamp01(b + (1 / k) * Math.log((m - (x - c)) / (x - c))); + } + + public double getM() { + return m; + } + + public double getB() { + return b; + } + + public double getK() { + return k; + } + + public double getC() { + return c; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public void setC(double c) { + this.c = c; + } + + @Override + public String toString() { + return new StringJoiner(", ", LogitCurve.class.getSimpleName() + "[", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .add("c=" + c) + .toString(); } } diff --git a/megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java b/megamek/src/megamek/ai/utility/NamedObject.java similarity index 64% rename from megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java rename to megamek/src/megamek/ai/utility/NamedObject.java index c2b0e2ce201..54d332bb1c7 100644 --- a/megamek/src/megamek/client/ui/ai/editor/UtilityAiEditor.java +++ b/megamek/src/megamek/ai/utility/NamedObject.java @@ -13,12 +13,8 @@ * */ -package megamek.client.ui.ai.editor; +package megamek.ai.utility; -public class UtilityAiEditor { - // TODO Implement a Utility AI editor - // which allows you to create a "personality" - // Each personality has a set of Decision Score Evaluators - // Each Decision Score Evaluator has a single Action and multiple considerations - // +public interface NamedObject { + String getName(); } diff --git a/megamek/src/megamek/ai/utility/ParabolicCurve.java b/megamek/src/megamek/ai/utility/ParabolicCurve.java index 310f799b6c1..1698204871f 100644 --- a/megamek/src/megamek/ai/utility/ParabolicCurve.java +++ b/megamek/src/megamek/ai/utility/ParabolicCurve.java @@ -15,20 +15,69 @@ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import java.awt.*; +import java.util.StringJoiner; + import static megamek.codeUtilities.MathUtility.clamp01; +@JsonTypeName("ParabolicCurve") public class ParabolicCurve implements Curve { - private final double m; - private final double b; - private final double k; + private double m; + private double b; + private double k; - public ParabolicCurve(double m, double b, double k) { + @JsonCreator + public ParabolicCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k) { this.m = m; this.b = b; this.k = k; } public double evaluate(double x) { - return clamp01(m * Math.pow(x, 2) + k * x + b); + return clamp01(-m * Math.pow(x - b, 2) + k); + } + + public double getM() { + return m; + } + + public double getB() { + return b; + } + + public double getK() { + return k; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public String toString() { + return new StringJoiner(", ", ParabolicCurve.class.getSimpleName() + "[", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .toString(); } } diff --git a/megamek/src/megamek/ai/utility/Profile.java b/megamek/src/megamek/ai/utility/Profile.java index 1e8cc4bf6ec..55d0237fc8a 100644 --- a/megamek/src/megamek/ai/utility/Profile.java +++ b/megamek/src/megamek/ai/utility/Profile.java @@ -15,28 +15,43 @@ package megamek.ai.utility; -import java.util.List; +import com.fasterxml.jackson.annotation.*; +import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; -public class Profile { +import java.util.List; +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TWProfile.class, name = "TWProfile"), +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Profile implements NamedObject { + @JsonProperty("id") + private final int id; + @JsonProperty("name") private final String name; + @JsonProperty("description") private String description; - private final List decisionScoreEvaluators; + @JsonProperty("decisionScoreEvaluator") + private final List> decisionScoreEvaluator; - public Profile(String name, String description, List decisionScoreEvaluators) { + @JsonCreator + public Profile(int id, String name, String description, List> decisionScoreEvaluator) { + this.id = id; this.name = name; this.description = description; - this.decisionScoreEvaluators = decisionScoreEvaluators; + this.decisionScoreEvaluator = decisionScoreEvaluator; } + @Override public String getName() { return name; } - public List getDecisionScoreEvaluators() { - return decisionScoreEvaluators; - } - public String getDescription() { return description; } @@ -44,4 +59,12 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public int getId() { + return id; + } + + public List> getDecisionScoreEvaluator() { + return decisionScoreEvaluator; + } } diff --git a/megamek/src/megamek/ai/utility/ScoredDecision.java b/megamek/src/megamek/ai/utility/ScoredDecision.java new file mode 100644 index 00000000000..a863625fa95 --- /dev/null +++ b/megamek/src/megamek/ai/utility/ScoredDecision.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import java.util.Comparator; + + +public class ScoredDecision implements Comparator> { + + private final double score; + private final Decision scoreEvaluator; + + private ScoredDecision(double score, Decision scoreEvaluator) { + this.score = score; + this.scoreEvaluator = scoreEvaluator; + } + + public double getScore() { + return score; + } + + public Decision getDecisionScoreEvaluator() { + return scoreEvaluator; + } + + @Override + public int compare(ScoredDecision o1, ScoredDecision o2) { + // Scores are negative because the priority queue is a min heap and I want max heap + return Double.compare(-o1.getScore(), -o2.getScore()); + } + + public static ScoredDecision of(double score, Decision scoreEvaluator) { + return new ScoredDecision<>(score, scoreEvaluator); + } +} diff --git a/megamek/src/megamek/ai/utility/World.java b/megamek/src/megamek/ai/utility/World.java new file mode 100644 index 00000000000..639fabc1b41 --- /dev/null +++ b/megamek/src/megamek/ai/utility/World.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import megamek.common.InGameObject; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public interface World { + + List getInGameObjects(); + + Map getTeamByPlayer(); + + @SuppressWarnings("unchecked") + default void rebuildStateForPlayer(int playerId) { + var teamByPlayer = getTeamByPlayer(); + var teamId = teamByPlayer.get(playerId); + + var myUnits = getInGameObjects() + .stream() + .filter(i -> i.getOwnerId() == playerId) + .map(i -> (IN_GAME_OBJECT) i) + .toList(); + + setMyUnits(myUnits); + + var alliedUnits = getInGameObjects().stream() + .filter(e -> e.getOwnerId() != playerId) + .filter(e -> (teamByPlayer.getOrDefault(e.getOwnerId(), 0) != 0) + && Objects.equals(teamByPlayer.getOrDefault(e.getOwnerId(), 0), teamId)) + .map(e -> (TARGETABLE) e) + .toList(); + + setAlliedUnits(alliedUnits); + + var enemyUnits = getInGameObjects().stream() + .filter(e -> e.getOwnerId() != playerId) + .filter(e -> (teamByPlayer.getOrDefault(e.getOwnerId(), 0) == 0) + || !Objects.equals(teamByPlayer.getOrDefault(e.getOwnerId(), 0), teamId)) + .map(e -> (TARGETABLE) e) + .toList(); + + setEnemyUnits(enemyUnits); + } + + void setMyUnits(List myUnits); + void setAlliedUnits(List alliedUnits); + void setEnemyUnits(List enemyUnits); + + List getMyUnits(); + List getAlliedUnits(); + List getEnemyUnits(); + + boolean useBooleanOption(String option); +} diff --git a/megamek/src/megamek/ai/utility/dummy/DummyAgent.java b/megamek/src/megamek/ai/utility/dummy/DummyAgent.java new file mode 100644 index 00000000000..2b858285a4c --- /dev/null +++ b/megamek/src/megamek/ai/utility/dummy/DummyAgent.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility.dummy; + +import megamek.ai.utility.Agent; +import megamek.ai.utility.Intelligence; +import megamek.ai.utility.World; +import megamek.client.IClient; +import megamek.client.bot.princess.Princess; +import megamek.common.Entity; +import megamek.logging.MMLogger; + +/** + * Dummy agent for testing purposes. + */ +public class DummyAgent implements Agent { + private static final MMLogger logger = MMLogger.create(DummyAgent.class); + + private final World world; + private final IClient client; + private final Intelligence intelligence; + + public DummyAgent(World world, IClient client, Intelligence intelligence) { + this.world = world; + this.client = client; + this.intelligence = intelligence; + } + + @Override + public int getId() { + return client.getLocalPlayerNumber(); + } + + @Override + public World getContext() { + return world; + } + + @Override + public Intelligence getIntelligence() { + return intelligence; + } + + @Override + public IClient getClient() { + return client; + } + +} diff --git a/megamek/src/megamek/ai/utility/dummy/DummyClient.java b/megamek/src/megamek/ai/utility/dummy/DummyClient.java new file mode 100644 index 00000000000..4bd219f1781 --- /dev/null +++ b/megamek/src/megamek/ai/utility/dummy/DummyClient.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility.dummy; + +import megamek.client.AbstractClient; +import megamek.client.IClient; +import megamek.common.IGame; +import megamek.logging.MMLogger; + +import java.util.Map; + +/** + * Dummy client for testing purposes. + */ +public class DummyClient implements IClient { + private static final MMLogger logger = MMLogger.create(DummyClient.class); + private final IGame game; + private int localPlayerNumber = -1; + private boolean myTurn; + + public DummyClient(IGame game) { + this.game = game; + } + + @Override + public String getName() { + return "DummyClient"; + } + + @Override + public IGame getGame() { + return game; + } + + @Override + public int getLocalPlayerNumber() { + return localPlayerNumber; + } + + public void setMyTurn(boolean myTurn) { + this.myTurn = myTurn; + } + + @Override + public boolean isMyTurn() { + return myTurn; + } + + @Override + public void setLocalPlayerNumber(int localPlayerNumber) { + this.localPlayerNumber = localPlayerNumber; + } + + @Override + public void sendChat(String message) { + logger.debug("Chat: {}", message); + } + + @Override + public int getPort() { + return 0; + } + + @Override + public String getHost() { + return "0.0.0.0"; + } + + @Override + public void die() { + // NOT IMPLEMENTED + } + + @Override + public boolean connect() { + return false; + } + + @Override + public Map getBots() { + // NOT IMPLEMENTED + return Map.of(); + } + + @Override + public void sendDone(boolean done) { + // NOT IMPLEMENTED + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/Duchess.java b/megamek/src/megamek/client/bot/duchess/Duchess.java index 5c12ad12899..8f0d569c9ce 100644 --- a/megamek/src/megamek/client/bot/duchess/Duchess.java +++ b/megamek/src/megamek/client/bot/duchess/Duchess.java @@ -14,89 +14,48 @@ package megamek.client.bot.duchess; -import megamek.client.bot.PhysicalOption; +import megamek.ai.utility.Agent; +import megamek.ai.utility.Intelligence; +import megamek.client.bot.duchess.ai.utility.tw.context.TWWorld; import megamek.client.bot.princess.Princess; -import megamek.common.Coords; import megamek.common.Entity; -import megamek.common.Minefield; -import megamek.common.MovePath; -import megamek.common.event.GamePlayerChatEvent; +import megamek.common.Game; import megamek.logging.MMLogger; -import java.util.Vector; - -public class Duchess extends Princess{ +public class Duchess implements Agent { private static final MMLogger logger = MMLogger.create(Duchess.class); - /** - * Constructor - initializes a new instance of the Princess bot. - * - * @param name The display name. - * @param host The host address to which to connect. - * @param port The port on the host where to connect. - */ - public Duchess(String name, String host, int port) { - super(name, host, port); - } - - - @Override - public void initialize() { - - } - - @Override - protected void processChat(GamePlayerChatEvent ge) { - - } - - @Override - protected void initMovement() { - - } - - @Override - protected void initFiring() { - - } + private final Intelligence intelligence; + private final TWWorld world; + private final Princess princess; - @Override - protected MovePath calculateMoveTurn() { - return null; + public Duchess(Game game, Intelligence intelligence, Princess princess) { + this.world = new TWWorld(game); + this.intelligence = intelligence; + this.princess = princess; } @Override - protected void calculateFiringTurn() { - + public int getId() { + return princess.getLocalPlayerNumber(); } @Override - protected void calculateDeployment() { - + public TWWorld getContext() { + return world; } @Override - protected PhysicalOption calculatePhysicalTurn() { - return null; + public Intelligence getIntelligence() { + return intelligence; } - @Override - protected MovePath continueMovementFor(Entity entity) { - return null; + public TWWorld getWorld() { + return world; } @Override - protected Vector calculateMinefieldDeployment() { - return null; - } - - @Override - protected Vector calculateArtyAutoHitHexes() { - return null; - } - - @Override - protected void checkMorale() { - + public Princess getClient() { + return princess; } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java new file mode 100644 index 00000000000..6824fe5af5a --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; +import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; +import megamek.common.Configuration; +import megamek.common.util.fileUtils.MegaMekFile; +import megamek.logging.MMLogger; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class TWUtilityAIRepository { + private static final MMLogger logger = MMLogger.create(TWUtilityAIRepository.class); + + private static TWUtilityAIRepository instance; + + public static final String EVALUATORS = "evaluators"; + public static final String CONSIDERATIONS = "considerations"; + public static final String DECISIONS = "decisions"; + public static final String PROFILES = "profiles"; + private final ObjectMapper mapper; + + private final Map profiles = new HashMap<>(); + private final Map decisions = new HashMap<>(); + private final Map considerations = new HashMap<>(); + private final Map decisionScoreEvaluators = new HashMap<>(); + + public static TWUtilityAIRepository getInstance() { + if (instance == null) { + instance = new TWUtilityAIRepository(); + } + return instance; + } + + private TWUtilityAIRepository() { + mapper = new ObjectMapper(new YAMLFactory()); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); + initialize(); + } + + private void initialize() { +// persistData(); + loadConsiderations(new MegaMekFile(Configuration.twAiDir(), CONSIDERATIONS).getFile()) + .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); + loadDecisionScoreEvaluators(new MegaMekFile(Configuration.twAiDir(), EVALUATORS).getFile()).forEach( + twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); + loadDecisions(new MegaMekFile(Configuration.twAiDir(), DECISIONS).getFile()).forEach( + twDecision -> decisions.put(twDecision.getName(), twDecision)); + loadProfiles(new MegaMekFile(Configuration.twAiDir(), PROFILES).getFile()).forEach( + twProfile -> profiles.put(twProfile.getName(), twProfile)); + persistDataToUserData(); + } + + public void reloadRepository() { + decisionScoreEvaluators.clear(); + considerations.clear(); + profiles.clear(); + decisions.clear(); + initialize(); + } + + public List getDecisions() { + return List.copyOf(decisions.values()); + } + + public List getConsiderations() { + return List.copyOf(considerations.values()); + } + + public List getDecisionScoreEvaluators() { + return List.copyOf(decisionScoreEvaluators.values()); + } + + public List getProfiles() { + return List.copyOf(profiles.values()); + } + + public boolean addDecision(TWDecision decision) { + if (decisions.containsKey(decision.getName())) { + logger.warn("Decision with name {} already exists", decision.getName()); + return false; + } + decisions.put(decision.getName(), decision); + return true; + } + + public boolean addConsideration(TWConsideration consideration) { + if (considerations.containsKey(consideration.getName())) { + logger.warn("Consideration with name {} already exists", consideration.getName()); + return false; + } + considerations.put(consideration.getName(), consideration); + return true; + } + + public boolean addDecisionScoreEvaluator(TWDecisionScoreEvaluator decisionScoreEvaluator) { + if (decisionScoreEvaluators.containsKey(decisionScoreEvaluator.getName())) { + logger.warn("DecisionScoreEvaluator with name {} already exists", decisionScoreEvaluator.getName()); + return false; + } + decisionScoreEvaluators.put(decisionScoreEvaluator.getName(), decisionScoreEvaluator); + return true; + } + + public boolean addProfile(TWProfile profile) { + if (profiles.containsKey(profile.getName())) { + logger.warn("Profile with name {} already exists", profile.getName()); + return false; + } + profiles.put(profile.getName(), profile); + return true; + } + + private void persistToFile(File outputFile, Collection objects) { + if (objects.isEmpty()) { + return; + } + try (SequenceWriter seqWriter = mapper.writer().writeValues(outputFile)) { + for (var object : objects) { + seqWriter.write(object); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private List loadDecisionScoreEvaluators(File inputFile) { + return loadObjects(inputFile, TWDecisionScoreEvaluator.class); + } + + private List loadDecisions(File inputFile) { + return loadObjects(inputFile, TWDecision.class); + } + + private List loadConsiderations(File inputFile) { + return loadObjects(inputFile, TWConsideration.class); + } + private List loadProfiles(File inputFile) { + return loadObjects(inputFile, TWProfile.class); + } + + private List loadObjects(File inputFile, Class clazz) { + List objects = new ArrayList<>(); + + if (inputFile.isDirectory()) { + var files = inputFile.listFiles(); + if (files != null) { + for (var file : files) { + if (file.isFile()) { + try (MappingIterator it = mapper.readerFor(clazz).readValues(file)) { + objects.addAll(it.readAll()); + } catch (IOException e) { + logger.error(e, "Could not load file: {}", file); + } + } + } + } + } else { + logger.formattedErrorDialog("Invalid directory", "Input file {} is not a directory", inputFile); + } + logger.info("Loaded {} objects of type {} from directory {}", objects.size(), clazz.getSimpleName(), inputFile); + return objects; + } + + public void persistData() { + var twAiDir = Configuration.twAiDir(); + createDirectoryStructureIfMissing(twAiDir); + persistToFile(new File(twAiDir, EVALUATORS + File.separator + "decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(twAiDir, CONSIDERATIONS + File.separator + "considerations.yaml"), considerations.values()); + persistToFile(new File(twAiDir, DECISIONS + File.separator + "decisions.yaml"), decisions.values()); + persistToFile(new File(twAiDir, PROFILES + File.separator + "profiles.yaml"), profiles.values()); + } + + public void persistDataToUserData() { + var userDataAiTwDir = Configuration.userDataAiTwDir(); + createDirectoryStructureIfMissing(userDataAiTwDir); + persistToFile(new File(userDataAiTwDir, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(userDataAiTwDir, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values()); + persistToFile(new File(userDataAiTwDir, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values()); + persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); + } + + private void createDirectoryStructureIfMissing(File directory) { + createDirIfNecessary(directory); + + var dirs = List.of(new File(directory, EVALUATORS), + new File(directory, CONSIDERATIONS), + new File(directory, DECISIONS), + new File(directory, PROFILES)); + + for (var dir : dirs) { + createDirIfNecessary(dir); + } + } + + private static void createDirIfNecessary(File dir) { + if (!dir.exists()) { + if (!dir.mkdirs()) { + logger.error("Could not create directory: {}", dir); + } + } + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java similarity index 60% rename from megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java rename to megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java index 5aea0b3ef22..468da618e17 100644 --- a/megamek/src/megamek/client/bot/duchess/considerations/MyUnitArmor.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java @@ -13,33 +13,26 @@ * */ -package megamek.client.bot.duchess.considerations; +package megamek.client.bot.duchess.ai.utility.tw.considerations; -import megamek.ai.utility.Consideration; -import megamek.ai.utility.Curve; +import com.fasterxml.jackson.annotation.JsonTypeName; import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; import static megamek.codeUtilities.MathUtility.clamp01; /** * This consideration is used to determine if a target is an easy target. */ -public class MyUnitArmor extends Consideration { +@JsonTypeName("MyUnitArmor") +public class MyUnitArmor extends TWConsideration { - - protected MyUnitArmor(Curve curve) { - super(curve); + public MyUnitArmor() { } @Override - public double score(DecisionContext context) { - var currentUnit = context.getCurrentUnit(); - if (currentUnit.isEmpty()) { - return 0d; - } - - var currentEntity = currentUnit.get(); - - return clamp01(currentEntity.getArmorRemainingPercent()); + public double score(DecisionContext context) { + var currentUnit = context.getCurrentUnit().orElseThrow(); + return clamp01(currentUnit.getArmorRemainingPercent()); } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitUnderThreat.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitUnderThreat.java new file mode 100644 index 00000000000..e749889a968 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitUnderThreat.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; + +import java.util.HashMap; +import java.util.Map; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if a target is an easy target. + */ +@JsonTypeName("MyUnitUnderThreat") +public class MyUnitUnderThreat extends TWConsideration { + + public MyUnitUnderThreat() { + } + + @Override + public double score(DecisionContext context) { + var currentUnit = context.getCurrentUnit().orElseThrow(); + Map threat = new HashMap<>(); + + for (var enemyUnit : context.getEnemyUnits()) { + var enemyRange = enemyUnit.getPosition().distance(currentUnit.getPosition()); + if (enemyUnit.getPosition().distance(currentUnit.getPosition()) <= enemyUnit.getMaxWeaponRange()) { + var maxDamage = context.getUnitMaxDamageAtRange(enemyUnit, enemyRange); + threat.put(enemyRange, threat.getOrDefault(enemyRange, 0d) + maxDamage); + } + } + + var maxThreat = threat.values().stream().max(Double::compareTo).orElse(0d); + + return clamp01( maxThreat / currentUnit.getTotalArmor()); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java new file mode 100644 index 00000000000..a0803ac69e7 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import megamek.ai.utility.Consideration; +import megamek.ai.utility.Curve; +import megamek.common.Entity; + +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class TWConsideration extends Consideration { + + public TWConsideration() { + } + + public TWConsideration(String name) { + super(name); + } + + public TWConsideration(String name, Curve curve) { + super(name, curve); + } + + public TWConsideration(String name, Curve curve, Map parameters) { + super(name, curve, parameters); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetWithinOptimalRange.java similarity index 51% rename from megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java rename to megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetWithinOptimalRange.java index 5dca0561b22..5d7de5464a9 100644 --- a/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinOptimalRange.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetWithinOptimalRange.java @@ -13,44 +13,33 @@ * */ -package megamek.client.bot.duchess.considerations; +package megamek.client.bot.duchess.ai.utility.tw.considerations; -import megamek.ai.utility.Consideration; -import megamek.ai.utility.Curve; +import com.fasterxml.jackson.annotation.JsonTypeName; import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; import static megamek.codeUtilities.MathUtility.clamp01; /** * This consideration is used to determine if a target is an easy target. */ -public class TargetWithinOptimalRange extends Consideration { +@JsonTypeName("TargetWithinOptimalRange") +public class TargetWithinOptimalRange extends TWConsideration { - - protected TargetWithinOptimalRange(Curve curve) { - super(curve); + public TargetWithinOptimalRange() { } @Override - public double score(DecisionContext context) { - var target = context.getTarget(); - var firingUnit = context.getFiringUnit(); - - if (target.isEmpty() || firingUnit.isEmpty()) { - return 0d; - } - - var targetEntity = target.get(); - var firingEntity = firingUnit.get(); - - if (!firingEntity.hasFiringSolutionFor(targetEntity.getId())) { - return 0d; - } - - var distance = firingEntity.getPosition().distance(targetEntity.getPosition()); - - var maxRange = firingEntity.getMaxWeaponRange(); - var bestRange = firingEntity.getOptimalRange(); + public double score(DecisionContext context) { + var targets = context.getTargets(); + var firingUnit = context.getCurrentUnit().orElseThrow(); + var distance = targets.stream().map(Entity::getPosition) + .mapToInt(coords -> firingUnit.getPosition().distance(coords)).max() + .orElse(Integer.MAX_VALUE);; + + var maxRange = firingUnit.getMaxWeaponRange(); + var bestRange = firingUnit.getOptimalRange(); if (distance <= bestRange) { return 1d; diff --git a/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetWithinRange.java similarity index 51% rename from megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java rename to megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetWithinRange.java index 42840cbdc2c..6adefe05f33 100644 --- a/megamek/src/megamek/client/bot/duchess/considerations/TargetWithinRange.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetWithinRange.java @@ -13,42 +13,35 @@ * */ -package megamek.client.bot.duchess.considerations; +package megamek.client.bot.duchess.ai.utility.tw.considerations; -import megamek.ai.utility.Consideration; -import megamek.ai.utility.Curve; +import com.fasterxml.jackson.annotation.JsonTypeName; import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; import static megamek.codeUtilities.MathUtility.clamp01; /** * This consideration is used to determine if a target is an easy target. */ -public class TargetWithinRange extends Consideration { +@JsonTypeName("TargetWithinRange") +public class TargetWithinRange extends TWConsideration { - - protected TargetWithinRange(Curve curve) { - super(curve); + public TargetWithinRange() { } @Override - public double score(DecisionContext context) { - var target = context.getTarget(); - var firingUnit = context.getFiringUnit(); - - if (target.isEmpty() || firingUnit.isEmpty()) { - return 0d; - } - - var targetEntity = target.get(); - var firingEntity = firingUnit.get(); + public double score(DecisionContext context) { + var targets = context.getTargets(); + var firingUnit = context.getCurrentUnit().orElseThrow(); + var maxRange = firingUnit.getMaxWeaponRange(); - if (!firingEntity.hasFiringSolutionFor(targetEntity.getId())) { - return 0d; - } + var distance = targets.stream() + .map(Entity::getPosition) + .mapToInt(c -> c.distance(firingUnit.getPosition())) + .max() + .orElse(Integer.MAX_VALUE); - var distance = firingEntity.getPosition().distance(targetEntity.getPosition()); - var maxRange = firingEntity.getMaxWeaponRange(); return clamp01(1.00001d - (double) distance / maxRange); } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/context/TWWorld.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/context/TWWorld.java new file mode 100644 index 00000000000..155527785f8 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/context/TWWorld.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.context; + +import megamek.ai.utility.World; +import megamek.common.Entity; +import megamek.common.Game; +import megamek.common.InGameObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class TWWorld implements World { + + private final Game game; + private final List myUnits = new ArrayList<>(); + private final List alliedUnits = new ArrayList<>(); + private final List enemyUnits = new ArrayList<>(); + + public TWWorld(Game game) { + this.game = game; + } + + @Override + public List getInGameObjects() { + return game.getInGameObjects(); + } + + @Override + public Map getTeamByPlayer() { + return game.getTeamByPlayer(); + } + + @Override + public void setMyUnits(List myUnits) { + this.myUnits.clear(); + this.myUnits.addAll(myUnits); + } + + @Override + public void setAlliedUnits(List alliedUnits) { + this.alliedUnits.clear(); + this.alliedUnits.addAll(alliedUnits); + } + + @Override + public void setEnemyUnits(List enemyUnits) { + this.enemyUnits.clear(); + this.enemyUnits.addAll(enemyUnits); + } + + @Override + public List getMyUnits() { + return myUnits; + } + + @Override + public List getAlliedUnits() { + return alliedUnits; + } + + @Override + public List getEnemyUnits() { + return enemyUnits; + } + + @Override + public boolean useBooleanOption(String option) { + return game.getOptions().booleanOption(option); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java new file mode 100644 index 00000000000..aa47cd19e3a --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.decision; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.Action; +import megamek.ai.utility.Decision; +import megamek.ai.utility.DecisionScoreEvaluator; +import megamek.common.Entity; + +@JsonTypeName("TWDecision") +public class TWDecision extends Decision { + + public TWDecision() { + } + + public TWDecision(Action action, double weight, DecisionScoreEvaluator decisionScoreEvaluator) { + super(action, weight, decisionScoreEvaluator); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionContext.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionContext.java new file mode 100644 index 00000000000..1ae988fb9cc --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionContext.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ +package megamek.client.bot.duchess.ai.utility.tw.decision; + +import megamek.ai.utility.Agent; +import megamek.ai.utility.DecisionContext; +import megamek.ai.utility.World; +import megamek.common.Entity; +import megamek.common.options.OptionsConstants; + +import java.util.*; + +import static megamek.client.bot.princess.FireControl.getMaxDamageAtRange; + + +public class TWDecisionContext extends DecisionContext { + + public TWDecisionContext(Agent agent, World world) { + super(agent, world); + } + + public TWDecisionContext(Agent agent, World world, Entity currentUnit) { + super(agent, world, currentUnit); + } + + public TWDecisionContext(Agent agent, World world, Entity currentUnit, List targetUnits) { + super(agent, world, currentUnit, targetUnits); + } + + @Override + public double calculateUnitMaxDamageAtRange(Entity unit, int enemyRange) { + return getMaxDamageAtRange(unit, enemyRange, + getWorld().useBooleanOption(OptionsConstants.ADVCOMBAT_TACOPS_LOS_RANGE), + getWorld().useBooleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RANGE)); + } + + @Override + public double getBonusFactor(DecisionContext lastContext) { + return 0; + } +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java new file mode 100644 index 00000000000..639fc5be461 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.decision; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.Consideration; +import megamek.ai.utility.DecisionScoreEvaluator; +import megamek.common.Entity; + +import java.util.List; + +@JsonTypeName("TWDecisionScoreEvaluator") +@JsonIgnoreProperties(ignoreUnknown = true) +public class TWDecisionScoreEvaluator extends DecisionScoreEvaluator { + public TWDecisionScoreEvaluator() { + } + + public TWDecisionScoreEvaluator(String name, String description, String notes) { + super(name, description, notes); + } + + public TWDecisionScoreEvaluator(String name, String description, String notes, List> considerations) { + super(name, description, notes, considerations); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/intelligence/SimpleIntelligence.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/intelligence/SimpleIntelligence.java new file mode 100644 index 00000000000..b47b4b0cfdd --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/intelligence/SimpleIntelligence.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.intelligence; + +import megamek.ai.utility.Decision; +import megamek.ai.utility.DecisionMaker; +import megamek.ai.utility.Intelligence; +import megamek.ai.utility.ScoredDecision; +import megamek.common.Entity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.PriorityQueue; + +/** + * SimpleIntelligence does not implement memory or learning in any way, has no stickyness for decisions, and does not + * implement any special behaviors, uses BEST for decision process. + * + * @author Luana Coppio + */ +public class SimpleIntelligence implements Intelligence { + + private final List> decisions = new ArrayList<>(); + private final DecisionMaker decisionMaker = new DecisionMaker<>() { + @Override + public Optional> pickOne(PriorityQueue> scoredDecisions) { + return DecisionMaker.super.pickOne(scoredDecisions); + } + + @Override + public double getBonusFactor(Decision scoreEvaluator) { + return 0; + } + }; + + private SimpleIntelligence() { + // empty constructor + } + + @Override + public void update(Intelligence intelligence) { + // there is nothing to update + } + + @Override + public void addDecisionScoreEvaluator(Decision decision) { + decisions.add(decision); + } + + @Override + public List> getDecisions() { + return decisions; + } + + @Override + public DecisionMaker getDecisionMaker() { + return decisionMaker; + } + + @Override + public double getBonusFactor(Decision scoreEvaluator) { + return 0; + } + + public static SimpleIntelligence create() { + return new SimpleIntelligence(); + } +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java new file mode 100644 index 00000000000..7c91aa3522d --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.profile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionScoreEvaluator; +import megamek.ai.utility.Profile; +import megamek.common.Entity; + +import java.util.List; + +@JsonTypeName("TWProfile") +@JsonIgnoreProperties(ignoreUnknown = true) +public class TWProfile extends Profile { + @JsonCreator + public TWProfile( + @JsonProperty("id") int id, + @JsonProperty("name") String name, + @JsonProperty("description") String description, + @JsonProperty("decisionScoreEvaluator") List> decisionScoreEvaluator) + { + super(id, name, description, decisionScoreEvaluator); + } +} diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index bf4d3b86fa7..7eacf5ecc0a 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -1960,7 +1960,7 @@ protected void setAttackAsAimedOrCalled (WeaponFireInfo shot, /** * Gets an entity eligible to fire from a list contained in the fire control state. */ - Entity getEntityToFire(FireControlState fireControlState) { + public Entity getEntityToFire(FireControlState fireControlState) { if (fireControlState.getOrderedFiringEntities().isEmpty()) { initFiringEntities(fireControlState); } diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index a86cb9dd081..a21f69e9f71 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -139,7 +139,8 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, public static final String BOARD_REMOVE_WATER = "boardRemoveWater"; public static final String BOARD_REMOVE_BUILDINGS = "boardRemoveBuildings"; public static final String BOARD_FLATTEN = "boardFlatten"; - + // AI submenu + public static final String NEW_AI = "fileAiNew"; // unit list submenu public static final String FILE_UNITS_REINFORCE = "fileUnitsReinforce"; public static final String FILE_UNITS_REINFORCE_RAT = "fileUnitsReinforceRAT"; diff --git a/megamek/src/megamek/client/ui/swing/ExitsDialog.java b/megamek/src/megamek/client/ui/swing/ExitsDialog.java index 85d4d478622..5ea035197f8 100644 --- a/megamek/src/megamek/client/ui/swing/ExitsDialog.java +++ b/megamek/src/megamek/client/ui/swing/ExitsDialog.java @@ -54,7 +54,7 @@ public class ExitsDialog extends JDialog implements ActionListener { private JPanel panExits = new JPanel(new BorderLayout()); private JButton butDone = new JButton(Messages.getString("BoardEditor.Done")); - ExitsDialog(JFrame frame) { + public ExitsDialog(JFrame frame) { super(frame, Messages.getString("BoardEditor.SetExits"), true); setResizable(false); butDone.addActionListener(this); @@ -140,7 +140,7 @@ JToggleButton setupTButton(String iconName, String buttonName) { button.setMargin(new Insets(0, 0, 0, 0)); button.setBorder(BorderFactory.createEmptyBorder()); - + return button; } } diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index f55bf6535a3..9414745cca2 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -31,6 +31,8 @@ import megamek.client.ui.dialogs.BotConfigDialog; import megamek.client.ui.dialogs.helpDialogs.MMReadMeHelpDialog; import megamek.client.ui.enums.DialogResult; +import megamek.client.ui.swing.ai.editor.AiProfileEditor; +import megamek.client.ui.swing.ai.editor.UtilityAiEditor; import megamek.client.ui.swing.dialog.MainMenuUnitBrowserDialog; import megamek.client.ui.swing.gameConnectionDialogs.ConnectDialog; import megamek.client.ui.swing.gameConnectionDialogs.HostDialog; @@ -241,6 +243,11 @@ private void showMainMenu() { UIComponents.MainMenuButton.getComp(), true); skinEditB.setActionCommand(ClientGUI.MAIN_SKIN_NEW); skinEditB.addActionListener(actionListener); + MegaMekButton editAi = new MegaMekButton(Messages.getString("MegaMek.AiEditor.label"), + UIComponents.MainMenuButton.getComp(), true); + editAi.setActionCommand(ClientGUI.NEW_AI); + editAi.addActionListener(actionListener); + MegaMekButton quitB = new MegaMekButton(Messages.getString("MegaMek.Quit.label"), UIComponents.MainMenuButton.getComp(), true); quitB.setActionCommand(ClientGUI.MAIN_QUIT); @@ -273,8 +280,9 @@ private void showMainMenu() { skinEditB.setPreferredSize(minButtonDim); scenB.setPreferredSize(minButtonDim); loadB.setPreferredSize(minButtonDim); - quitB.setPreferredSize(minButtonDim); + editAi.setPreferredSize(minButtonDim); hostB.setPreferredSize(minButtonDim); + quitB.setPreferredSize(minButtonDim); connectB.setMinimumSize(minButtonDim); botB.setMinimumSize(minButtonDim); @@ -282,6 +290,7 @@ private void showMainMenu() { skinEditB.setMinimumSize(minButtonDim); scenB.setMinimumSize(minButtonDim); loadB.setMinimumSize(minButtonDim); + editAi.setMinimumSize(minButtonDim); quitB.setMinimumSize(minButtonDim); // layout @@ -331,6 +340,8 @@ private void showMainMenu() { c.gridy++; addBag(skinEditB, gridbag, c); c.gridy++; + addBag(editAi, gridbag, c); + c.gridy++; c.insets = new Insets(4, 4, 15, 12); addBag(quitB, gridbag, c); frame.validate(); @@ -339,6 +350,15 @@ private void showMainMenu() { frame.setLocationRelativeTo(null); } + /** + * Display the AI editor. + */ + void showAiEditor() { + AiProfileEditor editor = new AiProfileEditor(controller); + controller.aiEditor = editor; + launch(editor.getFrame()); + } + /** * Display the board editor. */ @@ -1021,6 +1041,9 @@ void unlaunch() { case ClientGUI.BOARD_NEW: showEditor(); break; + case ClientGUI.NEW_AI: + showAiEditor(); + break; case ClientGUI.MAIN_SKIN_NEW: showSkinEditor(); break; diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form new file mode 100644 index 00000000000..57ae06bb606 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form @@ -0,0 +1,295 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java new file mode 100644 index 00000000000..e7e51124fb7 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import com.intellij.uiDesigner.core.Spacer; +import megamek.ai.utility.Action; +import megamek.ai.utility.NamedObject; +import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; +import megamek.client.ui.swing.GUIPreferences; +import megamek.client.ui.swing.util.MegaMekController; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import java.awt.*; +import java.util.List; + +public class AiProfileEditor extends JFrame { + private final TWUtilityAIRepository sharedData = TWUtilityAIRepository.getInstance(); + private final GUIPreferences guip = GUIPreferences.getInstance(); + private final MegaMekController controller; + + private JButton newDecisionButton; + private JTree repositoryViewer; + private JTabbedPane dseEditorPane; + private JPanel dseConfigPane; + private JTextField nameDseTextField; + private JTextField descriptionDseTextField; + private JScrollPane considerationsScrollPane; + private JPanel considerationsPane; + private JTextField notesTextField; + private JTextField descriptionTextField; + private JTextField profileNameTextField; + private JButton newConsiderationButton; + private JPanel profilePane; + private JTable decisionScoreEvaluatorTable; + private JPanel decisionPane; + private JComboBox actionComboBox; + private JSpinner weightSpinner; + private JScrollPane evaluatorScrollPane; + private JPanel uAiEditorPanel; + + public AiProfileEditor(MegaMekController controller) { + this.controller = controller; + $$$setupUI$$$(); + initialize(); + setTitle("AI Profile Editor"); + setSize(1200, 800); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setContentPane(uAiEditorPanel); + setVisible(true); + } + + private void initialize() { + considerationsScrollPane.setViewportView(new ConsiderationPane()); + evaluatorScrollPane.setViewportView(new DecisionScoreEvaluatorPane()); + newDecisionButton.addActionListener(e -> { + var action = (Action) actionComboBox.getSelectedItem(); + var weight = (double) weightSpinner.getValue(); + var dse = new TWDecision(action, weight, sharedData.getDecisionScoreEvaluators().get(0)); + var model = decisionScoreEvaluatorTable.getModel(); + //noinspection unchecked + ((DecisionScoreEvaluatorTableModel) model).addRow(dse); + }); + } + + private void persistProfile() { + var model = (DecisionScoreEvaluatorTableModel) decisionScoreEvaluatorTable.getModel(); + var updatedList = model.getDecisions(); + System.out.println("== Updated DecisionScoreEvaluator List =="); + for (int i = 0; i < updatedList.size(); i++) { + var dse = updatedList.get(i); + System.out.printf("Row %d -> Decision: %s, Evaluator: %s%n", + i, + dse.getAction().getActionName(), + dse.getDecisionScoreEvaluator().getName()); + } + } + + public JFrame getFrame() { + return this; + } + + private void createUIComponents() { + weightSpinner = new JSpinner(new SpinnerNumberModel(1d, 0d, 4d, 0.01d)); + + var root = new DefaultMutableTreeNode("Utility AI Repository"); + addToMutableTreeNode(root, "Profiles", sharedData.getProfiles()); + addToMutableTreeNode(root, "Decisions", sharedData.getDecisions()); + addToMutableTreeNode(root, "Considerations", sharedData.getConsiderations()); + addToMutableTreeNode(root, "Decision Score Evaluators (DSE)", sharedData.getDecisionScoreEvaluators()); + DefaultTreeModel treeModel = new DefaultTreeModel(root); + repositoryViewer = new JTree(treeModel); + + actionComboBox = new JComboBox<>(Action.values()); + + var model = new DecisionScoreEvaluatorTableModel<>(sharedData.getDecisions()); + decisionScoreEvaluatorTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); + } + + private void addToMutableTreeNode(DefaultMutableTreeNode root, String nodeName, List items) { + var profilesNode = new DefaultMutableTreeNode(nodeName); + root.add(profilesNode); + for (var profile : items) { + profilesNode.add(new DefaultMutableTreeNode(profile.getName())); + } + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() { + createUIComponents(); + uAiEditorPanel = new JPanel(); + uAiEditorPanel.setLayout(new GridBagLayout()); + final JSplitPane splitPane1 = new JSplitPane(); + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(3, 3, 3, 3); + uAiEditorPanel.add(splitPane1, gbc); + final JPanel panel1 = new JPanel(); + panel1.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); + splitPane1.setLeftComponent(panel1); + newDecisionButton = new JButton(); + newDecisionButton.setText("New Decision"); + panel1.add(newDecisionButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, 1, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(233, 34), null, 0, false)); + newConsiderationButton = new JButton(); + newConsiderationButton.setText("New Consideration"); + panel1.add(newConsiderationButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, 1, null, new Dimension(233, 34), null, 0, false)); + panel1.add(repositoryViewer, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(233, 50), null, 0, false)); + final JPanel panel2 = new JPanel(); + panel2.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); + splitPane1.setRightComponent(panel2); + dseEditorPane = new JTabbedPane(); + panel2.add(dseEditorPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); + profilePane = new JPanel(); + profilePane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + dseEditorPane.addTab("Profile", profilePane); + final JScrollPane scrollPane1 = new JScrollPane(); + profilePane.add(scrollPane1, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + scrollPane1.setViewportView(decisionScoreEvaluatorTable); + final JPanel panel3 = new JPanel(); + panel3.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); + profilePane.add(panel3, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); + profileNameTextField = new JTextField(); + panel3.add(profileNameTextField, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + descriptionTextField = new JTextField(); + panel3.add(descriptionTextField, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + notesTextField = new JTextField(); + panel3.add(notesTextField, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + final JLabel label1 = new JLabel(); + label1.setText("Profile Name"); + panel3.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label2 = new JLabel(); + label2.setText("Description"); + panel3.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label3 = new JLabel(); + label3.setText("Notes"); + panel3.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + decisionPane = new JPanel(); + decisionPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + dseEditorPane.addTab("Decision", decisionPane); + evaluatorScrollPane = new JScrollPane(); + decisionPane.add(evaluatorScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + final JPanel panel4 = new JPanel(); + panel4.setLayout(new GridLayoutManager(1, 5, new Insets(0, 0, 0, 0), -1, -1)); + decisionPane.add(panel4, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); + final JLabel label4 = new JLabel(); + label4.setText("Action"); + panel4.add(label4, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + actionComboBox.setEditable(false); + panel4.add(actionComboBox, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(300, -1), null, null, 0, false)); + panel4.add(weightSpinner, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(100, -1), new Dimension(100, -1), new Dimension(100, -1), 0, false)); + final JLabel label5 = new JLabel(); + label5.setText("Weight"); + panel4.add(label5, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final Spacer spacer1 = new Spacer(); + panel4.add(spacer1, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + considerationsPane = new JPanel(); + considerationsPane.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); + considerationsPane.setName(""); + dseEditorPane.addTab("Considerations", considerationsPane); + dseConfigPane = new JPanel(); + dseConfigPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + considerationsPane.add(dseConfigPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); + final JLabel label6 = new JLabel(); + label6.setText("Name"); + dseConfigPane.add(label6, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label7 = new JLabel(); + label7.setText("Description"); + dseConfigPane.add(label7, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + nameDseTextField = new JTextField(); + dseConfigPane.add(nameDseTextField, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + descriptionDseTextField = new JTextField(); + dseConfigPane.add(descriptionDseTextField, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + considerationsScrollPane = new JScrollPane(); + considerationsPane.add(considerationsScrollPane, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { + return uAiEditorPanel; + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form new file mode 100644 index 00000000000..d494d9fd1c5 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form @@ -0,0 +1,91 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java new file mode 100644 index 00000000000..3c7935a8988 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; +import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; + +import javax.swing.*; +import java.awt.*; + +public class ConsiderationPane extends JPanel { + private JTextField considerationName; + private JComboBox considerationComboBox; + private JPanel curveContainer; + private JTable parametersTable; + private JPanel considerationPane; + private JPanel topThingsPane; + + public ConsiderationPane() { + $$$setupUI$$$(); + add(considerationPane); + } + + private void createUIComponents() { + parametersTable = new JTable(new ParametersTableModel()); + parametersTable.setModel(new ParametersTableModel()); + + considerationComboBox = new JComboBox<>(TWUtilityAIRepository.getInstance().getConsiderations().toArray(new TWConsideration[0])); + curveContainer = new CurvePane(); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() { + createUIComponents(); + considerationPane = new JPanel(); + considerationPane.setLayout(new GridBagLayout()); + topThingsPane = new JPanel(); + topThingsPane.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 3; + gbc.gridheight = 2; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + considerationPane.add(topThingsPane, gbc); + topThingsPane.add(considerationComboBox, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + considerationName = new JTextField(); + topThingsPane.add(considerationName, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + topThingsPane.add(curveContainer, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(800, 600), new Dimension(800, 600), null, 0, false)); + final JLabel label1 = new JLabel(); + label1.setText("Type"); + topThingsPane.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label2 = new JLabel(); + label2.setText("Name"); + topThingsPane.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label3 = new JLabel(); + label3.setText("Curve"); + topThingsPane.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label4 = new JLabel(); + label4.setText("Parameters"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 2; + gbc.weighty = 1.0; + gbc.anchor = GridBagConstraints.NORTHWEST; + considerationPane.add(label4, gbc); + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 2; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + considerationPane.add(parametersTable, gbc); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { + return considerationPane; + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form new file mode 100644 index 00000000000..6db9fea70eb --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form @@ -0,0 +1,108 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java new file mode 100644 index 00000000000..89d5d1bf528 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.ai.utility.*; + +import javax.swing.*; +import java.awt.*; +import java.util.concurrent.atomic.AtomicReference; + +public class CurvePane extends JPanel { + private JComboBox curveTypeComboBox; + private JSpinner bParamSpinner; + private JSpinner mParamSpinner; + private JSpinner kParamSpinner; + private JSpinner cParamSpinner; + private JPanel curveGraph; + private JPanel basePane; + + public CurvePane() { + $$$setupUI$$$(); + setLayout(new BorderLayout()); + add(basePane, BorderLayout.CENTER); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() { + createUIComponents(); + basePane = new JPanel(); + basePane.setLayout(new GridBagLayout()); + basePane.setBackground(new Color(-13947600)); + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 1; + gbc.gridwidth = 2; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + basePane.add(curveTypeComboBox, gbc); + final JLabel label1 = new JLabel(); + label1.setText("Type"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 1; + gbc.anchor = GridBagConstraints.WEST; + basePane.add(label1, gbc); + curveGraph.setBackground(new Color(-13947600)); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 11; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + basePane.add(curveGraph, gbc); + gbc = new GridBagConstraints(); + gbc.gridx = 4; + gbc.gridy = 1; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + basePane.add(bParamSpinner, gbc); + gbc = new GridBagConstraints(); + gbc.gridx = 6; + gbc.gridy = 1; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + basePane.add(mParamSpinner, gbc); + gbc = new GridBagConstraints(); + gbc.gridx = 8; + gbc.gridy = 1; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + basePane.add(kParamSpinner, gbc); + gbc = new GridBagConstraints(); + gbc.gridx = 10; + gbc.gridy = 1; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + basePane.add(cParamSpinner, gbc); + final JLabel label2 = new JLabel(); + label2.setText("b"); + gbc = new GridBagConstraints(); + gbc.gridx = 3; + gbc.gridy = 1; + gbc.anchor = GridBagConstraints.EAST; + basePane.add(label2, gbc); + final JLabel label3 = new JLabel(); + label3.setText("m"); + gbc = new GridBagConstraints(); + gbc.gridx = 5; + gbc.gridy = 1; + gbc.anchor = GridBagConstraints.EAST; + basePane.add(label3, gbc); + final JLabel label4 = new JLabel(); + label4.setText("k"); + gbc = new GridBagConstraints(); + gbc.gridx = 7; + gbc.gridy = 1; + gbc.anchor = GridBagConstraints.EAST; + basePane.add(label4, gbc); + final JLabel label5 = new JLabel(); + label5.setText("c"); + gbc = new GridBagConstraints(); + gbc.gridx = 9; + gbc.gridy = 1; + gbc.anchor = GridBagConstraints.EAST; + basePane.add(label5, gbc); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { + return basePane; + } + + private void createUIComponents() { + curveTypeComboBox = new JComboBox<>(DefaultCurve.values()); + AtomicReference selectedCurve = new AtomicReference<>(); + curveTypeComboBox.addActionListener(e1 -> { + if (curveTypeComboBox.getSelectedItem() != null) { + var curve = ((DefaultCurve) curveTypeComboBox.getSelectedItem()).getCurve(); + selectedCurve.set(curve); + curveGraph.repaint(); + + if (curve instanceof LinearCurve) { + bParamSpinner.setValue(((LinearCurve) curve).getB()); + mParamSpinner.setValue(((LinearCurve) curve).getM()); + kParamSpinner.setEnabled(false); + cParamSpinner.setEnabled(false); + } else if (curve instanceof ParabolicCurve) { + bParamSpinner.setValue(((ParabolicCurve) curve).getB()); + mParamSpinner.setValue(((ParabolicCurve) curve).getM()); + kParamSpinner.setEnabled(true); + kParamSpinner.setValue(((ParabolicCurve) curve).getK()); + cParamSpinner.setEnabled(false); + } else if (curve instanceof LogitCurve) { + bParamSpinner.setValue(((LogitCurve) curve).getB()); + mParamSpinner.setValue(((LogitCurve) curve).getM()); + kParamSpinner.setEnabled(true); + kParamSpinner.setValue(((LogitCurve) curve).getK()); + cParamSpinner.setEnabled(true); + cParamSpinner.setValue(((LogitCurve) curve).getC()); + } else if (curve instanceof LogisticCurve) { + bParamSpinner.setValue(((LogisticCurve) curve).getB()); + mParamSpinner.setValue(((LogisticCurve) curve).getM()); + kParamSpinner.setEnabled(true); + kParamSpinner.setValue(((LogisticCurve) curve).getK()); + cParamSpinner.setEnabled(true); + cParamSpinner.setValue(((LogisticCurve) curve).getC()); + } + } + }); + + curveGraph = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + if (selectedCurve.get() == null) { + return; + } + selectedCurve.get().drawCurve(g, getWidth(), getHeight(), Color.BLUE); + } + }; + + curveGraph.setPreferredSize(new Dimension(800, 600)); + bParamSpinner = new JSpinner(new SpinnerNumberModel(0d, -100d, 100d, 0.01d)); + mParamSpinner = new JSpinner(new SpinnerNumberModel(0d, -100d, 100d, 0.01d)); + kParamSpinner = new JSpinner(new SpinnerNumberModel(0d, -100d, 100d, 0.01d)); + cParamSpinner = new JSpinner(new SpinnerNumberModel(0d, -100d, 100d, 0.01d)); + + mParamSpinner.addChangeListener(e -> { + if (selectedCurve.get() != null) { + selectedCurve.get().setM((Double) mParamSpinner.getValue()); + } + curveGraph.repaint(); + }); + bParamSpinner.addChangeListener(e -> { + if (selectedCurve.get() != null) { + selectedCurve.get().setB((Double) bParamSpinner.getValue()); + } + curveGraph.repaint(); + }); + kParamSpinner.addChangeListener(e -> { + if (selectedCurve.get() != null) { + selectedCurve.get().setK((Double) kParamSpinner.getValue()); + } + curveGraph.repaint(); + }); + cParamSpinner.addChangeListener(e -> { + if (selectedCurve.get() != null) { + selectedCurve.get().setC((Double) cParamSpinner.getValue()); + } + curveGraph.repaint(); + }); + } +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form new file mode 100644 index 00000000000..fa5ecef3deb --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form @@ -0,0 +1,93 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java new file mode 100644 index 00000000000..c8c6289aa7b --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import javax.swing.*; +import java.awt.*; + +public class DecisionScoreEvaluatorPane extends JPanel { + private JTextField nameField; + private JTextField descriptionField; + private JTextField notesField; + private JPanel decisionScoreEvaluatorPane; + private JPanel considerationsPane; + ; + + public DecisionScoreEvaluatorPane() { + $$$setupUI$$$(); + setLayout(new BorderLayout()); + add(decisionScoreEvaluatorPane, BorderLayout.CENTER); + considerationsPane.add(new ConsiderationPane()); + considerationsPane.add(new ConsiderationPane()); + considerationsPane.add(new ConsiderationPane()); + considerationsPane.add(new ConsiderationPane()); + considerationsPane.add(new ConsiderationPane()); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() { + decisionScoreEvaluatorPane = new JPanel(); + decisionScoreEvaluatorPane.setLayout(new GridBagLayout()); + final JLabel label1 = new JLabel(); + label1.setText("Name"); + label1.setVerticalAlignment(0); + label1.setVerticalTextPosition(0); + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + decisionScoreEvaluatorPane.add(label1, gbc); + nameField = new JTextField(); + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + decisionScoreEvaluatorPane.add(nameField, gbc); + descriptionField = new JTextField(); + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 1; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + decisionScoreEvaluatorPane.add(descriptionField, gbc); + final JLabel label2 = new JLabel(); + label2.setText("Considerations"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 3; + gbc.weighty = 1.0; + gbc.anchor = GridBagConstraints.NORTHWEST; + decisionScoreEvaluatorPane.add(label2, gbc); + notesField = new JTextField(); + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 2; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + decisionScoreEvaluatorPane.add(notesField, gbc); + final JLabel label3 = new JLabel(); + label3.setText("Notes"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 2; + gbc.anchor = GridBagConstraints.WEST; + decisionScoreEvaluatorPane.add(label3, gbc); + final JLabel label4 = new JLabel(); + label4.setText("Description"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 1; + gbc.anchor = GridBagConstraints.WEST; + decisionScoreEvaluatorPane.add(label4, gbc); + final JScrollPane scrollPane1 = new JScrollPane(); + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 3; + gbc.gridheight = 2; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + decisionScoreEvaluatorPane.add(scrollPane1, gbc); + considerationsPane = new JPanel(); + considerationsPane.setLayout(new GridBagLayout()); + scrollPane1.setViewportView(considerationsPane); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { + return decisionScoreEvaluatorPane; + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java new file mode 100644 index 00000000000..013150847b4 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.ai.utility.Action; +import megamek.ai.utility.Decision; +import megamek.ai.utility.DecisionScoreEvaluator; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; +import java.util.List; + +public class DecisionScoreEvaluatorTable, DSE extends DecisionScoreEvaluator> extends JTable { + + private final Action[] actionList; + private final List dse; + + public DecisionScoreEvaluatorTable( + DecisionScoreEvaluatorTableModel model, Action[] actionList, List dse) { + super(model); + this.actionList = actionList; + this.dse = dse; + } + + public void createUIComponents() { + // + } + + @Override + public DecisionScoreEvaluatorTableModel getModel() { + return (DecisionScoreEvaluatorTableModel) super.getModel(); + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + // Decision is column 1, Evaluator is column 2 + if (column == 1) { + // Create a combo box for Decisions + JComboBox cb = new JComboBox<>( + actionList + ); + return new DefaultCellEditor(cb); + } else if (column == 2) { + // Create a combo box for Evaluators + var cb = new JComboBox<>( + dse.toArray(new DecisionScoreEvaluator[0]) + ); + return new DefaultCellEditor(cb); + } + return super.getCellEditor(row, column); + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java new file mode 100644 index 00000000000..476e822584a --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.ai.utility.Action; +import megamek.ai.utility.Decision; +import megamek.ai.utility.DecisionScoreEvaluator; + +import javax.swing.table.AbstractTableModel; +import java.util.ArrayList; +import java.util.List; + + +public class DecisionScoreEvaluatorTableModel> extends AbstractTableModel { + + private final List rows; + private final String[] columnNames = { "ID", "Decision", "Evaluator" }; + + public DecisionScoreEvaluatorTableModel(List initialRows) { + this.rows = new ArrayList<>(initialRows); + } + + public void addRow(DECISION dse) { + rows.add(dse); + fireTableRowsInserted(rows.size() - 1, rows.size() - 1); + } + + @Override + public int getRowCount() { + return rows.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + DECISION dse = rows.get(rowIndex); + return switch (columnIndex) { + case 0 -> rowIndex; // or some ID from dse + case 1 -> dse.getAction().getActionName(); + case 2 -> dse.getDecisionScoreEvaluator().getName(); + default -> null; + }; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + // Let's keep ID read-only, but allow editing for Decision & Evaluator + return columnIndex == 1 || columnIndex == 2; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + var dse = rows.get(rowIndex); + + switch (columnIndex) { + case 1: + if (aValue instanceof Action action) { + dse.setAction(action); + } + break; + case 2: + if (aValue instanceof DecisionScoreEvaluator decisionScoreEvaluator) { + dse.setDecisionScoreEvaluator(decisionScoreEvaluator); + } + break; + default: + // no-op + } + rows.set(rowIndex, dse); + fireTableCellUpdated(rowIndex, columnIndex); + } + + /** Helper to fetch the updated data */ + public List getDecisions() { + return rows; + } +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java new file mode 100644 index 00000000000..304194787d2 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.logging.MMLogger; + +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ParametersTableModel implements TableModel { + private static final MMLogger logger = MMLogger.create(ParametersTableModel.class); + private final Map hashRows = new HashMap<>(); + private final List rowValues = new ArrayList<>(); + private final String[] columnNames = { "Name", "Value" }; + private final Class[] columnClasses = { String.class, Object.class }; + private record Row(String name, Object value) {} + + public ParametersTableModel() { + } + + public ParametersTableModel(Map parameters) { + this.hashRows.putAll(parameters); + for (Map.Entry entry : parameters.entrySet()) { + rowValues.add(new Row(entry.getKey(), entry.getValue())); + } + } + + public void addRow(String parameterName, Object value) { + if (hashRows.containsKey(parameterName)) { + logger.formattedErrorDialog("Parameter already exists", + "Could not add parameter {}, another parameters with the same name is already present.", parameterName); + return; + } + hashRows.put(parameterName, value); + rowValues.add(new Row(parameterName, value)); + } + + public Map getParameters() { + return hashRows; + } + + @Override + public int getRowCount() { + return rowValues.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return columnClasses[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return true; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + var row = rowValues.get(rowIndex); + if (columnIndex == 0) { + return row.name; + } + return row.value; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + var row = rowValues.get(rowIndex); + if (columnIndex == 1) { + rowValues.set(rowIndex, new Row(row.name, aValue)); + hashRows.put(row.name, aValue); + } else { + rowValues.set(rowIndex, new Row((String) aValue, row.value)); + hashRows.remove(row.name); + hashRows.put((String) aValue, row.value); + } + } + + @Override + public void addTableModelListener(TableModelListener l) { + + } + + @Override + public void removeTableModelListener(TableModelListener l) { + + } +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java new file mode 100644 index 00000000000..8e77ea165e7 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java @@ -0,0 +1,2674 @@ +/* + * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ +package megamek.client.ui.swing.ai.editor; + +import megamek.MMConstants; +import megamek.client.event.BoardViewEvent; +import megamek.client.event.BoardViewListenerAdapter; +import megamek.client.ui.Messages; +import megamek.client.ui.dialogs.helpDialogs.AbstractHelpDialog; +import megamek.client.ui.dialogs.helpDialogs.BoardEditorHelpDialog; +import megamek.client.ui.enums.DialogResult; +import megamek.client.ui.swing.*; +import megamek.client.ui.swing.boardview.BoardEditorTooltip; +import megamek.client.ui.swing.boardview.BoardView; +import megamek.client.ui.swing.boardview.KeyBindingsOverlay; +import megamek.client.ui.swing.dialog.FloodDialog; +import megamek.client.ui.swing.dialog.LevelChangeDialog; +import megamek.client.ui.swing.dialog.MMConfirmDialog; +import megamek.client.ui.swing.dialog.MultiIntSelectorDialog; +import megamek.client.ui.swing.minimap.Minimap; +import megamek.client.ui.swing.tileset.HexTileset; +import megamek.client.ui.swing.tileset.TilesetManager; +import megamek.client.ui.swing.util.FontHandler; +import megamek.client.ui.swing.util.MegaMekController; +import megamek.client.ui.swing.util.StringDrawer; +import megamek.client.ui.swing.util.UIUtil; +import megamek.client.ui.swing.util.UIUtil.FixedYPanel; +import megamek.common.*; +import megamek.common.annotations.Nullable; +import megamek.common.util.BoardUtilities; +import megamek.common.util.ImageUtil; +import megamek.common.util.fileUtils.MegaMekFile; +import megamek.logging.MMLogger; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.border.TitledBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileFilter; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.event.*; +import java.io.*; +import java.util.List; +import java.util.*; + +import static megamek.common.Terrains.*; + +public class UtilityAiEditor extends JPanel implements ItemListener, ListSelectionListener, ActionListener, + DocumentListener, IMapSettingsObserver { + private final static MMLogger logger = MMLogger.create(UtilityAiEditor.class); + + + private static class TerrainHelper implements Comparable { + private final int terrainType; + + TerrainHelper(int terrain) { + terrainType = terrain; + } + + public int getTerrainType() { + return terrainType; + } + + @Override + public String toString() { + return Terrains.getEditorName(terrainType); + } + + public String getTerrainTooltip() { + return Terrains.getEditorTooltip(terrainType); + } + + @Override + public int compareTo(TerrainHelper o) { + return toString().compareTo(o.toString()); + } + + @Override + public boolean equals(Object other) { + if (other instanceof Integer) { + return getTerrainType() == (Integer) other; + } else if (!(other instanceof TerrainHelper)) { + return false; + } else { + return getTerrainType() == ((TerrainHelper) other).getTerrainType(); + } + } + + @Override + public int hashCode() { + return Objects.hash(getTerrainType()); + } + } + + /** + * Class to make it easier to display a Terrain in a JList or + * JComboBox. + * + * @author arlith + */ + private static class TerrainTypeHelper implements Comparable { + + Terrain terrain; + + TerrainTypeHelper(Terrain terrain) { + this.terrain = terrain; + } + + public Terrain getTerrain() { + return terrain; + } + + @Override + public String toString() { + String baseString = Terrains.getDisplayName(terrain.getType(), terrain.getLevel()); + if (baseString == null) { + baseString = Terrains.getEditorName(terrain.getType()); + baseString += " " + terrain.getLevel(); + } + if (terrain.hasExitsSpecified()) { + baseString += " (Exits: " + terrain.getExits() + ")"; + } + return baseString; + } + + public String getTooltip() { + return terrain.toString(); + } + + @Override + public int compareTo(TerrainTypeHelper o) { + return toString().compareTo(o.toString()); + } + } + + /** + * ListCellRenderer for rendering tooltips for each item in a list or combobox. + * Code from SourceForge: + * https://stackoverflow.com/questions/480261/java-swing-mouseover-text-on-jcombobox-items + */ + private static class ComboboxToolTipRenderer extends DefaultListCellRenderer { + + private TerrainHelper[] terrains; + private List terrainTypes; + + @Override + public Component getListCellRendererComponent(final JList list, final Object value, + final int index, final boolean isSelected, + final boolean cellHasFocus) { + if ((-1 < index) && (value != null)) { + if (terrainTypes != null) { + list.setToolTipText(terrainTypes.get(index).getTooltip()); + } else if (terrains != null) { + list.setToolTipText(terrains[index].getTerrainTooltip()); + } + } + return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + } + + public void setTerrains(TerrainHelper... terrains) { + this.terrains = terrains; + } + + public void setTerrainTypes(List terrainTypes) { + this.terrainTypes = terrainTypes; + } + } + + private final GUIPreferences guip = GUIPreferences.getInstance(); + + private static final int BASE_TERRAINBUTTON_ICON_WIDTH = 70; + private static final int BASE_ARROWBUTTON_ICON_WIDTH = 25; + private static final String CMD_EDIT_DEPLOYMENT_ZONES = "CMD_EDIT_DEPLOYMENT_ZONES"; + + // Components + private final JFrame frame = new JFrame(); + private final Game game = new Game(); + private Board board = game.getBoard(); + private BoardView bv; + boolean isDragging = false; + private Component bvc; + private final CommonMenuBar menuBar = CommonMenuBar.getMenuBarForBoardEditor(); + private AbstractHelpDialog help; + private CommonSettingsDialog setdlg; + private JDialog minimapW; + private final MegaMekController controller; + + // The current files + private File curfileImage; + private File curBoardFile; + + // The active hex "brush" + private HexCanvas canHex; + private Hex curHex = new Hex(); + + // Easy terrain access buttons + private final List terrainButtons = new ArrayList<>(); + private ScalingIconButton buttonLW, buttonLJ; + private ScalingIconButton buttonOW, buttonOJ; + private ScalingIconButton buttonWa, buttonSw, buttonRo; + private ScalingIconButton buttonRd, buttonCl, buttonBu; + private ScalingIconButton buttonMd, buttonPv, buttonSn; + private ScalingIconButton buttonIc, buttonTu, buttonMg; + private ScalingIconButton buttonBr, buttonFT; + private final List brushButtons = new ArrayList<>(); + private ScalingIconToggleButton buttonBrush1, buttonBrush2, buttonBrush3; + private ScalingIconToggleButton buttonUpDn, buttonOOC; + + // The brush size: 1 = 1 hex, 2 = radius 1, 3 = radius 2 + private int brushSize = 1; + private int hexLeveltoDraw = -1000; + private EditorTextField texElev; + private ScalingIconButton butElevUp; + private ScalingIconButton butElevDown; + private JList lisTerrain; + private ComboboxToolTipRenderer lisTerrainRenderer; + private ScalingIconButton butDelTerrain; + private JComboBox choTerrainType; + private EditorTextField texTerrainLevel; + private JCheckBox cheTerrExitSpecified; + private EditorTextField texTerrExits; + private ScalingIconButton butTerrExits; + private JCheckBox cheRoadsAutoExit; + private JButton copyButton = new JButton(Messages.getString("BoardEditor.copyButton")); + private JButton pasteButton = new JButton(Messages.getString("BoardEditor.pasteButton")); + private ScalingIconButton butExitUp, butExitDown; + private JComboBox choTheme; + private ScalingIconButton butTerrDown, butTerrUp; + private JButton butAddTerrain; + private JButton butBoardNew; + private JButton butBoardOpen; + private JButton butBoardSave; + private JButton butBoardSaveAs; + private JButton butBoardSaveAsImage; + private JButton butBoardValidate; + private JButton butSourceFile; + private MapSettings mapSettings = MapSettings.getInstance(); + private JButton butExpandMap; + private Coords lastClicked; + private final JLabel labTheme = new JLabel(Messages.getString("BoardEditor.labTheme"), SwingConstants.LEFT); + + private final FixedYPanel panelHexSettings = new FixedYPanel(); + private final FixedYPanel panelTerrSettings = new FixedYPanel(new GridLayout(0, 2, 4, 4)); + private final FixedYPanel panelBoardSettings = new FixedYPanel(); + + // Help Texts + private final JLabel labHelp1 = new JLabel(Messages.getString("BoardEditor.helpText"), SwingConstants.LEFT); + private final JLabel labHelp2 = new JLabel(Messages.getString("BoardEditor.helpText2"), SwingConstants.LEFT); + + // Undo / Redo + private final List undoButtons = new ArrayList<>(); + private ScalingIconButton buttonUndo, buttonRedo; + private final Stack> undoStack = new Stack<>(); + private final Stack> redoStack = new Stack<>(); + private HashSet currentUndoSet; + private HashSet currentUndoCoords; + + // Tracker for board changes; unfortunately this is not equal to + // undoStack == empty because saving the board doesn't empty the + // undo stack but makes the board unchanged. + /** Tracks if the board has changes over the last saved version. */ + private boolean hasChanges = false; + /** Tracks if the board can return to the last saved version. */ + private boolean canReturnToSaved = true; + /** + * The undo stack size at the last save. Used to track saved status of the + * board. + */ + private int savedUndoStackSize = 0; + + // Misc + private File loadPath = Configuration.boardsDir(); + + /** + * Special purpose indicator, keeps terrain list + * from de-selecting when clicking it + */ + private boolean terrListBlocker = false; + + /** + * Special purpose indicator, prevents an update + * loop when the terrain level or exits field is changed + */ + private boolean noTextFieldUpdate = false; + + /** + * A MouseAdapter that closes a JLabel when clicked + */ + private final MouseAdapter clickToHide = new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + if (e.getSource() instanceof JLabel) { + ((JLabel) e.getSource()).setVisible(false); + } + } + }; + + /** + * Flag that indicates whether hotkeys should be ignored or not. This is + * used for disabling hot keys when various dialogs are displayed. + */ + private boolean ignoreHotKeys = false; + + /** + * Creates and lays out a new Board Editor frame. + */ + public UtilityAiEditor(MegaMekController c) { + controller = c; + try { + bv = new BoardView(game, controller, null); + bv.addOverlay(new KeyBindingsOverlay(bv)); + bv.setUseLosTool(false); + bv.setDisplayInvalidFields(true); + bv.setTooltipProvider(new BoardEditorTooltip(game, bv)); + bvc = bv.getComponent(true); + } catch (IOException e) { + JOptionPane.showMessageDialog(frame, + Messages.getString("BoardEditor.CouldntInitialize") + e, + Messages.getString("BoardEditor.FatalError"), JOptionPane.ERROR_MESSAGE); + frame.dispose(); + } + + // Add a mouse listener for mouse button release + // to handle Undo + bv.getPanel().addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + // Act only if the user actually drew something + if ((currentUndoSet != null) && !currentUndoSet.isEmpty()) { + // Since this draw action is finished, push the + // drawn hexes onto the Undo Stack and get ready + // for a new draw action + undoStack.push(currentUndoSet); + currentUndoSet = null; + buttonUndo.setEnabled(true); + // Drawing something disables any redo actions + redoStack.clear(); + buttonRedo.setEnabled(false); + // When Undo (without Redo) has been used after saving + // and the user draws on the board, then it can + // no longer know if it's been returned to the saved state + // and it will always be treated as changed. + if (savedUndoStackSize > undoStack.size()) { + canReturnToSaved = false; + } + hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize); + } + // Mark the title when the board has changes + setFrameTitle(); + } + } + }); + bv.addBoardViewListener(new BoardViewListenerAdapter() { + @Override + public void hexMoused(BoardViewEvent b) { + Coords c = b.getCoords(); + // return if there are no or no valid coords or if we click the same hex again + // unless Raise/Lower Terrain is active which should let us click the same hex + if ((c == null) || (c.equals(lastClicked) && !buttonUpDn.isSelected()) + || !board.contains(c)) { + return; + } + lastClicked = c; + bv.cursor(c); + boolean isALT = (b.getModifiers() & InputEvent.ALT_DOWN_MASK) != 0; + boolean isSHIFT = (b.getModifiers() & InputEvent.SHIFT_DOWN_MASK) != 0; + boolean isCTRL = (b.getModifiers() & InputEvent.CTRL_DOWN_MASK) != 0; + boolean isLMB = (b.getButton() == MouseEvent.BUTTON1); + + // Raise/Lower Terrain is selected + if (buttonUpDn.isSelected()) { + // Mouse Button released + if (b.getType() == BoardViewEvent.BOARD_HEX_CLICKED) { + hexLeveltoDraw = -1000; + isDragging = false; + } + + // Mouse Button clicked or dragged + if ((b.getType() == BoardViewEvent.BOARD_HEX_DRAGGED) && isLMB) { + if (!isDragging) { + hexLeveltoDraw = board.getHex(c).getLevel(); + if (isALT) { + hexLeveltoDraw--; + } else if (isSHIFT) { + hexLeveltoDraw++; + } + isDragging = true; + } + } + + // CORRECTION, click outside the board then drag inside??? + if (hexLeveltoDraw != -1000) { + LinkedList allBrushHexes = getBrushCoords(c); + for (Coords h : allBrushHexes) { + if (!buttonOOC.isSelected() || board.getHex(h).isClearHex()) { + saveToUndo(h); + relevelHex(h); + } + } + } + // ------- End Raise/Lower Terrain + } else if (isLMB || (b.getModifiers() & InputEvent.BUTTON1_DOWN_MASK) != 0) { + // 'isLMB' is true if a button 1 is associated to a click or release but not + // while dragging. + // The left button down mask is checked because we could be dragging. + + // Normal texture paint + if (isALT) { // ALT-Click + setCurrentHex(board.getHex(b.getCoords())); + } else { + LinkedList allBrushHexes = getBrushCoords(c); + for (Coords h : allBrushHexes) { + // test if texture overwriting is active + if ((!buttonOOC.isSelected() || board.getHex(h).isClearHex()) + && curHex.isValid(null)) { + saveToUndo(h); + if (isCTRL) { // CTRL-Click + paintHex(h); + } else if (isSHIFT) { // SHIFT-Click + addToHex(h); + } else { // Normal click + retextureHex(h); + } + } + } + } + } + } + }); + + setupEditorPanel(); + setupFrame(); + frame.setVisible(true); + if (GUIPreferences.getInstance().getNagForMapEdReadme()) { + String title = Messages.getString("BoardEditor.readme.title"); + String body = Messages.getString("BoardEditor.readme.message"); + ConfirmDialog confirm = new ConfirmDialog(frame, title, body, true); + confirm.setVisible(true); + if (!confirm.getShowAgain()) { + GUIPreferences.getInstance().setNagForMapEdReadme(false); + } + if (confirm.getAnswer()) { + showHelp(); + } + } + } + + /** + * Sets up the frame that will display the editor. + */ + private void setupFrame() { + setFrameTitle(); + frame.add(bvc, BorderLayout.CENTER); + frame.add(this, BorderLayout.EAST); + menuBar.addActionListener(this); + frame.setJMenuBar(menuBar); + if (GUIPreferences.getInstance().getWindowSizeHeight() != 0) { + frame.setLocation(GUIPreferences.getInstance().getWindowPosX(), + GUIPreferences.getInstance().getWindowPosY()); + frame.setSize(GUIPreferences.getInstance().getWindowSizeWidth(), + GUIPreferences.getInstance().getWindowSizeHeight()); + } else { + frame.setSize(800, 600); + } + + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + // When the board has changes, ask the user + if (hasChanges && (showSavePrompt() == DialogResult.CANCELLED)) { + return; + } + // otherwise: exit the Map Editor + minimapW.setVisible(false); + if (controller != null) { + controller.removeAllActions(); + controller.aiEditor = null; + } + bv.dispose(); + frame.dispose(); + } + }); + } + + /** + * Shows a prompt to save the current board. When the board is actually saved or + * the user presses + * "No" (don't want to save), returns DialogResult.CONFIRMED. In this case, the + * action (loading a board + * or leaving the board editor) that led to this prompt may be continued. + * In all other cases, returns DialogResult.CANCELLED, meaning the action should + * not be continued. + * + * @return DialogResult.CANCELLED (cancel action) or CONFIRMED (continue action) + */ + private DialogResult showSavePrompt() { + ignoreHotKeys = true; + int savePrompt = JOptionPane.showConfirmDialog(null, + Messages.getString("BoardEditor.exitprompt"), + Messages.getString("BoardEditor.exittitle"), + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + ignoreHotKeys = false; + // When the user cancels or did not actually save the board, don't load anything + if (((savePrompt == JOptionPane.YES_OPTION) && !boardSave(false)) + || (savePrompt == JOptionPane.CANCEL_OPTION) + || (savePrompt == JOptionPane.CLOSED_OPTION)) { + return DialogResult.CANCELLED; + } else { + return DialogResult.CONFIRMED; + } + } + + /** + * Sets up Scaling Icon Buttons + */ + private ScalingIconButton prepareButton(String iconName, String buttonName, + List bList, int width) { + // Get the normal icon + File file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + ".png").getFile(); + Image imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); + if (imageButton == null) { + imageButton = ImageUtil.failStandardImage(); + } + ScalingIconButton button = new ScalingIconButton(imageButton, width); + + // Get the hover icon + file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_H.png").getFile(); + imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); + button.setRolloverImage(imageButton); + + // Get the disabled icon, if any + file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_G.png").getFile(); + imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); + button.setDisabledImage(imageButton); + + String tt = Messages.getString("BoardEditor." + iconName + "TT"); + if (!tt.isBlank()) { + button.setToolTipText(tt); + } + button.setMargin(new Insets(0, 0, 0, 0)); + if (bList != null) { + bList.add(button); + } + button.addActionListener(this); + return button; + } + + /** + * Sets up Scaling Icon ToggleButtons + */ + private ScalingIconToggleButton prepareToggleButton(String iconName, String buttonName, + List bList, int width) { + // Get the normal icon + File file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + ".png").getFile(); + Image imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); + if (imageButton == null) { + imageButton = ImageUtil.failStandardImage(); + } + ScalingIconToggleButton button = new ScalingIconToggleButton(imageButton, width); + + // Get the hover icon + file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_H.png").getFile(); + imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); + button.setRolloverImage(imageButton); + + // Get the selected icon, if any + file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_S.png").getFile(); + imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); + button.setSelectedImage(imageButton); + + button.setToolTipText(Messages.getString("BoardEditor." + iconName + "TT")); + if (bList != null) { + bList.add(button); + } + button.addActionListener(this); + return button; + } + + /** + * Sets up the editor panel, which goes on the right of the map and has + * controls for editing the current square. + */ + private void setupEditorPanel() { + // Help Texts + labHelp1.addMouseListener(clickToHide); + labHelp2.addMouseListener(clickToHide); + labHelp1.setAlignmentX(Component.CENTER_ALIGNMENT); + labHelp2.setAlignmentX(Component.CENTER_ALIGNMENT); + + // Buttons to ease setting common terrain types + buttonLW = prepareButton("ButtonLW", "Woods", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonLJ = prepareButton("ButtonLJ", "Jungle", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonOW = prepareButton("ButtonLLW", "Low Woods", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonOJ = prepareButton("ButtonLLJ", "Low Jungle", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonWa = prepareButton("ButtonWa", "Water", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonSw = prepareButton("ButtonSw", "Swamp", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonRo = prepareButton("ButtonRo", "Rough", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonMd = prepareButton("ButtonMd", "Mud", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonPv = prepareButton("ButtonPv", "Pavement", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonSn = prepareButton("ButtonSn", "Snow", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonBu = prepareButton("ButtonBu", "Buildings", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonRd = prepareButton("ButtonRd", "Roads", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonBr = prepareButton("ButtonBr", "Bridges", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonFT = prepareButton("ButtonFT", "Fuel Tanks", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonIc = prepareButton("ButtonIc", "Ice", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonTu = prepareButton("ButtonTu", "Tundra", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonMg = prepareButton("ButtonMg", "Magma", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + buttonCl = prepareButton("ButtonCl", "Clear", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); + + buttonBrush1 = prepareToggleButton("ButtonHex1", "Brush1", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); + buttonBrush2 = prepareToggleButton("ButtonHex7", "Brush2", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); + buttonBrush3 = prepareToggleButton("ButtonHex19", "Brush3", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); + ButtonGroup brushGroup = new ButtonGroup(); + brushGroup.add(buttonBrush1); + brushGroup.add(buttonBrush2); + brushGroup.add(buttonBrush3); + buttonOOC = prepareToggleButton("ButtonOOC", "OOC", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); + buttonUpDn = prepareToggleButton("ButtonUpDn", "UpDown", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); + + buttonUndo = prepareButton("ButtonUndo", "Undo", undoButtons, BASE_ARROWBUTTON_ICON_WIDTH); + buttonRedo = prepareButton("ButtonRedo", "Redo", undoButtons, BASE_ARROWBUTTON_ICON_WIDTH); + buttonUndo.setEnabled(false); + buttonRedo.setEnabled(false); + buttonUndo.setActionCommand(ClientGUI.BOARD_UNDO); + buttonRedo.setActionCommand(ClientGUI.BOARD_REDO); + + MouseWheelListener wheelListener = e -> { + int terrain; + if (e.getSource() == buttonRo) { + terrain = Terrains.ROUGH; + } else if (e.getSource() == buttonSw) { + terrain = Terrains.SWAMP; + } else if (e.getSource() == buttonWa) { + terrain = Terrains.WATER; + } else if (e.getSource() == buttonLW) { + terrain = Terrains.WOODS; + } else if (e.getSource() == buttonLJ) { + terrain = Terrains.JUNGLE; + } else if (e.getSource() == buttonOW) { + terrain = Terrains.WOODS; + } else if (e.getSource() == buttonOJ) { + terrain = Terrains.JUNGLE; + } else if (e.getSource() == buttonMd) { + terrain = Terrains.MUD; + } else if (e.getSource() == buttonPv) { + terrain = Terrains.PAVEMENT; + } else if (e.getSource() == buttonIc) { + terrain = Terrains.ICE; + } else if (e.getSource() == buttonSn) { + terrain = Terrains.SNOW; + } else if (e.getSource() == buttonTu) { + terrain = Terrains.TUNDRA; + } else if (e.getSource() == buttonMg) { + terrain = Terrains.MAGMA; + } else { + return; + } + + Hex saveHex = curHex.duplicate(); + // change the terrain level by wheel direction if present, + // or set to 1 if not present + int newLevel = 1; + if (curHex.containsTerrain(terrain)) { + newLevel = curHex.terrainLevel(terrain) + (e.getWheelRotation() < 0 ? 1 : -1); + } else if (!e.isShiftDown()) { + curHex.removeAllTerrains(); + } + addSetTerrainEasy(terrain, newLevel); + // Add or adapt elevation helper terrain for foliage + // When the elevation was 1, it stays 1 (L1 Foliage, TO p.36) + // Otherwise, it is set to 3 for Ultra W/J and 2 otherwise (TW foliage) + if ((terrain == Terrains.WOODS) || (terrain == Terrains.JUNGLE)) { + int elev = curHex.terrainLevel(Terrains.FOLIAGE_ELEV); + if ((elev != 1) && (newLevel == 3)) { + elev = 3; + } else if (elev != 1) { + elev = 2; + } + curHex.addTerrain(new Terrain(Terrains.FOLIAGE_ELEV, elev)); + } + // Reset the terrain to the former state + // if the new would be invalid. + if (!curHex.isValid(null)) { + curHex = saveHex; + } + + refreshTerrainList(); + repaintWorkingHex(); + }; + + buttonSw.addMouseWheelListener(wheelListener); + buttonWa.addMouseWheelListener(wheelListener); + buttonRo.addMouseWheelListener(wheelListener); + buttonLJ.addMouseWheelListener(wheelListener); + buttonLW.addMouseWheelListener(wheelListener); + buttonOJ.addMouseWheelListener(wheelListener); + buttonOW.addMouseWheelListener(wheelListener); + buttonMd.addMouseWheelListener(wheelListener); + buttonPv.addMouseWheelListener(wheelListener); + buttonSn.addMouseWheelListener(wheelListener); + buttonIc.addMouseWheelListener(wheelListener); + buttonTu.addMouseWheelListener(wheelListener); + buttonMg.addMouseWheelListener(wheelListener); + + // Mouse wheel behaviour for the BUILDINGS button + // Always ADDS the building. + buttonBu.addMouseWheelListener(e -> { + // If we don't have at least one of the building values, overwrite the current + // hex + if (!curHex.containsTerrain(Terrains.BLDG_CF) + && !curHex.containsTerrain(Terrains.BLDG_ELEV) + && !curHex.containsTerrain(Terrains.BUILDING)) { + curHex.removeAllTerrains(); + } + // Restore mandatory building parts if some are missing + setBasicBuilding(false); + int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1; + + if (e.isShiftDown()) { + int oldLevel = curHex.getTerrain(Terrains.BLDG_CF).getLevel(); + int newLevel = Math.max(10, oldLevel + (wheelDir * 5)); + curHex.addTerrain(new Terrain(Terrains.BLDG_CF, newLevel)); + } else if (e.isControlDown()) { + int oldLevel = curHex.getTerrain(Terrains.BUILDING).getLevel(); + int newLevel = Math.max(1, Math.min(4, oldLevel + wheelDir)); // keep between 1 and 4 + + if (newLevel != oldLevel) { + Terrain curTerr = curHex.getTerrain(Terrains.BUILDING); + curHex.addTerrain(new Terrain(Terrains.BUILDING, + newLevel, curTerr.hasExitsSpecified(), curTerr.getExits())); + + // Set the CF to the appropriate standard value *IF* it is the appropriate value + // now, + // i.e. if the user has not manually set it to something else + int curCF = curHex.getTerrain(Terrains.BLDG_CF).getLevel(); + if (curCF == Building.getDefaultCF(oldLevel)) { + curHex.addTerrain(new Terrain(Terrains.BLDG_CF, Building.getDefaultCF(newLevel))); + } + } + } else { + int oldLevel = curHex.getTerrain(Terrains.BLDG_ELEV).getLevel(); + int newLevel = Math.max(1, oldLevel + wheelDir); + curHex.addTerrain(new Terrain(Terrains.BLDG_ELEV, newLevel)); + } + + refreshTerrainList(); + repaintWorkingHex(); + }); + + // Mouse wheel behaviour for the BRIDGE button + buttonBr.addMouseWheelListener(e -> { + // If we don't have at least one of the bridge values, overwrite the current hex + if (!curHex.containsTerrain(Terrains.BRIDGE_CF) + && !curHex.containsTerrain(Terrains.BRIDGE_ELEV) + && !curHex.containsTerrain(Terrains.BRIDGE)) { + curHex.removeAllTerrains(); + } + setBasicBridge(); + int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1; + int terrainType; + int newLevel; + + if (e.isShiftDown()) { + terrainType = Terrains.BRIDGE_CF; + int oldLevel = curHex.getTerrain(terrainType).getLevel(); + newLevel = Math.max(10, oldLevel + wheelDir * 10); + curHex.addTerrain(new Terrain(terrainType, newLevel)); + } else if (e.isControlDown()) { + Terrain terrain = curHex.getTerrain(Terrains.BRIDGE); + boolean hasExits = terrain.hasExitsSpecified(); + int exits = terrain.getExits(); + newLevel = Math.max(1, terrain.getLevel() + wheelDir); + curHex.addTerrain(new Terrain(Terrains.BRIDGE, newLevel, hasExits, exits)); + } else { + terrainType = Terrains.BRIDGE_ELEV; + int oldLevel = curHex.getTerrain(terrainType).getLevel(); + newLevel = Math.max(0, oldLevel + wheelDir); + curHex.addTerrain(new Terrain(terrainType, newLevel)); + } + + refreshTerrainList(); + repaintWorkingHex(); + }); + + // Mouse wheel behaviour for the FUELTANKS button + buttonFT.addMouseWheelListener(e -> { + // If we don't have at least one of the fuel tank values, overwrite the current + // hex + if (!curHex.containsTerrain(Terrains.FUEL_TANK) + && !curHex.containsTerrain(Terrains.FUEL_TANK_CF) + && !curHex.containsTerrain(Terrains.FUEL_TANK_ELEV) + && !curHex.containsTerrain(Terrains.FUEL_TANK_MAGN)) { + curHex.removeAllTerrains(); + } + setBasicFuelTank(); + int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1; + int terrainType; + int newLevel; + + if (e.isShiftDown()) { + terrainType = Terrains.FUEL_TANK_CF; + int oldLevel = curHex.getTerrain(terrainType).getLevel(); + newLevel = Math.max(10, oldLevel + wheelDir * 10); + } else if (e.isControlDown()) { + terrainType = Terrains.FUEL_TANK_MAGN; + int oldLevel = curHex.getTerrain(terrainType).getLevel(); + newLevel = Math.max(10, oldLevel + wheelDir * 10); + } else { + terrainType = Terrains.FUEL_TANK_ELEV; + int oldLevel = curHex.getTerrain(terrainType).getLevel(); + newLevel = Math.max(1, oldLevel + wheelDir); + } + + curHex.addTerrain(new Terrain(terrainType, newLevel)); + refreshTerrainList(); + repaintWorkingHex(); + }); + + FixedYPanel terrainButtonPanel = new FixedYPanel(new GridLayout(0, 4, 2, 2)); + addManyButtons(terrainButtonPanel, terrainButtons); + + FixedYPanel brushButtonPanel = new FixedYPanel(new GridLayout(0, 3, 2, 2)); + addManyButtons(brushButtonPanel, brushButtons); + buttonBrush1.setSelected(true); + + FixedYPanel undoButtonPanel = new FixedYPanel(new GridLayout(1, 2, 2, 2)); + addManyButtons(undoButtonPanel, List.of(buttonUndo, buttonRedo)); + + // Hex Elevation Control + texElev = new EditorTextField("0", 3); + texElev.addActionListener(this); + texElev.getDocument().addDocumentListener(this); + + butElevUp = prepareButton("ButtonHexUP", "Raise Hex Elevation", null, BASE_ARROWBUTTON_ICON_WIDTH); + butElevUp.setName("butElevUp"); + butElevUp.setToolTipText(Messages.getString("BoardEditor.butElevUp.toolTipText")); + + butElevDown = prepareButton("ButtonHexDN", "Lower Hex Elevation", null, BASE_ARROWBUTTON_ICON_WIDTH); + butElevDown.setName("butElevDown"); + butElevDown.setToolTipText(Messages.getString("BoardEditor.butElevDown.toolTipText")); + + // Terrain List + lisTerrainRenderer = new ComboboxToolTipRenderer(); + lisTerrain = new JList<>(new DefaultListModel<>()); + lisTerrain.addListSelectionListener(this); + lisTerrain.setCellRenderer(lisTerrainRenderer); + lisTerrain.setVisibleRowCount(6); + lisTerrain.setPrototypeCellValue(new TerrainTypeHelper(new Terrain(WATER, 2))); + lisTerrain.setFixedCellWidth(180); + refreshTerrainList(); + + // Terrain List, Preview, Delete + FixedYPanel panlisHex = new FixedYPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); + butDelTerrain = prepareButton("buttonRemT", "Delete Terrain", null, BASE_ARROWBUTTON_ICON_WIDTH); + butDelTerrain.setEnabled(false); + canHex = new HexCanvas(); + panlisHex.add(butDelTerrain); + panlisHex.add(new JScrollPane(lisTerrain)); + panlisHex.add(canHex); + + // Build the terrain list for the chooser ComboBox, + // excluding terrains that are handled internally + ArrayList tList = new ArrayList<>(); + for (int i = 1; i < Terrains.SIZE; i++) { + if (!Terrains.AUTOMATIC.contains(i)) { + tList.add(new TerrainHelper(i)); + } + } + TerrainHelper[] terrains = new TerrainHelper[tList.size()]; + tList.toArray(terrains); + Arrays.sort(terrains); + texTerrainLevel = new EditorTextField("0", 2, 0); + texTerrainLevel.addActionListener(this); + texTerrainLevel.getDocument().addDocumentListener(this); + choTerrainType = new JComboBox<>(terrains); + ComboboxToolTipRenderer renderer = new ComboboxToolTipRenderer(); + renderer.setTerrains(terrains); + choTerrainType.setRenderer(renderer); + // Selecting a terrain type in the Dropdown should deselect + // all in the terrain overview list except when selected from there + choTerrainType.addActionListener(e -> { + if (!terrListBlocker) { + lisTerrain.clearSelection(); + + // if we've selected DEPLOYMENT ZONE, disable the "exits" buttons + // and make the "exits" popup point to a multi-select list that lets + // the user choose which deployment zones will be flagged here + + // otherwise, re-enable all the buttons and reset the "exits" popup to its + // normal behavior + if (((TerrainHelper) choTerrainType.getSelectedItem()).getTerrainType() == Terrains.DEPLOYMENT_ZONE) { + butExitUp.setEnabled(false); + butExitDown.setEnabled(false); + texTerrExits.setEnabled(false); + cheTerrExitSpecified.setEnabled(false); + cheTerrExitSpecified.setText("Zones");// Messages.getString("BoardEditor.deploymentZoneIDs")); + butTerrExits.setActionCommand(CMD_EDIT_DEPLOYMENT_ZONES); + } else { + butExitUp.setEnabled(true); + butExitDown.setEnabled(true); + texTerrExits.setEnabled(true); + cheTerrExitSpecified.setEnabled(true); + cheTerrExitSpecified.setText(Messages.getString("BoardEditor.cheTerrExitSpecified")); + butTerrExits.setActionCommand(""); + } + } + }); + butAddTerrain = new JButton(Messages.getString("BoardEditor.butAddTerrain")); + butTerrUp = prepareButton("ButtonTLUP", "Increase Terrain Level", null, BASE_ARROWBUTTON_ICON_WIDTH); + butTerrDown = prepareButton("ButtonTLDN", "Decrease Terrain Level", null, BASE_ARROWBUTTON_ICON_WIDTH); + + // Exits + cheTerrExitSpecified = new JCheckBox(Messages.getString("BoardEditor.cheTerrExitSpecified")); + cheTerrExitSpecified.addActionListener(this); + butTerrExits = prepareButton("ButtonExitA", Messages.getString("BoardEditor.butTerrExits"), null, + BASE_ARROWBUTTON_ICON_WIDTH); + texTerrExits = new EditorTextField("0", 2, 0); + texTerrExits.addActionListener(this); + texTerrExits.getDocument().addDocumentListener(this); + butExitUp = prepareButton("ButtonEXUP", "Increase Exit / Gfx", null, BASE_ARROWBUTTON_ICON_WIDTH); + butExitDown = prepareButton("ButtonEXDN", "Decrease Exit / Gfx", null, BASE_ARROWBUTTON_ICON_WIDTH); + + // Copy and Paste + FixedYPanel panCopyPaste = new FixedYPanel(new FlowLayout(FlowLayout.RIGHT, 4, 4)); + panCopyPaste.add(pasteButton); + panCopyPaste.add(copyButton); + copyButton.addActionListener(e -> copyWorkingHexToClipboard()); + pasteButton.addActionListener(e -> pasteFromClipboard()); + + // Arrows and text fields for type and exits + JPanel panUP = new JPanel(new GridLayout(1, 0, 4, 4)); + panUP.add(butTerrUp); + panUP.add(butExitUp); + panUP.add(butTerrExits); + JPanel panTex = new JPanel(new GridLayout(1, 0, 4, 4)); + panTex.add(texTerrainLevel); + panTex.add(texTerrExits); + panTex.add(cheTerrExitSpecified); + JPanel panDN = new JPanel(new GridLayout(1, 0, 4, 4)); + panDN.add(butTerrDown); + panDN.add(butExitDown); + panDN.add(Box.createHorizontalStrut(5)); + + // Auto Exits to Pavement + cheRoadsAutoExit = new JCheckBox(Messages.getString("BoardEditor.cheRoadsAutoExit")); + cheRoadsAutoExit.addItemListener(this); + cheRoadsAutoExit.setSelected(true); + + // Theme + JPanel panTheme = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); + choTheme = new JComboBox<>(); + TilesetManager tileMan = bv.getTilesetManager(); + Set themes = tileMan.getThemes(); + for (String s : themes) { + choTheme.addItem(s); + } + choTheme.addActionListener(this); + panTheme.add(labTheme); + panTheme.add(choTheme); + + // The hex settings panel (elevation, theme) + panelHexSettings.setBorder(new TitledBorder("Hex Settings")); + panelHexSettings.add(butElevUp); + panelHexSettings.add(texElev); + panelHexSettings.add(butElevDown); + panelHexSettings.add(panTheme); + + // The terrain settings panel (type, level, exits) + panelTerrSettings.setBorder(new TitledBorder("Terrain Settings")); + panelTerrSettings.add(Box.createVerticalStrut(5)); + panelTerrSettings.add(panUP); + + panelTerrSettings.add(choTerrainType); + panelTerrSettings.add(panTex); + + panelTerrSettings.add(butAddTerrain); + panelTerrSettings.add(panDN); + + // The board settings panel (Auto exit roads to pavement) + panelBoardSettings.setBorder(new TitledBorder("Board Settings")); + panelBoardSettings.add(cheRoadsAutoExit); + + // Board Buttons (Save, Load...) + butBoardNew = new JButton(Messages.getString("BoardEditor.butBoardNew")); + butBoardNew.setActionCommand(ClientGUI.BOARD_NEW); + + butExpandMap = new JButton(Messages.getString("BoardEditor.butExpandMap")); + butExpandMap.setActionCommand(ClientGUI.BOARD_RESIZE); + + butBoardOpen = new JButton(Messages.getString("BoardEditor.butBoardOpen")); + butBoardOpen.setActionCommand(ClientGUI.BOARD_OPEN); + + butBoardSave = new JButton(Messages.getString("BoardEditor.butBoardSave")); + butBoardSave.setActionCommand(ClientGUI.BOARD_SAVE); + + butBoardSaveAs = new JButton(Messages.getString("BoardEditor.butBoardSaveAs")); + butBoardSaveAs.setActionCommand(ClientGUI.BOARD_SAVE_AS); + + butBoardSaveAsImage = new JButton(Messages.getString("BoardEditor.butBoardSaveAsImage")); + butBoardSaveAsImage.setActionCommand(ClientGUI.BOARD_SAVE_AS_IMAGE); + + butBoardValidate = new JButton(Messages.getString("BoardEditor.butBoardValidate")); + butBoardValidate.setActionCommand(ClientGUI.BOARD_VALIDATE); + + butSourceFile = new JButton(Messages.getString("BoardEditor.butSourceFile")); + butSourceFile.setActionCommand(ClientGUI.BOARD_SOURCEFILE); + + addManyActionListeners(butBoardValidate, butBoardSaveAsImage, butBoardSaveAs, butBoardSave); + addManyActionListeners(butBoardOpen, butExpandMap, butBoardNew); + addManyActionListeners(butDelTerrain, butAddTerrain, butSourceFile); + + JPanel panButtons = new JPanel(new GridLayout(3, 3, 2, 2)); + addManyButtons(panButtons, List.of(butBoardNew, butBoardSave, butBoardOpen, + butExpandMap, butBoardSaveAs, butBoardSaveAsImage, butBoardValidate)); + if (Desktop.isDesktopSupported()) { + panButtons.add(butSourceFile); + } + + // Arrange everything + setLayout(new BorderLayout()); + Box centerPanel = Box.createVerticalBox(); + centerPanel.add(labHelp1); + centerPanel.add(labHelp2); + centerPanel.add(Box.createVerticalStrut(10)); + centerPanel.add(terrainButtonPanel); + centerPanel.add(Box.createVerticalStrut(10)); + centerPanel.add(brushButtonPanel); + centerPanel.add(Box.createVerticalStrut(10)); + centerPanel.add(undoButtonPanel); + centerPanel.add(Box.createVerticalGlue()); + centerPanel.add(panelBoardSettings); + centerPanel.add(panelHexSettings); + centerPanel.add(panelTerrSettings); + centerPanel.add(panlisHex); + centerPanel.add(panCopyPaste); + var scrCenterPanel = new JScrollPane(centerPanel); + scrCenterPanel.getVerticalScrollBar().setUnitIncrement(16); + add(scrCenterPanel, BorderLayout.CENTER); + add(panButtons, BorderLayout.PAGE_END); + minimapW = Minimap.createMinimap(frame, bv, game, null); + minimapW.setVisible(guip.getMinimapEnabled()); + } + + /** + * Returns coords that the active brush will paint on; + * returns only coords that are valid, i.e. on the board + */ + private LinkedList getBrushCoords(Coords center) { + var result = new LinkedList(); + // The center hex itself is always part of the brush + result.add(center); + // Add surrounding hexes for the big brush + if (brushSize >= 2) { + result.addAll(center.allAdjacent()); + } + if (brushSize == 3) { + result.addAll(center.allAtDistance(2)); + } + // Remove coords that are not on the board + result.removeIf(c -> !board.contains(c)); + return result; + } + + // Helper to shorten the code + private void addManyActionListeners(JButton... buttons) { + for (JButton button : buttons) { + button.addActionListener(this); + } + } + + // Helper to shorten the code + private void addManyButtons(JPanel panel, List terrainButtons) { + terrainButtons.forEach(panel::add); + } + + /** + * Save the hex at c into the current undo Set + */ + private void saveToUndo(Coords c) { + // Create a new set of hexes to save for undoing + // This will be filled as long as the mouse is dragged + if (currentUndoSet == null) { + currentUndoSet = new HashSet<>(); + currentUndoCoords = new HashSet<>(); + } + if (!currentUndoCoords.contains(c)) { + Hex hex = board.getHex(c).duplicate(); + // Newly drawn board hexes do not know their Coords + hex.setCoords(c); + currentUndoSet.add(hex); + currentUndoCoords.add(c); + } + } + + private void resetUndo() { + currentUndoSet = null; + currentUndoCoords = null; + undoStack.clear(); + redoStack.clear(); + buttonUndo.setEnabled(false); + buttonRedo.setEnabled(false); + } + + /** + * Changes the hex level at Coords c. Expects c + * to be on the board. + */ + private void relevelHex(Coords c) { + Hex newHex = board.getHex(c).duplicate(); + newHex.setLevel(hexLeveltoDraw); + board.resetStoredElevation(); + board.setHex(c, newHex); + + } + + /** + * Apply the current Hex to the Board at the specified location. + */ + void paintHex(Coords c) { + board.resetStoredElevation(); + board.setHex(c, curHex.duplicate()); + } + + /** + * Apply the current Hex to the Board at the specified location. + */ + public void retextureHex(Coords c) { + if (board.contains(c)) { + Hex newHex = curHex.duplicate(); + newHex.setLevel(board.getHex(c).getLevel()); + board.resetStoredElevation(); + board.setHex(c, newHex); + } + } + + /** + * Apply the current Hex to the Board at the specified location. + */ + public void addToHex(Coords c) { + if (board.contains(c)) { + Hex newHex = curHex.duplicate(); + Hex oldHex = board.getHex(c); + newHex.setLevel(oldHex.getLevel()); + int[] terrainTypes = oldHex.getTerrainTypes(); + for (int terrainID : terrainTypes) { + if (!newHex.containsTerrain(terrainID) && oldHex.containsTerrain(terrainID)) { + newHex.addTerrain(oldHex.getTerrain(terrainID)); + } + } + newHex.setTheme(oldHex.getTheme()); + board.resetStoredElevation(); + board.setHex(c, newHex); + } + } + + /** + * Sets the working hex to hex; + * used for mouse ALT-click (eyedropper function). + * + * @param hex hex to set. + */ + void setCurrentHex(Hex hex) { + curHex = hex.duplicate(); + texElev.setText(Integer.toString(curHex.getLevel())); + refreshTerrainList(); + if (lisTerrain.getModel().getSize() > 0) { + lisTerrain.setSelectedIndex(0); + refreshTerrainFromList(); + } + choTheme.setSelectedItem(curHex.getTheme()); + repaint(); + repaintWorkingHex(); + } + + private void repaintWorkingHex() { + if (curHex != null) { + TilesetManager tm = bv.getTilesetManager(); + tm.clearHex(curHex); + } + canHex.repaint(); + lastClicked = null; + } + + /** + * Refreshes the terrain list to match the current hex + */ + private void refreshTerrainList() { + TerrainTypeHelper selectedEntry = lisTerrain.getSelectedValue(); + ((DefaultListModel) lisTerrain.getModel()).removeAllElements(); + lisTerrainRenderer.setTerrainTypes(null); + int[] terrainTypes = curHex.getTerrainTypes(); + List types = new ArrayList<>(); + for (final int terrainType : terrainTypes) { + final Terrain terrain = curHex.getTerrain(terrainType); + if ((terrain != null) && !Terrains.AUTOMATIC.contains(terrainType)) { + final TerrainTypeHelper tth = new TerrainTypeHelper(terrain); + types.add(tth); + } + } + Collections.sort(types); + for (final TerrainTypeHelper tth : types) { + ((DefaultListModel) lisTerrain.getModel()).addElement(tth); + } + lisTerrainRenderer.setTerrainTypes(types); + // Reselect the formerly selected terrain if possible + if (selectedEntry != null) { + selectTerrain(selectedEntry.getTerrain()); + } + } + + /** + * Returns a new instance of the terrain that is currently entered in the + * terrain input fields + */ + private Terrain enteredTerrain() { + int type = ((TerrainHelper) Objects.requireNonNull(choTerrainType.getSelectedItem())).getTerrainType(); + int level = texTerrainLevel.getNumber(); + // For the terrain subtypes that only add to a main terrain type exits make no + // sense at all. Therefore simply do not add them + if ((type == Terrains.BLDG_ARMOR) || (type == Terrains.BLDG_CF) + || (type == Terrains.BLDG_ELEV) || (type == Terrains.BLDG_CLASS) + || (type == Terrains.BLDG_BASE_COLLAPSED) || (type == Terrains.BLDG_BASEMENT_TYPE) + || (type == Terrains.BRIDGE_CF) || (type == Terrains.BRIDGE_ELEV) + || (type == Terrains.FUEL_TANK_CF) || (type == Terrains.FUEL_TANK_ELEV) + || (type == Terrains.FUEL_TANK_MAGN)) { + return new Terrain(type, level, false, 0); + } else { + boolean exitsSpecified = cheTerrExitSpecified.isSelected(); + int exits = texTerrExits.getNumber(); + return new Terrain(type, level, exitsSpecified, exits); + } + } + + /** + * Add or set the terrain to the list based on the fields. + */ + private void addSetTerrain() { + Terrain toAdd = enteredTerrain(); + if (((toAdd.getType() == Terrains.BLDG_ELEV) || (toAdd.getType() == Terrains.BRIDGE_ELEV)) + && (toAdd.getLevel() < 0)) { + texTerrainLevel.setNumber(0); + JOptionPane.showMessageDialog(frame, + Messages.getString("BoardEditor.BridgeBuildingElevError"), + Messages.getString("BoardEditor.invalidTerrainTitle"), + JOptionPane.ERROR_MESSAGE); + return; + } + + curHex.addTerrain(toAdd); + + noTextFieldUpdate = true; + refreshTerrainList(); + repaintWorkingHex(); + noTextFieldUpdate = false; + } + + /** + * Cycle the terrain level (mouse wheel behavior) from the easy access buttons + */ + private void addSetTerrainEasy(int type, int level) { + boolean exitsSpecified = false; + int exits = 0; + Terrain present = curHex.getTerrain(type); + if (present != null) { + exitsSpecified = present.hasExitsSpecified(); + exits = present.getExits(); + } + Terrain toAdd = new Terrain(type, level, exitsSpecified, exits); + curHex.addTerrain(toAdd); + refreshTerrainList(); + repaintWorkingHex(); + } + + /** + * Sets valid basic Fuel Tank values as far as they are missing + */ + private void setBasicFuelTank() { + // There is only fuel_tank:1, so this can be set + curHex.addTerrain(new Terrain(Terrains.FUEL_TANK, 1, true, 0)); + + if (!curHex.containsTerrain(Terrains.FUEL_TANK_CF)) { + curHex.addTerrain(new Terrain(Terrains.FUEL_TANK_CF, 40, false, 0)); + } + + if (!curHex.containsTerrain(Terrains.FUEL_TANK_ELEV)) { + curHex.addTerrain(new Terrain(Terrains.FUEL_TANK_ELEV, 1, false, 0)); + } + + if (!curHex.containsTerrain(Terrains.FUEL_TANK_MAGN)) { + curHex.addTerrain(new Terrain(Terrains.FUEL_TANK_MAGN, 100, false, 0)); + } + + refreshTerrainList(); + selectTerrain(new Terrain(Terrains.FUEL_TANK_ELEV, 1)); + repaintWorkingHex(); + } + + /** + * Sets valid basic bridge values as far as they are missing + */ + private void setBasicBridge() { + if (!curHex.containsTerrain(Terrains.BRIDGE_CF)) { + curHex.addTerrain(new Terrain(Terrains.BRIDGE_CF, 40, false, 0)); + } + + if (!curHex.containsTerrain(Terrains.BRIDGE_ELEV)) { + curHex.addTerrain(new Terrain(Terrains.BRIDGE_ELEV, 1, false, 0)); + } + + if (!curHex.containsTerrain(Terrains.BRIDGE)) { + curHex.addTerrain(new Terrain(Terrains.BRIDGE, 1, false, 0)); + } + + refreshTerrainList(); + selectTerrain(new Terrain(Terrains.BRIDGE_ELEV, 1)); + repaintWorkingHex(); + } + + /** + * Sets valid basic Building values as far as they are missing + */ + private void setBasicBuilding(boolean ALT_Held) { + if (!curHex.containsTerrain(Terrains.BLDG_CF)) { + curHex.addTerrain(new Terrain(Terrains.BLDG_CF, 15, false, 0)); + } + + if (!curHex.containsTerrain(Terrains.BLDG_ELEV)) { + curHex.addTerrain(new Terrain(Terrains.BLDG_ELEV, 1, false, 0)); + } + + if (!curHex.containsTerrain(Terrains.BUILDING)) { + curHex.addTerrain(new Terrain(Terrains.BUILDING, 1, ALT_Held, 0)); + } + + // When clicked with ALT, only toggle the exits + if (ALT_Held) { + Terrain curTerr = curHex.getTerrain(Terrains.BUILDING); + curHex.addTerrain(new Terrain(Terrains.BUILDING, + curTerr.getLevel(), !curTerr.hasExitsSpecified(), curTerr.getExits())); + } + + refreshTerrainList(); + selectTerrain(new Terrain(Terrains.BLDG_ELEV, 1)); + repaintWorkingHex(); + } + + /** + * Set all the appropriate terrain fields to match the currently selected + * terrain in the list. + */ + private void refreshTerrainFromList() { + if (lisTerrain.isSelectionEmpty()) { + butDelTerrain.setEnabled(false); + } else { + butDelTerrain.setEnabled(true); + Terrain terrain = new Terrain(lisTerrain.getSelectedValue().getTerrain()); + terrain = curHex.getTerrain(terrain.getType()); + TerrainHelper terrainHelper = new TerrainHelper(terrain.getType()); + terrListBlocker = true; + choTerrainType.setSelectedItem(terrainHelper); + texTerrainLevel.setText(Integer.toString(terrain.getLevel())); + setExitsState(terrain.hasExitsSpecified()); + texTerrExits.setNumber(terrain.getExits()); + terrListBlocker = false; + } + } + + /** + * Updates the selected terrain in the terrain list if + * a terrain is actually selected + */ + private void updateWhenSelected() { + if (!lisTerrain.isSelectionEmpty()) { + addSetTerrain(); + } + } + + public void boardNew(boolean showDialog) { + boolean userCancel = false; + if (showDialog) { + RandomMapDialog rmd = new RandomMapDialog(frame, this, null, mapSettings); + userCancel = rmd.activateDialog(bv.getTilesetManager().getThemes()); + } + if (!userCancel) { + board = BoardUtilities.generateRandom(mapSettings); + // "Initialize" all hexes to add internally handled terrains + correctExits(); + game.setBoard(board); + curBoardFile = null; + choTheme.setSelectedItem(mapSettings.getTheme()); + setupUiFreshBoard(); + } + } + + public void boardResize() { + ResizeMapDialog emd = new ResizeMapDialog(frame, this, null, mapSettings); + boolean userCancel = emd.activateDialog(bv.getTilesetManager().getThemes()); + if (!userCancel) { + board = BoardUtilities.generateRandom(mapSettings); + + // Implant the old board + int west = emd.getExpandWest(); + int north = emd.getExpandNorth(); + int east = emd.getExpandEast(); + int south = emd.getExpandSouth(); + board = implantOldBoard(game, west, north, east, south); + + game.setBoard(board); + curBoardFile = null; + setupUiFreshBoard(); + } + } + + // When we resize a board, implant the old board's hexes where they should be in + // the new board + public Board implantOldBoard(Game game, int west, int north, int east, int south) { + Board oldBoard = game.getBoard(); + for (int x = 0; x < oldBoard.getWidth(); x++) { + for (int y = 0; y < oldBoard.getHeight(); y++) { + int newX = x + west; + int odd = x & 1 & west; + int newY = y + north + odd; + if (oldBoard.contains(x, y) && board.contains(newX, newY)) { + Hex oldHex = oldBoard.getHex(x, y); + Hex hex = board.getHex(newX, newY); + hex.removeAllTerrains(); + hex.setLevel(oldHex.getLevel()); + int[] terrainTypes = oldHex.getTerrainTypes(); + for (int terrainID : terrainTypes) { + if (!hex.containsTerrain(terrainID) && oldHex.containsTerrain(terrainID)) { + hex.addTerrain(oldHex.getTerrain(terrainID)); + } + } + hex.setTheme(oldHex.getTheme()); + board.setHex(newX, newY, hex); + board.resetStoredElevation(); + } + } + } + return board; + } + + @Override + public void updateMapSettings(MapSettings newSettings) { + mapSettings = newSettings; + } + + public void loadBoard() { + JFileChooser fc = new JFileChooser(loadPath); + setDialogSize(fc); + fc.setDialogTitle(Messages.getString("BoardEditor.loadBoard")); + fc.setFileFilter(new BoardFileFilter()); + int returnVal = fc.showOpenDialog(frame); + saveDialogSize(fc); + if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) { + // I want a file, y'know! + return; + } + loadBoard(fc.getSelectedFile()); + } + + public void loadBoard(File file) { + try (InputStream is = new FileInputStream(file)) { + // tell the board to load! + board.load(is, null, true); + Set boardTags = board.getTags(); + // Board generation in a game always calls BoardUtilities.combine + // This serves no purpose here, but is necessary to create + // flipBGVert/flipBGHoriz lists for the board, which is necessary + // for the background image to work in the BoardEditor + board = BoardUtilities.combine(board.getWidth(), board.getHeight(), 1, 1, + new Board[] { board }, Collections.singletonList(false), MapSettings.MEDIUM_GROUND); + game.setBoard(board); + // BoardUtilities.combine does not preserve tags, so add them back + for (String tag : boardTags) { + board.addTag(tag); + } + cheRoadsAutoExit.setSelected(board.getRoadsAutoExit()); + mapSettings.setBoardSize(board.getWidth(), board.getHeight()); + curBoardFile = file; + RecentBoardList.addBoard(curBoardFile); + loadPath = curBoardFile.getParentFile(); + + // Now, *after* initialization of the board which will correct some errors, + // do a board validation + validateBoard(false); + refreshTerrainList(); + setupUiFreshBoard(); + } catch (IOException ex) { + logger.error(ex, "loadBoard"); + showBoardLoadError(ex); + initializeBoardIfEmpty(); + } + } + + private void showBoardLoadError(Exception ex) { + String message = Messages.getString("BoardEditor.loadBoardError") + System.lineSeparator() + ex.getMessage(); + String title = Messages.getString("Error"); + JOptionPane.showMessageDialog(frame, message, title, JOptionPane.ERROR_MESSAGE); + } + + private void initializeBoardIfEmpty() { + if ((board == null) || (board.getWidth() == 0) || (board.getHeight() == 0)) { + boardNew(false); + } + } + + /** + * Will do board.initializeHex() for all hexes, correcting + * building and road connection issues for those hexes that do not have + * the exits check set. + */ + private void correctExits() { + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { + board.initializeHex(x, y); + } + } + } + + /** + * Saves the board in PNG image format. + */ + private void boardSaveImage(boolean ignoreUnits) { + if (curfileImage == null) { + boardSaveAsImage(ignoreUnits); + return; + } + JDialog waitD = new JDialog(frame, Messages.getString("BoardEditor.waitDialog.title")); + waitD.add(new JLabel(Messages.getString("BoardEditor.waitDialog.message"))); + waitD.setSize(250, 130); + // move to middle of screen + waitD.setLocation( + (frame.getSize().width / 2) - (waitD.getSize().width / 2), + (frame.getSize().height / 2) - (waitD.getSize().height / 2)); + waitD.setVisible(true); + frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + waitD.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + // save! + try { + ImageIO.write(bv.getEntireBoardImage(ignoreUnits, false), "png", curfileImage); + } catch (IOException e) { + logger.error(e, "boardSaveImage"); + } + waitD.setVisible(false); + frame.setCursor(Cursor.getDefaultCursor()); + } + + /** + * Saves the board to a .board file. + * When saveAs is true, acts as a Save As... by opening a file chooser dialog. + * When saveAs is false, it will directly save to the current board file name, + * if it is known and otherwise act as Save As... + */ + private boolean boardSave(boolean saveAs) { + // Correct connection issues and do a validation. + correctExits(); + validateBoard(false); + + // Choose a board file to save to if this was + // called as "Save As..." or there is no current filename + if ((curBoardFile == null) || saveAs) { + if (!chooseSaveBoardFile()) { + return false; + } + } + + // write the board + try (OutputStream os = new FileOutputStream(curBoardFile)) { + board.save(os); + + // Adapt to successful save + butSourceFile.setEnabled(true); + savedUndoStackSize = undoStack.size(); + hasChanges = false; + RecentBoardList.addBoard(curBoardFile); + setFrameTitle(); + return true; + } catch (IOException e) { + logger.error(e, "boardSave"); + return false; + } + } + + /** + * Shows a dialog for choosing a .board file to save to. + * Sets curBoardFile and returns true when a valid file was chosen. + * Returns false otherwise. + */ + private boolean chooseSaveBoardFile() { + JFileChooser fc = new JFileChooser(loadPath); + setDialogSize(fc); + fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100); + fc.setDialogTitle(Messages.getString("BoardEditor.saveBoardAs")); + fc.setFileFilter(new BoardFileFilter()); + int returnVal = fc.showSaveDialog(frame); + saveDialogSize(fc); + if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) { + return false; + } + File choice = fc.getSelectedFile(); + // make sure the file ends in board + if (!choice.getName().toLowerCase().endsWith(".board")) { + try { + choice = new File(choice.getCanonicalPath() + ".board"); + } catch (IOException ignored) { + return false; + } + } + curBoardFile = choice; + return true; + } + + /** + * Opens a file dialog box to select a file to save as; saves the board to + * the file as an image. Useful for printing boards. + */ + private void boardSaveAsImage(boolean ignoreUnits) { + JFileChooser fc = new JFileChooser("."); + setDialogSize(fc); + fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100); + fc.setDialogTitle(Messages.getString("BoardEditor.saveAsImage")); + fc.setFileFilter(new FileFilter() { + @Override + public boolean accept(File dir) { + return (dir.getName().endsWith(".png") || dir.isDirectory()); + } + + @Override + public String getDescription() { + return ".png"; + } + }); + int returnVal = fc.showSaveDialog(frame); + saveDialogSize(fc); + if ((returnVal != JFileChooser.APPROVE_OPTION) + || (fc.getSelectedFile() == null)) { + // I want a file, y'know! + return; + } + curfileImage = fc.getSelectedFile(); + + // make sure the file ends in png + if (!curfileImage.getName().toLowerCase().endsWith(".png")) { + try { + curfileImage = new File(curfileImage.getCanonicalPath() + ".png"); + } catch (IOException ignored) { + // failure! + return; + } + } + boardSaveImage(ignoreUnits); + } + + // + // ItemListener + // + @Override + public void itemStateChanged(ItemEvent ie) { + if (ie.getSource().equals(cheRoadsAutoExit)) { + // Set the new value for the option, and refresh the board. + board.setRoadsAutoExit(cheRoadsAutoExit.isSelected()); + bv.updateBoard(); + repaintWorkingHex(); + } + } + + // + // TextListener + // + @Override + public void changedUpdate(DocumentEvent te) { + if (te.getDocument().equals(texElev.getDocument())) { + int value; + try { + value = Integer.parseInt(texElev.getText()); + } catch (NumberFormatException ex) { + return; + } + if (value != curHex.getLevel()) { + curHex.setLevel(value); + repaintWorkingHex(); + } + } else if (te.getDocument().equals(texTerrainLevel.getDocument())) { + // prevent updating the terrain from looping back to + // update the text fields that have just been edited + if (!terrListBlocker) { + noTextFieldUpdate = true; + updateWhenSelected(); + noTextFieldUpdate = false; + } + } else if (te.getDocument().equals(texTerrExits.getDocument())) { + // prevent updating the terrain from looping back to + // update the text fields that have just been edited + if (!terrListBlocker) { + noTextFieldUpdate = true; + setExitsState(true); + updateWhenSelected(); + noTextFieldUpdate = false; + } + } + } + + @Override + public void insertUpdate(DocumentEvent event) { + changedUpdate(event); + } + + @Override + public void removeUpdate(DocumentEvent event) { + changedUpdate(event); + } + + /** Called when the user selects the "Help->About" menu item. */ + private void showAbout() { + new CommonAboutDialog(frame).setVisible(true); + } + + /** Called when the user selects the "Help->Contents" menu item. */ + private void showHelp() { + if (help == null) { + help = new BoardEditorHelpDialog(frame); + } + help.setVisible(true); // Show the help dialog. + } + + /** Called when the user selects the "View->Client Settings" menu item. */ + private void showSettings() { + if (setdlg == null) { + setdlg = new CommonSettingsDialog(frame); + } + setdlg.setVisible(true); + } + + /** + * Adjusts some UI and internal settings for a freshly + * loaded or freshly generated board. + */ + private void setupUiFreshBoard() { + // Reset the Undo stack and the board has no changes + savedUndoStackSize = 0; + canReturnToSaved = true; + resetUndo(); + hasChanges = false; + // When a board was loaded, we have a file, otherwise not + butSourceFile.setEnabled(curBoardFile != null); + // Adjust the UI + bvc.doLayout(); + setFrameTitle(); + } + + /** + * Performs board validation. When showPositiveResult is true, + * the result of the validation will be shown in a dialog. + * Otherwise, only a negative result (the board has errors) will + * be shown. + */ + private void validateBoard(boolean showPositiveResult) { + List errors = new ArrayList<>(); + board.isValid(errors); + if ((!errors.isEmpty()) || showPositiveResult) { + showBoardValidationReport(errors); + } + } + + /** + * Shows a board validation report dialog, reporting either + * the contents of errBuff or that the board has no errors. + */ + private void showBoardValidationReport(List errors) { + ignoreHotKeys = true; + if ((errors != null) && !errors.isEmpty()) { + String title = Messages.getString("BoardEditor.invalidBoard.title"); + String msg = Messages.getString("BoardEditor.invalidBoard.report"); + msg += String.join("\n", errors); + JTextArea textArea = new JTextArea(msg); + JScrollPane scrollPane = new JScrollPane(textArea); + textArea.setLineWrap(true); + textArea.setWrapStyleWord(true); + scrollPane.setPreferredSize(new Dimension(getWidth(), getHeight() / 2)); + JOptionPane.showMessageDialog(frame, scrollPane, title, JOptionPane.ERROR_MESSAGE); + } else { + String title = Messages.getString("BoardEditor.validBoard.title"); + String msg = Messages.getString("BoardEditor.validBoard.report"); + JOptionPane.showMessageDialog(frame, msg, title, JOptionPane.INFORMATION_MESSAGE); + } + ignoreHotKeys = false; + } + + // + // ActionListener + // + @Override + public void actionPerformed(ActionEvent ae) { + if (ae.getActionCommand().startsWith(ClientGUI.BOARD_RECENT)) { + if (hasChanges && (showSavePrompt() == DialogResult.CANCELLED)) { + return; + } + String recentBoard = ae.getActionCommand().substring(ClientGUI.BOARD_RECENT.length() + 1); + loadBoard(new File(recentBoard)); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_NEW)) { + ignoreHotKeys = true; + boardNew(true); + ignoreHotKeys = false; + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_RESIZE)) { + ignoreHotKeys = true; + boardResize(); + ignoreHotKeys = false; + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_OPEN)) { + ignoreHotKeys = true; + loadBoard(); + ignoreHotKeys = false; + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE)) { + ignoreHotKeys = true; + boardSave(false); + ignoreHotKeys = false; + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE_AS)) { + ignoreHotKeys = true; + boardSave(true); + ignoreHotKeys = false; + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE_AS_IMAGE)) { + ignoreHotKeys = true; + boardSaveAsImage(false); + ignoreHotKeys = false; + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SOURCEFILE)) { + if (curBoardFile != null) { + try { + Desktop.getDesktop().open(curBoardFile); + } catch (IOException e) { + ignoreHotKeys = true; + JOptionPane.showMessageDialog(frame, + Messages.getString("BoardEditor.OpenFileError", curBoardFile.toString()) + + e.getMessage()); + logger.error(e, "actionPerformed"); + ignoreHotKeys = false; + } + } + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_VALIDATE)) { + correctExits(); + validateBoard(true); + } else if (ae.getSource().equals(butDelTerrain) && !lisTerrain.isSelectionEmpty()) { + Terrain toRemove = new Terrain(lisTerrain.getSelectedValue().getTerrain()); + curHex.removeTerrain(toRemove.getType()); + refreshTerrainList(); + repaintWorkingHex(); + } else if (ae.getSource().equals(butAddTerrain)) { + addSetTerrain(); + } else if (ae.getSource().equals(butElevUp) && (curHex.getLevel() < 9)) { + curHex.setLevel(curHex.getLevel() + 1); + texElev.incValue(); + repaintWorkingHex(); + } else if (ae.getSource().equals(butElevDown) && (curHex.getLevel() > -5)) { + curHex.setLevel(curHex.getLevel() - 1); + texElev.decValue(); + repaintWorkingHex(); + } else if (ae.getSource().equals(butTerrUp)) { + texTerrainLevel.incValue(); + updateWhenSelected(); + } else if (ae.getSource().equals(butTerrDown)) { + texTerrainLevel.decValue(); + updateWhenSelected(); + } else if (ae.getSource().equals(texTerrainLevel)) { + updateWhenSelected(); + } else if (ae.getSource().equals(texTerrExits)) { + int exitsVal = texTerrExits.getNumber(); + if (exitsVal == 0) { + setExitsState(false); + } else if (exitsVal > 63) { + texTerrExits.setNumber(63); + } + updateWhenSelected(); + } else if (ae.getSource().equals(butTerrExits)) { + int exitsVal; + + if (ae.getActionCommand().equals(CMD_EDIT_DEPLOYMENT_ZONES)) { + var dlg = new MultiIntSelectorDialog(frame, "BoardEditor.deploymentZoneSelectorName", + "BoardEditor.deploymentZoneSelectorTitle", "BoardEditor.deploymentZoneSelectorDescription", + Board.MAX_DEPLOYMENT_ZONE_NUMBER, Board.exitsAsIntList(texTerrExits.getNumber())); + dlg.setVisible(true); + exitsVal = Board.IntListAsExits(dlg.getSelectedItems()); + texTerrExits.setNumber(exitsVal); + } else { + ExitsDialog ed = new ExitsDialog(frame); + exitsVal = texTerrExits.getNumber(); + ed.setExits(exitsVal); + ed.setVisible(true); + exitsVal = ed.getExits(); + texTerrExits.setNumber(exitsVal); + } + setExitsState(exitsVal != 0); + updateWhenSelected(); + } else if (ae.getSource().equals(cheTerrExitSpecified)) { + noTextFieldUpdate = true; + updateWhenSelected(); + noTextFieldUpdate = false; + setExitsState(cheTerrExitSpecified.isSelected()); + } else if (ae.getSource().equals(butExitUp)) { + setExitsState(true); + texTerrExits.incValue(); + updateWhenSelected(); + } else if (ae.getSource().equals(butExitDown)) { + texTerrExits.decValue(); + setExitsState(texTerrExits.getNumber() != 0); + updateWhenSelected(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_MINI_MAP)) { + guip.toggleMinimapEnabled(); + minimapW.setVisible(guip.getMinimapEnabled()); + } else if (ae.getActionCommand().equals(ClientGUI.HELP_ABOUT)) { + showAbout(); + } else if (ae.getActionCommand().equals(ClientGUI.HELP_CONTENTS)) { + showHelp(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_CLIENT_SETTINGS)) { + showSettings(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_ZOOM_IN)) { + bv.zoomIn(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_ZOOM_OUT)) { + bv.zoomOut(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_TOGGLE_ISOMETRIC)) { + bv.toggleIsometric(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_CHANGE_THEME)) { + String newTheme = bv.changeTheme(); + if (newTheme != null) { + choTheme.setSelectedItem(newTheme); + } + } else if (ae.getSource().equals(choTheme)) { + curHex.setTheme((String) choTheme.getSelectedItem()); + repaintWorkingHex(); + } else if (ae.getSource().equals(buttonLW)) { + setConvenientTerrain(ae, new Terrain(Terrains.WOODS, 1), new Terrain(Terrains.FOLIAGE_ELEV, 2)); + } else if (ae.getSource().equals(buttonOW)) { + setConvenientTerrain(ae, new Terrain(Terrains.WOODS, 1), new Terrain(Terrains.FOLIAGE_ELEV, 1)); + } else if (ae.getSource().equals(buttonMg)) { + setConvenientTerrain(ae, new Terrain(Terrains.MAGMA, 1)); + } else if (ae.getSource().equals(buttonLJ)) { + setConvenientTerrain(ae, new Terrain(Terrains.JUNGLE, 1), new Terrain(Terrains.FOLIAGE_ELEV, 2)); + } else if (ae.getSource().equals(buttonOJ)) { + setConvenientTerrain(ae, new Terrain(Terrains.JUNGLE, 1), new Terrain(Terrains.FOLIAGE_ELEV, 1)); + } else if (ae.getSource().equals(buttonWa)) { + buttonUpDn.setSelected(false); + if ((ae.getModifiers() & ActionEvent.CTRL_MASK) != 0) { + int rapidsLevel = curHex.containsTerrain(Terrains.RAPIDS, 1) ? 2 : 1; + if (!curHex.containsTerrain(Terrains.WATER) + || (curHex.getTerrain(Terrains.WATER).getLevel() == 0)) { + setConvenientTerrain(ae, new Terrain(Terrains.RAPIDS, rapidsLevel), + new Terrain(Terrains.WATER, 1)); + } else { + setConvenientTerrain(ae, new Terrain(Terrains.RAPIDS, rapidsLevel), + curHex.getTerrain(Terrains.WATER)); + } + } else { + if ((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { + curHex.removeAllTerrains(); + } + setConvenientTerrain(ae, new Terrain(Terrains.WATER, 1)); + } + } else if (ae.getSource().equals(buttonSw)) { + setConvenientTerrain(ae, new Terrain(Terrains.SWAMP, 1)); + } else if (ae.getSource().equals(buttonRo)) { + setConvenientTerrain(ae, new Terrain(Terrains.ROUGH, 1)); + } else if (ae.getSource().equals(buttonPv)) { + setConvenientTerrain(ae, new Terrain(Terrains.PAVEMENT, 1)); + } else if (ae.getSource().equals(buttonMd)) { + setConvenientTerrain(ae, new Terrain(Terrains.MUD, 1)); + } else if (ae.getSource().equals(buttonTu)) { + setConvenientTerrain(ae, new Terrain(Terrains.TUNDRA, 1)); + } else if (ae.getSource().equals(buttonIc)) { + setConvenientTerrain(ae, new Terrain(Terrains.ICE, 1)); + } else if (ae.getSource().equals(buttonSn)) { + setConvenientTerrain(ae, new Terrain(Terrains.SNOW, 1)); + } else if (ae.getSource().equals(buttonCl)) { + curHex.removeAllTerrains(); + buttonUpDn.setSelected(false); + refreshTerrainList(); + repaintWorkingHex(); + } else if (ae.getSource().equals(buttonBrush1)) { + brushSize = 1; + lastClicked = null; + } else if (ae.getSource().equals(buttonBrush2)) { + brushSize = 2; + lastClicked = null; + } else if (ae.getSource().equals(buttonBrush3)) { + brushSize = 3; + lastClicked = null; + } else if (ae.getSource().equals(buttonBu)) { + buttonUpDn.setSelected(false); + if (((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) + && ((ae.getModifiers() & ActionEvent.ALT_MASK) == 0)) { + curHex.removeAllTerrains(); + } + setBasicBuilding((ae.getModifiers() & ActionEvent.ALT_MASK) != 0); + } else if (ae.getSource().equals(buttonBr)) { + if ((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { + curHex.removeAllTerrains(); + } + buttonUpDn.setSelected(false); + setBasicBridge(); + } else if (ae.getSource().equals(buttonFT)) { + if ((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { + curHex.removeAllTerrains(); + } + buttonUpDn.setSelected(false); + setBasicFuelTank(); + } else if (ae.getSource().equals(buttonRd)) { + setConvenientTerrain(ae, new Terrain(Terrains.ROAD, 1)); + } else if (ae.getSource().equals(buttonUpDn)) { + // Not so useful to only do on clear terrain + buttonOOC.setSelected(false); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_UNDO)) { + // The button should not be active when the stack is empty, but + // let's check nevertheless + if (undoStack.isEmpty()) { + buttonUndo.setEnabled(false); + } else { + HashSet recentHexes = undoStack.pop(); + HashSet redoHexes = new HashSet<>(); + for (Hex hex : recentHexes) { + // Retrieve the board hex for Redo + Hex rHex = board.getHex(hex.getCoords()).duplicate(); + rHex.setCoords(hex.getCoords()); + redoHexes.add(rHex); + // and undo the board hex + board.setHex(hex.getCoords(), hex); + } + redoStack.push(redoHexes); + if (undoStack.isEmpty()) { + buttonUndo.setEnabled(false); + } + hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize); + buttonRedo.setEnabled(true); + currentUndoSet = null; // should be anyway + correctExits(); + } + setFrameTitle(); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REDO)) { + // The button should not be active when the stack is empty, but + // let's check nevertheless + if (redoStack.isEmpty()) { + buttonRedo.setEnabled(false); + } else { + HashSet recentHexes = redoStack.pop(); + HashSet undoHexes = new HashSet<>(); + for (Hex hex : recentHexes) { + Hex rHex = board.getHex(hex.getCoords()).duplicate(); + rHex.setCoords(hex.getCoords()); + undoHexes.add(rHex); + board.setHex(hex.getCoords(), hex); + } + undoStack.push(undoHexes); + if (redoStack.isEmpty()) { + buttonRedo.setEnabled(false); + } + buttonUndo.setEnabled(true); + hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize); + currentUndoSet = null; // should be anyway + correctExits(); + } + setFrameTitle(); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_RAISE)) { + boardChangeLevel(); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_CLEAR)) { + boardClear(); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_FLOOD)) { + boardFlood(); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_WATER)) { + boardRemoveTerrain(WATER, WATER_FLUFF); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_ROADS)) { + boardRemoveTerrain(ROAD, ROAD_FLUFF, BRIDGE, BRIDGE_CF, BRIDGE_ELEV); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_FORESTS)) { + boardRemoveTerrain(WOODS, JUNGLE, FOLIAGE_ELEV); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_BUILDINGS)) { + boardRemoveTerrain(BUILDING, BLDG_ARMOR, BLDG_CF, BLDG_CLASS, + BLDG_FLUFF, BLDG_BASE_COLLAPSED, BLDG_BASEMENT_TYPE, BLDG_ELEV, + FUEL_TANK, FUEL_TANK_CF, FUEL_TANK_ELEV, FUEL_TANK_MAGN); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_FLATTEN)) { + boardFlatten(); + } else if (ae.getActionCommand().equals(ClientGUI.VIEW_RESET_WINDOW_POSITIONS)) { + minimapW.setBounds(0, 0, minimapW.getWidth(), minimapW.getHeight()); + } + } + + /** Flattens the board, setting all hexes to level 0. */ + private void boardFlatten() { + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { + Coords c = new Coords(x, y); + if (board.getHex(c).getLevel() != 0) { + saveToUndo(c); + Hex newHex = board.getHex(c).duplicate(); + newHex.setLevel(0); + board.setHex(c, newHex); + } + } + } + correctExits(); + endCurrentUndoSet(); + } + + /** Removes the given terrain type(s) from the board. */ + private void boardRemoveTerrain(int type, int... types) { + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { + Coords c = new Coords(x, y); + if (board.getHex(c).containsTerrain(type) || board.getHex(c).containsAnyTerrainOf(types)) { + saveToUndo(c); + Hex newHex = board.getHex(c).duplicate(); + newHex.removeTerrain(type); + for (int additional : types) { + newHex.removeTerrain(additional); + } + board.setHex(c, newHex); + } + } + } + correctExits(); + endCurrentUndoSet(); + } + + /** + * Asks for confirmation and clears the whole board (sets all hexes to clear + * level 0). + */ + private void boardClear() { + if (!MMConfirmDialog.confirm(frame, + Messages.getString("BoardEditor.clearTitle"), Messages.getString("BoardEditor.clearMsg"))) { + return; + } + board.resetStoredElevation(); + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { + Coords c = new Coords(x, y); + saveToUndo(c); + board.setHex(c, new Hex(0)); + } + } + correctExits(); + endCurrentUndoSet(); + } + + /** + * "Pushes" the current set of undoable hexes as a package to the stack, meaning + * that a + * paint or other action is finished. + */ + private void endCurrentUndoSet() { + if ((currentUndoSet != null) && !currentUndoSet.isEmpty()) { + undoStack.push(currentUndoSet); + currentUndoSet = null; + buttonUndo.setEnabled(true); + // Drawing something disables any redo actions + redoStack.clear(); + buttonRedo.setEnabled(false); + // When Undo (without Redo) has been used after saving + // and the user draws on the board, then it can + // no longer know if it's been returned to the saved state + // and it will always be treated as changed. + if (savedUndoStackSize > undoStack.size()) { + canReturnToSaved = false; + } + hasChanges = !canReturnToSaved || (undoStack.size() != savedUndoStackSize); + } + } + + /** + * Asks for a level delta and changes the level of all the board's hexes by that + * delta. + */ + private void boardChangeLevel() { + var dlg = new LevelChangeDialog(frame); + dlg.setVisible(true); + if (!dlg.getResult().isConfirmed() || (dlg.getLevelChange() == 0)) { + return; + } + + board.resetStoredElevation(); + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { + Coords c = new Coords(x, y); + saveToUndo(c); + Hex newHex = board.getHex(c).duplicate(); + newHex.setLevel(newHex.getLevel() + dlg.getLevelChange()); + board.setHex(c, newHex); + } + } + correctExits(); + endCurrentUndoSet(); + } + + /** + * Asks for flooding info and then floods the whole board with water up to a + * level. + */ + private void boardFlood() { + var dlg = new FloodDialog(frame); + dlg.setVisible(true); + if (!dlg.getResult().isConfirmed()) { + return; + } + + int surface = dlg.getLevelChange(); + board.resetStoredElevation(); + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { + Coords c = new Coords(x, y); + Hex hex = board.getHex(c); + if (hex.getLevel() < surface) { + saveToUndo(c); + Hex newHex = hex.duplicate(); + int presentDepth = hex.containsTerrain(Terrains.WATER) ? hex.terrainLevel(Terrains.WATER) : 0; + if (dlg.getRemoveTerrain()) { + newHex.removeAllTerrains(); + // Restore bridges if they're above the water + if (hex.containsTerrain(BRIDGE) + && (hex.getLevel() + hex.getTerrain(BRIDGE_ELEV).getLevel() >= surface)) { + newHex.addTerrain(hex.getTerrain(BRIDGE)); + newHex.addTerrain(new Terrain(BRIDGE_ELEV, + hex.getLevel() + hex.getTerrain(BRIDGE_ELEV).getLevel() - surface)); + newHex.addTerrain(hex.getTerrain(BRIDGE_CF)); + } + } + int addedWater = surface - hex.getLevel(); + newHex.addTerrain(new Terrain(Terrains.WATER, addedWater + presentDepth)); + newHex.setLevel(newHex.getLevel() + addedWater); + board.setHex(c, newHex); + } + } + } + correctExits(); + endCurrentUndoSet(); + } + + private void setConvenientTerrain(ActionEvent event, Terrain... terrains) { + if (terrains.length == 0) { + return; + } + if ((event.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { + curHex.removeAllTerrains(); + } + buttonUpDn.setSelected(false); + for (var terrain : terrains) { + curHex.addTerrain(terrain); + } + refreshTerrainList(); + repaintWorkingHex(); + selectTerrain(terrains[0]); + } + + /** + * Selects the given terrain in the terrain list, if possible. All but terrain + * type is ignored. + */ + private void selectTerrain(Terrain terrain) { + for (int i = 0; i < lisTerrain.getModel().getSize(); i++) { + Terrain listEntry = lisTerrain.getModel().getElementAt(i).getTerrain(); + if (listEntry.getType() == terrain.getType()) { + lisTerrain.setSelectedIndex(i); + return; + } + } + } + + /** + * Sets the "Use Exits" checkbox to newState and adapts the coloring of the + * textfield accordingly. + * Use this instead of setting the checkbox state directly. + */ + private void setExitsState(boolean newState) { + cheTerrExitSpecified.setSelected(newState); + if (cheTerrExitSpecified.isSelected()) { + texTerrExits.setForeground(null); + } else { + texTerrExits.setForeground(UIUtil.uiGray()); + } + } + + @Override + public void valueChanged(ListSelectionEvent event) { + if (event.getValueIsAdjusting()) { + return; + } + if (event.getSource().equals(lisTerrain) && !noTextFieldUpdate) { + refreshTerrainFromList(); + } + } + + /** + * Displays the currently selected hex picture, in component form + */ + private class HexCanvas extends JPanel { + + /** Returns list or an empty list when list is null. */ + private List safeList(List list) { + return list == null ? Collections.emptyList() : list; + } + + StringDrawer invalidString = new StringDrawer(Messages.getString("BoardEditor.INVALID")) + .at(HexTileset.HEX_W / 2, HexTileset.HEX_H / 2).color(guip.getWarningColor()) + .outline(Color.WHITE, 1).font(FontHandler.notoFont().deriveFont(Font.BOLD)).center(); + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + if (curHex != null) { + // draw the terrain images + TilesetManager tm = bv.getTilesetManager(); + g.drawImage(tm.baseFor(curHex), 0, 0, HexTileset.HEX_W, HexTileset.HEX_H, this); + for (final Image newVar : safeList(tm.supersFor(curHex))) { + g.drawImage(newVar, 0, 0, this); + } + for (final Image newVar : safeList(tm.orthoFor(curHex))) { + g.drawImage(newVar, 0, 0, this); + } + UIUtil.setHighQualityRendering(g); + // add level and INVALID if necessary + g.setColor(getForeground()); + g.setFont(new Font(MMConstants.FONT_SANS_SERIF, Font.PLAIN, 9)); + g.drawString(Messages.getString("BoardEditor.LEVEL") + curHex.getLevel(), 24, 70); + List errors = new ArrayList<>(); + if (!curHex.isValid(errors)) { + invalidString.draw(g); + String tooltip = Messages.getString("BoardEditor.invalidHex") + String.join("
", errors); + setToolTipText(tooltip); + } else { + setToolTipText(null); + } + } else { + g.clearRect(0, 0, 72, 72); + } + } + + // Make the hex stubborn when resizing the frame + @Override + public Dimension getPreferredSize() { + return new Dimension(90, 90); + } + + @Override + public Dimension getMinimumSize() { + return new Dimension(90, 90); + } + } + + /** + * @return the frame this is displayed in + */ + public JFrame getFrame() { + return frame; + } + + /** + * Returns true if a dialog is visible on top of the ClientGUI. + * For example, the MegaMekController should ignore hotkeys + * if there is a dialog, like the CommonSettingsDialog, open. + * + * @return whether hot keys should be ignored or not + */ + public boolean shouldIgnoreHotKeys() { + return ignoreHotKeys + || UIUtil.isModalDialogDisplayed() + || ((help != null) && help.isVisible()) + || ((setdlg != null) && setdlg.isVisible()) + || texElev.hasFocus() || texTerrainLevel.hasFocus() || texTerrExits.hasFocus(); + } + + private void setDialogSize(JFileChooser dialog) { + int width = guip.getBoardEditLoadWidth(); + int height = guip.getBoardEditLoadHeight(); + dialog.setPreferredSize(new Dimension(width, height)); + } + + private void saveDialogSize(JComponent dialog) { + guip.setBoardEditLoadHeight(dialog.getHeight()); + guip.setBoardEditLoadWidth(dialog.getWidth()); + } + + /** + * Sets the Board Editor frame title, adding the current file name if any + * and a "*" if the board has unsaved changes. + */ + private void setFrameTitle() { + String title = (curBoardFile == null) ? Messages.getString("BoardEditor.title") + : Messages.getString("BoardEditor.title0", curBoardFile); + frame.setTitle(title + (hasChanges ? "*" : "")); + } + + private void copyWorkingHexToClipboard() { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(new StringSelection(curHex.getClipboardString()), null); + } + + private void pasteFromClipboard() { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable contents = clipboard.getContents(null); + if ((contents != null) && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + try { + String clipboardString = (String) contents.getTransferData(DataFlavor.stringFlavor); + Hex pastedHex = Hex.parseClipboardString(clipboardString); + if (pastedHex != null) { + setCurrentHex(pastedHex); + } + } catch (Exception ex) { + logger.error(ex, "pasteFromClipboard"); + } + } + } + + /** + * Specialized field for the BoardEditor that supports + * MouseWheel changes. + * + * @author Simon + */ + public static class EditorTextField extends JTextField { + private int minValue = Integer.MIN_VALUE; + private int maxValue = Integer.MAX_VALUE; + + /** + * Creates an EditorTextField based on JTextField. This is a + * specialized field for the BoardEditor that supports + * MouseWheel changes. + * + * @param text the initial text + * @param columns as in JTextField + * + * @see JTextField#JTextField(String, int) + */ + public EditorTextField(String text, int columns) { + super(text, columns); + // Automatically select all text when clicking the text field + addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + selectAll(); + } + }); + addMouseWheelListener(e -> { + if (e.getWheelRotation() < 0) { + incValue(); + } else { + decValue(); + } + }); + setMargin(new Insets(1, 1, 1, 1)); + setHorizontalAlignment(JTextField.CENTER); + setFont(new Font(MMConstants.FONT_SANS_SERIF, Font.BOLD, 20)); + setCursor(Cursor.getDefaultCursor()); + } + + /** + * Creates an EditorTextField based on JTextField. This is a + * specialized field for the BoardEditor that supports + * MouseWheel changes. + * + * @param text the initial text + * @param columns as in JTextField + * @param minimum a minimum value that the EditorTextField + * will generally adhere to when its own methods are used + * to change its value. + * + * @see JTextField#JTextField(String, int) + * + * @author Simon/Juliez + */ + public EditorTextField(String text, int columns, int minimum) { + this(text, columns); + minValue = minimum; + } + + /** + * Creates an EditorTextField based on JTextField. This is a + * specialized field for the BoardEditor that supports + * MouseWheel changes. + * + * @param text the initial text + * @param columns as in JTextField + * @param minimum a minimum value that the EditorTextField + * will generally adhere to when its own methods are used + * to change its value. + * @param maximum a maximum value that the EditorTextField + * will generally adhere to when its own methods are used + * to change its value. + * + * @see JTextField#JTextField(String, int) + * + * @author Simon/Juliez + */ + public EditorTextField(String text, int columns, int minimum, int maximum) { + this(text, columns); + minValue = minimum; + maxValue = maximum; + } + + /** + * Increases the EditorTextField's number by one, if a number + * is present. + */ + public void incValue() { + int newValue = getNumber() + 1; + setNumber(newValue); + } + + /** + * Lowers the EditorTextField's number by one, if a number + * is present and if that number is higher than the minimum + * value. + */ + public void decValue() { + setNumber(getNumber() - 1); + } + + /** + * Sets the text to newValue. If newValue is lower + * than the EditorTextField's minimum value, the minimum value will + * be set instead. + * + * @param newValue the value to be set + */ + public void setNumber(int newValue) { + int value = Math.max(newValue, minValue); + value = Math.min(value, maxValue); + setText(Integer.toString(value)); + } + + /** + * Returns the text in the EditorTextField's as an int. + * Returns 0 when no parsable number (only letters) are present. + */ + public int getNumber() { + try { + return Integer.parseInt(getText()); + } catch (NumberFormatException ex) { + return 0; + } + } + } + + /** + * A specialized JButton that only shows an icon but scales that icon according + * to the current GUI scaling when its rescale() method is called. + */ + private static class ScalingIconButton extends JButton { + + private final Image baseImage; + private Image baseRolloverImage; + private Image baseDisabledImage; + private final int baseWidth; + + ScalingIconButton(Image image, int width) { + super(); + Objects.requireNonNull(image); + baseImage = image; + baseWidth = width; + rescale(); + } + + /** Adapts all images of this button to the current gui scale. */ + void rescale() { + int realWidth = UIUtil.scaleForGUI(baseWidth); + int realHeight = baseImage.getHeight(null) * realWidth / baseImage.getWidth(null); + setIcon(new ImageIcon(ImageUtil.getScaledImage(baseImage, realWidth, realHeight))); + + if (baseRolloverImage != null) { + realHeight = baseRolloverImage.getHeight(null) * realWidth / baseRolloverImage.getWidth(null); + setRolloverIcon(new ImageIcon(ImageUtil.getScaledImage(baseRolloverImage, realWidth, realHeight))); + } else { + setRolloverIcon(null); + } + + if (baseDisabledImage != null) { + realHeight = baseDisabledImage.getHeight(null) * realWidth / baseDisabledImage.getWidth(null); + setDisabledIcon(new ImageIcon(ImageUtil.getScaledImage(baseDisabledImage, realWidth, realHeight))); + } else { + setDisabledIcon(null); + } + } + + /** + * Sets the unscaled base image to use as a mouse hover image for the button. + * image may be null. Passing null disables the hover image. + */ + void setRolloverImage(@Nullable Image image) { + baseRolloverImage = image; + } + + /** + * Sets the unscaled base image to use as a button disabled image for the + * button. + * image may be null. Passing null disables the button disabled image. + */ + void setDisabledImage(@Nullable Image image) { + baseDisabledImage = image; + } + } + + /** + * A specialized JToggleButton that only shows an icon but scales that icon + * according + * to the current GUI scaling when its rescale() method is called. + */ + private static class ScalingIconToggleButton extends JToggleButton { + + private final Image baseImage; + private Image baseRolloverImage; + private Image baseSelectedImage; + private final int baseWidth; + + ScalingIconToggleButton(Image image, int width) { + super(); + Objects.requireNonNull(image); + baseImage = image; + baseWidth = width; + rescale(); + } + + /** Adapts all images of this button to the current gui scale. */ + void rescale() { + int realWidth = UIUtil.scaleForGUI(baseWidth); + int realHeight = baseImage.getHeight(null) * realWidth / baseImage.getWidth(null); + setIcon(new ImageIcon(ImageUtil.getScaledImage(baseImage, realWidth, realHeight))); + + if (baseRolloverImage != null) { + realHeight = baseRolloverImage.getHeight(null) * realWidth / baseRolloverImage.getWidth(null); + setRolloverIcon(new ImageIcon(ImageUtil.getScaledImage(baseRolloverImage, realWidth, realHeight))); + } else { + setRolloverIcon(null); + } + + if (baseSelectedImage != null) { + realHeight = baseSelectedImage.getHeight(null) * realWidth / baseSelectedImage.getWidth(null); + setSelectedIcon(new ImageIcon(ImageUtil.getScaledImage(baseSelectedImage, realWidth, realHeight))); + } else { + setSelectedIcon(null); + } + } + + /** + * Sets the unscaled base image to use as a mouse hover image for the button. + * image may be null. Passing null disables the hover image. + */ + void setRolloverImage(@Nullable Image image) { + baseRolloverImage = image; + } + + /** + * Sets the unscaled base image to use as a "toggle button is selected" image + * for the button. + * image may be null. Passing null disables the "is selected" image. + */ + void setSelectedImage(@Nullable Image image) { + baseSelectedImage = image; + } + } +} diff --git a/megamek/src/megamek/client/ui/swing/util/MegaMekController.java b/megamek/src/megamek/client/ui/swing/util/MegaMekController.java index 41f4eac01a2..421c6743484 100644 --- a/megamek/src/megamek/client/ui/swing/util/MegaMekController.java +++ b/megamek/src/megamek/client/ui/swing/util/MegaMekController.java @@ -20,13 +20,16 @@ */ package megamek.client.ui.swing.util; -import java.awt.KeyEventDispatcher; +import megamek.client.ui.swing.BoardEditor; +import megamek.client.ui.swing.GUIPreferences; +import megamek.client.ui.swing.IClientGUI; +import megamek.client.ui.swing.ai.editor.AiProfileEditor; + +import java.awt.*; import java.awt.event.KeyEvent; import java.util.*; import java.util.function.Supplier; -import megamek.client.ui.swing.*; - /** * This class implements a KeyEventDispatcher, which handles all generated * KeyEvents. If the KeyEvent correspondes to a registerd hotkey, the action for @@ -66,6 +69,7 @@ public interface KeyBindAction { private static final int MAX_REPEAT_RATE = 100; + public AiProfileEditor aiEditor = null; public BoardEditor boardEditor = null; public IClientGUI clientgui = null; @@ -248,8 +252,8 @@ public synchronized void removeAllActions() { } /** - * Start a new repeating timer task for the given KeyCommandBind. If the given - * KeyCommandBind already has a repeating task, a new one is not added. Also, + * Start a new repeating timer task for the given KeyCommandBind. If the given + * KeyCommandBind already has a repeating task, a new one is not added. Also, * if there is no mapped CommandAction for the given KeyCommandBind no task is scheduled. */ protected void startRepeating(KeyCommandBind kcb, final CommandAction action) { diff --git a/megamek/src/megamek/common/Configuration.java b/megamek/src/megamek/common/Configuration.java index 2500751ff84..daf15fa48d7 100644 --- a/megamek/src/megamek/common/Configuration.java +++ b/megamek/src/megamek/common/Configuration.java @@ -123,7 +123,8 @@ public final class Configuration { private static final String DEFAULT_DIR_NAME_IMG_UNIVERSE = "universe"; private static final String DEFAULT_DIR_ORBITAL_BOMBARDMENT = "orbital_bombardment"; private static final String DEFAULT_DIR_NUKE = "nuke"; - + public static final String DEFAULT_DIR_AI = "ai"; + public static final String DEFAULT_DIR_TW_AI = "tw"; private Configuration() { } @@ -344,6 +345,29 @@ public static File nukeHexesDir() { return new File(hexesDir(), DEFAULT_DIR_NUKE); } + /** + * Return the ai directory, which is relative to the data directory. + * @return {@link File} containing the path to the ai directory. + */ + public static File aiDir() { + return new File(dataDir(), DEFAULT_DIR_AI); + } + + /** + * Return the ai directory, which is relative to the data directory. + * @return {@link File} containing the path to the ai directory. + */ + public static File twAiDir() { + return new File(aiDir(), DEFAULT_DIR_TW_AI); + } + + /** + * Return the userdata ai tw directory, which is relative to the data directory. + * @return {@link File} containing the path to the ai directory. + */ + public static File userDataAiTwDir() { + return new File(Configuration.userdataDir(), DEFAULT_DIR_AI + File.separator + DEFAULT_DIR_TW_AI); + } /** * Get the fluff images directory, which is relative to the images * directory. diff --git a/megamek/src/megamek/common/preference/ClientPreferences.java b/megamek/src/megamek/common/preference/ClientPreferences.java index 95edf63a33c..002c1efef4d 100644 --- a/megamek/src/megamek/common/preference/ClientPreferences.java +++ b/megamek/src/megamek/common/preference/ClientPreferences.java @@ -44,6 +44,7 @@ public class ClientPreferences extends PreferenceStoreProxy { public static final String GAMELOG_KEEP = "KeepGameLog"; public static final String GAMELOG_FILENAME = "GameLogFilename"; public static final String AUTO_RESOLVE_GAMELOG_FILENAME = "AutoResolveGameLogFilename"; + public static final String AI_DIRECTORY = "AIDirectory"; public static final String STAMP_FILENAMES = "StampFilenames"; public static final String STAMP_FORMAT = "StampFormat"; public static final String SHOW_UNIT_ID = "ShowUnitId"; @@ -90,6 +91,7 @@ public ClientPreferences(IPreferenceStore store) { store.setDefault(GAMELOG_KEEP, true); store.setDefault(GAMELOG_FILENAME, "gamelog.html"); store.setDefault(AUTO_RESOLVE_GAMELOG_FILENAME, "simulation.html"); + store.setDefault(AI_DIRECTORY, store.getDefaultString(DATA_DIRECTORY) + File.separator + "ai"); store.setDefault(STAMP_FORMAT, "_yyyy-MM-dd_HH-mm-ss"); store.setDefault(UNIT_START_CHAR, 'A'); store.setDefault(GUI_NAME, "swing"); @@ -200,6 +202,10 @@ public void setGoalPlayers(int n) { store.setValue(GOAL_PLAYERS, n); } + public String getAiDirectory() { + return store.getString(AI_DIRECTORY); + } + public String getGameLogFilename() { return store.getString(GAMELOG_FILENAME); } @@ -284,6 +290,10 @@ public void setMaxPathfinderTime(int i) { store.setValue(MAX_PATHFINDER_TIME, i); } + public void setAiDirectory(String name) { + store.setValue(AI_DIRECTORY, name); + } + public void setGameLogFilename(String name) { store.setValue(GAMELOG_FILENAME, name); } diff --git a/megamek/src/megamek/logging/MMLogger.java b/megamek/src/megamek/logging/MMLogger.java index c9cc3f26d59..f05ff35a177 100644 --- a/megamek/src/megamek/logging/MMLogger.java +++ b/megamek/src/megamek/logging/MMLogger.java @@ -165,6 +165,18 @@ public void error(Throwable exception, String message) { exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.ERROR, null, message, exception); } + /** + * Error Level Logging w/ Exception + * + * @param exception Exception that was caught via a try/catch block. + * @param message Additional message to report to the log file. + */ + public void error(Throwable exception, String message, Object... args) { + Sentry.captureException(exception); + message = String.format(message, args); + exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.ERROR, null, message, exception); + } + /** * Error Level Logging w/ Exception * @@ -206,6 +218,40 @@ public void error(Throwable exception, String message, String title) { } } + /** + * Formatted Error Level Logging w/ Exception w/ Dialog. + * + * @param message Message to write to the log file AND be displayed in the + * error pane. + * @param title Title of the error message box. + */ + public void formattedErrorDialog(Throwable e, String title, String message, Object... args) { + message = String.format(message, args); + error(e, message); + Sentry.captureException(e); + try { + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); + } catch (Exception ignored) { + // if the message dialog crashes, we don't really care + } + } + + /** + * Formatted Error Level Logging w/o Exception w/ Dialog. + * + * @param message Message to write to the log file AND be displayed in the + * error pane. + * @param title Title of the error message box. + */ + public void formattedErrorDialog(String title, String message, Object... args) { + message = String.format(message, args); + try { + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); + } catch (Exception ignored) { + // if the message dialog crashes, we don't really care + } + } + /** * Error Level Logging w/o Exception w/ Dialog. * From c6a6b76f64ba1110ec150e3e773d4cd9c6d43fcf Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 25 Dec 2024 19:21:50 -0300 Subject: [PATCH 03/16] feat: curve graph working, multi hover graph working, AI editor halfway there --- megamek/src/megamek/MegaMek.java | 5 + megamek/src/megamek/ai/utility/Curve.java | 48 +++++++- megamek/src/megamek/ai/utility/Decision.java | 2 +- .../src/megamek/ai/utility/DefaultCurve.java | 4 +- .../megamek/client/ui/swing/MegaMekGUI.java | 48 ++++---- .../ui/swing/ai/editor/AiProfileEditor.form | 51 ++++++--- .../ui/swing/ai/editor/AiProfileEditor.java | 81 ++++++++++---- .../ui/swing/ai/editor/ConsiderationPane.form | 88 ++++++++++----- .../ui/swing/ai/editor/ConsiderationPane.java | 89 +++++++++++---- .../client/ui/swing/ai/editor/CurveGraph.java | 76 +++++++++++++ .../client/ui/swing/ai/editor/CurvePane.form | 2 +- .../client/ui/swing/ai/editor/CurvePane.java | 15 +-- .../ai/editor/DecisionScoreEvaluatorPane.form | 92 ++++++++-------- .../ai/editor/DecisionScoreEvaluatorPane.java | 104 ++++++++++-------- .../ui/swing/ai/editor/HoverStateModel.java | 49 +++++++++ 15 files changed, 546 insertions(+), 208 deletions(-) create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/CurveGraph.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/HoverStateModel.java diff --git a/megamek/src/megamek/MegaMek.java b/megamek/src/megamek/MegaMek.java index e8a520d1863..2eb08d67cf1 100644 --- a/megamek/src/megamek/MegaMek.java +++ b/megamek/src/megamek/MegaMek.java @@ -70,6 +70,11 @@ public class MegaMek { private static final MMLogger logger = MMLogger.create(MegaMek.class); private static final SanityInputFilter sanityInputFilter = new SanityInputFilter(); + public static boolean isDevelopment() { + // env variable mm.profile=dev + return "dev".equals(System.getenv("mm.profile")); + } + public static void main(String... args) { ObjectInputFilter.Config.setSerialFilter(sanityInputFilter); diff --git a/megamek/src/megamek/ai/utility/Curve.java b/megamek/src/megamek/ai/utility/Curve.java index 97f252e9eb3..f5b66c7a095 100644 --- a/megamek/src/megamek/ai/utility/Curve.java +++ b/megamek/src/megamek/ai/utility/Curve.java @@ -37,15 +37,53 @@ public interface Curve { double evaluate(double x); default void drawAxes(Graphics g, int width, int height) { - // Center lines - g.setColor(Color.LIGHT_GRAY); - g.drawLine(0, height/2, width, height/2); // X-axis - g.drawLine(width/2, 0, width/2, height); // Y-axis + // Draw axis labels (0 to 1 with 0.05 increments) + int padding = 10; // Padding for text from the axis lines + double increment = 0.05; - // Restore color to black + // Draw Y-axis labels + for (double y = 0.0; y <= 1.0; y += increment) { + int yPos = (int) (height - (y * height)); // Map 0-1 range to pixel coordinates + g.setColor(Color.BLACK); + g.drawLine(0, yPos, width, yPos); // X-axis + + g.setColor(Color.WHITE); + g.drawString(String.format("%.2f", y), padding, yPos + 5); // Label + + } + + // Draw X-axis labels + for (double x = 0.0; x <= 1.0; x += increment) { + int xPos = (int) (x * width); // Map 0-1 range to pixel coordinates + g.setColor(Color.BLACK); + g.drawLine(xPos, 0, xPos, height); // Y-axis + + g.setColor(Color.WHITE); + g.drawString(String.format("%.2f", x), xPos - 10, height); // Label + } + + + + // Restore color g.setColor(Color.BLACK); } + default void drawPoint(Graphics g, int width, int height, Color color, double xPosNormalized) { + Graphics2D g2d = (Graphics2D) g; + g2d.setColor(color); + g2d.setStroke(new BasicStroke(1)); + double y = this.evaluate(xPosNormalized); + + int px1 = (int)(xPosNormalized * width); + int py1 = 0; + + g2d.drawLine(px1, py1, px1, height); + g2d.drawString( + String.format("Input: %.2f, Eval: %.2f", xPosNormalized, y), + 40, 20 // Position of the text + ); + } + default void drawCurve(Graphics g, int width, int height, Color color) { Graphics2D g2d = (Graphics2D) g; g2d.setColor(color); diff --git a/megamek/src/megamek/ai/utility/Decision.java b/megamek/src/megamek/ai/utility/Decision.java index 3e9abd2784b..926ff4d1f9e 100644 --- a/megamek/src/megamek/ai/utility/Decision.java +++ b/megamek/src/megamek/ai/utility/Decision.java @@ -51,7 +51,7 @@ public Decision(Action action, double weight, DecisionScoreEvaluator
- + + @@ -12,7 +13,6 @@ - @@ -80,16 +80,23 @@ - + - + + + - + + + + + + @@ -153,7 +160,7 @@ - + @@ -161,15 +168,7 @@ - - - - - - - - - + @@ -218,6 +217,14 @@ + + + + + + + + @@ -278,9 +285,19 @@ - + + + + - + + + + + + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index e7e51124fb7..7f25dc186ad 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -53,23 +53,57 @@ public class AiProfileEditor extends JFrame { private JPanel decisionPane; private JComboBox actionComboBox; private JSpinner weightSpinner; - private JScrollPane evaluatorScrollPane; private JPanel uAiEditorPanel; + private JPanel considerationsPane1; + private JScrollPane profileScrollPane; + private JPanel decisionScoreEvaluatorPanel; public AiProfileEditor(MegaMekController controller) { this.controller = controller; $$$setupUI$$$(); initialize(); setTitle("AI Profile Editor"); - setSize(1200, 800); + setSize(1200, 1000); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setContentPane(uAiEditorPanel); setVisible(true); } private void initialize() { - considerationsScrollPane.setViewportView(new ConsiderationPane()); - evaluatorScrollPane.setViewportView(new DecisionScoreEvaluatorPane()); + + // Set layout for decisionScoreEvaluatorPanel + decisionScoreEvaluatorPanel.setLayout(new GridBagLayout()); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = GridBagConstraints.RELATIVE; + gbc.fill = GridBagConstraints.NONE; // Do not stretch + gbc.weightx = 0.0; + gbc.weighty = 0.0; + gbc.anchor = GridBagConstraints.NORTHEAST; // Align to the top-right + gbc.insets = new Insets(0, 0, 0, 0); // No padding + + // Add DecisionScoreEvaluatorPane to the panel + decisionScoreEvaluatorPanel.add(new DecisionScoreEvaluatorPane(), gbc); + +// +// GridBagConstraints gbc = new GridBagConstraints(); +// gbc.gridx = 0; +// gbc.gridy = GridBagConstraints.RELATIVE; +// gbc.fill = GridBagConstraints.HORIZONTAL; +// gbc.weightx = 0.0; +// gbc.weighty = 0.0; +// gbc.anchor = GridBagConstraints.NORTHEAST; + var hover = new HoverStateModel(); + var considerations = List.of(new ConsiderationPane(), + new ConsiderationPane(), + new ConsiderationPane(), + new ConsiderationPane()); + for (var c : considerations) { + considerationsPane1.add(c, gbc); + c.setHoverStateModel(hover); + } + newDecisionButton.addActionListener(e -> { var action = (Action) actionComboBox.getSelectedItem(); var weight = (double) weightSpinner.getValue(); @@ -106,8 +140,8 @@ private void createUIComponents() { addToMutableTreeNode(root, "Considerations", sharedData.getConsiderations()); addToMutableTreeNode(root, "Decision Score Evaluators (DSE)", sharedData.getDecisionScoreEvaluators()); DefaultTreeModel treeModel = new DefaultTreeModel(root); - repositoryViewer = new JTree(treeModel); + repositoryViewer = new JTree(treeModel); actionComboBox = new JComboBox<>(Action.values()); var model = new DecisionScoreEvaluatorTableModel<>(sharedData.getDecisions()); @@ -132,17 +166,9 @@ private void addToMutableTreeNode(DefaultMutableTreeNode private void $$$setupUI$$$() { createUIComponents(); uAiEditorPanel = new JPanel(); - uAiEditorPanel.setLayout(new GridBagLayout()); + uAiEditorPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); final JSplitPane splitPane1 = new JSplitPane(); - GridBagConstraints gbc; - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - gbc.insets = new Insets(3, 3, 3, 3); - uAiEditorPanel.add(splitPane1, gbc); + uAiEditorPanel.add(splitPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); final JPanel panel1 = new JPanel(); panel1.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); splitPane1.setLeftComponent(panel1); @@ -161,9 +187,14 @@ private void addToMutableTreeNode(DefaultMutableTreeNode profilePane = new JPanel(); profilePane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); dseEditorPane.addTab("Profile", profilePane); - final JScrollPane scrollPane1 = new JScrollPane(); - profilePane.add(scrollPane1, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); - scrollPane1.setViewportView(decisionScoreEvaluatorTable); + profileScrollPane = new JScrollPane(); + profileScrollPane.setWheelScrollingEnabled(true); + profilePane.add(profileScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + decisionScoreEvaluatorTable.setColumnSelectionAllowed(false); + decisionScoreEvaluatorTable.setFillsViewportHeight(true); + decisionScoreEvaluatorTable.setMinimumSize(new Dimension(150, 32)); + decisionScoreEvaluatorTable.setPreferredScrollableViewportSize(new Dimension(150, 32)); + profileScrollPane.setViewportView(decisionScoreEvaluatorTable); final JPanel panel3 = new JPanel(); panel3.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); profilePane.add(panel3, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); @@ -183,12 +214,10 @@ private void addToMutableTreeNode(DefaultMutableTreeNode label3.setText("Notes"); panel3.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); decisionPane = new JPanel(); - decisionPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + decisionPane.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); dseEditorPane.addTab("Decision", decisionPane); - evaluatorScrollPane = new JScrollPane(); - decisionPane.add(evaluatorScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); final JPanel panel4 = new JPanel(); - panel4.setLayout(new GridLayoutManager(1, 5, new Insets(0, 0, 0, 0), -1, -1)); + panel4.setLayout(new GridLayoutManager(2, 5, new Insets(0, 0, 0, 0), -1, -1)); decisionPane.add(panel4, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); final JLabel label4 = new JLabel(); label4.setText("Action"); @@ -201,6 +230,9 @@ private void addToMutableTreeNode(DefaultMutableTreeNode panel4.add(label5, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final Spacer spacer1 = new Spacer(); panel4.add(spacer1, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + decisionScoreEvaluatorPanel = new JPanel(); + decisionScoreEvaluatorPanel.setLayout(new GridBagLayout()); + panel4.add(decisionScoreEvaluatorPanel, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); considerationsPane = new JPanel(); considerationsPane.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); considerationsPane.setName(""); @@ -219,7 +251,12 @@ private void addToMutableTreeNode(DefaultMutableTreeNode descriptionDseTextField = new JTextField(); dseConfigPane.add(descriptionDseTextField, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); considerationsScrollPane = new JScrollPane(); + considerationsScrollPane.setHorizontalScrollBarPolicy(31); + considerationsScrollPane.setWheelScrollingEnabled(true); considerationsPane.add(considerationsScrollPane, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + considerationsPane1 = new JPanel(); + considerationsPane1.setLayout(new GridBagLayout()); + considerationsScrollPane.setViewportView(considerationsPane1); } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form index d494d9fd1c5..ee4383298fa 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form @@ -7,10 +7,10 @@ - + - + @@ -18,13 +18,13 @@ - + - + @@ -33,59 +33,95 @@ - - - + + + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index 3c7935a8988..42bc9948a20 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -17,6 +17,7 @@ import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; +import com.intellij.uiDesigner.core.Spacer; import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; @@ -30,10 +31,37 @@ public class ConsiderationPane extends JPanel { private JTable parametersTable; private JPanel considerationPane; private JPanel topThingsPane; + private JButton hideButton; + private JButton showButton; + private JButton newParameterButton; public ConsiderationPane() { $$$setupUI$$$(); add(considerationPane); + +// considerationComboBox.addActionListener(e -> { +// TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); +// considerationName.setText(consideration.getName()); +// ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); +// }); + + hideButton.addActionListener(e -> { + curveContainer.setVisible(false); + showButton.setVisible(true); + hideButton.setVisible(false); + }); + + showButton.addActionListener(e -> { + curveContainer.setVisible(true); + hideButton.setVisible(true); + showButton.setVisible(false); + }); + + showButton.setVisible(false); + } + + public void setHoverStateModel(HoverStateModel model) { + ((CurvePane) this.curveContainer).setHoverStateModel(model); } private void createUIComponents() { @@ -56,44 +84,67 @@ private void createUIComponents() { considerationPane = new JPanel(); considerationPane.setLayout(new GridBagLayout()); topThingsPane = new JPanel(); - topThingsPane.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); + topThingsPane.setLayout(new GridLayoutManager(6, 5, new Insets(0, 0, 0, 0), -1, -1)); GridBagConstraints gbc; gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; - gbc.gridwidth = 3; + gbc.gridwidth = 5; gbc.gridheight = 2; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH; considerationPane.add(topThingsPane, gbc); - topThingsPane.add(considerationComboBox, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + topThingsPane.add(considerationComboBox, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); considerationName = new JTextField(); - topThingsPane.add(considerationName, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); - topThingsPane.add(curveContainer, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(800, 600), new Dimension(800, 600), null, 0, false)); + topThingsPane.add(considerationName, new GridConstraints(3, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + topThingsPane.add(curveContainer, new GridConstraints(5, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(400, 300), new Dimension(400, 300), null, 0, false)); final JLabel label1 = new JLabel(); - label1.setText("Type"); - topThingsPane.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + label1.setText("Curve:"); + topThingsPane.add(label1, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label2 = new JLabel(); - label2.setText("Name"); - topThingsPane.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + label2.setText("Name:"); + topThingsPane.add(label2, new GridConstraints(2, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label3 = new JLabel(); - label3.setText("Curve"); - topThingsPane.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + label3.setText("Type:"); + topThingsPane.add(label3, new GridConstraints(0, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + hideButton = new JButton(); + hideButton.setText("Hide"); + topThingsPane.add(hideButton, new GridConstraints(4, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + showButton = new JButton(); + showButton.setText("Show"); + topThingsPane.add(showButton, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final Spacer spacer1 = new Spacer(); + topThingsPane.add(spacer1, new GridConstraints(4, 3, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 3; + gbc.gridwidth = 5; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + considerationPane.add(parametersTable, gbc); final JLabel label4 = new JLabel(); - label4.setText("Parameters"); + label4.setText("Parameters:"); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 2; - gbc.weighty = 1.0; - gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.gridwidth = 2; + gbc.anchor = GridBagConstraints.WEST; considerationPane.add(label4, gbc); + final JPanel spacer2 = new JPanel(); gbc = new GridBagConstraints(); - gbc.gridx = 1; + gbc.gridx = 3; gbc.gridy = 2; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - considerationPane.add(parametersTable, gbc); + gbc.gridwidth = 2; + gbc.fill = GridBagConstraints.HORIZONTAL; + considerationPane.add(spacer2, gbc); + newParameterButton = new JButton(); + newParameterButton.setText("New Parameter"); + gbc = new GridBagConstraints(); + gbc.gridx = 2; + gbc.gridy = 2; + gbc.fill = GridBagConstraints.HORIZONTAL; + considerationPane.add(newParameterButton, gbc); } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurveGraph.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurveGraph.java new file mode 100644 index 00000000000..e246613a11b --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurveGraph.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.ai.utility.Curve; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.concurrent.atomic.AtomicReference; + +public class CurveGraph extends JPanel { + + private HoverStateModel hoverStateModel; + private final AtomicReference selectedCurve; + + public CurveGraph(AtomicReference selectedCurve) { + this.selectedCurve = selectedCurve; + this.setHoverStateModel(new HoverStateModel()); + } + + public CurveGraph(HoverStateModel model, AtomicReference selectedCurve) { + this.selectedCurve = selectedCurve; + this.hoverStateModel = model; + } + + public void setHoverStateModel(HoverStateModel model) { + if (this.hoverStateModel != null) { + this.hoverStateModel.removeListener(this::repaint); + } + + this.hoverStateModel = model; + + // React to mouse movement + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + double relativeX = e.getX() / (double) getWidth(); + hoverStateModel.setHoveringRelativeXPosition(relativeX); // Update shared state + } + }); + + // React to hover state changes + hoverStateModel.addListener(this::repaint); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (selectedCurve.get() == null) { + return; + } + var curve = selectedCurve.get(); + curve.drawAxes(g, getWidth(), getHeight()); + curve.drawCurve(g, getWidth(), getHeight(), Color.BLUE); + + double hoverX = hoverStateModel.getHoveringRelativeXPosition(); + curve.drawPoint(g, getWidth(), getHeight(), Color.GREEN, hoverX); + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form index 6db9fea70eb..6c3117d6e6c 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form @@ -2,7 +2,7 @@
- + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index 89d5d1bf528..203c695b3a9 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -29,6 +29,7 @@ public class CurvePane extends JPanel { private JSpinner cParamSpinner; private JPanel curveGraph; private JPanel basePane; + private HoverStateModel hoverStateModel; public CurvePane() { $$$setupUI$$$(); @@ -36,6 +37,10 @@ public CurvePane() { add(basePane, BorderLayout.CENTER); } + public void setHoverStateModel(HoverStateModel model) { + ((CurveGraph) this.curveGraph).setHoverStateModel(model); + } + /** * Method generated by IntelliJ IDEA GUI Designer * >>> IMPORTANT!! <<< @@ -175,15 +180,7 @@ private void createUIComponents() { } }); - curveGraph = new JPanel() { - @Override - protected void paintComponent(Graphics g) { - if (selectedCurve.get() == null) { - return; - } - selectedCurve.get().drawCurve(g, getWidth(), getHeight(), Color.BLUE); - } - }; + curveGraph = new CurveGraph(selectedCurve); curveGraph.setPreferredSize(new Dimension(800, 600)); bParamSpinner = new JSpinner(new SpinnerNumberModel(0d, -100d, 100d, 0.01d)); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form index fa5ecef3deb..95be0a26bad 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form @@ -2,92 +2,98 @@ - + - + - - + + + + - - - - - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - - - + + - + + + - + - + - + - + - + - - - - - - - - - - - - - - - - diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index c8c6289aa7b..a815b70fcb1 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -17,6 +17,7 @@ import javax.swing.*; import java.awt.*; +import java.util.List; public class DecisionScoreEvaluatorPane extends JPanel { private JTextField nameField; @@ -24,17 +25,29 @@ public class DecisionScoreEvaluatorPane extends JPanel { private JTextField notesField; private JPanel decisionScoreEvaluatorPane; private JPanel considerationsPane; - ; + public DecisionScoreEvaluatorPane() { $$$setupUI$$$(); setLayout(new BorderLayout()); add(decisionScoreEvaluatorPane, BorderLayout.CENTER); - considerationsPane.add(new ConsiderationPane()); - considerationsPane.add(new ConsiderationPane()); - considerationsPane.add(new ConsiderationPane()); - considerationsPane.add(new ConsiderationPane()); - considerationsPane.add(new ConsiderationPane()); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = GridBagConstraints.RELATIVE; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + gbc.anchor = GridBagConstraints.NORTHEAST; + var hoverState = new HoverStateModel(); + var considerations = List.of(new ConsiderationPane(), + new ConsiderationPane(), + new ConsiderationPane(), + new ConsiderationPane()); + for (var c : considerations) { + c.setHoverStateModel(hoverState); + considerationsPane.add(c, gbc); + } } /** @@ -47,74 +60,77 @@ public DecisionScoreEvaluatorPane() { private void $$$setupUI$$$() { decisionScoreEvaluatorPane = new JPanel(); decisionScoreEvaluatorPane.setLayout(new GridBagLayout()); - final JLabel label1 = new JLabel(); - label1.setText("Name"); - label1.setVerticalAlignment(0); - label1.setVerticalTextPosition(0); + nameField = new JTextField(); GridBagConstraints gbc; gbc = new GridBagConstraints(); gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - decisionScoreEvaluatorPane.add(label1, gbc); - nameField = new JTextField(); - gbc = new GridBagConstraints(); - gbc.gridx = 1; - gbc.gridy = 0; + gbc.gridy = 1; gbc.weightx = 1.0; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; decisionScoreEvaluatorPane.add(nameField, gbc); descriptionField = new JTextField(); gbc = new GridBagConstraints(); - gbc.gridx = 1; - gbc.gridy = 1; + gbc.gridx = 0; + gbc.gridy = 3; gbc.weightx = 1.0; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; decisionScoreEvaluatorPane.add(descriptionField, gbc); - final JLabel label2 = new JLabel(); - label2.setText("Considerations"); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 3; - gbc.weighty = 1.0; - gbc.anchor = GridBagConstraints.NORTHWEST; - decisionScoreEvaluatorPane.add(label2, gbc); notesField = new JTextField(); gbc = new GridBagConstraints(); - gbc.gridx = 1; - gbc.gridy = 2; + gbc.gridx = 0; + gbc.gridy = 5; gbc.weightx = 1.0; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; decisionScoreEvaluatorPane.add(notesField, gbc); + final JScrollPane scrollPane1 = new JScrollPane(); + scrollPane1.setHorizontalScrollBarPolicy(31); + scrollPane1.setMaximumSize(new Dimension(800, 32767)); + scrollPane1.setMinimumSize(new Dimension(800, 600)); + scrollPane1.setWheelScrollingEnabled(true); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 7; + gbc.gridheight = 2; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + decisionScoreEvaluatorPane.add(scrollPane1, gbc); + considerationsPane = new JPanel(); + considerationsPane.setLayout(new GridBagLayout()); + considerationsPane.setMaximumSize(new Dimension(800, 2147483647)); + considerationsPane.setMinimumSize(new Dimension(800, 600)); + scrollPane1.setViewportView(considerationsPane); + final JLabel label1 = new JLabel(); + label1.setText("Considerations"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 6; + gbc.anchor = GridBagConstraints.WEST; + decisionScoreEvaluatorPane.add(label1, gbc); + final JLabel label2 = new JLabel(); + label2.setText("Notes:"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 4; + gbc.anchor = GridBagConstraints.WEST; + decisionScoreEvaluatorPane.add(label2, gbc); final JLabel label3 = new JLabel(); - label3.setText("Notes"); + label3.setText("Description:"); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 2; gbc.anchor = GridBagConstraints.WEST; decisionScoreEvaluatorPane.add(label3, gbc); final JLabel label4 = new JLabel(); - label4.setText("Description"); + label4.setText("Name:"); gbc = new GridBagConstraints(); gbc.gridx = 0; - gbc.gridy = 1; + gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; decisionScoreEvaluatorPane.add(label4, gbc); - final JScrollPane scrollPane1 = new JScrollPane(); - gbc = new GridBagConstraints(); - gbc.gridx = 1; - gbc.gridy = 3; - gbc.gridheight = 2; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - decisionScoreEvaluatorPane.add(scrollPane1, gbc); - considerationsPane = new JPanel(); - considerationsPane.setLayout(new GridBagLayout()); - scrollPane1.setViewportView(considerationsPane); } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/HoverStateModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/HoverStateModel.java new file mode 100644 index 00000000000..14b8aae4e26 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/HoverStateModel.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import java.util.ArrayList; +import java.util.List; + +public class HoverStateModel { + private double hoveringRelativeXPosition = -1; // -1 means no hovering + private final List listeners = new ArrayList<>(); + + public double getHoveringRelativeXPosition() { + return hoveringRelativeXPosition; + } + + public void setHoveringRelativeXPosition(double position) { + if (this.hoveringRelativeXPosition != position) { + this.hoveringRelativeXPosition = position; + notifyListeners(); + } + } + + public void addListener(Runnable listener) { + listeners.add(listener); + } + + private void notifyListeners() { + for (Runnable listener : listeners) { + listener.run(); + } + } + + public void removeListener(Runnable listener) { + listeners.remove(listener); + } +} From e87f6a17bc0d67c14e100346d97fdcb5f37dcea9 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Thu, 26 Dec 2024 22:40:29 -0300 Subject: [PATCH 04/16] feat: i18n the forms --- .../common/options/messages.properties | 18 ++ .../common/options/messages_de.properties | 18 ++ .../common/options/messages_en.properties | 18 ++ .../common/options/messages_es.properties | 18 ++ .../common/options/messages_ru.properties | 18 ++ .../ai/utility/tw/TWUtilityAIRepository.java | 24 +-- .../ui/swing/ai/editor/AiProfileEditor.form | 25 +-- .../ui/swing/ai/editor/AiProfileEditor.java | 140 +++++++++++++-- .../ui/swing/ai/editor/ConsiderationPane.form | 82 +++++---- .../ui/swing/ai/editor/ConsiderationPane.java | 161 ++++++++++++------ .../client/ui/swing/ai/editor/CurvePane.form | 2 +- .../client/ui/swing/ai/editor/CurvePane.java | 48 +++++- .../ai/editor/DecisionScoreEvaluatorPane.form | 12 +- .../ai/editor/DecisionScoreEvaluatorPane.java | 58 ++++++- .../swing/ai/editor/ParametersTableModel.java | 29 ++-- 15 files changed, 521 insertions(+), 150 deletions(-) diff --git a/megamek/i18n/megamek/common/options/messages.properties b/megamek/i18n/megamek/common/options/messages.properties index 17f1fe80a6e..de47cf271dc 100644 --- a/megamek/i18n/megamek/common/options/messages.properties +++ b/megamek/i18n/megamek/common/options/messages.properties @@ -1023,3 +1023,21 @@ WeaponQuirksInfo.option.misrepaired_weapon.displayableName=Misrepaired Weapon WeaponQuirksInfo.option.misrepaired_weapon.description=+1 to hit. WeaponQuirksInfo.option.misreplaced_weapon.displayableName=Misreplaced Weapon WeaponQuirksInfo.option.misreplaced_weapon.description=+1 to hit. +aiEditor.profile=Profile +aiEditor.action=Action +aiEditor.weight=Weight +aiEditor.profile.name=Profile Name +AiEditor.description=Description +aiEditor.notes=Notes +aiEditor.newConsideration=New Consideration +aiEditor.newDecision=New Decision +aiEditor.curve=Curve: +aiEditor.name=Name: +aiEditor.type=Type: +aiEditor.hide=Hide +aiEditor.show=Show +aiEditor.parameters=Parameters: +aiEditor.new.parameter=New Parameter +aiEditor.curve.type=Type +aiEditor.considerations=Considerations +aiEditor.description=Description: diff --git a/megamek/i18n/megamek/common/options/messages_de.properties b/megamek/i18n/megamek/common/options/messages_de.properties index 19d9d8459a4..9c32ff651e8 100644 --- a/megamek/i18n/megamek/common/options/messages_de.properties +++ b/megamek/i18n/megamek/common/options/messages_de.properties @@ -202,3 +202,21 @@ PilotOptionsInfo.option.edge_when_explosion.displayableName= Edge für Explosi PilotOptionsInfo.option.edge_when_explosion.description=Kritische Treffer bei explosiven Ausrüstungen werden erneut gewürfelt. PilotOptionsInfo.option.edge_when_masc_fails.displayableName= Edge für MASC/Supercharger Ausfälle. PilotOptionsInfo.option.edge_when_masc_fails.description=MASC/Supercharger Ausfälle werden mit Edge nachgewalzt +aiEditor.profile=Profile +aiEditor.action=Action +aiEditor.weight=Weight +aiEditor.profile.name=Profile Name +AiEditor.description=Description +aiEditor.notes=Notes +aiEditor.newConsideration=New Consideration +aiEditor.newDecision=New Decision +aiEditor.curve=Curve: +aiEditor.name=Name: +aiEditor.type=Type: +aiEditor.hide=Hide +aiEditor.show=Show +aiEditor.parameters=Parameters: +aiEditor.new.parameter=New Parameter +aiEditor.curve.type=Type +aiEditor.considerations=Considerations +aiEditor.description=Description: diff --git a/megamek/i18n/megamek/common/options/messages_en.properties b/megamek/i18n/megamek/common/options/messages_en.properties index e69de29bb2d..6e518e10f23 100644 --- a/megamek/i18n/megamek/common/options/messages_en.properties +++ b/megamek/i18n/megamek/common/options/messages_en.properties @@ -0,0 +1,18 @@ +aiEditor.action=Action +aiEditor.considerations=Considerations +aiEditor.curve=Curve: +aiEditor.curve.type=Type +AiEditor.description=Description +aiEditor.description=Description: +aiEditor.hide=Hide +aiEditor.name=Name: +aiEditor.new.parameter=New Parameter +aiEditor.newConsideration=New Consideration +aiEditor.newDecision=New Decision +aiEditor.notes=Notes +aiEditor.parameters=Parameters: +aiEditor.profile=Profile +aiEditor.profile.name=Profile Name +aiEditor.show=Show +aiEditor.type=Type: +aiEditor.weight=Weight \ No newline at end of file diff --git a/megamek/i18n/megamek/common/options/messages_es.properties b/megamek/i18n/megamek/common/options/messages_es.properties index b7396e84b4d..0f0ae947f19 100644 --- a/megamek/i18n/megamek/common/options/messages_es.properties +++ b/megamek/i18n/megamek/common/options/messages_es.properties @@ -986,3 +986,21 @@ WeaponQuirksInfo.option.misrepaired_weapon.displayableName=Arma mal reparada WeaponQuirksInfo.option.misrepaired_weapon.description=+1 al golpe. WeaponQuirksInfo.option.misreplaced_weapon.displayableName=Arma mal reemplazada WeaponQuirksInfo.option.misreplaced_weapon.description=+1 al golpe. +aiEditor.profile=Profile +aiEditor.action=Action +aiEditor.weight=Weight +aiEditor.profile.name=Profile Name +AiEditor.description=Description +aiEditor.notes=Notes +aiEditor.newConsideration=New Consideration +aiEditor.newDecision=New Decision +aiEditor.curve=Curve: +aiEditor.name=Name: +aiEditor.type=Type: +aiEditor.hide=Hide +aiEditor.show=Show +aiEditor.parameters=Parameters: +aiEditor.new.parameter=New Parameter +aiEditor.curve.type=Type +aiEditor.considerations=Considerations +aiEditor.description=Description: diff --git a/megamek/i18n/megamek/common/options/messages_ru.properties b/megamek/i18n/megamek/common/options/messages_ru.properties index ea50d37ea18..0dd5e98220e 100644 --- a/megamek/i18n/megamek/common/options/messages_ru.properties +++ b/megamek/i18n/megamek/common/options/messages_ru.properties @@ -779,3 +779,21 @@ WeaponQuirksInfo.option.static_feed.displayableName=Статичное пита WeaponQuirksInfo.option.static_feed.description=Нет игрового эффекта.\nЕще не имплементировано. (RS NTNU 3145 стр 15) WeaponQuirksInfo.option.non_functional.displayableName=Нефункционально WeaponQuirksInfo.option.non_functional.description=Нет игрового эффекта.\nЕще не имплементировано. (RS NTNU 3145 стр 14) +aiEditor.profile=Profile +aiEditor.action=Action +aiEditor.weight=Weight +aiEditor.profile.name=Profile Name +AiEditor.description=Description +aiEditor.notes=Notes +aiEditor.newConsideration=New Consideration +aiEditor.newDecision=New Decision +aiEditor.curve=Curve: +aiEditor.name=Name: +aiEditor.type=Type: +aiEditor.hide=Hide +aiEditor.show=Show +aiEditor.parameters=Parameters: +aiEditor.new.parameter=New Parameter +aiEditor.curve.type=Type +aiEditor.considerations=Considerations +aiEditor.description=Description: diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java index 6824fe5af5a..bc32bee723c 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -98,40 +98,32 @@ public List getProfiles() { return List.copyOf(profiles.values()); } - public boolean addDecision(TWDecision decision) { + public void addDecision(TWDecision decision) { if (decisions.containsKey(decision.getName())) { - logger.warn("Decision with name {} already exists", decision.getName()); - return false; + logger.info("Decision with name {} already exists, overwriting", decision.getName()); } decisions.put(decision.getName(), decision); - return true; } - public boolean addConsideration(TWConsideration consideration) { + public void addConsideration(TWConsideration consideration) { if (considerations.containsKey(consideration.getName())) { - logger.warn("Consideration with name {} already exists", consideration.getName()); - return false; + logger.info("Consideration with name {} already exists, overwriting", consideration.getName()); } considerations.put(consideration.getName(), consideration); - return true; } - public boolean addDecisionScoreEvaluator(TWDecisionScoreEvaluator decisionScoreEvaluator) { + public void addDecisionScoreEvaluator(TWDecisionScoreEvaluator decisionScoreEvaluator) { if (decisionScoreEvaluators.containsKey(decisionScoreEvaluator.getName())) { - logger.warn("DecisionScoreEvaluator with name {} already exists", decisionScoreEvaluator.getName()); - return false; + logger.info("DecisionScoreEvaluator with name {} already exists, overwriting", decisionScoreEvaluator.getName()); } decisionScoreEvaluators.put(decisionScoreEvaluator.getName(), decisionScoreEvaluator); - return true; } - public boolean addProfile(TWProfile profile) { + public void addProfile(TWProfile profile) { if (profiles.containsKey(profile.getName())) { - logger.warn("Profile with name {} already exists", profile.getName()); - return false; + logger.info("Profile with name {} already exists, overwriting", profile.getName()); } profiles.put(profile.getName(), profile); - return true; } private void persistToFile(File outputFile, Collection objects) { diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form index 6db19c9393d..386658a9047 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form @@ -10,7 +10,7 @@ - + @@ -32,7 +32,7 @@
- + @@ -42,7 +42,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -137,7 +137,7 @@ - + @@ -145,7 +145,7 @@ - + @@ -153,7 +153,7 @@ - + @@ -181,7 +181,8 @@ - + + @@ -209,7 +210,8 @@ - + + @@ -217,9 +219,10 @@ - + + - + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 7f25dc186ad..058e1f6a3b6 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -22,6 +22,8 @@ import megamek.ai.utility.NamedObject; import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; +import megamek.client.ui.Messages; +import megamek.client.ui.enums.DialogResult; import megamek.client.ui.swing.GUIPreferences; import megamek.client.ui.swing.util.MegaMekController; @@ -29,7 +31,11 @@ import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.lang.reflect.Method; import java.util.List; +import java.util.ResourceBundle; public class AiProfileEditor extends JFrame { private final TWUtilityAIRepository sharedData = TWUtilityAIRepository.getInstance(); @@ -58,6 +64,9 @@ public class AiProfileEditor extends JFrame { private JScrollPane profileScrollPane; private JPanel decisionScoreEvaluatorPanel; + private boolean hasChanges = false; + private boolean ignoreHotKeys = false; + public AiProfileEditor(MegaMekController controller) { this.controller = controller; $$$setupUI$$$(); @@ -112,6 +121,41 @@ private void initialize() { //noinspection unchecked ((DecisionScoreEvaluatorTableModel) model).addRow(dse); }); + + this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + // When the board has changes, ask the user + if (!hasChanges || (showSavePrompt() != DialogResult.CANCELLED)) { + if (controller != null) { + controller.removeAllActions(); + controller.aiEditor = null; + } + getFrame().dispose(); + } + + } + }); + } + + private DialogResult showSavePrompt() { + ignoreHotKeys = true; + int savePrompt = JOptionPane.showConfirmDialog(null, + Messages.getString("BoardEditor.exitprompt"), + Messages.getString("BoardEditor.exittitle"), + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + ignoreHotKeys = false; + // When the user cancels or did not actually save the board, don't load anything + if (((savePrompt == JOptionPane.YES_OPTION) && !hasChanges) + || (savePrompt == JOptionPane.CANCEL_OPTION) + || (savePrompt == JOptionPane.CLOSED_OPTION)) { + return DialogResult.CANCELLED; + } else { + persistProfile(); + return DialogResult.CONFIRMED; + } } private void persistProfile() { @@ -124,6 +168,7 @@ private void persistProfile() { i, dse.getAction().getActionName(), dse.getDecisionScoreEvaluator().getName()); + sharedData.addDecision(dse); } } @@ -168,15 +213,15 @@ private void addToMutableTreeNode(DefaultMutableTreeNode uAiEditorPanel = new JPanel(); uAiEditorPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); final JSplitPane splitPane1 = new JSplitPane(); - uAiEditorPanel.add(splitPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); + uAiEditorPanel.add(splitPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(200, 200), null, 0, false)); final JPanel panel1 = new JPanel(); panel1.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); splitPane1.setLeftComponent(panel1); newDecisionButton = new JButton(); - newDecisionButton.setText("New Decision"); + this.$$$loadButtonText$$$(newDecisionButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.newDecision")); panel1.add(newDecisionButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, 1, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(233, 34), null, 0, false)); newConsiderationButton = new JButton(); - newConsiderationButton.setText("New Consideration"); + this.$$$loadButtonText$$$(newConsiderationButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.newConsideration")); panel1.add(newConsiderationButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, 1, null, new Dimension(233, 34), null, 0, false)); panel1.add(repositoryViewer, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(233, 50), null, 0, false)); final JPanel panel2 = new JPanel(); @@ -186,7 +231,7 @@ private void addToMutableTreeNode(DefaultMutableTreeNode panel2.add(dseEditorPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); profilePane = new JPanel(); profilePane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); - dseEditorPane.addTab("Profile", profilePane); + dseEditorPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile"), profilePane); profileScrollPane = new JScrollPane(); profileScrollPane.setWheelScrollingEnabled(true); profilePane.add(profileScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); @@ -205,13 +250,13 @@ private void addToMutableTreeNode(DefaultMutableTreeNode notesTextField = new JTextField(); panel3.add(notesTextField, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); final JLabel label1 = new JLabel(); - label1.setText("Profile Name"); + this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile.name")); panel3.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label2 = new JLabel(); - label2.setText("Description"); + this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "AiEditor.description")); panel3.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label3 = new JLabel(); - label3.setText("Notes"); + this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.notes")); panel3.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); decisionPane = new JPanel(); decisionPane.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); @@ -220,19 +265,19 @@ private void addToMutableTreeNode(DefaultMutableTreeNode panel4.setLayout(new GridLayoutManager(2, 5, new Insets(0, 0, 0, 0), -1, -1)); decisionPane.add(panel4, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); final JLabel label4 = new JLabel(); - label4.setText("Action"); + this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.action")); panel4.add(label4, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); actionComboBox.setEditable(false); panel4.add(actionComboBox, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(300, -1), null, null, 0, false)); panel4.add(weightSpinner, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(100, -1), new Dimension(100, -1), new Dimension(100, -1), 0, false)); final JLabel label5 = new JLabel(); - label5.setText("Weight"); + this.$$$loadLabelText$$$(label5, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.weight")); panel4.add(label5, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final Spacer spacer1 = new Spacer(); panel4.add(spacer1, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); decisionScoreEvaluatorPanel = new JPanel(); - decisionScoreEvaluatorPanel.setLayout(new GridBagLayout()); - panel4.add(decisionScoreEvaluatorPanel, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); + decisionScoreEvaluatorPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); + panel4.add(decisionScoreEvaluatorPanel, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); considerationsPane = new JPanel(); considerationsPane.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); considerationsPane.setName(""); @@ -257,6 +302,79 @@ private void addToMutableTreeNode(DefaultMutableTreeNode considerationsPane1 = new JPanel(); considerationsPane1.setLayout(new GridBagLayout()); considerationsScrollPane.setViewportView(considerationsPane1); + label4.setLabelFor(actionComboBox); + label5.setLabelFor(weightSpinner); + } + + private static Method $$$cachedGetBundleMethod$$$ = null; + + private String $$$getMessageFromBundle$$$(String path, String key) { + ResourceBundle bundle; + try { + Class thisClass = this.getClass(); + if ($$$cachedGetBundleMethod$$$ == null) { + Class dynamicBundleClass = thisClass.getClassLoader().loadClass("com.intellij.DynamicBundle"); + $$$cachedGetBundleMethod$$$ = dynamicBundleClass.getMethod("getBundle", String.class, Class.class); + } + bundle = (ResourceBundle) $$$cachedGetBundleMethod$$$.invoke(null, path, thisClass); + } catch (Exception e) { + bundle = ResourceBundle.getBundle(path); + } + return bundle.getString(key); + } + + /** + * @noinspection ALL + */ + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) break; + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + /** + * @noinspection ALL + */ + private void $$$loadButtonText$$$(AbstractButton component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) break; + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form index ee4383298fa..608d09010dd 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form @@ -1,30 +1,30 @@
- + + - + - + - - + - + - + @@ -33,7 +33,7 @@ - + @@ -44,26 +44,30 @@ - + + + - + - + - + + - + - + + @@ -71,7 +75,7 @@ - + @@ -79,49 +83,53 @@ - + - + - + - - - - - - - - - - - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index 42bc9948a20..f47c517bc67 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -23,6 +23,10 @@ import javax.swing.*; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.ResourceBundle; public class ConsiderationPane extends JPanel { private JTextField considerationName; @@ -56,8 +60,13 @@ public ConsiderationPane() { hideButton.setVisible(true); showButton.setVisible(false); }); - showButton.setVisible(false); + + newParameterButton.addActionListener(e -> { + var model = (ParametersTableModel) parametersTable.getModel(); + var newParameterName = model.newParameterName(); + model.addRow(newParameterName, 0); + }); } public void setHoverStateModel(HoverStateModel model) { @@ -82,69 +91,117 @@ private void createUIComponents() { private void $$$setupUI$$$() { createUIComponents(); considerationPane = new JPanel(); - considerationPane.setLayout(new GridBagLayout()); + considerationPane.setLayout(new GridLayoutManager(4, 6, new Insets(0, 0, 0, 0), -1, -1)); topThingsPane = new JPanel(); - topThingsPane.setLayout(new GridLayoutManager(6, 5, new Insets(0, 0, 0, 0), -1, -1)); - GridBagConstraints gbc; - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 5; - gbc.gridheight = 2; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - considerationPane.add(topThingsPane, gbc); - topThingsPane.add(considerationComboBox, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + topThingsPane.setLayout(new GridLayoutManager(6, 6, new Insets(0, 0, 0, 0), -1, -1)); + considerationPane.add(topThingsPane, new GridConstraints(0, 0, 2, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + topThingsPane.add(considerationComboBox, new GridConstraints(1, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); considerationName = new JTextField(); - topThingsPane.add(considerationName, new GridConstraints(3, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); - topThingsPane.add(curveContainer, new GridConstraints(5, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(400, 300), new Dimension(400, 300), null, 0, false)); + topThingsPane.add(considerationName, new GridConstraints(3, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); + topThingsPane.add(curveContainer, new GridConstraints(5, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(400, 300), new Dimension(400, 300), null, 0, false)); final JLabel label1 = new JLabel(); - label1.setText("Curve:"); - topThingsPane.add(label1, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.curve")); + topThingsPane.add(label1, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(66, 17), null, 0, false)); final JLabel label2 = new JLabel(); - label2.setText("Name:"); - topThingsPane.add(label2, new GridConstraints(2, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.name")); + topThingsPane.add(label2, new GridConstraints(2, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label3 = new JLabel(); - label3.setText("Type:"); - topThingsPane.add(label3, new GridConstraints(0, 0, 1, 5, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.type")); + topThingsPane.add(label3, new GridConstraints(0, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); hideButton = new JButton(); - hideButton.setText("Hide"); + this.$$$loadButtonText$$$(hideButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.hide")); topThingsPane.add(hideButton, new GridConstraints(4, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); showButton = new JButton(); - showButton.setText("Show"); + this.$$$loadButtonText$$$(showButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.show")); topThingsPane.add(showButton, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final Spacer spacer1 = new Spacer(); - topThingsPane.add(spacer1, new GridConstraints(4, 3, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 3; - gbc.gridwidth = 5; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - considerationPane.add(parametersTable, gbc); + topThingsPane.add(spacer1, new GridConstraints(4, 3, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); final JLabel label4 = new JLabel(); - label4.setText("Parameters:"); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.anchor = GridBagConstraints.WEST; - considerationPane.add(label4, gbc); - final JPanel spacer2 = new JPanel(); - gbc = new GridBagConstraints(); - gbc.gridx = 3; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.fill = GridBagConstraints.HORIZONTAL; - considerationPane.add(spacer2, gbc); + this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.parameters")); + considerationPane.add(label4, new GridConstraints(2, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); newParameterButton = new JButton(); - newParameterButton.setText("New Parameter"); - gbc = new GridBagConstraints(); - gbc.gridx = 2; - gbc.gridy = 2; - gbc.fill = GridBagConstraints.HORIZONTAL; - considerationPane.add(newParameterButton, gbc); + this.$$$loadButtonText$$$(newParameterButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.new.parameter")); + considerationPane.add(newParameterButton, new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JScrollPane scrollPane1 = new JScrollPane(); + considerationPane.add(scrollPane1, new GridConstraints(3, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + parametersTable.setFillsViewportHeight(false); + parametersTable.setPreferredScrollableViewportSize(new Dimension(100, 75)); + scrollPane1.setViewportView(parametersTable); + final Spacer spacer2 = new Spacer(); + considerationPane.add(spacer2, new GridConstraints(2, 3, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + label2.setLabelFor(considerationName); + label3.setLabelFor(considerationComboBox); + } + + private static Method $$$cachedGetBundleMethod$$$ = null; + + private String $$$getMessageFromBundle$$$(String path, String key) { + ResourceBundle bundle; + try { + Class thisClass = this.getClass(); + if ($$$cachedGetBundleMethod$$$ == null) { + Class dynamicBundleClass = thisClass.getClassLoader().loadClass("com.intellij.DynamicBundle"); + $$$cachedGetBundleMethod$$$ = dynamicBundleClass.getMethod("getBundle", String.class, Class.class); + } + bundle = (ResourceBundle) $$$cachedGetBundleMethod$$$.invoke(null, path, thisClass); + } catch (Exception e) { + bundle = ResourceBundle.getBundle(path); + } + return bundle.getString(key); + } + + /** + * @noinspection ALL + */ + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) break; + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + /** + * @noinspection ALL + */ + private void $$$loadButtonText$$$(AbstractButton component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) break; + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form index 6c3117d6e6c..a8ea6545e30 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form @@ -22,7 +22,7 @@ - + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index 203c695b3a9..5b01c6ca71d 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -19,6 +19,8 @@ import javax.swing.*; import java.awt.*; +import java.lang.reflect.Method; +import java.util.ResourceBundle; import java.util.concurrent.atomic.AtomicReference; public class CurvePane extends JPanel { @@ -62,7 +64,7 @@ public void setHoverStateModel(HoverStateModel model) { gbc.fill = GridBagConstraints.HORIZONTAL; basePane.add(curveTypeComboBox, gbc); final JLabel label1 = new JLabel(); - label1.setText("Type"); + this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.curve.type")); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 1; @@ -135,6 +137,50 @@ public void setHoverStateModel(HoverStateModel model) { basePane.add(label5, gbc); } + private static Method $$$cachedGetBundleMethod$$$ = null; + + private String $$$getMessageFromBundle$$$(String path, String key) { + ResourceBundle bundle; + try { + Class thisClass = this.getClass(); + if ($$$cachedGetBundleMethod$$$ == null) { + Class dynamicBundleClass = thisClass.getClassLoader().loadClass("com.intellij.DynamicBundle"); + $$$cachedGetBundleMethod$$$ = dynamicBundleClass.getMethod("getBundle", String.class, Class.class); + } + bundle = (ResourceBundle) $$$cachedGetBundleMethod$$$.invoke(null, path, thisClass); + } catch (Exception e) { + bundle = ResourceBundle.getBundle(path); + } + return bundle.getString(key); + } + + /** + * @noinspection ALL + */ + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) break; + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + /** * @noinspection ALL */ diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form index 95be0a26bad..9628e36e03a 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form @@ -64,7 +64,8 @@ - + + @@ -73,7 +74,8 @@ - + + @@ -82,7 +84,8 @@ - + + @@ -91,7 +94,8 @@ - + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index a815b70fcb1..4d7fff2add8 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -17,7 +17,9 @@ import javax.swing.*; import java.awt.*; +import java.lang.reflect.Method; import java.util.List; +import java.util.ResourceBundle; public class DecisionScoreEvaluatorPane extends JPanel { private JTextField nameField; @@ -104,33 +106,81 @@ public DecisionScoreEvaluatorPane() { considerationsPane.setMinimumSize(new Dimension(800, 600)); scrollPane1.setViewportView(considerationsPane); final JLabel label1 = new JLabel(); - label1.setText("Considerations"); + this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.considerations")); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 6; gbc.anchor = GridBagConstraints.WEST; decisionScoreEvaluatorPane.add(label1, gbc); final JLabel label2 = new JLabel(); - label2.setText("Notes:"); + this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.notes")); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 4; gbc.anchor = GridBagConstraints.WEST; decisionScoreEvaluatorPane.add(label2, gbc); final JLabel label3 = new JLabel(); - label3.setText("Description:"); + this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.description")); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 2; gbc.anchor = GridBagConstraints.WEST; decisionScoreEvaluatorPane.add(label3, gbc); final JLabel label4 = new JLabel(); - label4.setText("Name:"); + this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.name")); gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; decisionScoreEvaluatorPane.add(label4, gbc); + label1.setLabelFor(scrollPane1); + label2.setLabelFor(notesField); + label3.setLabelFor(descriptionField); + label4.setLabelFor(nameField); + } + + private static Method $$$cachedGetBundleMethod$$$ = null; + + private String $$$getMessageFromBundle$$$(String path, String key) { + ResourceBundle bundle; + try { + Class thisClass = this.getClass(); + if ($$$cachedGetBundleMethod$$$ == null) { + Class dynamicBundleClass = thisClass.getClassLoader().loadClass("com.intellij.DynamicBundle"); + $$$cachedGetBundleMethod$$$ = dynamicBundleClass.getMethod("getBundle", String.class, Class.class); + } + bundle = (ResourceBundle) $$$cachedGetBundleMethod$$$.invoke(null, path, thisClass); + } catch (Exception e) { + bundle = ResourceBundle.getBundle(path); + } + return bundle.getString(key); + } + + /** + * @noinspection ALL + */ + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) break; + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java index 304194787d2..be97b999664 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java @@ -17,14 +17,13 @@ import megamek.logging.MMLogger; -import javax.swing.event.TableModelListener; -import javax.swing.table.TableModel; +import javax.swing.table.AbstractTableModel; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -public class ParametersTableModel implements TableModel { +public class ParametersTableModel extends AbstractTableModel { private static final MMLogger logger = MMLogger.create(ParametersTableModel.class); private final Map hashRows = new HashMap<>(); private final List rowValues = new ArrayList<>(); @@ -50,6 +49,15 @@ public void addRow(String parameterName, Object value) { } hashRows.put(parameterName, value); rowValues.add(new Row(parameterName, value)); + fireTableRowsInserted(rowValues.size() - 1, rowValues.size() - 1); + } + + public String newParameterName() { + int i = 0; + while (hashRows.containsKey("Parameter " + i)) { + i++; + } + return "Parameter " + i; } public Map getParameters() { @@ -97,19 +105,14 @@ public void setValueAt(Object aValue, int rowIndex, int columnIndex) { rowValues.set(rowIndex, new Row(row.name, aValue)); hashRows.put(row.name, aValue); } else { + if (hashRows.containsKey((String) aValue)) { + logger.formattedErrorDialog("Parameter already exists", + "Could not rename parameter %s, another parameters with the same name is already present.", aValue); + return; + } rowValues.set(rowIndex, new Row((String) aValue, row.value)); hashRows.remove(row.name); hashRows.put((String) aValue, row.value); } } - - @Override - public void addTableModelListener(TableModelListener l) { - - } - - @Override - public void removeTableModelListener(TableModelListener l) { - - } } From cfa0b8b170d912a99682490a6337103876c08bfe Mon Sep 17 00:00:00 2001 From: Scoppio Date: Fri, 27 Dec 2024 23:57:43 -0300 Subject: [PATCH 05/16] feat: improving on the AI Editor, adding feedback on the JTree, tabs, etc --- .../i18n/megamek/client/messages.properties | 2 + .../common/options/messages.properties | 16 +- .../common/options/messages_de.properties | 3 + .../common/options/messages_en.properties | 2 + .../common/options/messages_es.properties | 3 + .../common/options/messages_ru.properties | 3 + .../src/megamek/ai/utility/Consideration.java | 2 +- megamek/src/megamek/ai/utility/Curve.java | 2 + megamek/src/megamek/ai/utility/Decision.java | 2 +- .../src/megamek/ai/utility/DefaultCurve.java | 22 +- .../src/megamek/ai/utility/LinearCurve.java | 7 +- .../src/megamek/ai/utility/LogisticCurve.java | 7 +- .../src/megamek/ai/utility/LogitCurve.java | 7 +- .../megamek/ai/utility/ParabolicCurve.java | 5 + megamek/src/megamek/ai/utility/Profile.java | 12 +- .../ai/utility/tw/TWUtilityAIRepository.java | 108 +++++-- .../tw/considerations/MyUnitArmor.java | 1 + .../tw/considerations/TWConsideration.java | 5 + .../ai/utility/tw/decision/TWDecision.java | 12 + .../tw/decision/TWDecisionScoreEvaluator.java | 5 + .../ai/utility/tw/profile/TWProfile.java | 5 +- .../ui/swing/ai/editor/AiProfileEditor.form | 108 +++---- .../ui/swing/ai/editor/AiProfileEditor.java | 288 +++++++++++------- .../ui/swing/ai/editor/ConsiderationPane.form | 30 +- .../ui/swing/ai/editor/ConsiderationPane.java | 44 +-- .../client/ui/swing/ai/editor/CurvePane.java | 75 +++-- .../ai/editor/DecisionScoreEvaluatorPane.form | 16 +- .../ai/editor/DecisionScoreEvaluatorPane.java | 106 +++---- .../editor/DecisionScoreEvaluatorTable.java | 6 +- ...ableModel.java => DecisionTableModel.java} | 4 +- .../swing/ai/editor/ParametersTableModel.java | 10 + 31 files changed, 513 insertions(+), 405 deletions(-) rename megamek/src/megamek/client/ui/swing/ai/editor/{DecisionScoreEvaluatorTableModel.java => DecisionTableModel.java} (93%) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index aba96a02359..99c5adcc97b 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4825,7 +4825,9 @@ Bot.commands.aggression=Aggression Bot.commands.bravery=Bravery Bot.commands.avoid=Self-Preservation Bot.commands.caution=Piloting Caution +aiEditor.tree.title=Princess Data #### TacOps movement and damage descriptions TacOps.leaping.leg_damage=leaping (leg damage) TacOps.leaping.fall_damage=leaping (fall) + diff --git a/megamek/i18n/megamek/common/options/messages.properties b/megamek/i18n/megamek/common/options/messages.properties index de47cf271dc..ccf08f2e72c 100644 --- a/megamek/i18n/megamek/common/options/messages.properties +++ b/megamek/i18n/megamek/common/options/messages.properties @@ -1031,13 +1031,19 @@ AiEditor.description=Description aiEditor.notes=Notes aiEditor.newConsideration=New Consideration aiEditor.newDecision=New Decision -aiEditor.curve=Curve: -aiEditor.name=Name: -aiEditor.type=Type: +aiEditor.curve=Curve +aiEditor.name=Name +aiEditor.type=Type aiEditor.hide=Hide aiEditor.show=Show -aiEditor.parameters=Parameters: +aiEditor.parameters=Parameters aiEditor.new.parameter=New Parameter aiEditor.curve.type=Type aiEditor.considerations=Considerations -aiEditor.description=Description: +aiEditor.description=Description +aiEditor.considerationType=Consideration +aiEditor.tab.dse=Decision Score Evaluator +aiEditor.tab.decision=Decision +aiEditor.tab.consideration=Consideration +aiEditor.tree.title=Princess Data +aiEditor.tab.dse=Decision Score Evaluator \ No newline at end of file diff --git a/megamek/i18n/megamek/common/options/messages_de.properties b/megamek/i18n/megamek/common/options/messages_de.properties index 9c32ff651e8..72505ab6401 100644 --- a/megamek/i18n/megamek/common/options/messages_de.properties +++ b/megamek/i18n/megamek/common/options/messages_de.properties @@ -220,3 +220,6 @@ aiEditor.new.parameter=New Parameter aiEditor.curve.type=Type aiEditor.considerations=Considerations aiEditor.description=Description: +aiEditor.tab.considerations=Considerations +aiEditor.tab.decision=Decision +aiEditor.tab.consideration=Consideration diff --git a/megamek/i18n/megamek/common/options/messages_en.properties b/megamek/i18n/megamek/common/options/messages_en.properties index 6e518e10f23..cedbdc6f9dd 100644 --- a/megamek/i18n/megamek/common/options/messages_en.properties +++ b/megamek/i18n/megamek/common/options/messages_en.properties @@ -14,5 +14,7 @@ aiEditor.parameters=Parameters: aiEditor.profile=Profile aiEditor.profile.name=Profile Name aiEditor.show=Show +aiEditor.tab.consideration=Consideration +aiEditor.tab.decision=Decision aiEditor.type=Type: aiEditor.weight=Weight \ No newline at end of file diff --git a/megamek/i18n/megamek/common/options/messages_es.properties b/megamek/i18n/megamek/common/options/messages_es.properties index 0f0ae947f19..99668353780 100644 --- a/megamek/i18n/megamek/common/options/messages_es.properties +++ b/megamek/i18n/megamek/common/options/messages_es.properties @@ -1004,3 +1004,6 @@ aiEditor.new.parameter=New Parameter aiEditor.curve.type=Type aiEditor.considerations=Considerations aiEditor.description=Description: +aiEditor.tab.considerations=Considerations +aiEditor.tab.decision=Decision +aiEditor.tab.consideration=Consideration diff --git a/megamek/i18n/megamek/common/options/messages_ru.properties b/megamek/i18n/megamek/common/options/messages_ru.properties index 0dd5e98220e..e514adab2f9 100644 --- a/megamek/i18n/megamek/common/options/messages_ru.properties +++ b/megamek/i18n/megamek/common/options/messages_ru.properties @@ -797,3 +797,6 @@ aiEditor.new.parameter=New Parameter aiEditor.curve.type=Type aiEditor.considerations=Considerations aiEditor.description=Description: +aiEditor.tab.considerations=Considerations +aiEditor.tab.decision=Decision +aiEditor.tab.consideration=Consideration diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java index 25b2ace43ae..c5ed311bb75 100644 --- a/megamek/src/megamek/ai/utility/Consideration.java +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -118,7 +118,7 @@ public void setName(String name) { @Override public String toString() { - return new StringJoiner(", ", Consideration.class.getSimpleName() + "[", "]") + return new StringJoiner(", ", Consideration.class.getSimpleName() + " [", "]") .add("name='" + name + "'") .add("curve=" + curve) .add("parameters=" + parameters) diff --git a/megamek/src/megamek/ai/utility/Curve.java b/megamek/src/megamek/ai/utility/Curve.java index f5b66c7a095..3f7d6f12606 100644 --- a/megamek/src/megamek/ai/utility/Curve.java +++ b/megamek/src/megamek/ai/utility/Curve.java @@ -36,6 +36,8 @@ public interface Curve { double evaluate(double x); + Curve copy(); + default void drawAxes(Graphics g, int width, int height) { // Draw axis labels (0 to 1 with 0.05 increments) int padding = 10; // Padding for text from the axis lines diff --git a/megamek/src/megamek/ai/utility/Decision.java b/megamek/src/megamek/ai/utility/Decision.java index 926ff4d1f9e..d55a3ab02dd 100644 --- a/megamek/src/megamek/ai/utility/Decision.java +++ b/megamek/src/megamek/ai/utility/Decision.java @@ -96,7 +96,7 @@ public void setDecisionContext(DecisionContext decis @Override public String toString() { - return new StringJoiner(", ", Decision.class.getSimpleName() + "[", "]") + return new StringJoiner(", ", Decision.class.getSimpleName() + " [", "]") .add("action=" + action) .add("weight=" + weight) .add("score=" + score) diff --git a/megamek/src/megamek/ai/utility/DefaultCurve.java b/megamek/src/megamek/ai/utility/DefaultCurve.java index 4cdd60d4032..10aff611dee 100644 --- a/megamek/src/megamek/ai/utility/DefaultCurve.java +++ b/megamek/src/megamek/ai/utility/DefaultCurve.java @@ -17,16 +17,16 @@ public enum DefaultCurve { - LinearIncreasing(new LinearCurve(1.0, 0)), + Linear(new LinearCurve(1.0, 0)), LinearDecreasing(new LinearCurve(-1.0, 1)), - ParabolicPositive(new ParabolicCurve(4.0, 0.5, 1.0)), + Parabolic(new ParabolicCurve(4.0, 0.5, 1.0)), ParabolicNegative(new ParabolicCurve(-4.0, 0.5, 0.0)), - LogisticIncreasing(new LogisticCurve(1.0, 0.5, 10.0, 0.0)), + Logistic(new LogisticCurve(1.0, 0.5, 10.0, 0.0)), LogisticDecreasing(new LogisticCurve(1.0, 0.5, -10.0, 0.0)), - LogitIncreasing(new LogitCurve(1.0, 0.5, -15.0, 0.0)), + Logit(new LogitCurve(1.0, 0.5, -15.0, 0.0)), LogitDecreasing(new LogitCurve(1.0, 0.5, 15.0, 0.0)); private final Curve curve; @@ -35,6 +35,20 @@ public enum DefaultCurve { this.curve = curve; } + public static DefaultCurve fromCurve(Curve curve) { + if (curve instanceof LinearCurve) { + return Linear; + } else if (curve instanceof ParabolicCurve) { + return Parabolic; + } else if (curve instanceof LogisticCurve) { + return Logistic; + } else if (curve instanceof LogitCurve) { + return Logit; + } + // Return Linear as default + return Linear; + } + public Curve getCurve() { return curve; } diff --git a/megamek/src/megamek/ai/utility/LinearCurve.java b/megamek/src/megamek/ai/utility/LinearCurve.java index fe9d844a18a..9a3b5eae258 100644 --- a/megamek/src/megamek/ai/utility/LinearCurve.java +++ b/megamek/src/megamek/ai/utility/LinearCurve.java @@ -37,6 +37,11 @@ public LinearCurve( this.b = b; } + @Override + public LinearCurve copy() { + return new LinearCurve(m, b); + } + public double evaluate(double x) { return clamp01(m * x + b); } @@ -61,7 +66,7 @@ public void setB(double b) { @Override public String toString() { - return new StringJoiner(", ", LinearCurve.class.getSimpleName() + "[", "]") + return new StringJoiner(", ", LinearCurve.class.getSimpleName() + " [", "]") .add("m=" + m) .add("b=" + b) .toString(); diff --git a/megamek/src/megamek/ai/utility/LogisticCurve.java b/megamek/src/megamek/ai/utility/LogisticCurve.java index 749d5bcbeb1..71174b93acb 100644 --- a/megamek/src/megamek/ai/utility/LogisticCurve.java +++ b/megamek/src/megamek/ai/utility/LogisticCurve.java @@ -44,6 +44,11 @@ public LogisticCurve( this.c = c; } + @Override + public LogisticCurve copy() { + return new LogisticCurve(m, b, k, c); + } + public double evaluate(double x) { return clamp01(m * (1 / (1 + Math.exp(-k * (x - b)))) + c); } @@ -86,7 +91,7 @@ public void setC(double c) { @Override public String toString() { - return new StringJoiner(", ", LogisticCurve.class.getSimpleName() + "[", "]") + return new StringJoiner(", ", LogisticCurve.class.getSimpleName() + " [", "]") .add("m=" + m) .add("b=" + b) .add("k=" + k) diff --git a/megamek/src/megamek/ai/utility/LogitCurve.java b/megamek/src/megamek/ai/utility/LogitCurve.java index f70cb08658d..0b608d6dc39 100644 --- a/megamek/src/megamek/ai/utility/LogitCurve.java +++ b/megamek/src/megamek/ai/utility/LogitCurve.java @@ -43,6 +43,11 @@ public LogitCurve( this.c = c; } + @Override + public LogitCurve copy() { + return new LogitCurve(m, b, k, c); + } + public double evaluate(double x) { if (x <= c) { x = c + 0.0001; @@ -91,7 +96,7 @@ public void setC(double c) { @Override public String toString() { - return new StringJoiner(", ", LogitCurve.class.getSimpleName() + "[", "]") + return new StringJoiner(", ", LogitCurve.class.getSimpleName() + " [", "]") .add("m=" + m) .add("b=" + b) .add("k=" + k) diff --git a/megamek/src/megamek/ai/utility/ParabolicCurve.java b/megamek/src/megamek/ai/utility/ParabolicCurve.java index 1698204871f..b102b2fd018 100644 --- a/megamek/src/megamek/ai/utility/ParabolicCurve.java +++ b/megamek/src/megamek/ai/utility/ParabolicCurve.java @@ -41,6 +41,11 @@ public ParabolicCurve( this.k = k; } + @Override + public ParabolicCurve copy() { + return new ParabolicCurve(m, b, k); + } + public double evaluate(double x) { return clamp01(-m * Math.pow(x - b, 2) + k); } diff --git a/megamek/src/megamek/ai/utility/Profile.java b/megamek/src/megamek/ai/utility/Profile.java index 55d0237fc8a..08c216fd95f 100644 --- a/megamek/src/megamek/ai/utility/Profile.java +++ b/megamek/src/megamek/ai/utility/Profile.java @@ -36,15 +36,15 @@ public class Profile implements NamedObject { private final String name; @JsonProperty("description") private String description; - @JsonProperty("decisionScoreEvaluator") - private final List> decisionScoreEvaluator; + @JsonProperty("decisions") + private final List> decisions; @JsonCreator - public Profile(int id, String name, String description, List> decisionScoreEvaluator) { + public Profile(int id, String name, String description, List> decisions) { this.id = id; this.name = name; this.description = description; - this.decisionScoreEvaluator = decisionScoreEvaluator; + this.decisions = decisions; } @Override @@ -64,7 +64,7 @@ public int getId() { return id; } - public List> getDecisionScoreEvaluator() { - return decisionScoreEvaluator; + public List> getDecisions() { + return decisions; } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java index bc32bee723c..768dee1cf43 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -98,6 +98,14 @@ public List getProfiles() { return List.copyOf(profiles.values()); } + public boolean hasDecision(String name) { + return decisions.containsKey(name); + } + + public void removeDecision(TWDecision decision) { + decisions.remove(decision.getName()); + } + public void addDecision(TWDecision decision) { if (decisions.containsKey(decision.getName())) { logger.info("Decision with name {} already exists, overwriting", decision.getName()); @@ -105,6 +113,14 @@ public void addDecision(TWDecision decision) { decisions.put(decision.getName(), decision); } + public boolean hasConsideration(String name) { + return considerations.containsKey(name); + } + + public void removeConsideration(TWConsideration consideration) { + considerations.remove(consideration.getName()); + } + public void addConsideration(TWConsideration consideration) { if (considerations.containsKey(consideration.getName())) { logger.info("Consideration with name {} already exists, overwriting", consideration.getName()); @@ -112,6 +128,14 @@ public void addConsideration(TWConsideration consideration) { considerations.put(consideration.getName(), consideration); } + public boolean hasDecisionScoreEvaluator(String name) { + return decisionScoreEvaluators.containsKey(name); + } + + public void removeDecisionScoreEvaluator(TWDecisionScoreEvaluator decisionScoreEvaluator) { + decisionScoreEvaluators.remove(decisionScoreEvaluator.getName()); + } + public void addDecisionScoreEvaluator(TWDecisionScoreEvaluator decisionScoreEvaluator) { if (decisionScoreEvaluators.containsKey(decisionScoreEvaluator.getName())) { logger.info("DecisionScoreEvaluator with name {} already exists, overwriting", decisionScoreEvaluator.getName()); @@ -119,6 +143,14 @@ public void addDecisionScoreEvaluator(TWDecisionScoreEvaluator decisionScoreEval decisionScoreEvaluators.put(decisionScoreEvaluator.getName(), decisionScoreEvaluator); } + public boolean hasProfile(String name) { + return profiles.containsKey(name); + } + + public void removeProfile(TWProfile profile) { + profiles.remove(profile.getName()); + } + public void addProfile(TWProfile profile) { if (profiles.containsKey(profile.getName())) { logger.info("Profile with name {} already exists, overwriting", profile.getName()); @@ -126,19 +158,6 @@ public void addProfile(TWProfile profile) { profiles.put(profile.getName(), profile); } - private void persistToFile(File outputFile, Collection objects) { - if (objects.isEmpty()) { - return; - } - try (SequenceWriter seqWriter = mapper.writer().writeValues(outputFile)) { - for (var object : objects) { - seqWriter.write(object); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - private List loadDecisionScoreEvaluators(File inputFile) { return loadObjects(inputFile, TWDecisionScoreEvaluator.class); } @@ -155,28 +174,46 @@ private List loadProfiles(File inputFile) { } private List loadObjects(File inputFile, Class clazz) { - List objects = new ArrayList<>(); - if (inputFile.isDirectory()) { - var files = inputFile.listFiles(); - if (files != null) { - for (var file : files) { - if (file.isFile()) { - try (MappingIterator it = mapper.readerFor(clazz).readValues(file)) { - objects.addAll(it.readAll()); - } catch (IOException e) { - logger.error(e, "Could not load file: {}", file); - } - } - } + var objects = objectsFromDirectory(inputFile, clazz); + logger.info("Loaded {} objects of type {} from directory {}", objects.size(), clazz.getSimpleName(), inputFile); + return objects; + } + if (inputFile.isFile()) { + var objects = objectsFromFile(clazz, inputFile); + if (objects != null) { + logger.info("Loaded {} objects of type {} from file {}", objects.size(), clazz.getSimpleName(), inputFile); + return objects; + } + } + logger.formattedErrorDialog("Invalid directory", "Input file {} is not a directory", inputFile); + return Collections.emptyList(); + } + + private List objectsFromDirectory(File inputFile, Class clazz) { + List objects = new ArrayList<>(); + var files = inputFile.listFiles(); + if (files != null) { + for (var file : files) { + objects.addAll(objectsFromFile(clazz, file)); } - } else { - logger.formattedErrorDialog("Invalid directory", "Input file {} is not a directory", inputFile); } - logger.info("Loaded {} objects of type {} from directory {}", objects.size(), clazz.getSimpleName(), inputFile); return objects; } + private List objectsFromFile(Class clazz, File file) { + if (file.isFile()) { + try (MappingIterator it = mapper.readerFor(clazz).readValues(file)) { + return it.readAll(); + } catch (IOException e) { + logger.error(e, "Could not load file: {}", file); + } + } else if (file.isDirectory()) { + return objectsFromDirectory(file, clazz); + } + return Collections.emptyList(); + } + public void persistData() { var twAiDir = Configuration.twAiDir(); createDirectoryStructureIfMissing(twAiDir); @@ -195,6 +232,19 @@ public void persistDataToUserData() { persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); } + private void persistToFile(File outputFile, Collection objects) { + if (objects.isEmpty()) { + return; + } + try (SequenceWriter seqWriter = mapper.writer().writeValues(outputFile)) { + for (var object : objects) { + seqWriter.write(object); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private void createDirectoryStructureIfMissing(File directory) { createDirIfNecessary(directory); diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java index 468da618e17..a7aae3dbdbd 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java @@ -35,4 +35,5 @@ public double score(DecisionContext context) { var currentUnit = context.getCurrentUnit().orElseThrow(); return clamp01(currentUnit.getArmorRemainingPercent()); } + } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java index a0803ac69e7..35a95b5cd29 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TWConsideration.java @@ -39,4 +39,9 @@ public TWConsideration(String name, Curve curve) { public TWConsideration(String name, Curve curve, Map parameters) { super(name, curve, parameters); } + + @Override + public String toString() { + return this.getName(); + } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java index aa47cd19e3a..bf2a6367d9f 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecision.java @@ -21,6 +21,9 @@ import megamek.ai.utility.DecisionScoreEvaluator; import megamek.common.Entity; +import java.util.HashMap; +import java.util.StringJoiner; + @JsonTypeName("TWDecision") public class TWDecision extends Decision { @@ -30,4 +33,13 @@ public TWDecision() { public TWDecision(Action action, double weight, DecisionScoreEvaluator decisionScoreEvaluator) { super(action, weight, decisionScoreEvaluator); } + + public TWDecision(Action action, double weight) { + this(action, weight, new TWDecisionScoreEvaluator()); + } + + @Override + public String toString() { + return this.getName(); + } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java index 639fc5be461..3366aca4898 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java @@ -36,4 +36,9 @@ public TWDecisionScoreEvaluator(String name, String description, String notes) { public TWDecisionScoreEvaluator(String name, String description, String notes, List> considerations) { super(name, description, notes, considerations); } + + @Override + public String toString() { + return this.getName(); + } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java index 7c91aa3522d..3f9b0017a0e 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.Decision; import megamek.ai.utility.DecisionScoreEvaluator; import megamek.ai.utility.Profile; import megamek.common.Entity; @@ -33,8 +34,8 @@ public TWProfile( @JsonProperty("id") int id, @JsonProperty("name") String name, @JsonProperty("description") String description, - @JsonProperty("decisionScoreEvaluator") List> decisionScoreEvaluator) + @JsonProperty("decisions") List> decisions) { - super(id, name, description, decisionScoreEvaluator); + super(id, name, description, decisions); } } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form index 386658a9047..d60569f4f1f 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form @@ -63,16 +63,14 @@ - + - - - + - + @@ -82,14 +80,14 @@ - + - + @@ -100,7 +98,7 @@ - + @@ -124,14 +122,6 @@ - - - - - - - - @@ -148,22 +138,14 @@ - - - - - - - - - + - + @@ -219,7 +201,7 @@ - + @@ -232,61 +214,19 @@ - + - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -294,7 +234,8 @@ - + + @@ -304,6 +245,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 058e1f6a3b6..b4233f31e3e 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -21,7 +21,10 @@ import megamek.ai.utility.Action; import megamek.ai.utility.NamedObject; import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; +import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; +import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; import megamek.client.ui.Messages; import megamek.client.ui.enums.DialogResult; import megamek.client.ui.swing.GUIPreferences; @@ -30,7 +33,10 @@ import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.lang.reflect.Method; @@ -44,27 +50,24 @@ public class AiProfileEditor extends JFrame { private JButton newDecisionButton; private JTree repositoryViewer; - private JTabbedPane dseEditorPane; - private JPanel dseConfigPane; - private JTextField nameDseTextField; - private JTextField descriptionDseTextField; - private JScrollPane considerationsScrollPane; - private JPanel considerationsPane; - private JTextField notesTextField; + private JTabbedPane mainEditorTabbedPane; + private JPanel dseTabPane; private JTextField descriptionTextField; private JTextField profileNameTextField; private JButton newConsiderationButton; - private JPanel profilePane; - private JTable decisionScoreEvaluatorTable; - private JPanel decisionPane; + private JPanel profileTabPane; + private JTable profileDecisionTable; + private JPanel decisionTabPane; private JComboBox actionComboBox; private JSpinner weightSpinner; private JPanel uAiEditorPanel; - private JPanel considerationsPane1; private JScrollPane profileScrollPane; - private JPanel decisionScoreEvaluatorPanel; + private JPanel decisionTabDsePanel; + private JPanel dsePane; + private JPanel considerationTabPane; + private JPanel considerationEditorPanel; - private boolean hasChanges = false; + private boolean hasChanges = true; private boolean ignoreHotKeys = false; public AiProfileEditor(MegaMekController controller) { @@ -73,53 +76,36 @@ public AiProfileEditor(MegaMekController controller) { initialize(); setTitle("AI Profile Editor"); setSize(1200, 1000); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setContentPane(uAiEditorPanel); setVisible(true); } private void initialize() { - - // Set layout for decisionScoreEvaluatorPanel - decisionScoreEvaluatorPanel.setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = GridBagConstraints.RELATIVE; - gbc.fill = GridBagConstraints.NONE; // Do not stretch + gbc.fill = GridBagConstraints.EAST; gbc.weightx = 0.0; gbc.weighty = 0.0; - gbc.anchor = GridBagConstraints.NORTHEAST; // Align to the top-right - gbc.insets = new Insets(0, 0, 0, 0); // No padding - - // Add DecisionScoreEvaluatorPane to the panel - decisionScoreEvaluatorPanel.add(new DecisionScoreEvaluatorPane(), gbc); - -// -// GridBagConstraints gbc = new GridBagConstraints(); -// gbc.gridx = 0; -// gbc.gridy = GridBagConstraints.RELATIVE; -// gbc.fill = GridBagConstraints.HORIZONTAL; -// gbc.weightx = 0.0; -// gbc.weighty = 0.0; -// gbc.anchor = GridBagConstraints.NORTHEAST; - var hover = new HoverStateModel(); - var considerations = List.of(new ConsiderationPane(), - new ConsiderationPane(), - new ConsiderationPane(), - new ConsiderationPane()); - for (var c : considerations) { - considerationsPane1.add(c, gbc); - c.setHoverStateModel(hover); - } + gbc.anchor = GridBagConstraints.NORTHEAST; + gbc.insets = new Insets(0, 0, 0, 0); newDecisionButton.addActionListener(e -> { var action = (Action) actionComboBox.getSelectedItem(); var weight = (double) weightSpinner.getValue(); - var dse = new TWDecision(action, weight, sharedData.getDecisionScoreEvaluators().get(0)); - var model = decisionScoreEvaluatorTable.getModel(); + var dse = new TWDecision(action, weight); + var model = profileDecisionTable.getModel(); //noinspection unchecked - ((DecisionScoreEvaluatorTableModel) model).addRow(dse); + ((DecisionTableModel) model).addRow(dse); + }); + + newConsiderationButton.addActionListener(e -> { + var action = (Action) actionComboBox.getSelectedItem(); + var weight = (double) weightSpinner.getValue(); + var dse = new TWDecision(action, weight); + var model = profileDecisionTable.getModel(); + //noinspection unchecked + ((DecisionTableModel) model).addRow(dse); }); this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); @@ -137,6 +123,74 @@ public void windowClosing(WindowEvent e) { } }); + + + // Add mouse listener for double-click events + repositoryViewer.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + TreePath path = repositoryViewer.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + if (node.isLeaf()) { + handleNodeAction(node); + } + } + } + } + }); + + repositoryViewer.addTreeSelectionListener(e -> { + TreePath path = e.getPath(); + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + if (node.isLeaf()) { + handleNodeAction(node); + } + } + }); + } + + + private void handleNodeAction(DefaultMutableTreeNode node) { + var obj = node.getUserObject(); + if (obj instanceof TWDecision twDecision) { + openDecision(twDecision); + } else if (obj instanceof TWProfile twProfile) { + openProfile(twProfile); + } else if (obj instanceof TWDecisionScoreEvaluator twDse) { + openDecisionScoreEvaluator(twDse); + } else if (obj instanceof TWConsideration twConsideration) { + openConsideration(twConsideration); + } + } + +// decisionScoreEvaluatorTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); +// decisionTabDsePanel = new DecisionScoreEvaluatorPane(); +// dsePane = new DecisionScoreEvaluatorPane(); + + private void openConsideration(TWConsideration twConsideration) { + ((ConsiderationPane) considerationEditorPanel).setConsideration(twConsideration); + mainEditorTabbedPane.setSelectedComponent(considerationTabPane); + } + + private void openDecision(TWDecision twDecision) { + ((DecisionScoreEvaluatorPane) decisionTabDsePanel).setDecisionScoreEvaluator(twDecision.getDecisionScoreEvaluator()); + mainEditorTabbedPane.setSelectedComponent(decisionTabPane); + } + + private void openProfile(TWProfile twProfile) { + // profileTab.setProfile(twProfile); + profileNameTextField.setText(twProfile.getName()); + descriptionTextField.setText(twProfile.getDescription()); + profileDecisionTable.setModel(new DecisionTableModel<>(twProfile.getDecisions())); + mainEditorTabbedPane.setSelectedComponent(profileTabPane); + } + + private void openDecisionScoreEvaluator(TWDecisionScoreEvaluator twDse) { + ((DecisionScoreEvaluatorPane) dsePane).setDecisionScoreEvaluator(twDse); + mainEditorTabbedPane.setSelectedComponent(dseTabPane); } private DialogResult showSavePrompt() { @@ -159,7 +213,8 @@ private DialogResult showSavePrompt() { } private void persistProfile() { - var model = (DecisionScoreEvaluatorTableModel) decisionScoreEvaluatorTable.getModel(); + //noinspection unchecked + var model = (DecisionTableModel) profileDecisionTable.getModel(); var updatedList = model.getDecisions(); System.out.println("== Updated DecisionScoreEvaluator List =="); for (int i = 0; i < updatedList.size(); i++) { @@ -170,34 +225,55 @@ private void persistProfile() { dse.getDecisionScoreEvaluator().getName()); sharedData.addDecision(dse); } + sharedData.persistDataToUserData(); } public JFrame getFrame() { return this; } + private enum TreeViewHelper { + PROFILES("Profiles"), + DECISIONS("Decisions"), + DSE("Decision Score Evaluators (DSE)"), + CONSIDERATIONS("Considerations"); + + private final String name; + + TreeViewHelper(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + private void createUIComponents() { weightSpinner = new JSpinner(new SpinnerNumberModel(1d, 0d, 4d, 0.01d)); - var root = new DefaultMutableTreeNode("Utility AI Repository"); - addToMutableTreeNode(root, "Profiles", sharedData.getProfiles()); - addToMutableTreeNode(root, "Decisions", sharedData.getDecisions()); - addToMutableTreeNode(root, "Considerations", sharedData.getConsiderations()); - addToMutableTreeNode(root, "Decision Score Evaluators (DSE)", sharedData.getDecisionScoreEvaluators()); + var root = new DefaultMutableTreeNode(Messages.getString("aiEditor.tree.title")); + addToMutableTreeNode(root, TreeViewHelper.PROFILES.getName(), sharedData.getProfiles()); + addToMutableTreeNode(root, TreeViewHelper.DECISIONS.getName(), sharedData.getDecisions()); + addToMutableTreeNode(root, TreeViewHelper.DSE.getName(), sharedData.getDecisionScoreEvaluators()); + addToMutableTreeNode(root, TreeViewHelper.CONSIDERATIONS.getName(), sharedData.getConsiderations()); DefaultTreeModel treeModel = new DefaultTreeModel(root); repositoryViewer = new JTree(treeModel); actionComboBox = new JComboBox<>(Action.values()); - - var model = new DecisionScoreEvaluatorTableModel<>(sharedData.getDecisions()); - decisionScoreEvaluatorTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); + var model = new DecisionTableModel<>(sharedData.getDecisions()); + profileDecisionTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); + decisionTabDsePanel = new DecisionScoreEvaluatorPane(); + dsePane = new DecisionScoreEvaluatorPane(); + considerationEditorPanel = new ConsiderationPane(); } private void addToMutableTreeNode(DefaultMutableTreeNode root, String nodeName, List items) { - var profilesNode = new DefaultMutableTreeNode(nodeName); - root.add(profilesNode); - for (var profile : items) { - profilesNode.add(new DefaultMutableTreeNode(profile.getName())); + var categoryNode = new DefaultMutableTreeNode(nodeName); + root.add(categoryNode); + for (var item : items) { + var childNode = new DefaultMutableTreeNode(item); + categoryNode.add(childNode); } } @@ -227,83 +303,65 @@ private void addToMutableTreeNode(DefaultMutableTreeNode final JPanel panel2 = new JPanel(); panel2.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); splitPane1.setRightComponent(panel2); - dseEditorPane = new JTabbedPane(); - panel2.add(dseEditorPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); - profilePane = new JPanel(); - profilePane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); - dseEditorPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile"), profilePane); + mainEditorTabbedPane = new JTabbedPane(); + panel2.add(mainEditorTabbedPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + profileTabPane = new JPanel(); + profileTabPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile"), profileTabPane); profileScrollPane = new JScrollPane(); profileScrollPane.setWheelScrollingEnabled(true); - profilePane.add(profileScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); - decisionScoreEvaluatorTable.setColumnSelectionAllowed(false); - decisionScoreEvaluatorTable.setFillsViewportHeight(true); - decisionScoreEvaluatorTable.setMinimumSize(new Dimension(150, 32)); - decisionScoreEvaluatorTable.setPreferredScrollableViewportSize(new Dimension(150, 32)); - profileScrollPane.setViewportView(decisionScoreEvaluatorTable); + profileTabPane.add(profileScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + profileDecisionTable.setColumnSelectionAllowed(false); + profileDecisionTable.setFillsViewportHeight(true); + profileDecisionTable.setMinimumSize(new Dimension(150, 32)); + profileDecisionTable.setPreferredScrollableViewportSize(new Dimension(150, 32)); + profileScrollPane.setViewportView(profileDecisionTable); final JPanel panel3 = new JPanel(); - panel3.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); - profilePane.add(panel3, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); + panel3.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + profileTabPane.add(panel3, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); profileNameTextField = new JTextField(); panel3.add(profileNameTextField, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); descriptionTextField = new JTextField(); panel3.add(descriptionTextField, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); - notesTextField = new JTextField(); - panel3.add(notesTextField, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); final JLabel label1 = new JLabel(); this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile.name")); panel3.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label2 = new JLabel(); this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "AiEditor.description")); panel3.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - final JLabel label3 = new JLabel(); - this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.notes")); - panel3.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - decisionPane = new JPanel(); - decisionPane.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); - dseEditorPane.addTab("Decision", decisionPane); + decisionTabPane = new JPanel(); + decisionTabPane.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); + mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.tab.decision"), decisionTabPane); final JPanel panel4 = new JPanel(); panel4.setLayout(new GridLayoutManager(2, 5, new Insets(0, 0, 0, 0), -1, -1)); - decisionPane.add(panel4, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); - final JLabel label4 = new JLabel(); - this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.action")); - panel4.add(label4, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + decisionTabPane.add(panel4, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); + final JLabel label3 = new JLabel(); + this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.action")); + panel4.add(label3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); actionComboBox.setEditable(false); panel4.add(actionComboBox, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(300, -1), null, null, 0, false)); panel4.add(weightSpinner, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(100, -1), new Dimension(100, -1), new Dimension(100, -1), 0, false)); - final JLabel label5 = new JLabel(); - this.$$$loadLabelText$$$(label5, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.weight")); - panel4.add(label5, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label4 = new JLabel(); + this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.weight")); + panel4.add(label4, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final Spacer spacer1 = new Spacer(); panel4.add(spacer1, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); - decisionScoreEvaluatorPanel = new JPanel(); - decisionScoreEvaluatorPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); - panel4.add(decisionScoreEvaluatorPanel, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); - considerationsPane = new JPanel(); - considerationsPane.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); - considerationsPane.setName(""); - dseEditorPane.addTab("Considerations", considerationsPane); - dseConfigPane = new JPanel(); - dseConfigPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); - considerationsPane.add(dseConfigPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); - final JLabel label6 = new JLabel(); - label6.setText("Name"); - dseConfigPane.add(label6, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - final JLabel label7 = new JLabel(); - label7.setText("Description"); - dseConfigPane.add(label7, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - nameDseTextField = new JTextField(); - dseConfigPane.add(nameDseTextField, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); - descriptionDseTextField = new JTextField(); - dseConfigPane.add(descriptionDseTextField, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); - considerationsScrollPane = new JScrollPane(); - considerationsScrollPane.setHorizontalScrollBarPolicy(31); - considerationsScrollPane.setWheelScrollingEnabled(true); - considerationsPane.add(considerationsScrollPane, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); - considerationsPane1 = new JPanel(); - considerationsPane1.setLayout(new GridBagLayout()); - considerationsScrollPane.setViewportView(considerationsPane1); - label4.setLabelFor(actionComboBox); - label5.setLabelFor(weightSpinner); + panel4.add(decisionTabDsePanel, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + dseTabPane = new JPanel(); + dseTabPane.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); + dseTabPane.setName(""); + mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.tab.dse"), dseTabPane); + final JScrollPane scrollPane1 = new JScrollPane(); + scrollPane1.setHorizontalScrollBarPolicy(31); + scrollPane1.setWheelScrollingEnabled(true); + dseTabPane.add(scrollPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + scrollPane1.setViewportView(dsePane); + considerationTabPane = new JPanel(); + considerationTabPane.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); + mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.tab.consideration"), considerationTabPane); + considerationTabPane.add(considerationEditorPanel, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + label3.setLabelFor(actionComboBox); + label4.setLabelFor(weightSpinner); } private static Method $$$cachedGetBundleMethod$$$ = null; diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form index 608d09010dd..ed5082e5039 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form @@ -67,7 +67,7 @@ - + @@ -101,35 +101,15 @@ - + - + - + + - - - - - - - - - - - - - - - - - - - - - diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index f47c517bc67..bce744b7601 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -18,8 +18,10 @@ import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.uiDesigner.core.Spacer; +import megamek.ai.utility.Consideration; import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; +import megamek.common.Entity; import javax.swing.*; import java.awt.*; @@ -37,17 +39,19 @@ public class ConsiderationPane extends JPanel { private JPanel topThingsPane; private JButton hideButton; private JButton showButton; - private JButton newParameterButton; public ConsiderationPane() { $$$setupUI$$$(); add(considerationPane); -// considerationComboBox.addActionListener(e -> { -// TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); -// considerationName.setText(consideration.getName()); -// ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); -// }); + considerationComboBox.addActionListener(e -> { + TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); + if (consideration != null) { + considerationName.setText(consideration.getName()); + ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); + ((CurvePane) (curveContainer)).setCurve(consideration.getCurve()); + } + }); hideButton.addActionListener(e -> { curveContainer.setVisible(false); @@ -61,16 +65,17 @@ public ConsiderationPane() { showButton.setVisible(false); }); showButton.setVisible(false); + } - newParameterButton.addActionListener(e -> { - var model = (ParametersTableModel) parametersTable.getModel(); - var newParameterName = model.newParameterName(); - model.addRow(newParameterName, 0); - }); + public void setConsideration(Consideration consideration) { + considerationComboBox.setSelectedItem(consideration); + considerationName.setText(consideration.getName()); + ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); + ((CurvePane) curveContainer).setCurve(consideration.getCurve()); } public void setHoverStateModel(HoverStateModel model) { - ((CurvePane) this.curveContainer).setHoverStateModel(model); + ((CurvePane) curveContainer).setHoverStateModel(model); } private void createUIComponents() { @@ -78,6 +83,8 @@ private void createUIComponents() { parametersTable.setModel(new ParametersTableModel()); considerationComboBox = new JComboBox<>(TWUtilityAIRepository.getInstance().getConsiderations().toArray(new TWConsideration[0])); + considerationComboBox.setSelectedItem(null); + curveContainer = new CurvePane(); } @@ -106,7 +113,7 @@ private void createUIComponents() { this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.name")); topThingsPane.add(label2, new GridConstraints(2, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label3 = new JLabel(); - this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.type")); + this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.considerationType")); topThingsPane.add(label3, new GridConstraints(0, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); hideButton = new JButton(); this.$$$loadButtonText$$$(hideButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.hide")); @@ -119,16 +126,9 @@ private void createUIComponents() { final JLabel label4 = new JLabel(); this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.parameters")); considerationPane.add(label4, new GridConstraints(2, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - newParameterButton = new JButton(); - this.$$$loadButtonText$$$(newParameterButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.new.parameter")); - considerationPane.add(newParameterButton, new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - final JScrollPane scrollPane1 = new JScrollPane(); - considerationPane.add(scrollPane1, new GridConstraints(3, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); parametersTable.setFillsViewportHeight(false); - parametersTable.setPreferredScrollableViewportSize(new Dimension(100, 75)); - scrollPane1.setViewportView(parametersTable); - final Spacer spacer2 = new Spacer(); - considerationPane.add(spacer2, new GridConstraints(2, 3, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + parametersTable.setPreferredScrollableViewportSize(new Dimension(100, 50)); + considerationPane.add(parametersTable, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); label2.setLabelFor(considerationName); label3.setLabelFor(considerationComboBox); } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index 5b01c6ca71d..5ea11d9e8f3 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -31,7 +31,8 @@ public class CurvePane extends JPanel { private JSpinner cParamSpinner; private JPanel curveGraph; private JPanel basePane; - private HoverStateModel hoverStateModel; + + private final AtomicReference selectedCurve = new AtomicReference<>(); public CurvePane() { $$$setupUI$$$(); @@ -39,6 +40,12 @@ public CurvePane() { add(basePane, BorderLayout.CENTER); } + public void setCurve(Curve curve) { + curveTypeComboBox.setSelectedItem(DefaultCurve.fromCurve(curve)); + selectedCurve.set(curve.copy()); + updateCurveDataUI(); + } + public void setHoverStateModel(HoverStateModel model) { ((CurveGraph) this.curveGraph).setHoverStateModel(model); } @@ -190,39 +197,12 @@ public void setHoverStateModel(HoverStateModel model) { private void createUIComponents() { curveTypeComboBox = new JComboBox<>(DefaultCurve.values()); - AtomicReference selectedCurve = new AtomicReference<>(); + curveTypeComboBox.addActionListener(e1 -> { if (curveTypeComboBox.getSelectedItem() != null) { - var curve = ((DefaultCurve) curveTypeComboBox.getSelectedItem()).getCurve(); - selectedCurve.set(curve); - curveGraph.repaint(); - - if (curve instanceof LinearCurve) { - bParamSpinner.setValue(((LinearCurve) curve).getB()); - mParamSpinner.setValue(((LinearCurve) curve).getM()); - kParamSpinner.setEnabled(false); - cParamSpinner.setEnabled(false); - } else if (curve instanceof ParabolicCurve) { - bParamSpinner.setValue(((ParabolicCurve) curve).getB()); - mParamSpinner.setValue(((ParabolicCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((ParabolicCurve) curve).getK()); - cParamSpinner.setEnabled(false); - } else if (curve instanceof LogitCurve) { - bParamSpinner.setValue(((LogitCurve) curve).getB()); - mParamSpinner.setValue(((LogitCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((LogitCurve) curve).getK()); - cParamSpinner.setEnabled(true); - cParamSpinner.setValue(((LogitCurve) curve).getC()); - } else if (curve instanceof LogisticCurve) { - bParamSpinner.setValue(((LogisticCurve) curve).getB()); - mParamSpinner.setValue(((LogisticCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((LogisticCurve) curve).getK()); - cParamSpinner.setEnabled(true); - cParamSpinner.setValue(((LogisticCurve) curve).getC()); - } + var curve = ((DefaultCurve) curveTypeComboBox.getSelectedItem()).getCurve().copy(); + selectedCurve.set(curve.copy()); + updateCurveDataUI(); } }); @@ -259,4 +239,35 @@ private void createUIComponents() { curveGraph.repaint(); }); } + + private void updateCurveDataUI() { + curveGraph.repaint(); + var curve = selectedCurve.get(); + if (curve instanceof LinearCurve) { + bParamSpinner.setValue(((LinearCurve) curve).getB()); + mParamSpinner.setValue(((LinearCurve) curve).getM()); + kParamSpinner.setEnabled(false); + cParamSpinner.setEnabled(false); + } else if (curve instanceof ParabolicCurve) { + bParamSpinner.setValue(((ParabolicCurve) curve).getB()); + mParamSpinner.setValue(((ParabolicCurve) curve).getM()); + kParamSpinner.setEnabled(true); + kParamSpinner.setValue(((ParabolicCurve) curve).getK()); + cParamSpinner.setEnabled(false); + } else if (curve instanceof LogitCurve) { + bParamSpinner.setValue(((LogitCurve) curve).getB()); + mParamSpinner.setValue(((LogitCurve) curve).getM()); + kParamSpinner.setEnabled(true); + kParamSpinner.setValue(((LogitCurve) curve).getK()); + cParamSpinner.setEnabled(true); + cParamSpinner.setValue(((LogitCurve) curve).getC()); + } else if (curve instanceof LogisticCurve) { + bParamSpinner.setValue(((LogisticCurve) curve).getB()); + mParamSpinner.setValue(((LogisticCurve) curve).getM()); + kParamSpinner.setEnabled(true); + kParamSpinner.setValue(((LogisticCurve) curve).getK()); + cParamSpinner.setEnabled(true); + cParamSpinner.setValue(((LogisticCurve) curve).getC()); + } + } } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form index 9628e36e03a..67da208fd11 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form @@ -1,8 +1,9 @@
- + + - + @@ -12,7 +13,6 @@ - @@ -21,7 +21,6 @@ - @@ -30,14 +29,12 @@ - - @@ -47,7 +44,8 @@ - + + @@ -61,7 +59,6 @@ - @@ -71,7 +68,6 @@ - @@ -81,7 +77,6 @@ - @@ -91,7 +86,6 @@ - diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index 4d7fff2add8..99464a285c1 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -15,6 +15,12 @@ package megamek.client.ui.swing.ai.editor; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import megamek.ai.utility.DecisionScoreEvaluator; +import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; +import megamek.common.Entity; + import javax.swing.*; import java.awt.*; import java.lang.reflect.Method; @@ -27,28 +33,30 @@ public class DecisionScoreEvaluatorPane extends JPanel { private JTextField notesField; private JPanel decisionScoreEvaluatorPane; private JPanel considerationsPane; - + private final HoverStateModel hoverStateModel; public DecisionScoreEvaluatorPane() { $$$setupUI$$$(); - setLayout(new BorderLayout()); - add(decisionScoreEvaluatorPane, BorderLayout.CENTER); + add(decisionScoreEvaluatorPane, BorderLayout.WEST); + hoverStateModel = new HoverStateModel(); + } + + public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { + nameField.setText(dse.getName()); + descriptionField.setText(dse.getDescription()); + notesField.setText(dse.getNotes()); + considerationsPane.removeAll(); + var considerations = dse.getConsiderations(); + considerationsPane.setLayout(new GridLayoutManager(considerations.size() * 2, 1, new Insets(0, 0, 0, 0), -1, -1)); + + int row = 0; + for (var consideration : considerations) { + var c = new ConsiderationPane(); + c.setConsideration(consideration); + c.setHoverStateModel(hoverStateModel); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = GridBagConstraints.RELATIVE; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.weighty = 0.0; - gbc.anchor = GridBagConstraints.NORTHEAST; - var hoverState = new HoverStateModel(); - var considerations = List.of(new ConsiderationPane(), - new ConsiderationPane(), - new ConsiderationPane(), - new ConsiderationPane()); - for (var c : considerations) { - c.setHoverStateModel(hoverState); - considerationsPane.add(c, gbc); + considerationsPane.add(c, new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); + considerationsPane.add(new JSeparator(), new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); } } @@ -61,78 +69,36 @@ public DecisionScoreEvaluatorPane() { */ private void $$$setupUI$$$() { decisionScoreEvaluatorPane = new JPanel(); - decisionScoreEvaluatorPane.setLayout(new GridBagLayout()); + decisionScoreEvaluatorPane.setLayout(new GridLayoutManager(9, 1, new Insets(0, 0, 0, 0), -1, -1)); nameField = new JTextField(); - GridBagConstraints gbc; - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 1; - gbc.weightx = 1.0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - decisionScoreEvaluatorPane.add(nameField, gbc); + decisionScoreEvaluatorPane.add(nameField, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); descriptionField = new JTextField(); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 3; - gbc.weightx = 1.0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - decisionScoreEvaluatorPane.add(descriptionField, gbc); + decisionScoreEvaluatorPane.add(descriptionField, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); notesField = new JTextField(); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 5; - gbc.weightx = 1.0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - decisionScoreEvaluatorPane.add(notesField, gbc); + decisionScoreEvaluatorPane.add(notesField, new GridConstraints(5, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); final JScrollPane scrollPane1 = new JScrollPane(); scrollPane1.setHorizontalScrollBarPolicy(31); scrollPane1.setMaximumSize(new Dimension(800, 32767)); scrollPane1.setMinimumSize(new Dimension(800, 600)); scrollPane1.setWheelScrollingEnabled(true); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 7; - gbc.gridheight = 2; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - decisionScoreEvaluatorPane.add(scrollPane1, gbc); + decisionScoreEvaluatorPane.add(scrollPane1, new GridConstraints(7, 0, 2, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); considerationsPane = new JPanel(); - considerationsPane.setLayout(new GridBagLayout()); + considerationsPane.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); considerationsPane.setMaximumSize(new Dimension(800, 2147483647)); considerationsPane.setMinimumSize(new Dimension(800, 600)); scrollPane1.setViewportView(considerationsPane); final JLabel label1 = new JLabel(); this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.considerations")); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 6; - gbc.anchor = GridBagConstraints.WEST; - decisionScoreEvaluatorPane.add(label1, gbc); + decisionScoreEvaluatorPane.add(label1, new GridConstraints(6, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label2 = new JLabel(); this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.notes")); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 4; - gbc.anchor = GridBagConstraints.WEST; - decisionScoreEvaluatorPane.add(label2, gbc); + decisionScoreEvaluatorPane.add(label2, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label3 = new JLabel(); this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.description")); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 2; - gbc.anchor = GridBagConstraints.WEST; - decisionScoreEvaluatorPane.add(label3, gbc); + decisionScoreEvaluatorPane.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label4 = new JLabel(); this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.name")); - gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - decisionScoreEvaluatorPane.add(label4, gbc); + decisionScoreEvaluatorPane.add(label4, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); label1.setLabelFor(scrollPane1); label2.setLabelFor(notesField); label3.setLabelFor(descriptionField); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java index 013150847b4..9444a573e0a 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java @@ -29,7 +29,7 @@ public class DecisionScoreEvaluatorTable, DSE ext private final List dse; public DecisionScoreEvaluatorTable( - DecisionScoreEvaluatorTableModel model, Action[] actionList, List dse) { + DecisionTableModel model, Action[] actionList, List dse) { super(model); this.actionList = actionList; this.dse = dse; @@ -40,8 +40,8 @@ public void createUIComponents() { } @Override - public DecisionScoreEvaluatorTableModel getModel() { - return (DecisionScoreEvaluatorTableModel) super.getModel(); + public DecisionTableModel getModel() { + return (DecisionTableModel) super.getModel(); } @Override diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java similarity index 93% rename from megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java rename to megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java index 476e822584a..0281690ef5d 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java @@ -24,12 +24,12 @@ import java.util.List; -public class DecisionScoreEvaluatorTableModel> extends AbstractTableModel { +public class DecisionTableModel> extends AbstractTableModel { private final List rows; private final String[] columnNames = { "ID", "Decision", "Evaluator" }; - public DecisionScoreEvaluatorTableModel(List initialRows) { + public DecisionTableModel(List initialRows) { this.rows = new ArrayList<>(initialRows); } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java index be97b999664..92e576a30c8 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java @@ -41,6 +41,16 @@ public ParametersTableModel(Map parameters) { } } + public void setParameters(Map parameters) { + hashRows.clear(); + rowValues.clear(); + for (Map.Entry entry : parameters.entrySet()) { + hashRows.put(entry.getKey(), entry.getValue()); + rowValues.add(new Row(entry.getKey(), entry.getValue())); + } + fireTableDataChanged(); + } + public void addRow(String parameterName, Object value) { if (hashRows.containsKey(parameterName)) { logger.formattedErrorDialog("Parameter already exists", From f8422891982fe850cea4cd52100276f04414d40a Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sat, 28 Dec 2024 00:23:47 -0300 Subject: [PATCH 06/16] feat: small improvements on a couple of forms --- .../ui/swing/ai/editor/AiProfileEditor.form | 29 ++++++--- .../ui/swing/ai/editor/AiProfileEditor.java | 29 ++++++--- .../ui/swing/ai/editor/ConsiderationPane.form | 36 ++++------- .../ui/swing/ai/editor/ConsiderationPane.java | 60 +++---------------- .../client/ui/swing/ai/editor/CurvePane.form | 20 +++++-- .../client/ui/swing/ai/editor/CurvePane.java | 2 + 6 files changed, 74 insertions(+), 102 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form index d60569f4f1f..195aebb7973 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form @@ -201,15 +201,22 @@ - - + - - + + + + + + + + + + @@ -253,15 +260,21 @@ - - + - - + + + + + + + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index b4233f31e3e..c858f4f01c7 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -66,6 +66,8 @@ public class AiProfileEditor extends JFrame { private JPanel dsePane; private JPanel considerationTabPane; private JPanel considerationEditorPanel; + private ConsiderationPane considerationPane; + private boolean hasChanges = true; private boolean ignoreHotKeys = false; @@ -89,6 +91,9 @@ private void initialize() { gbc.weighty = 0.0; gbc.anchor = GridBagConstraints.NORTHEAST; gbc.insets = new Insets(0, 0, 0, 0); + considerationPane = new ConsiderationPane(); + considerationPane.setMinimumSize(new Dimension(considerationEditorPanel.getWidth(), considerationEditorPanel.getHeight())); + considerationEditorPanel.add(considerationPane, gbc); newDecisionButton.addActionListener(e -> { var action = (Action) actionComboBox.getSelectedItem(); @@ -171,7 +176,7 @@ private void handleNodeAction(DefaultMutableTreeNode node) { // dsePane = new DecisionScoreEvaluatorPane(); private void openConsideration(TWConsideration twConsideration) { - ((ConsiderationPane) considerationEditorPanel).setConsideration(twConsideration); + considerationPane.setConsideration(twConsideration); mainEditorTabbedPane.setSelectedComponent(considerationTabPane); } @@ -265,7 +270,7 @@ private void createUIComponents() { profileDecisionTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); decisionTabDsePanel = new DecisionScoreEvaluatorPane(); dsePane = new DecisionScoreEvaluatorPane(); - considerationEditorPanel = new ConsiderationPane(); + } private void addToMutableTreeNode(DefaultMutableTreeNode root, String nodeName, List items) { @@ -346,20 +351,26 @@ private void addToMutableTreeNode(DefaultMutableTreeNode panel4.add(label4, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final Spacer spacer1 = new Spacer(); panel4.add(spacer1, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); - panel4.add(decisionTabDsePanel, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + final JScrollPane scrollPane1 = new JScrollPane(); + panel4.add(scrollPane1, new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + scrollPane1.setViewportView(decisionTabDsePanel); dseTabPane = new JPanel(); dseTabPane.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); dseTabPane.setName(""); mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.tab.dse"), dseTabPane); - final JScrollPane scrollPane1 = new JScrollPane(); - scrollPane1.setHorizontalScrollBarPolicy(31); - scrollPane1.setWheelScrollingEnabled(true); - dseTabPane.add(scrollPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); - scrollPane1.setViewportView(dsePane); + final JScrollPane scrollPane2 = new JScrollPane(); + scrollPane2.setHorizontalScrollBarPolicy(31); + scrollPane2.setWheelScrollingEnabled(true); + dseTabPane.add(scrollPane2, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + scrollPane2.setViewportView(dsePane); considerationTabPane = new JPanel(); considerationTabPane.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.tab.consideration"), considerationTabPane); - considerationTabPane.add(considerationEditorPanel, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + final JScrollPane scrollPane3 = new JScrollPane(); + considerationTabPane.add(scrollPane3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + considerationEditorPanel = new JPanel(); + considerationEditorPanel.setLayout(new GridBagLayout()); + scrollPane3.setViewportView(considerationEditorPanel); label3.setLabelFor(actionComboBox); label4.setLabelFor(weightSpinner); } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form index ed5082e5039..4bf0bed4a4c 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.form @@ -1,17 +1,22 @@ - + - + - + + + + + + - + @@ -70,32 +75,11 @@ - - - - - - - - - - - - - - - - - - - - - - + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index bce744b7601..ea670b355b0 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -37,8 +37,6 @@ public class ConsiderationPane extends JPanel { private JTable parametersTable; private JPanel considerationPane; private JPanel topThingsPane; - private JButton hideButton; - private JButton showButton; public ConsiderationPane() { $$$setupUI$$$(); @@ -52,19 +50,6 @@ public ConsiderationPane() { ((CurvePane) (curveContainer)).setCurve(consideration.getCurve()); } }); - - hideButton.addActionListener(e -> { - curveContainer.setVisible(false); - showButton.setVisible(true); - hideButton.setVisible(false); - }); - - showButton.addActionListener(e -> { - curveContainer.setVisible(true); - hideButton.setVisible(true); - showButton.setVisible(false); - }); - showButton.setVisible(false); } public void setConsideration(Consideration consideration) { @@ -98,10 +83,14 @@ private void createUIComponents() { private void $$$setupUI$$$() { createUIComponents(); considerationPane = new JPanel(); - considerationPane.setLayout(new GridLayoutManager(4, 6, new Insets(0, 0, 0, 0), -1, -1)); + considerationPane.setLayout(new GridLayoutManager(4, 1, new Insets(0, 0, 0, 0), -1, -1)); + considerationPane.setAutoscrolls(false); + considerationPane.setMaximumSize(new Dimension(800, 600)); + considerationPane.setMinimumSize(new Dimension(800, 300)); + considerationPane.setPreferredSize(new Dimension(800, 600)); topThingsPane = new JPanel(); topThingsPane.setLayout(new GridLayoutManager(6, 6, new Insets(0, 0, 0, 0), -1, -1)); - considerationPane.add(topThingsPane, new GridConstraints(0, 0, 2, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + considerationPane.add(topThingsPane, new GridConstraints(0, 0, 2, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); topThingsPane.add(considerationComboBox, new GridConstraints(1, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); considerationName = new JTextField(); topThingsPane.add(considerationName, new GridConstraints(3, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); @@ -115,17 +104,9 @@ private void createUIComponents() { final JLabel label3 = new JLabel(); this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.considerationType")); topThingsPane.add(label3, new GridConstraints(0, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - hideButton = new JButton(); - this.$$$loadButtonText$$$(hideButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.hide")); - topThingsPane.add(hideButton, new GridConstraints(4, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - showButton = new JButton(); - this.$$$loadButtonText$$$(showButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.show")); - topThingsPane.add(showButton, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - final Spacer spacer1 = new Spacer(); - topThingsPane.add(spacer1, new GridConstraints(4, 3, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); final JLabel label4 = new JLabel(); this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.parameters")); - considerationPane.add(label4, new GridConstraints(2, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + considerationPane.add(label4, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); parametersTable.setFillsViewportHeight(false); parametersTable.setPreferredScrollableViewportSize(new Dimension(100, 50)); considerationPane.add(parametersTable, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); @@ -177,33 +158,6 @@ private void createUIComponents() { } } - /** - * @noinspection ALL - */ - private void $$$loadButtonText$$$(AbstractButton component, String text) { - StringBuffer result = new StringBuffer(); - boolean haveMnemonic = false; - char mnemonic = '\0'; - int mnemonicIndex = -1; - for (int i = 0; i < text.length(); i++) { - if (text.charAt(i) == '&') { - i++; - if (i == text.length()) break; - if (!haveMnemonic && text.charAt(i) != '&') { - haveMnemonic = true; - mnemonic = text.charAt(i); - mnemonicIndex = result.length(); - } - } - result.append(text.charAt(i)); - } - component.setText(result.toString()); - if (haveMnemonic) { - component.setMnemonic(mnemonic); - component.setDisplayedMnemonicIndex(mnemonicIndex); - } - } - /** * @noinspection ALL */ diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form index a8ea6545e30..b41a313626e 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.form @@ -2,7 +2,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -41,28 +41,36 @@ - + + + - + + + - + + + - + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index 5ea11d9e8f3..fe645758a2b 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -15,6 +15,8 @@ package megamek.client.ui.swing.ai.editor; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; import megamek.ai.utility.*; import javax.swing.*; From c47fb960d358b7d41cc06a91759def0371a39d72 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sat, 28 Dec 2024 17:41:02 -0300 Subject: [PATCH 07/16] feat: added common menu bar, removed trash code --- .../i18n/megamek/client/messages.properties | 19 + .../megamek/client/ui/swing/ClientGUI.java | 15 + .../client/ui/swing/CommonMenuBar.java | 77 + .../client/ui/swing/RecentProfileList.java | 138 + .../ui/swing/ai/editor/AiProfileEditor.java | 25 + .../ui/swing/ai/editor/UtilityAiEditor.java | 2674 ----------------- 6 files changed, 274 insertions(+), 2674 deletions(-) create mode 100644 megamek/src/megamek/client/ui/swing/RecentProfileList.java delete mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 99c5adcc97b..55b387606a7 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4831,3 +4831,22 @@ aiEditor.tree.title=Princess Data TacOps.leaping.leg_damage=leaping (leg damage) TacOps.leaping.fall_damage=leaping (fall) +### AI Editor +CommonMenuBar.AIEditorMenu=AI Editor +CommonMenuBar.aiEditor.New=New Profile +CommonMenuBar.aiEditor.Open=Open Profile +CommonMenuBar.aiEditor.RecentProfile=Recent Profiles +CommonMenuBar.aiEditor.Save=Save Profile +CommonMenuBar.aiEditor.SaveAs=Save Profile As +CommonMenuBar.aiEditor.ReloadFromDisk=Reload Profile from Disk +CommonMenuBar.aiEditor.Undo=Undo +CommonMenuBar.aiEditor.Redo=Redo +CommonMenuBar.aiEditor.NewDecision=New Decision +CommonMenuBar.aiEditor.NewConsideration=New Consideration +CommonMenuBar.aiEditor.NewDecisionScoreEvaluator=New Decision Score Evaluator +CommonMenuBar.aiEditor.Export=Export Profile +CommonMenuBar.aiEditor.ExportConsiderations=Export Considerations +CommonMenuBar.aiEditor.ExportDSE=Export Decision Score Evaluators +CommonMenuBar.aiEditor.Import=Import Profile +CommonMenuBar.aiEditor.ImportConsiderations=Import Considerations +CommonMenuBar.aiEditor.ImportDSE=Import Decision Score Evaluators diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index a21f69e9f71..19580b69e7f 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -191,6 +191,21 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, public static final String FIRE_SAVE_WEAPON_ORDER = "saveWeaponOrder"; // endregion fire menu + public static final String AI_EDITOR_NEW = "aiEditorNew"; + public static final String AI_EDITOR_OPEN = "aiEditorOpen"; + public static final String AI_EDITOR_RECENT_PROFILE = "aiEditorRecentProfile"; + public static final String AI_EDITOR_SAVE = "aiEditorSave"; + public static final String AI_EDITOR_SAVE_AS = "aiEditorSaveAs"; + public static final String AI_EDITOR_RELOAD_FROM_DISK = "aiEditorReloadFromDisk"; + public static final String AI_EDITOR_UNDO = "aiEditorUndo"; + public static final String AI_EDITOR_REDO = "aiEditorRedo"; + public static final String AI_EDITOR_NEW_DECISION = "aiEditorNewDecision"; + public static final String AI_EDITOR_NEW_CONSIDERATION = "aiEditorNewConsideration"; + public static final String AI_EDITOR_NEW_DECISION_SCORE_EVALUATOR = "aiEditorNewDecisionScoreEvaluator"; + public static final String AI_EDITOR_EXPORT = "aiEditorExport"; + public static final String AI_EDITOR_IMPORT = "aiEditorImport"; + + // region help menu public static final String HELP_CONTENTS = "helpContents"; public static final String HELP_SKINNING = "helpSkinning"; diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java index e89fe1c9417..b3d6164c0f7 100644 --- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java +++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java @@ -64,6 +64,9 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen /** True when this menu is attached to the board editor. */ private boolean isBoardEditor = false; + /** True when this menu is attached to the AI editor. */ + private boolean isAiEditor = false; + /** True when this menu is attached to the game's main menu. */ private boolean isMainMenu = false; @@ -163,6 +166,22 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen private final JMenuItem viewIncGUIScale = new JMenuItem(getString("CommonMenuBar.viewIncGUIScale")); private final JMenuItem viewDecGUIScale = new JMenuItem(getString("CommonMenuBar.viewDecGUIScale")); + // The AI Editor Menu + private final JMenuItem aiEditorNew = new JMenuItem(getString("CommonMenuBar.aiEditor.New")); + private final JMenuItem aiEditorOpen = new JMenuItem(getString("CommonMenuBar.aiEditor.Open")); + private final JMenu aiEditorRecentProfile = new JMenu(getString("CommonMenuBar.aiEditor.RecentProfile")); + private final JMenuItem aiEditorSave = new JMenuItem(getString("CommonMenuBar.aiEditor.Save")); + private final JMenuItem aiEditorSaveAs = new JMenuItem(getString("CommonMenuBar.aiEditor.SaveAs")); + private final JMenuItem aiEditorReloadFromDisk = new JMenuItem(getString("CommonMenuBar.aiEditor.ReloadFromDisk")); + private final JMenuItem aiEditorUndo = new JMenuItem(getString("CommonMenuBar.aiEditor.Undo")); + private final JMenuItem aiEditorRedo = new JMenuItem(getString("CommonMenuBar.aiEditor.Redo")); + private final JMenuItem aiEditorNewDecision = new JMenuItem(getString("CommonMenuBar.aiEditor.NewDecision")); + private final JMenuItem aiEditorNewConsideration = new JMenuItem(getString("CommonMenuBar.aiEditor.NewConsideration")); + private final JMenuItem aiEditorNewDecisionScoreEvaluator = new JMenuItem( + getString("CommonMenuBar.aiEditor.NewDecisionScoreEvaluator")); + private final JMenuItem aiEditorExport = new JMenuItem(getString("CommonMenuBar.aiEditor.Export")); + private final JMenuItem aiEditorImport = new JMenuItem(getString("CommonMenuBar.aiEditor.Import")); + // The Help menu private final JMenuItem helpContents = new JMenuItem(getString("CommonMenuBar.helpContents")); private final JMenuItem helpSkinning = new JMenuItem(getString("CommonMenuBar.helpSkinning")); @@ -195,6 +214,13 @@ public static CommonMenuBar getMenuBarForBoardEditor() { return menuBar; } + public static CommonMenuBar getMenuBarForAiEditor() { + var menuBar = new CommonMenuBar(); + menuBar.isAiEditor = true; + menuBar.updateEnabledStates(); + return menuBar; + } + public static CommonMenuBar getMenuBarForMainMenu() { var menuBar = new CommonMenuBar(); menuBar.isMainMenu = true; @@ -356,6 +382,33 @@ public CommonMenuBar() { toggleCFWarning.setToolTipText(Messages.getString("CommonMenuBar.viewToggleCFWarningToolTip")); toggleCFWarning.setSelected(GUIP.getShowCFWarnings()); + menu = new JMenu(Messages.getString("CommonMenuBar.AIEditorMenu")); + menu.setMnemonic(VK_A); + add(menu); + initMenuItem(aiEditorNew, menu, AI_EDITOR_NEW); + initMenuItem(aiEditorOpen, menu, AI_EDITOR_OPEN); + initMenuItem(aiEditorRecentProfile, menu, AI_EDITOR_RECENT_PROFILE); + initializeRecentAiProfilesMenu(); + menu.addSeparator(); + initMenuItem(aiEditorSave, menu, AI_EDITOR_SAVE); + initMenuItem(aiEditorSaveAs, menu, AI_EDITOR_SAVE_AS); + initMenuItem(aiEditorReloadFromDisk, menu, AI_EDITOR_RELOAD_FROM_DISK); + menu.addSeparator(); + initMenuItem(aiEditorUndo, menu, AI_EDITOR_UNDO); + initMenuItem(aiEditorRedo, menu, AI_EDITOR_REDO); + menu.addSeparator(); + initMenuItem(aiEditorNewDecision, menu, AI_EDITOR_NEW_DECISION); + aiEditorNewDecision.setSelected(GUIP.getShowSensorRange()); + initMenuItem(aiEditorNewConsideration, menu, AI_EDITOR_NEW_CONSIDERATION); + aiEditorNewConsideration.setSelected(GUIP.getShowSensorRange()); + initMenuItem(aiEditorNewDecisionScoreEvaluator, menu, AI_EDITOR_NEW_DECISION_SCORE_EVALUATOR); + aiEditorNewDecisionScoreEvaluator.setSelected(GUIP.getShowSensorRange()); + menu.addSeparator(); + initMenuItem(aiEditorExport, menu, AI_EDITOR_EXPORT); + initMenuItem(aiEditorImport, menu, AI_EDITOR_IMPORT); + + + // Create the Help menu menu = new JMenu(Messages.getString("CommonMenuBar.HelpMenu")); menu.setMnemonic(VK_H); @@ -558,6 +611,20 @@ private synchronized void updateEnabledStates() { viewMekDisplay.setEnabled(isInGameBoardView); viewForceDisplay.setEnabled(isInGameBoardView); fireSaveWeaponOrder.setEnabled(isInGameBoardView); + + aiEditorExport.setEnabled(isAiEditor); + aiEditorImport.setEnabled(isAiEditor); + aiEditorNew.setEnabled(isAiEditor); + aiEditorOpen.setEnabled(isAiEditor); + aiEditorRecentProfile.setEnabled(isAiEditor); + aiEditorSave.setEnabled(isAiEditor); + aiEditorSaveAs.setEnabled(isAiEditor); + aiEditorReloadFromDisk.setEnabled(isAiEditor); + aiEditorUndo.setEnabled(isAiEditor); + aiEditorRedo.setEnabled(isAiEditor); + aiEditorNewDecision.setEnabled(isAiEditor); + aiEditorNewConsideration.setEnabled(isAiEditor); + aiEditorNewDecisionScoreEvaluator.setEnabled(isAiEditor); } /** @@ -646,4 +713,14 @@ private void initializeRecentBoardsMenu() { } boardRecent.setEnabled(!recentBoards.isEmpty()); } + + private void initializeRecentAiProfilesMenu() { + List recentProfiles = RecentProfileList.getRecentProfiles(); + aiEditorRecentProfile.removeAll(); + for (String recentProfile : recentProfiles) { + JMenuItem item = new JMenuItem(recentProfile); + initMenuItem(item, aiEditorRecentProfile, AI_EDITOR_RECENT_PROFILE + "|" + recentProfile); + } + aiEditorRecentProfile.setEnabled(!recentProfiles.isEmpty()); + } } diff --git a/megamek/src/megamek/client/ui/swing/RecentProfileList.java b/megamek/src/megamek/client/ui/swing/RecentProfileList.java new file mode 100644 index 00000000000..f21615a8773 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/RecentProfileList.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.client.ui.swing; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import megamek.common.Configuration; +import megamek.common.preference.IPreferenceChangeListener; +import megamek.common.preference.PreferenceChangeEvent; +import megamek.logging.MMLogger; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This class keeps a list of recently opened profile files and makes it available statically. It automatically + * writes the list to a file in the MM's mmconf directory. + */ +public final class RecentProfileList { + + public static final String RECENT_PROFILE_UPDATED = "recent_profile_event"; + + private static final MMLogger LOGGER = MMLogger.create(RecentProfileList.class); + private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .disable(YAMLGenerator.Feature.SPLIT_LINES) + ); + private static final int MAX_RECENT_PROFILES = 10; + private static final String RECENT_PROFILES_FILENAME = "recent_profiles.yml"; + private static final File RECENT_PROFILE_FILE = new File(Configuration.configDir(), RECENT_PROFILES_FILENAME); + + private static final RecentProfileList INSTANCE = new RecentProfileList(); + private static final List LISTENERS = new CopyOnWriteArrayList<>(); + + private List recentProfiles = null; + + /** + * @return A list of the most recently opened board files. Can be empty. + */ + public static List getRecentProfiles() { + INSTANCE.initialize(); + return INSTANCE.recentProfiles; + } + + /** + * Adds a new board to the recent board files, replacing the oldest if the list is full. Also + * saves the list to file. + * + * @param board The board filename (full path) + */ + public static void addProfile(String board) { + INSTANCE.addBProfile(board); + } + + /** + * Adds a new board to the recent board files, replacing the oldest if the list is full. Also + * saves the list to file. + * + * @param profile The profile file + */ + public static void addProfile(File profile) { + addProfile(profile.toString()); + } + + /** + * Adds a listener for recent board changes. The event will have the name {@link #RECENT_PROFILE_UPDATED}. + */ + public static void addListener(IPreferenceChangeListener listener) { + if (!LISTENERS.contains(listener)) { + LISTENERS.add(listener); + } + } + + public static void removeListener(IPreferenceChangeListener listener) { + LISTENERS.remove(listener); + } + + private void addBProfile(String board) { + initialize(); + // remove and add so there is only one copy of each and the new board is at the end of the list + recentProfiles.remove(board); + recentProfiles.add(board); + while (recentProfiles.size() > MAX_RECENT_PROFILES) { + recentProfiles.remove(0); + } + saveRecentBoards(); + LISTENERS.forEach(l -> l.preferenceChange( + new PreferenceChangeEvent(board, RECENT_PROFILE_UPDATED, null, null))); + } + + private void saveRecentBoards() { + try { + YAML_MAPPER.writeValue(RECENT_PROFILE_FILE, INSTANCE.recentProfiles); + } catch (IOException e) { + LOGGER.error("Could not save recent board list", e); + } + } + + private void initialize() { + if (INSTANCE.recentProfiles == null) { + try { + TypeReference> typeRef = new TypeReference<>() { }; + INSTANCE.recentProfiles = YAML_MAPPER.readValue(RECENT_PROFILE_FILE, typeRef); + } catch (FileNotFoundException e) { + // ignore, this happens when no list has been saved yet + } catch (IOException e) { + LOGGER.error("Could not load recent board list", e); + } + if (INSTANCE.recentProfiles == null) { + INSTANCE.recentProfiles = new ArrayList<>(); + } + } + } + + private RecentProfileList() { } +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index c858f4f01c7..18b02151c44 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -27,6 +27,7 @@ import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; import megamek.client.ui.Messages; import megamek.client.ui.enums.DialogResult; +import megamek.client.ui.swing.CommonMenuBar; import megamek.client.ui.swing.GUIPreferences; import megamek.client.ui.swing.util.MegaMekController; @@ -68,6 +69,7 @@ public class AiProfileEditor extends JFrame { private JPanel considerationEditorPanel; private ConsiderationPane considerationPane; + private final CommonMenuBar menuBar = CommonMenuBar.getMenuBarForAiEditor(); private boolean hasChanges = true; private boolean ignoreHotKeys = false; @@ -155,6 +157,29 @@ public void mouseClicked(MouseEvent e) { } } }); + + menuBar.addActionListener(e -> { + if (!ignoreHotKeys) { + switch (e.getActionCommand()) { + case "Save": + persistProfile(); + break; + case "Close": + if (!hasChanges || (showSavePrompt() != DialogResult.CANCELLED)) { + if (controller != null) { + controller.removeAllActions(); + controller.aiEditor = null; + } + getFrame().dispose(); + } + break; + case "Help": +// controller.showHelp("aiEditor"); + break; + } + } + }); + getFrame().setJMenuBar(menuBar); } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java deleted file mode 100644 index 8e77ea165e7..00000000000 --- a/megamek/src/megamek/client/ui/swing/ai/editor/UtilityAiEditor.java +++ /dev/null @@ -1,2674 +0,0 @@ -/* - * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - * - */ -package megamek.client.ui.swing.ai.editor; - -import megamek.MMConstants; -import megamek.client.event.BoardViewEvent; -import megamek.client.event.BoardViewListenerAdapter; -import megamek.client.ui.Messages; -import megamek.client.ui.dialogs.helpDialogs.AbstractHelpDialog; -import megamek.client.ui.dialogs.helpDialogs.BoardEditorHelpDialog; -import megamek.client.ui.enums.DialogResult; -import megamek.client.ui.swing.*; -import megamek.client.ui.swing.boardview.BoardEditorTooltip; -import megamek.client.ui.swing.boardview.BoardView; -import megamek.client.ui.swing.boardview.KeyBindingsOverlay; -import megamek.client.ui.swing.dialog.FloodDialog; -import megamek.client.ui.swing.dialog.LevelChangeDialog; -import megamek.client.ui.swing.dialog.MMConfirmDialog; -import megamek.client.ui.swing.dialog.MultiIntSelectorDialog; -import megamek.client.ui.swing.minimap.Minimap; -import megamek.client.ui.swing.tileset.HexTileset; -import megamek.client.ui.swing.tileset.TilesetManager; -import megamek.client.ui.swing.util.FontHandler; -import megamek.client.ui.swing.util.MegaMekController; -import megamek.client.ui.swing.util.StringDrawer; -import megamek.client.ui.swing.util.UIUtil; -import megamek.client.ui.swing.util.UIUtil.FixedYPanel; -import megamek.common.*; -import megamek.common.annotations.Nullable; -import megamek.common.util.BoardUtilities; -import megamek.common.util.ImageUtil; -import megamek.common.util.fileUtils.MegaMekFile; -import megamek.logging.MMLogger; - -import javax.imageio.ImageIO; -import javax.swing.*; -import javax.swing.border.TitledBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.filechooser.FileFilter; -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; -import java.awt.event.*; -import java.io.*; -import java.util.List; -import java.util.*; - -import static megamek.common.Terrains.*; - -public class UtilityAiEditor extends JPanel implements ItemListener, ListSelectionListener, ActionListener, - DocumentListener, IMapSettingsObserver { - private final static MMLogger logger = MMLogger.create(UtilityAiEditor.class); - - - private static class TerrainHelper implements Comparable { - private final int terrainType; - - TerrainHelper(int terrain) { - terrainType = terrain; - } - - public int getTerrainType() { - return terrainType; - } - - @Override - public String toString() { - return Terrains.getEditorName(terrainType); - } - - public String getTerrainTooltip() { - return Terrains.getEditorTooltip(terrainType); - } - - @Override - public int compareTo(TerrainHelper o) { - return toString().compareTo(o.toString()); - } - - @Override - public boolean equals(Object other) { - if (other instanceof Integer) { - return getTerrainType() == (Integer) other; - } else if (!(other instanceof TerrainHelper)) { - return false; - } else { - return getTerrainType() == ((TerrainHelper) other).getTerrainType(); - } - } - - @Override - public int hashCode() { - return Objects.hash(getTerrainType()); - } - } - - /** - * Class to make it easier to display a Terrain in a JList or - * JComboBox. - * - * @author arlith - */ - private static class TerrainTypeHelper implements Comparable { - - Terrain terrain; - - TerrainTypeHelper(Terrain terrain) { - this.terrain = terrain; - } - - public Terrain getTerrain() { - return terrain; - } - - @Override - public String toString() { - String baseString = Terrains.getDisplayName(terrain.getType(), terrain.getLevel()); - if (baseString == null) { - baseString = Terrains.getEditorName(terrain.getType()); - baseString += " " + terrain.getLevel(); - } - if (terrain.hasExitsSpecified()) { - baseString += " (Exits: " + terrain.getExits() + ")"; - } - return baseString; - } - - public String getTooltip() { - return terrain.toString(); - } - - @Override - public int compareTo(TerrainTypeHelper o) { - return toString().compareTo(o.toString()); - } - } - - /** - * ListCellRenderer for rendering tooltips for each item in a list or combobox. - * Code from SourceForge: - * https://stackoverflow.com/questions/480261/java-swing-mouseover-text-on-jcombobox-items - */ - private static class ComboboxToolTipRenderer extends DefaultListCellRenderer { - - private TerrainHelper[] terrains; - private List terrainTypes; - - @Override - public Component getListCellRendererComponent(final JList list, final Object value, - final int index, final boolean isSelected, - final boolean cellHasFocus) { - if ((-1 < index) && (value != null)) { - if (terrainTypes != null) { - list.setToolTipText(terrainTypes.get(index).getTooltip()); - } else if (terrains != null) { - list.setToolTipText(terrains[index].getTerrainTooltip()); - } - } - return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - } - - public void setTerrains(TerrainHelper... terrains) { - this.terrains = terrains; - } - - public void setTerrainTypes(List terrainTypes) { - this.terrainTypes = terrainTypes; - } - } - - private final GUIPreferences guip = GUIPreferences.getInstance(); - - private static final int BASE_TERRAINBUTTON_ICON_WIDTH = 70; - private static final int BASE_ARROWBUTTON_ICON_WIDTH = 25; - private static final String CMD_EDIT_DEPLOYMENT_ZONES = "CMD_EDIT_DEPLOYMENT_ZONES"; - - // Components - private final JFrame frame = new JFrame(); - private final Game game = new Game(); - private Board board = game.getBoard(); - private BoardView bv; - boolean isDragging = false; - private Component bvc; - private final CommonMenuBar menuBar = CommonMenuBar.getMenuBarForBoardEditor(); - private AbstractHelpDialog help; - private CommonSettingsDialog setdlg; - private JDialog minimapW; - private final MegaMekController controller; - - // The current files - private File curfileImage; - private File curBoardFile; - - // The active hex "brush" - private HexCanvas canHex; - private Hex curHex = new Hex(); - - // Easy terrain access buttons - private final List terrainButtons = new ArrayList<>(); - private ScalingIconButton buttonLW, buttonLJ; - private ScalingIconButton buttonOW, buttonOJ; - private ScalingIconButton buttonWa, buttonSw, buttonRo; - private ScalingIconButton buttonRd, buttonCl, buttonBu; - private ScalingIconButton buttonMd, buttonPv, buttonSn; - private ScalingIconButton buttonIc, buttonTu, buttonMg; - private ScalingIconButton buttonBr, buttonFT; - private final List brushButtons = new ArrayList<>(); - private ScalingIconToggleButton buttonBrush1, buttonBrush2, buttonBrush3; - private ScalingIconToggleButton buttonUpDn, buttonOOC; - - // The brush size: 1 = 1 hex, 2 = radius 1, 3 = radius 2 - private int brushSize = 1; - private int hexLeveltoDraw = -1000; - private EditorTextField texElev; - private ScalingIconButton butElevUp; - private ScalingIconButton butElevDown; - private JList lisTerrain; - private ComboboxToolTipRenderer lisTerrainRenderer; - private ScalingIconButton butDelTerrain; - private JComboBox choTerrainType; - private EditorTextField texTerrainLevel; - private JCheckBox cheTerrExitSpecified; - private EditorTextField texTerrExits; - private ScalingIconButton butTerrExits; - private JCheckBox cheRoadsAutoExit; - private JButton copyButton = new JButton(Messages.getString("BoardEditor.copyButton")); - private JButton pasteButton = new JButton(Messages.getString("BoardEditor.pasteButton")); - private ScalingIconButton butExitUp, butExitDown; - private JComboBox choTheme; - private ScalingIconButton butTerrDown, butTerrUp; - private JButton butAddTerrain; - private JButton butBoardNew; - private JButton butBoardOpen; - private JButton butBoardSave; - private JButton butBoardSaveAs; - private JButton butBoardSaveAsImage; - private JButton butBoardValidate; - private JButton butSourceFile; - private MapSettings mapSettings = MapSettings.getInstance(); - private JButton butExpandMap; - private Coords lastClicked; - private final JLabel labTheme = new JLabel(Messages.getString("BoardEditor.labTheme"), SwingConstants.LEFT); - - private final FixedYPanel panelHexSettings = new FixedYPanel(); - private final FixedYPanel panelTerrSettings = new FixedYPanel(new GridLayout(0, 2, 4, 4)); - private final FixedYPanel panelBoardSettings = new FixedYPanel(); - - // Help Texts - private final JLabel labHelp1 = new JLabel(Messages.getString("BoardEditor.helpText"), SwingConstants.LEFT); - private final JLabel labHelp2 = new JLabel(Messages.getString("BoardEditor.helpText2"), SwingConstants.LEFT); - - // Undo / Redo - private final List undoButtons = new ArrayList<>(); - private ScalingIconButton buttonUndo, buttonRedo; - private final Stack> undoStack = new Stack<>(); - private final Stack> redoStack = new Stack<>(); - private HashSet currentUndoSet; - private HashSet currentUndoCoords; - - // Tracker for board changes; unfortunately this is not equal to - // undoStack == empty because saving the board doesn't empty the - // undo stack but makes the board unchanged. - /** Tracks if the board has changes over the last saved version. */ - private boolean hasChanges = false; - /** Tracks if the board can return to the last saved version. */ - private boolean canReturnToSaved = true; - /** - * The undo stack size at the last save. Used to track saved status of the - * board. - */ - private int savedUndoStackSize = 0; - - // Misc - private File loadPath = Configuration.boardsDir(); - - /** - * Special purpose indicator, keeps terrain list - * from de-selecting when clicking it - */ - private boolean terrListBlocker = false; - - /** - * Special purpose indicator, prevents an update - * loop when the terrain level or exits field is changed - */ - private boolean noTextFieldUpdate = false; - - /** - * A MouseAdapter that closes a JLabel when clicked - */ - private final MouseAdapter clickToHide = new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - if (e.getSource() instanceof JLabel) { - ((JLabel) e.getSource()).setVisible(false); - } - } - }; - - /** - * Flag that indicates whether hotkeys should be ignored or not. This is - * used for disabling hot keys when various dialogs are displayed. - */ - private boolean ignoreHotKeys = false; - - /** - * Creates and lays out a new Board Editor frame. - */ - public UtilityAiEditor(MegaMekController c) { - controller = c; - try { - bv = new BoardView(game, controller, null); - bv.addOverlay(new KeyBindingsOverlay(bv)); - bv.setUseLosTool(false); - bv.setDisplayInvalidFields(true); - bv.setTooltipProvider(new BoardEditorTooltip(game, bv)); - bvc = bv.getComponent(true); - } catch (IOException e) { - JOptionPane.showMessageDialog(frame, - Messages.getString("BoardEditor.CouldntInitialize") + e, - Messages.getString("BoardEditor.FatalError"), JOptionPane.ERROR_MESSAGE); - frame.dispose(); - } - - // Add a mouse listener for mouse button release - // to handle Undo - bv.getPanel().addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) { - // Act only if the user actually drew something - if ((currentUndoSet != null) && !currentUndoSet.isEmpty()) { - // Since this draw action is finished, push the - // drawn hexes onto the Undo Stack and get ready - // for a new draw action - undoStack.push(currentUndoSet); - currentUndoSet = null; - buttonUndo.setEnabled(true); - // Drawing something disables any redo actions - redoStack.clear(); - buttonRedo.setEnabled(false); - // When Undo (without Redo) has been used after saving - // and the user draws on the board, then it can - // no longer know if it's been returned to the saved state - // and it will always be treated as changed. - if (savedUndoStackSize > undoStack.size()) { - canReturnToSaved = false; - } - hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize); - } - // Mark the title when the board has changes - setFrameTitle(); - } - } - }); - bv.addBoardViewListener(new BoardViewListenerAdapter() { - @Override - public void hexMoused(BoardViewEvent b) { - Coords c = b.getCoords(); - // return if there are no or no valid coords or if we click the same hex again - // unless Raise/Lower Terrain is active which should let us click the same hex - if ((c == null) || (c.equals(lastClicked) && !buttonUpDn.isSelected()) - || !board.contains(c)) { - return; - } - lastClicked = c; - bv.cursor(c); - boolean isALT = (b.getModifiers() & InputEvent.ALT_DOWN_MASK) != 0; - boolean isSHIFT = (b.getModifiers() & InputEvent.SHIFT_DOWN_MASK) != 0; - boolean isCTRL = (b.getModifiers() & InputEvent.CTRL_DOWN_MASK) != 0; - boolean isLMB = (b.getButton() == MouseEvent.BUTTON1); - - // Raise/Lower Terrain is selected - if (buttonUpDn.isSelected()) { - // Mouse Button released - if (b.getType() == BoardViewEvent.BOARD_HEX_CLICKED) { - hexLeveltoDraw = -1000; - isDragging = false; - } - - // Mouse Button clicked or dragged - if ((b.getType() == BoardViewEvent.BOARD_HEX_DRAGGED) && isLMB) { - if (!isDragging) { - hexLeveltoDraw = board.getHex(c).getLevel(); - if (isALT) { - hexLeveltoDraw--; - } else if (isSHIFT) { - hexLeveltoDraw++; - } - isDragging = true; - } - } - - // CORRECTION, click outside the board then drag inside??? - if (hexLeveltoDraw != -1000) { - LinkedList allBrushHexes = getBrushCoords(c); - for (Coords h : allBrushHexes) { - if (!buttonOOC.isSelected() || board.getHex(h).isClearHex()) { - saveToUndo(h); - relevelHex(h); - } - } - } - // ------- End Raise/Lower Terrain - } else if (isLMB || (b.getModifiers() & InputEvent.BUTTON1_DOWN_MASK) != 0) { - // 'isLMB' is true if a button 1 is associated to a click or release but not - // while dragging. - // The left button down mask is checked because we could be dragging. - - // Normal texture paint - if (isALT) { // ALT-Click - setCurrentHex(board.getHex(b.getCoords())); - } else { - LinkedList allBrushHexes = getBrushCoords(c); - for (Coords h : allBrushHexes) { - // test if texture overwriting is active - if ((!buttonOOC.isSelected() || board.getHex(h).isClearHex()) - && curHex.isValid(null)) { - saveToUndo(h); - if (isCTRL) { // CTRL-Click - paintHex(h); - } else if (isSHIFT) { // SHIFT-Click - addToHex(h); - } else { // Normal click - retextureHex(h); - } - } - } - } - } - } - }); - - setupEditorPanel(); - setupFrame(); - frame.setVisible(true); - if (GUIPreferences.getInstance().getNagForMapEdReadme()) { - String title = Messages.getString("BoardEditor.readme.title"); - String body = Messages.getString("BoardEditor.readme.message"); - ConfirmDialog confirm = new ConfirmDialog(frame, title, body, true); - confirm.setVisible(true); - if (!confirm.getShowAgain()) { - GUIPreferences.getInstance().setNagForMapEdReadme(false); - } - if (confirm.getAnswer()) { - showHelp(); - } - } - } - - /** - * Sets up the frame that will display the editor. - */ - private void setupFrame() { - setFrameTitle(); - frame.add(bvc, BorderLayout.CENTER); - frame.add(this, BorderLayout.EAST); - menuBar.addActionListener(this); - frame.setJMenuBar(menuBar); - if (GUIPreferences.getInstance().getWindowSizeHeight() != 0) { - frame.setLocation(GUIPreferences.getInstance().getWindowPosX(), - GUIPreferences.getInstance().getWindowPosY()); - frame.setSize(GUIPreferences.getInstance().getWindowSizeWidth(), - GUIPreferences.getInstance().getWindowSizeHeight()); - } else { - frame.setSize(800, 600); - } - - frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); - frame.addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - // When the board has changes, ask the user - if (hasChanges && (showSavePrompt() == DialogResult.CANCELLED)) { - return; - } - // otherwise: exit the Map Editor - minimapW.setVisible(false); - if (controller != null) { - controller.removeAllActions(); - controller.aiEditor = null; - } - bv.dispose(); - frame.dispose(); - } - }); - } - - /** - * Shows a prompt to save the current board. When the board is actually saved or - * the user presses - * "No" (don't want to save), returns DialogResult.CONFIRMED. In this case, the - * action (loading a board - * or leaving the board editor) that led to this prompt may be continued. - * In all other cases, returns DialogResult.CANCELLED, meaning the action should - * not be continued. - * - * @return DialogResult.CANCELLED (cancel action) or CONFIRMED (continue action) - */ - private DialogResult showSavePrompt() { - ignoreHotKeys = true; - int savePrompt = JOptionPane.showConfirmDialog(null, - Messages.getString("BoardEditor.exitprompt"), - Messages.getString("BoardEditor.exittitle"), - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE); - ignoreHotKeys = false; - // When the user cancels or did not actually save the board, don't load anything - if (((savePrompt == JOptionPane.YES_OPTION) && !boardSave(false)) - || (savePrompt == JOptionPane.CANCEL_OPTION) - || (savePrompt == JOptionPane.CLOSED_OPTION)) { - return DialogResult.CANCELLED; - } else { - return DialogResult.CONFIRMED; - } - } - - /** - * Sets up Scaling Icon Buttons - */ - private ScalingIconButton prepareButton(String iconName, String buttonName, - List bList, int width) { - // Get the normal icon - File file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + ".png").getFile(); - Image imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); - if (imageButton == null) { - imageButton = ImageUtil.failStandardImage(); - } - ScalingIconButton button = new ScalingIconButton(imageButton, width); - - // Get the hover icon - file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_H.png").getFile(); - imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); - button.setRolloverImage(imageButton); - - // Get the disabled icon, if any - file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_G.png").getFile(); - imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); - button.setDisabledImage(imageButton); - - String tt = Messages.getString("BoardEditor." + iconName + "TT"); - if (!tt.isBlank()) { - button.setToolTipText(tt); - } - button.setMargin(new Insets(0, 0, 0, 0)); - if (bList != null) { - bList.add(button); - } - button.addActionListener(this); - return button; - } - - /** - * Sets up Scaling Icon ToggleButtons - */ - private ScalingIconToggleButton prepareToggleButton(String iconName, String buttonName, - List bList, int width) { - // Get the normal icon - File file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + ".png").getFile(); - Image imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); - if (imageButton == null) { - imageButton = ImageUtil.failStandardImage(); - } - ScalingIconToggleButton button = new ScalingIconToggleButton(imageButton, width); - - // Get the hover icon - file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_H.png").getFile(); - imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); - button.setRolloverImage(imageButton); - - // Get the selected icon, if any - file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/" + iconName + "_S.png").getFile(); - imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath()); - button.setSelectedImage(imageButton); - - button.setToolTipText(Messages.getString("BoardEditor." + iconName + "TT")); - if (bList != null) { - bList.add(button); - } - button.addActionListener(this); - return button; - } - - /** - * Sets up the editor panel, which goes on the right of the map and has - * controls for editing the current square. - */ - private void setupEditorPanel() { - // Help Texts - labHelp1.addMouseListener(clickToHide); - labHelp2.addMouseListener(clickToHide); - labHelp1.setAlignmentX(Component.CENTER_ALIGNMENT); - labHelp2.setAlignmentX(Component.CENTER_ALIGNMENT); - - // Buttons to ease setting common terrain types - buttonLW = prepareButton("ButtonLW", "Woods", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonLJ = prepareButton("ButtonLJ", "Jungle", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonOW = prepareButton("ButtonLLW", "Low Woods", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonOJ = prepareButton("ButtonLLJ", "Low Jungle", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonWa = prepareButton("ButtonWa", "Water", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonSw = prepareButton("ButtonSw", "Swamp", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonRo = prepareButton("ButtonRo", "Rough", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonMd = prepareButton("ButtonMd", "Mud", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonPv = prepareButton("ButtonPv", "Pavement", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonSn = prepareButton("ButtonSn", "Snow", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonBu = prepareButton("ButtonBu", "Buildings", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonRd = prepareButton("ButtonRd", "Roads", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonBr = prepareButton("ButtonBr", "Bridges", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonFT = prepareButton("ButtonFT", "Fuel Tanks", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonIc = prepareButton("ButtonIc", "Ice", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonTu = prepareButton("ButtonTu", "Tundra", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonMg = prepareButton("ButtonMg", "Magma", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - buttonCl = prepareButton("ButtonCl", "Clear", terrainButtons, BASE_TERRAINBUTTON_ICON_WIDTH); - - buttonBrush1 = prepareToggleButton("ButtonHex1", "Brush1", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); - buttonBrush2 = prepareToggleButton("ButtonHex7", "Brush2", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); - buttonBrush3 = prepareToggleButton("ButtonHex19", "Brush3", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); - ButtonGroup brushGroup = new ButtonGroup(); - brushGroup.add(buttonBrush1); - brushGroup.add(buttonBrush2); - brushGroup.add(buttonBrush3); - buttonOOC = prepareToggleButton("ButtonOOC", "OOC", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); - buttonUpDn = prepareToggleButton("ButtonUpDn", "UpDown", brushButtons, BASE_ARROWBUTTON_ICON_WIDTH); - - buttonUndo = prepareButton("ButtonUndo", "Undo", undoButtons, BASE_ARROWBUTTON_ICON_WIDTH); - buttonRedo = prepareButton("ButtonRedo", "Redo", undoButtons, BASE_ARROWBUTTON_ICON_WIDTH); - buttonUndo.setEnabled(false); - buttonRedo.setEnabled(false); - buttonUndo.setActionCommand(ClientGUI.BOARD_UNDO); - buttonRedo.setActionCommand(ClientGUI.BOARD_REDO); - - MouseWheelListener wheelListener = e -> { - int terrain; - if (e.getSource() == buttonRo) { - terrain = Terrains.ROUGH; - } else if (e.getSource() == buttonSw) { - terrain = Terrains.SWAMP; - } else if (e.getSource() == buttonWa) { - terrain = Terrains.WATER; - } else if (e.getSource() == buttonLW) { - terrain = Terrains.WOODS; - } else if (e.getSource() == buttonLJ) { - terrain = Terrains.JUNGLE; - } else if (e.getSource() == buttonOW) { - terrain = Terrains.WOODS; - } else if (e.getSource() == buttonOJ) { - terrain = Terrains.JUNGLE; - } else if (e.getSource() == buttonMd) { - terrain = Terrains.MUD; - } else if (e.getSource() == buttonPv) { - terrain = Terrains.PAVEMENT; - } else if (e.getSource() == buttonIc) { - terrain = Terrains.ICE; - } else if (e.getSource() == buttonSn) { - terrain = Terrains.SNOW; - } else if (e.getSource() == buttonTu) { - terrain = Terrains.TUNDRA; - } else if (e.getSource() == buttonMg) { - terrain = Terrains.MAGMA; - } else { - return; - } - - Hex saveHex = curHex.duplicate(); - // change the terrain level by wheel direction if present, - // or set to 1 if not present - int newLevel = 1; - if (curHex.containsTerrain(terrain)) { - newLevel = curHex.terrainLevel(terrain) + (e.getWheelRotation() < 0 ? 1 : -1); - } else if (!e.isShiftDown()) { - curHex.removeAllTerrains(); - } - addSetTerrainEasy(terrain, newLevel); - // Add or adapt elevation helper terrain for foliage - // When the elevation was 1, it stays 1 (L1 Foliage, TO p.36) - // Otherwise, it is set to 3 for Ultra W/J and 2 otherwise (TW foliage) - if ((terrain == Terrains.WOODS) || (terrain == Terrains.JUNGLE)) { - int elev = curHex.terrainLevel(Terrains.FOLIAGE_ELEV); - if ((elev != 1) && (newLevel == 3)) { - elev = 3; - } else if (elev != 1) { - elev = 2; - } - curHex.addTerrain(new Terrain(Terrains.FOLIAGE_ELEV, elev)); - } - // Reset the terrain to the former state - // if the new would be invalid. - if (!curHex.isValid(null)) { - curHex = saveHex; - } - - refreshTerrainList(); - repaintWorkingHex(); - }; - - buttonSw.addMouseWheelListener(wheelListener); - buttonWa.addMouseWheelListener(wheelListener); - buttonRo.addMouseWheelListener(wheelListener); - buttonLJ.addMouseWheelListener(wheelListener); - buttonLW.addMouseWheelListener(wheelListener); - buttonOJ.addMouseWheelListener(wheelListener); - buttonOW.addMouseWheelListener(wheelListener); - buttonMd.addMouseWheelListener(wheelListener); - buttonPv.addMouseWheelListener(wheelListener); - buttonSn.addMouseWheelListener(wheelListener); - buttonIc.addMouseWheelListener(wheelListener); - buttonTu.addMouseWheelListener(wheelListener); - buttonMg.addMouseWheelListener(wheelListener); - - // Mouse wheel behaviour for the BUILDINGS button - // Always ADDS the building. - buttonBu.addMouseWheelListener(e -> { - // If we don't have at least one of the building values, overwrite the current - // hex - if (!curHex.containsTerrain(Terrains.BLDG_CF) - && !curHex.containsTerrain(Terrains.BLDG_ELEV) - && !curHex.containsTerrain(Terrains.BUILDING)) { - curHex.removeAllTerrains(); - } - // Restore mandatory building parts if some are missing - setBasicBuilding(false); - int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1; - - if (e.isShiftDown()) { - int oldLevel = curHex.getTerrain(Terrains.BLDG_CF).getLevel(); - int newLevel = Math.max(10, oldLevel + (wheelDir * 5)); - curHex.addTerrain(new Terrain(Terrains.BLDG_CF, newLevel)); - } else if (e.isControlDown()) { - int oldLevel = curHex.getTerrain(Terrains.BUILDING).getLevel(); - int newLevel = Math.max(1, Math.min(4, oldLevel + wheelDir)); // keep between 1 and 4 - - if (newLevel != oldLevel) { - Terrain curTerr = curHex.getTerrain(Terrains.BUILDING); - curHex.addTerrain(new Terrain(Terrains.BUILDING, - newLevel, curTerr.hasExitsSpecified(), curTerr.getExits())); - - // Set the CF to the appropriate standard value *IF* it is the appropriate value - // now, - // i.e. if the user has not manually set it to something else - int curCF = curHex.getTerrain(Terrains.BLDG_CF).getLevel(); - if (curCF == Building.getDefaultCF(oldLevel)) { - curHex.addTerrain(new Terrain(Terrains.BLDG_CF, Building.getDefaultCF(newLevel))); - } - } - } else { - int oldLevel = curHex.getTerrain(Terrains.BLDG_ELEV).getLevel(); - int newLevel = Math.max(1, oldLevel + wheelDir); - curHex.addTerrain(new Terrain(Terrains.BLDG_ELEV, newLevel)); - } - - refreshTerrainList(); - repaintWorkingHex(); - }); - - // Mouse wheel behaviour for the BRIDGE button - buttonBr.addMouseWheelListener(e -> { - // If we don't have at least one of the bridge values, overwrite the current hex - if (!curHex.containsTerrain(Terrains.BRIDGE_CF) - && !curHex.containsTerrain(Terrains.BRIDGE_ELEV) - && !curHex.containsTerrain(Terrains.BRIDGE)) { - curHex.removeAllTerrains(); - } - setBasicBridge(); - int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1; - int terrainType; - int newLevel; - - if (e.isShiftDown()) { - terrainType = Terrains.BRIDGE_CF; - int oldLevel = curHex.getTerrain(terrainType).getLevel(); - newLevel = Math.max(10, oldLevel + wheelDir * 10); - curHex.addTerrain(new Terrain(terrainType, newLevel)); - } else if (e.isControlDown()) { - Terrain terrain = curHex.getTerrain(Terrains.BRIDGE); - boolean hasExits = terrain.hasExitsSpecified(); - int exits = terrain.getExits(); - newLevel = Math.max(1, terrain.getLevel() + wheelDir); - curHex.addTerrain(new Terrain(Terrains.BRIDGE, newLevel, hasExits, exits)); - } else { - terrainType = Terrains.BRIDGE_ELEV; - int oldLevel = curHex.getTerrain(terrainType).getLevel(); - newLevel = Math.max(0, oldLevel + wheelDir); - curHex.addTerrain(new Terrain(terrainType, newLevel)); - } - - refreshTerrainList(); - repaintWorkingHex(); - }); - - // Mouse wheel behaviour for the FUELTANKS button - buttonFT.addMouseWheelListener(e -> { - // If we don't have at least one of the fuel tank values, overwrite the current - // hex - if (!curHex.containsTerrain(Terrains.FUEL_TANK) - && !curHex.containsTerrain(Terrains.FUEL_TANK_CF) - && !curHex.containsTerrain(Terrains.FUEL_TANK_ELEV) - && !curHex.containsTerrain(Terrains.FUEL_TANK_MAGN)) { - curHex.removeAllTerrains(); - } - setBasicFuelTank(); - int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1; - int terrainType; - int newLevel; - - if (e.isShiftDown()) { - terrainType = Terrains.FUEL_TANK_CF; - int oldLevel = curHex.getTerrain(terrainType).getLevel(); - newLevel = Math.max(10, oldLevel + wheelDir * 10); - } else if (e.isControlDown()) { - terrainType = Terrains.FUEL_TANK_MAGN; - int oldLevel = curHex.getTerrain(terrainType).getLevel(); - newLevel = Math.max(10, oldLevel + wheelDir * 10); - } else { - terrainType = Terrains.FUEL_TANK_ELEV; - int oldLevel = curHex.getTerrain(terrainType).getLevel(); - newLevel = Math.max(1, oldLevel + wheelDir); - } - - curHex.addTerrain(new Terrain(terrainType, newLevel)); - refreshTerrainList(); - repaintWorkingHex(); - }); - - FixedYPanel terrainButtonPanel = new FixedYPanel(new GridLayout(0, 4, 2, 2)); - addManyButtons(terrainButtonPanel, terrainButtons); - - FixedYPanel brushButtonPanel = new FixedYPanel(new GridLayout(0, 3, 2, 2)); - addManyButtons(brushButtonPanel, brushButtons); - buttonBrush1.setSelected(true); - - FixedYPanel undoButtonPanel = new FixedYPanel(new GridLayout(1, 2, 2, 2)); - addManyButtons(undoButtonPanel, List.of(buttonUndo, buttonRedo)); - - // Hex Elevation Control - texElev = new EditorTextField("0", 3); - texElev.addActionListener(this); - texElev.getDocument().addDocumentListener(this); - - butElevUp = prepareButton("ButtonHexUP", "Raise Hex Elevation", null, BASE_ARROWBUTTON_ICON_WIDTH); - butElevUp.setName("butElevUp"); - butElevUp.setToolTipText(Messages.getString("BoardEditor.butElevUp.toolTipText")); - - butElevDown = prepareButton("ButtonHexDN", "Lower Hex Elevation", null, BASE_ARROWBUTTON_ICON_WIDTH); - butElevDown.setName("butElevDown"); - butElevDown.setToolTipText(Messages.getString("BoardEditor.butElevDown.toolTipText")); - - // Terrain List - lisTerrainRenderer = new ComboboxToolTipRenderer(); - lisTerrain = new JList<>(new DefaultListModel<>()); - lisTerrain.addListSelectionListener(this); - lisTerrain.setCellRenderer(lisTerrainRenderer); - lisTerrain.setVisibleRowCount(6); - lisTerrain.setPrototypeCellValue(new TerrainTypeHelper(new Terrain(WATER, 2))); - lisTerrain.setFixedCellWidth(180); - refreshTerrainList(); - - // Terrain List, Preview, Delete - FixedYPanel panlisHex = new FixedYPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); - butDelTerrain = prepareButton("buttonRemT", "Delete Terrain", null, BASE_ARROWBUTTON_ICON_WIDTH); - butDelTerrain.setEnabled(false); - canHex = new HexCanvas(); - panlisHex.add(butDelTerrain); - panlisHex.add(new JScrollPane(lisTerrain)); - panlisHex.add(canHex); - - // Build the terrain list for the chooser ComboBox, - // excluding terrains that are handled internally - ArrayList tList = new ArrayList<>(); - for (int i = 1; i < Terrains.SIZE; i++) { - if (!Terrains.AUTOMATIC.contains(i)) { - tList.add(new TerrainHelper(i)); - } - } - TerrainHelper[] terrains = new TerrainHelper[tList.size()]; - tList.toArray(terrains); - Arrays.sort(terrains); - texTerrainLevel = new EditorTextField("0", 2, 0); - texTerrainLevel.addActionListener(this); - texTerrainLevel.getDocument().addDocumentListener(this); - choTerrainType = new JComboBox<>(terrains); - ComboboxToolTipRenderer renderer = new ComboboxToolTipRenderer(); - renderer.setTerrains(terrains); - choTerrainType.setRenderer(renderer); - // Selecting a terrain type in the Dropdown should deselect - // all in the terrain overview list except when selected from there - choTerrainType.addActionListener(e -> { - if (!terrListBlocker) { - lisTerrain.clearSelection(); - - // if we've selected DEPLOYMENT ZONE, disable the "exits" buttons - // and make the "exits" popup point to a multi-select list that lets - // the user choose which deployment zones will be flagged here - - // otherwise, re-enable all the buttons and reset the "exits" popup to its - // normal behavior - if (((TerrainHelper) choTerrainType.getSelectedItem()).getTerrainType() == Terrains.DEPLOYMENT_ZONE) { - butExitUp.setEnabled(false); - butExitDown.setEnabled(false); - texTerrExits.setEnabled(false); - cheTerrExitSpecified.setEnabled(false); - cheTerrExitSpecified.setText("Zones");// Messages.getString("BoardEditor.deploymentZoneIDs")); - butTerrExits.setActionCommand(CMD_EDIT_DEPLOYMENT_ZONES); - } else { - butExitUp.setEnabled(true); - butExitDown.setEnabled(true); - texTerrExits.setEnabled(true); - cheTerrExitSpecified.setEnabled(true); - cheTerrExitSpecified.setText(Messages.getString("BoardEditor.cheTerrExitSpecified")); - butTerrExits.setActionCommand(""); - } - } - }); - butAddTerrain = new JButton(Messages.getString("BoardEditor.butAddTerrain")); - butTerrUp = prepareButton("ButtonTLUP", "Increase Terrain Level", null, BASE_ARROWBUTTON_ICON_WIDTH); - butTerrDown = prepareButton("ButtonTLDN", "Decrease Terrain Level", null, BASE_ARROWBUTTON_ICON_WIDTH); - - // Exits - cheTerrExitSpecified = new JCheckBox(Messages.getString("BoardEditor.cheTerrExitSpecified")); - cheTerrExitSpecified.addActionListener(this); - butTerrExits = prepareButton("ButtonExitA", Messages.getString("BoardEditor.butTerrExits"), null, - BASE_ARROWBUTTON_ICON_WIDTH); - texTerrExits = new EditorTextField("0", 2, 0); - texTerrExits.addActionListener(this); - texTerrExits.getDocument().addDocumentListener(this); - butExitUp = prepareButton("ButtonEXUP", "Increase Exit / Gfx", null, BASE_ARROWBUTTON_ICON_WIDTH); - butExitDown = prepareButton("ButtonEXDN", "Decrease Exit / Gfx", null, BASE_ARROWBUTTON_ICON_WIDTH); - - // Copy and Paste - FixedYPanel panCopyPaste = new FixedYPanel(new FlowLayout(FlowLayout.RIGHT, 4, 4)); - panCopyPaste.add(pasteButton); - panCopyPaste.add(copyButton); - copyButton.addActionListener(e -> copyWorkingHexToClipboard()); - pasteButton.addActionListener(e -> pasteFromClipboard()); - - // Arrows and text fields for type and exits - JPanel panUP = new JPanel(new GridLayout(1, 0, 4, 4)); - panUP.add(butTerrUp); - panUP.add(butExitUp); - panUP.add(butTerrExits); - JPanel panTex = new JPanel(new GridLayout(1, 0, 4, 4)); - panTex.add(texTerrainLevel); - panTex.add(texTerrExits); - panTex.add(cheTerrExitSpecified); - JPanel panDN = new JPanel(new GridLayout(1, 0, 4, 4)); - panDN.add(butTerrDown); - panDN.add(butExitDown); - panDN.add(Box.createHorizontalStrut(5)); - - // Auto Exits to Pavement - cheRoadsAutoExit = new JCheckBox(Messages.getString("BoardEditor.cheRoadsAutoExit")); - cheRoadsAutoExit.addItemListener(this); - cheRoadsAutoExit.setSelected(true); - - // Theme - JPanel panTheme = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); - choTheme = new JComboBox<>(); - TilesetManager tileMan = bv.getTilesetManager(); - Set themes = tileMan.getThemes(); - for (String s : themes) { - choTheme.addItem(s); - } - choTheme.addActionListener(this); - panTheme.add(labTheme); - panTheme.add(choTheme); - - // The hex settings panel (elevation, theme) - panelHexSettings.setBorder(new TitledBorder("Hex Settings")); - panelHexSettings.add(butElevUp); - panelHexSettings.add(texElev); - panelHexSettings.add(butElevDown); - panelHexSettings.add(panTheme); - - // The terrain settings panel (type, level, exits) - panelTerrSettings.setBorder(new TitledBorder("Terrain Settings")); - panelTerrSettings.add(Box.createVerticalStrut(5)); - panelTerrSettings.add(panUP); - - panelTerrSettings.add(choTerrainType); - panelTerrSettings.add(panTex); - - panelTerrSettings.add(butAddTerrain); - panelTerrSettings.add(panDN); - - // The board settings panel (Auto exit roads to pavement) - panelBoardSettings.setBorder(new TitledBorder("Board Settings")); - panelBoardSettings.add(cheRoadsAutoExit); - - // Board Buttons (Save, Load...) - butBoardNew = new JButton(Messages.getString("BoardEditor.butBoardNew")); - butBoardNew.setActionCommand(ClientGUI.BOARD_NEW); - - butExpandMap = new JButton(Messages.getString("BoardEditor.butExpandMap")); - butExpandMap.setActionCommand(ClientGUI.BOARD_RESIZE); - - butBoardOpen = new JButton(Messages.getString("BoardEditor.butBoardOpen")); - butBoardOpen.setActionCommand(ClientGUI.BOARD_OPEN); - - butBoardSave = new JButton(Messages.getString("BoardEditor.butBoardSave")); - butBoardSave.setActionCommand(ClientGUI.BOARD_SAVE); - - butBoardSaveAs = new JButton(Messages.getString("BoardEditor.butBoardSaveAs")); - butBoardSaveAs.setActionCommand(ClientGUI.BOARD_SAVE_AS); - - butBoardSaveAsImage = new JButton(Messages.getString("BoardEditor.butBoardSaveAsImage")); - butBoardSaveAsImage.setActionCommand(ClientGUI.BOARD_SAVE_AS_IMAGE); - - butBoardValidate = new JButton(Messages.getString("BoardEditor.butBoardValidate")); - butBoardValidate.setActionCommand(ClientGUI.BOARD_VALIDATE); - - butSourceFile = new JButton(Messages.getString("BoardEditor.butSourceFile")); - butSourceFile.setActionCommand(ClientGUI.BOARD_SOURCEFILE); - - addManyActionListeners(butBoardValidate, butBoardSaveAsImage, butBoardSaveAs, butBoardSave); - addManyActionListeners(butBoardOpen, butExpandMap, butBoardNew); - addManyActionListeners(butDelTerrain, butAddTerrain, butSourceFile); - - JPanel panButtons = new JPanel(new GridLayout(3, 3, 2, 2)); - addManyButtons(panButtons, List.of(butBoardNew, butBoardSave, butBoardOpen, - butExpandMap, butBoardSaveAs, butBoardSaveAsImage, butBoardValidate)); - if (Desktop.isDesktopSupported()) { - panButtons.add(butSourceFile); - } - - // Arrange everything - setLayout(new BorderLayout()); - Box centerPanel = Box.createVerticalBox(); - centerPanel.add(labHelp1); - centerPanel.add(labHelp2); - centerPanel.add(Box.createVerticalStrut(10)); - centerPanel.add(terrainButtonPanel); - centerPanel.add(Box.createVerticalStrut(10)); - centerPanel.add(brushButtonPanel); - centerPanel.add(Box.createVerticalStrut(10)); - centerPanel.add(undoButtonPanel); - centerPanel.add(Box.createVerticalGlue()); - centerPanel.add(panelBoardSettings); - centerPanel.add(panelHexSettings); - centerPanel.add(panelTerrSettings); - centerPanel.add(panlisHex); - centerPanel.add(panCopyPaste); - var scrCenterPanel = new JScrollPane(centerPanel); - scrCenterPanel.getVerticalScrollBar().setUnitIncrement(16); - add(scrCenterPanel, BorderLayout.CENTER); - add(panButtons, BorderLayout.PAGE_END); - minimapW = Minimap.createMinimap(frame, bv, game, null); - minimapW.setVisible(guip.getMinimapEnabled()); - } - - /** - * Returns coords that the active brush will paint on; - * returns only coords that are valid, i.e. on the board - */ - private LinkedList getBrushCoords(Coords center) { - var result = new LinkedList(); - // The center hex itself is always part of the brush - result.add(center); - // Add surrounding hexes for the big brush - if (brushSize >= 2) { - result.addAll(center.allAdjacent()); - } - if (brushSize == 3) { - result.addAll(center.allAtDistance(2)); - } - // Remove coords that are not on the board - result.removeIf(c -> !board.contains(c)); - return result; - } - - // Helper to shorten the code - private void addManyActionListeners(JButton... buttons) { - for (JButton button : buttons) { - button.addActionListener(this); - } - } - - // Helper to shorten the code - private void addManyButtons(JPanel panel, List terrainButtons) { - terrainButtons.forEach(panel::add); - } - - /** - * Save the hex at c into the current undo Set - */ - private void saveToUndo(Coords c) { - // Create a new set of hexes to save for undoing - // This will be filled as long as the mouse is dragged - if (currentUndoSet == null) { - currentUndoSet = new HashSet<>(); - currentUndoCoords = new HashSet<>(); - } - if (!currentUndoCoords.contains(c)) { - Hex hex = board.getHex(c).duplicate(); - // Newly drawn board hexes do not know their Coords - hex.setCoords(c); - currentUndoSet.add(hex); - currentUndoCoords.add(c); - } - } - - private void resetUndo() { - currentUndoSet = null; - currentUndoCoords = null; - undoStack.clear(); - redoStack.clear(); - buttonUndo.setEnabled(false); - buttonRedo.setEnabled(false); - } - - /** - * Changes the hex level at Coords c. Expects c - * to be on the board. - */ - private void relevelHex(Coords c) { - Hex newHex = board.getHex(c).duplicate(); - newHex.setLevel(hexLeveltoDraw); - board.resetStoredElevation(); - board.setHex(c, newHex); - - } - - /** - * Apply the current Hex to the Board at the specified location. - */ - void paintHex(Coords c) { - board.resetStoredElevation(); - board.setHex(c, curHex.duplicate()); - } - - /** - * Apply the current Hex to the Board at the specified location. - */ - public void retextureHex(Coords c) { - if (board.contains(c)) { - Hex newHex = curHex.duplicate(); - newHex.setLevel(board.getHex(c).getLevel()); - board.resetStoredElevation(); - board.setHex(c, newHex); - } - } - - /** - * Apply the current Hex to the Board at the specified location. - */ - public void addToHex(Coords c) { - if (board.contains(c)) { - Hex newHex = curHex.duplicate(); - Hex oldHex = board.getHex(c); - newHex.setLevel(oldHex.getLevel()); - int[] terrainTypes = oldHex.getTerrainTypes(); - for (int terrainID : terrainTypes) { - if (!newHex.containsTerrain(terrainID) && oldHex.containsTerrain(terrainID)) { - newHex.addTerrain(oldHex.getTerrain(terrainID)); - } - } - newHex.setTheme(oldHex.getTheme()); - board.resetStoredElevation(); - board.setHex(c, newHex); - } - } - - /** - * Sets the working hex to hex; - * used for mouse ALT-click (eyedropper function). - * - * @param hex hex to set. - */ - void setCurrentHex(Hex hex) { - curHex = hex.duplicate(); - texElev.setText(Integer.toString(curHex.getLevel())); - refreshTerrainList(); - if (lisTerrain.getModel().getSize() > 0) { - lisTerrain.setSelectedIndex(0); - refreshTerrainFromList(); - } - choTheme.setSelectedItem(curHex.getTheme()); - repaint(); - repaintWorkingHex(); - } - - private void repaintWorkingHex() { - if (curHex != null) { - TilesetManager tm = bv.getTilesetManager(); - tm.clearHex(curHex); - } - canHex.repaint(); - lastClicked = null; - } - - /** - * Refreshes the terrain list to match the current hex - */ - private void refreshTerrainList() { - TerrainTypeHelper selectedEntry = lisTerrain.getSelectedValue(); - ((DefaultListModel) lisTerrain.getModel()).removeAllElements(); - lisTerrainRenderer.setTerrainTypes(null); - int[] terrainTypes = curHex.getTerrainTypes(); - List types = new ArrayList<>(); - for (final int terrainType : terrainTypes) { - final Terrain terrain = curHex.getTerrain(terrainType); - if ((terrain != null) && !Terrains.AUTOMATIC.contains(terrainType)) { - final TerrainTypeHelper tth = new TerrainTypeHelper(terrain); - types.add(tth); - } - } - Collections.sort(types); - for (final TerrainTypeHelper tth : types) { - ((DefaultListModel) lisTerrain.getModel()).addElement(tth); - } - lisTerrainRenderer.setTerrainTypes(types); - // Reselect the formerly selected terrain if possible - if (selectedEntry != null) { - selectTerrain(selectedEntry.getTerrain()); - } - } - - /** - * Returns a new instance of the terrain that is currently entered in the - * terrain input fields - */ - private Terrain enteredTerrain() { - int type = ((TerrainHelper) Objects.requireNonNull(choTerrainType.getSelectedItem())).getTerrainType(); - int level = texTerrainLevel.getNumber(); - // For the terrain subtypes that only add to a main terrain type exits make no - // sense at all. Therefore simply do not add them - if ((type == Terrains.BLDG_ARMOR) || (type == Terrains.BLDG_CF) - || (type == Terrains.BLDG_ELEV) || (type == Terrains.BLDG_CLASS) - || (type == Terrains.BLDG_BASE_COLLAPSED) || (type == Terrains.BLDG_BASEMENT_TYPE) - || (type == Terrains.BRIDGE_CF) || (type == Terrains.BRIDGE_ELEV) - || (type == Terrains.FUEL_TANK_CF) || (type == Terrains.FUEL_TANK_ELEV) - || (type == Terrains.FUEL_TANK_MAGN)) { - return new Terrain(type, level, false, 0); - } else { - boolean exitsSpecified = cheTerrExitSpecified.isSelected(); - int exits = texTerrExits.getNumber(); - return new Terrain(type, level, exitsSpecified, exits); - } - } - - /** - * Add or set the terrain to the list based on the fields. - */ - private void addSetTerrain() { - Terrain toAdd = enteredTerrain(); - if (((toAdd.getType() == Terrains.BLDG_ELEV) || (toAdd.getType() == Terrains.BRIDGE_ELEV)) - && (toAdd.getLevel() < 0)) { - texTerrainLevel.setNumber(0); - JOptionPane.showMessageDialog(frame, - Messages.getString("BoardEditor.BridgeBuildingElevError"), - Messages.getString("BoardEditor.invalidTerrainTitle"), - JOptionPane.ERROR_MESSAGE); - return; - } - - curHex.addTerrain(toAdd); - - noTextFieldUpdate = true; - refreshTerrainList(); - repaintWorkingHex(); - noTextFieldUpdate = false; - } - - /** - * Cycle the terrain level (mouse wheel behavior) from the easy access buttons - */ - private void addSetTerrainEasy(int type, int level) { - boolean exitsSpecified = false; - int exits = 0; - Terrain present = curHex.getTerrain(type); - if (present != null) { - exitsSpecified = present.hasExitsSpecified(); - exits = present.getExits(); - } - Terrain toAdd = new Terrain(type, level, exitsSpecified, exits); - curHex.addTerrain(toAdd); - refreshTerrainList(); - repaintWorkingHex(); - } - - /** - * Sets valid basic Fuel Tank values as far as they are missing - */ - private void setBasicFuelTank() { - // There is only fuel_tank:1, so this can be set - curHex.addTerrain(new Terrain(Terrains.FUEL_TANK, 1, true, 0)); - - if (!curHex.containsTerrain(Terrains.FUEL_TANK_CF)) { - curHex.addTerrain(new Terrain(Terrains.FUEL_TANK_CF, 40, false, 0)); - } - - if (!curHex.containsTerrain(Terrains.FUEL_TANK_ELEV)) { - curHex.addTerrain(new Terrain(Terrains.FUEL_TANK_ELEV, 1, false, 0)); - } - - if (!curHex.containsTerrain(Terrains.FUEL_TANK_MAGN)) { - curHex.addTerrain(new Terrain(Terrains.FUEL_TANK_MAGN, 100, false, 0)); - } - - refreshTerrainList(); - selectTerrain(new Terrain(Terrains.FUEL_TANK_ELEV, 1)); - repaintWorkingHex(); - } - - /** - * Sets valid basic bridge values as far as they are missing - */ - private void setBasicBridge() { - if (!curHex.containsTerrain(Terrains.BRIDGE_CF)) { - curHex.addTerrain(new Terrain(Terrains.BRIDGE_CF, 40, false, 0)); - } - - if (!curHex.containsTerrain(Terrains.BRIDGE_ELEV)) { - curHex.addTerrain(new Terrain(Terrains.BRIDGE_ELEV, 1, false, 0)); - } - - if (!curHex.containsTerrain(Terrains.BRIDGE)) { - curHex.addTerrain(new Terrain(Terrains.BRIDGE, 1, false, 0)); - } - - refreshTerrainList(); - selectTerrain(new Terrain(Terrains.BRIDGE_ELEV, 1)); - repaintWorkingHex(); - } - - /** - * Sets valid basic Building values as far as they are missing - */ - private void setBasicBuilding(boolean ALT_Held) { - if (!curHex.containsTerrain(Terrains.BLDG_CF)) { - curHex.addTerrain(new Terrain(Terrains.BLDG_CF, 15, false, 0)); - } - - if (!curHex.containsTerrain(Terrains.BLDG_ELEV)) { - curHex.addTerrain(new Terrain(Terrains.BLDG_ELEV, 1, false, 0)); - } - - if (!curHex.containsTerrain(Terrains.BUILDING)) { - curHex.addTerrain(new Terrain(Terrains.BUILDING, 1, ALT_Held, 0)); - } - - // When clicked with ALT, only toggle the exits - if (ALT_Held) { - Terrain curTerr = curHex.getTerrain(Terrains.BUILDING); - curHex.addTerrain(new Terrain(Terrains.BUILDING, - curTerr.getLevel(), !curTerr.hasExitsSpecified(), curTerr.getExits())); - } - - refreshTerrainList(); - selectTerrain(new Terrain(Terrains.BLDG_ELEV, 1)); - repaintWorkingHex(); - } - - /** - * Set all the appropriate terrain fields to match the currently selected - * terrain in the list. - */ - private void refreshTerrainFromList() { - if (lisTerrain.isSelectionEmpty()) { - butDelTerrain.setEnabled(false); - } else { - butDelTerrain.setEnabled(true); - Terrain terrain = new Terrain(lisTerrain.getSelectedValue().getTerrain()); - terrain = curHex.getTerrain(terrain.getType()); - TerrainHelper terrainHelper = new TerrainHelper(terrain.getType()); - terrListBlocker = true; - choTerrainType.setSelectedItem(terrainHelper); - texTerrainLevel.setText(Integer.toString(terrain.getLevel())); - setExitsState(terrain.hasExitsSpecified()); - texTerrExits.setNumber(terrain.getExits()); - terrListBlocker = false; - } - } - - /** - * Updates the selected terrain in the terrain list if - * a terrain is actually selected - */ - private void updateWhenSelected() { - if (!lisTerrain.isSelectionEmpty()) { - addSetTerrain(); - } - } - - public void boardNew(boolean showDialog) { - boolean userCancel = false; - if (showDialog) { - RandomMapDialog rmd = new RandomMapDialog(frame, this, null, mapSettings); - userCancel = rmd.activateDialog(bv.getTilesetManager().getThemes()); - } - if (!userCancel) { - board = BoardUtilities.generateRandom(mapSettings); - // "Initialize" all hexes to add internally handled terrains - correctExits(); - game.setBoard(board); - curBoardFile = null; - choTheme.setSelectedItem(mapSettings.getTheme()); - setupUiFreshBoard(); - } - } - - public void boardResize() { - ResizeMapDialog emd = new ResizeMapDialog(frame, this, null, mapSettings); - boolean userCancel = emd.activateDialog(bv.getTilesetManager().getThemes()); - if (!userCancel) { - board = BoardUtilities.generateRandom(mapSettings); - - // Implant the old board - int west = emd.getExpandWest(); - int north = emd.getExpandNorth(); - int east = emd.getExpandEast(); - int south = emd.getExpandSouth(); - board = implantOldBoard(game, west, north, east, south); - - game.setBoard(board); - curBoardFile = null; - setupUiFreshBoard(); - } - } - - // When we resize a board, implant the old board's hexes where they should be in - // the new board - public Board implantOldBoard(Game game, int west, int north, int east, int south) { - Board oldBoard = game.getBoard(); - for (int x = 0; x < oldBoard.getWidth(); x++) { - for (int y = 0; y < oldBoard.getHeight(); y++) { - int newX = x + west; - int odd = x & 1 & west; - int newY = y + north + odd; - if (oldBoard.contains(x, y) && board.contains(newX, newY)) { - Hex oldHex = oldBoard.getHex(x, y); - Hex hex = board.getHex(newX, newY); - hex.removeAllTerrains(); - hex.setLevel(oldHex.getLevel()); - int[] terrainTypes = oldHex.getTerrainTypes(); - for (int terrainID : terrainTypes) { - if (!hex.containsTerrain(terrainID) && oldHex.containsTerrain(terrainID)) { - hex.addTerrain(oldHex.getTerrain(terrainID)); - } - } - hex.setTheme(oldHex.getTheme()); - board.setHex(newX, newY, hex); - board.resetStoredElevation(); - } - } - } - return board; - } - - @Override - public void updateMapSettings(MapSettings newSettings) { - mapSettings = newSettings; - } - - public void loadBoard() { - JFileChooser fc = new JFileChooser(loadPath); - setDialogSize(fc); - fc.setDialogTitle(Messages.getString("BoardEditor.loadBoard")); - fc.setFileFilter(new BoardFileFilter()); - int returnVal = fc.showOpenDialog(frame); - saveDialogSize(fc); - if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) { - // I want a file, y'know! - return; - } - loadBoard(fc.getSelectedFile()); - } - - public void loadBoard(File file) { - try (InputStream is = new FileInputStream(file)) { - // tell the board to load! - board.load(is, null, true); - Set boardTags = board.getTags(); - // Board generation in a game always calls BoardUtilities.combine - // This serves no purpose here, but is necessary to create - // flipBGVert/flipBGHoriz lists for the board, which is necessary - // for the background image to work in the BoardEditor - board = BoardUtilities.combine(board.getWidth(), board.getHeight(), 1, 1, - new Board[] { board }, Collections.singletonList(false), MapSettings.MEDIUM_GROUND); - game.setBoard(board); - // BoardUtilities.combine does not preserve tags, so add them back - for (String tag : boardTags) { - board.addTag(tag); - } - cheRoadsAutoExit.setSelected(board.getRoadsAutoExit()); - mapSettings.setBoardSize(board.getWidth(), board.getHeight()); - curBoardFile = file; - RecentBoardList.addBoard(curBoardFile); - loadPath = curBoardFile.getParentFile(); - - // Now, *after* initialization of the board which will correct some errors, - // do a board validation - validateBoard(false); - refreshTerrainList(); - setupUiFreshBoard(); - } catch (IOException ex) { - logger.error(ex, "loadBoard"); - showBoardLoadError(ex); - initializeBoardIfEmpty(); - } - } - - private void showBoardLoadError(Exception ex) { - String message = Messages.getString("BoardEditor.loadBoardError") + System.lineSeparator() + ex.getMessage(); - String title = Messages.getString("Error"); - JOptionPane.showMessageDialog(frame, message, title, JOptionPane.ERROR_MESSAGE); - } - - private void initializeBoardIfEmpty() { - if ((board == null) || (board.getWidth() == 0) || (board.getHeight() == 0)) { - boardNew(false); - } - } - - /** - * Will do board.initializeHex() for all hexes, correcting - * building and road connection issues for those hexes that do not have - * the exits check set. - */ - private void correctExits() { - for (int x = 0; x < board.getWidth(); x++) { - for (int y = 0; y < board.getHeight(); y++) { - board.initializeHex(x, y); - } - } - } - - /** - * Saves the board in PNG image format. - */ - private void boardSaveImage(boolean ignoreUnits) { - if (curfileImage == null) { - boardSaveAsImage(ignoreUnits); - return; - } - JDialog waitD = new JDialog(frame, Messages.getString("BoardEditor.waitDialog.title")); - waitD.add(new JLabel(Messages.getString("BoardEditor.waitDialog.message"))); - waitD.setSize(250, 130); - // move to middle of screen - waitD.setLocation( - (frame.getSize().width / 2) - (waitD.getSize().width / 2), - (frame.getSize().height / 2) - (waitD.getSize().height / 2)); - waitD.setVisible(true); - frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - waitD.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - // save! - try { - ImageIO.write(bv.getEntireBoardImage(ignoreUnits, false), "png", curfileImage); - } catch (IOException e) { - logger.error(e, "boardSaveImage"); - } - waitD.setVisible(false); - frame.setCursor(Cursor.getDefaultCursor()); - } - - /** - * Saves the board to a .board file. - * When saveAs is true, acts as a Save As... by opening a file chooser dialog. - * When saveAs is false, it will directly save to the current board file name, - * if it is known and otherwise act as Save As... - */ - private boolean boardSave(boolean saveAs) { - // Correct connection issues and do a validation. - correctExits(); - validateBoard(false); - - // Choose a board file to save to if this was - // called as "Save As..." or there is no current filename - if ((curBoardFile == null) || saveAs) { - if (!chooseSaveBoardFile()) { - return false; - } - } - - // write the board - try (OutputStream os = new FileOutputStream(curBoardFile)) { - board.save(os); - - // Adapt to successful save - butSourceFile.setEnabled(true); - savedUndoStackSize = undoStack.size(); - hasChanges = false; - RecentBoardList.addBoard(curBoardFile); - setFrameTitle(); - return true; - } catch (IOException e) { - logger.error(e, "boardSave"); - return false; - } - } - - /** - * Shows a dialog for choosing a .board file to save to. - * Sets curBoardFile and returns true when a valid file was chosen. - * Returns false otherwise. - */ - private boolean chooseSaveBoardFile() { - JFileChooser fc = new JFileChooser(loadPath); - setDialogSize(fc); - fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100); - fc.setDialogTitle(Messages.getString("BoardEditor.saveBoardAs")); - fc.setFileFilter(new BoardFileFilter()); - int returnVal = fc.showSaveDialog(frame); - saveDialogSize(fc); - if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) { - return false; - } - File choice = fc.getSelectedFile(); - // make sure the file ends in board - if (!choice.getName().toLowerCase().endsWith(".board")) { - try { - choice = new File(choice.getCanonicalPath() + ".board"); - } catch (IOException ignored) { - return false; - } - } - curBoardFile = choice; - return true; - } - - /** - * Opens a file dialog box to select a file to save as; saves the board to - * the file as an image. Useful for printing boards. - */ - private void boardSaveAsImage(boolean ignoreUnits) { - JFileChooser fc = new JFileChooser("."); - setDialogSize(fc); - fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100); - fc.setDialogTitle(Messages.getString("BoardEditor.saveAsImage")); - fc.setFileFilter(new FileFilter() { - @Override - public boolean accept(File dir) { - return (dir.getName().endsWith(".png") || dir.isDirectory()); - } - - @Override - public String getDescription() { - return ".png"; - } - }); - int returnVal = fc.showSaveDialog(frame); - saveDialogSize(fc); - if ((returnVal != JFileChooser.APPROVE_OPTION) - || (fc.getSelectedFile() == null)) { - // I want a file, y'know! - return; - } - curfileImage = fc.getSelectedFile(); - - // make sure the file ends in png - if (!curfileImage.getName().toLowerCase().endsWith(".png")) { - try { - curfileImage = new File(curfileImage.getCanonicalPath() + ".png"); - } catch (IOException ignored) { - // failure! - return; - } - } - boardSaveImage(ignoreUnits); - } - - // - // ItemListener - // - @Override - public void itemStateChanged(ItemEvent ie) { - if (ie.getSource().equals(cheRoadsAutoExit)) { - // Set the new value for the option, and refresh the board. - board.setRoadsAutoExit(cheRoadsAutoExit.isSelected()); - bv.updateBoard(); - repaintWorkingHex(); - } - } - - // - // TextListener - // - @Override - public void changedUpdate(DocumentEvent te) { - if (te.getDocument().equals(texElev.getDocument())) { - int value; - try { - value = Integer.parseInt(texElev.getText()); - } catch (NumberFormatException ex) { - return; - } - if (value != curHex.getLevel()) { - curHex.setLevel(value); - repaintWorkingHex(); - } - } else if (te.getDocument().equals(texTerrainLevel.getDocument())) { - // prevent updating the terrain from looping back to - // update the text fields that have just been edited - if (!terrListBlocker) { - noTextFieldUpdate = true; - updateWhenSelected(); - noTextFieldUpdate = false; - } - } else if (te.getDocument().equals(texTerrExits.getDocument())) { - // prevent updating the terrain from looping back to - // update the text fields that have just been edited - if (!terrListBlocker) { - noTextFieldUpdate = true; - setExitsState(true); - updateWhenSelected(); - noTextFieldUpdate = false; - } - } - } - - @Override - public void insertUpdate(DocumentEvent event) { - changedUpdate(event); - } - - @Override - public void removeUpdate(DocumentEvent event) { - changedUpdate(event); - } - - /** Called when the user selects the "Help->About" menu item. */ - private void showAbout() { - new CommonAboutDialog(frame).setVisible(true); - } - - /** Called when the user selects the "Help->Contents" menu item. */ - private void showHelp() { - if (help == null) { - help = new BoardEditorHelpDialog(frame); - } - help.setVisible(true); // Show the help dialog. - } - - /** Called when the user selects the "View->Client Settings" menu item. */ - private void showSettings() { - if (setdlg == null) { - setdlg = new CommonSettingsDialog(frame); - } - setdlg.setVisible(true); - } - - /** - * Adjusts some UI and internal settings for a freshly - * loaded or freshly generated board. - */ - private void setupUiFreshBoard() { - // Reset the Undo stack and the board has no changes - savedUndoStackSize = 0; - canReturnToSaved = true; - resetUndo(); - hasChanges = false; - // When a board was loaded, we have a file, otherwise not - butSourceFile.setEnabled(curBoardFile != null); - // Adjust the UI - bvc.doLayout(); - setFrameTitle(); - } - - /** - * Performs board validation. When showPositiveResult is true, - * the result of the validation will be shown in a dialog. - * Otherwise, only a negative result (the board has errors) will - * be shown. - */ - private void validateBoard(boolean showPositiveResult) { - List errors = new ArrayList<>(); - board.isValid(errors); - if ((!errors.isEmpty()) || showPositiveResult) { - showBoardValidationReport(errors); - } - } - - /** - * Shows a board validation report dialog, reporting either - * the contents of errBuff or that the board has no errors. - */ - private void showBoardValidationReport(List errors) { - ignoreHotKeys = true; - if ((errors != null) && !errors.isEmpty()) { - String title = Messages.getString("BoardEditor.invalidBoard.title"); - String msg = Messages.getString("BoardEditor.invalidBoard.report"); - msg += String.join("\n", errors); - JTextArea textArea = new JTextArea(msg); - JScrollPane scrollPane = new JScrollPane(textArea); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - scrollPane.setPreferredSize(new Dimension(getWidth(), getHeight() / 2)); - JOptionPane.showMessageDialog(frame, scrollPane, title, JOptionPane.ERROR_MESSAGE); - } else { - String title = Messages.getString("BoardEditor.validBoard.title"); - String msg = Messages.getString("BoardEditor.validBoard.report"); - JOptionPane.showMessageDialog(frame, msg, title, JOptionPane.INFORMATION_MESSAGE); - } - ignoreHotKeys = false; - } - - // - // ActionListener - // - @Override - public void actionPerformed(ActionEvent ae) { - if (ae.getActionCommand().startsWith(ClientGUI.BOARD_RECENT)) { - if (hasChanges && (showSavePrompt() == DialogResult.CANCELLED)) { - return; - } - String recentBoard = ae.getActionCommand().substring(ClientGUI.BOARD_RECENT.length() + 1); - loadBoard(new File(recentBoard)); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_NEW)) { - ignoreHotKeys = true; - boardNew(true); - ignoreHotKeys = false; - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_RESIZE)) { - ignoreHotKeys = true; - boardResize(); - ignoreHotKeys = false; - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_OPEN)) { - ignoreHotKeys = true; - loadBoard(); - ignoreHotKeys = false; - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE)) { - ignoreHotKeys = true; - boardSave(false); - ignoreHotKeys = false; - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE_AS)) { - ignoreHotKeys = true; - boardSave(true); - ignoreHotKeys = false; - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE_AS_IMAGE)) { - ignoreHotKeys = true; - boardSaveAsImage(false); - ignoreHotKeys = false; - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SOURCEFILE)) { - if (curBoardFile != null) { - try { - Desktop.getDesktop().open(curBoardFile); - } catch (IOException e) { - ignoreHotKeys = true; - JOptionPane.showMessageDialog(frame, - Messages.getString("BoardEditor.OpenFileError", curBoardFile.toString()) - + e.getMessage()); - logger.error(e, "actionPerformed"); - ignoreHotKeys = false; - } - } - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_VALIDATE)) { - correctExits(); - validateBoard(true); - } else if (ae.getSource().equals(butDelTerrain) && !lisTerrain.isSelectionEmpty()) { - Terrain toRemove = new Terrain(lisTerrain.getSelectedValue().getTerrain()); - curHex.removeTerrain(toRemove.getType()); - refreshTerrainList(); - repaintWorkingHex(); - } else if (ae.getSource().equals(butAddTerrain)) { - addSetTerrain(); - } else if (ae.getSource().equals(butElevUp) && (curHex.getLevel() < 9)) { - curHex.setLevel(curHex.getLevel() + 1); - texElev.incValue(); - repaintWorkingHex(); - } else if (ae.getSource().equals(butElevDown) && (curHex.getLevel() > -5)) { - curHex.setLevel(curHex.getLevel() - 1); - texElev.decValue(); - repaintWorkingHex(); - } else if (ae.getSource().equals(butTerrUp)) { - texTerrainLevel.incValue(); - updateWhenSelected(); - } else if (ae.getSource().equals(butTerrDown)) { - texTerrainLevel.decValue(); - updateWhenSelected(); - } else if (ae.getSource().equals(texTerrainLevel)) { - updateWhenSelected(); - } else if (ae.getSource().equals(texTerrExits)) { - int exitsVal = texTerrExits.getNumber(); - if (exitsVal == 0) { - setExitsState(false); - } else if (exitsVal > 63) { - texTerrExits.setNumber(63); - } - updateWhenSelected(); - } else if (ae.getSource().equals(butTerrExits)) { - int exitsVal; - - if (ae.getActionCommand().equals(CMD_EDIT_DEPLOYMENT_ZONES)) { - var dlg = new MultiIntSelectorDialog(frame, "BoardEditor.deploymentZoneSelectorName", - "BoardEditor.deploymentZoneSelectorTitle", "BoardEditor.deploymentZoneSelectorDescription", - Board.MAX_DEPLOYMENT_ZONE_NUMBER, Board.exitsAsIntList(texTerrExits.getNumber())); - dlg.setVisible(true); - exitsVal = Board.IntListAsExits(dlg.getSelectedItems()); - texTerrExits.setNumber(exitsVal); - } else { - ExitsDialog ed = new ExitsDialog(frame); - exitsVal = texTerrExits.getNumber(); - ed.setExits(exitsVal); - ed.setVisible(true); - exitsVal = ed.getExits(); - texTerrExits.setNumber(exitsVal); - } - setExitsState(exitsVal != 0); - updateWhenSelected(); - } else if (ae.getSource().equals(cheTerrExitSpecified)) { - noTextFieldUpdate = true; - updateWhenSelected(); - noTextFieldUpdate = false; - setExitsState(cheTerrExitSpecified.isSelected()); - } else if (ae.getSource().equals(butExitUp)) { - setExitsState(true); - texTerrExits.incValue(); - updateWhenSelected(); - } else if (ae.getSource().equals(butExitDown)) { - texTerrExits.decValue(); - setExitsState(texTerrExits.getNumber() != 0); - updateWhenSelected(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_MINI_MAP)) { - guip.toggleMinimapEnabled(); - minimapW.setVisible(guip.getMinimapEnabled()); - } else if (ae.getActionCommand().equals(ClientGUI.HELP_ABOUT)) { - showAbout(); - } else if (ae.getActionCommand().equals(ClientGUI.HELP_CONTENTS)) { - showHelp(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_CLIENT_SETTINGS)) { - showSettings(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_ZOOM_IN)) { - bv.zoomIn(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_ZOOM_OUT)) { - bv.zoomOut(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_TOGGLE_ISOMETRIC)) { - bv.toggleIsometric(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_CHANGE_THEME)) { - String newTheme = bv.changeTheme(); - if (newTheme != null) { - choTheme.setSelectedItem(newTheme); - } - } else if (ae.getSource().equals(choTheme)) { - curHex.setTheme((String) choTheme.getSelectedItem()); - repaintWorkingHex(); - } else if (ae.getSource().equals(buttonLW)) { - setConvenientTerrain(ae, new Terrain(Terrains.WOODS, 1), new Terrain(Terrains.FOLIAGE_ELEV, 2)); - } else if (ae.getSource().equals(buttonOW)) { - setConvenientTerrain(ae, new Terrain(Terrains.WOODS, 1), new Terrain(Terrains.FOLIAGE_ELEV, 1)); - } else if (ae.getSource().equals(buttonMg)) { - setConvenientTerrain(ae, new Terrain(Terrains.MAGMA, 1)); - } else if (ae.getSource().equals(buttonLJ)) { - setConvenientTerrain(ae, new Terrain(Terrains.JUNGLE, 1), new Terrain(Terrains.FOLIAGE_ELEV, 2)); - } else if (ae.getSource().equals(buttonOJ)) { - setConvenientTerrain(ae, new Terrain(Terrains.JUNGLE, 1), new Terrain(Terrains.FOLIAGE_ELEV, 1)); - } else if (ae.getSource().equals(buttonWa)) { - buttonUpDn.setSelected(false); - if ((ae.getModifiers() & ActionEvent.CTRL_MASK) != 0) { - int rapidsLevel = curHex.containsTerrain(Terrains.RAPIDS, 1) ? 2 : 1; - if (!curHex.containsTerrain(Terrains.WATER) - || (curHex.getTerrain(Terrains.WATER).getLevel() == 0)) { - setConvenientTerrain(ae, new Terrain(Terrains.RAPIDS, rapidsLevel), - new Terrain(Terrains.WATER, 1)); - } else { - setConvenientTerrain(ae, new Terrain(Terrains.RAPIDS, rapidsLevel), - curHex.getTerrain(Terrains.WATER)); - } - } else { - if ((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { - curHex.removeAllTerrains(); - } - setConvenientTerrain(ae, new Terrain(Terrains.WATER, 1)); - } - } else if (ae.getSource().equals(buttonSw)) { - setConvenientTerrain(ae, new Terrain(Terrains.SWAMP, 1)); - } else if (ae.getSource().equals(buttonRo)) { - setConvenientTerrain(ae, new Terrain(Terrains.ROUGH, 1)); - } else if (ae.getSource().equals(buttonPv)) { - setConvenientTerrain(ae, new Terrain(Terrains.PAVEMENT, 1)); - } else if (ae.getSource().equals(buttonMd)) { - setConvenientTerrain(ae, new Terrain(Terrains.MUD, 1)); - } else if (ae.getSource().equals(buttonTu)) { - setConvenientTerrain(ae, new Terrain(Terrains.TUNDRA, 1)); - } else if (ae.getSource().equals(buttonIc)) { - setConvenientTerrain(ae, new Terrain(Terrains.ICE, 1)); - } else if (ae.getSource().equals(buttonSn)) { - setConvenientTerrain(ae, new Terrain(Terrains.SNOW, 1)); - } else if (ae.getSource().equals(buttonCl)) { - curHex.removeAllTerrains(); - buttonUpDn.setSelected(false); - refreshTerrainList(); - repaintWorkingHex(); - } else if (ae.getSource().equals(buttonBrush1)) { - brushSize = 1; - lastClicked = null; - } else if (ae.getSource().equals(buttonBrush2)) { - brushSize = 2; - lastClicked = null; - } else if (ae.getSource().equals(buttonBrush3)) { - brushSize = 3; - lastClicked = null; - } else if (ae.getSource().equals(buttonBu)) { - buttonUpDn.setSelected(false); - if (((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) - && ((ae.getModifiers() & ActionEvent.ALT_MASK) == 0)) { - curHex.removeAllTerrains(); - } - setBasicBuilding((ae.getModifiers() & ActionEvent.ALT_MASK) != 0); - } else if (ae.getSource().equals(buttonBr)) { - if ((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { - curHex.removeAllTerrains(); - } - buttonUpDn.setSelected(false); - setBasicBridge(); - } else if (ae.getSource().equals(buttonFT)) { - if ((ae.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { - curHex.removeAllTerrains(); - } - buttonUpDn.setSelected(false); - setBasicFuelTank(); - } else if (ae.getSource().equals(buttonRd)) { - setConvenientTerrain(ae, new Terrain(Terrains.ROAD, 1)); - } else if (ae.getSource().equals(buttonUpDn)) { - // Not so useful to only do on clear terrain - buttonOOC.setSelected(false); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_UNDO)) { - // The button should not be active when the stack is empty, but - // let's check nevertheless - if (undoStack.isEmpty()) { - buttonUndo.setEnabled(false); - } else { - HashSet recentHexes = undoStack.pop(); - HashSet redoHexes = new HashSet<>(); - for (Hex hex : recentHexes) { - // Retrieve the board hex for Redo - Hex rHex = board.getHex(hex.getCoords()).duplicate(); - rHex.setCoords(hex.getCoords()); - redoHexes.add(rHex); - // and undo the board hex - board.setHex(hex.getCoords(), hex); - } - redoStack.push(redoHexes); - if (undoStack.isEmpty()) { - buttonUndo.setEnabled(false); - } - hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize); - buttonRedo.setEnabled(true); - currentUndoSet = null; // should be anyway - correctExits(); - } - setFrameTitle(); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REDO)) { - // The button should not be active when the stack is empty, but - // let's check nevertheless - if (redoStack.isEmpty()) { - buttonRedo.setEnabled(false); - } else { - HashSet recentHexes = redoStack.pop(); - HashSet undoHexes = new HashSet<>(); - for (Hex hex : recentHexes) { - Hex rHex = board.getHex(hex.getCoords()).duplicate(); - rHex.setCoords(hex.getCoords()); - undoHexes.add(rHex); - board.setHex(hex.getCoords(), hex); - } - undoStack.push(undoHexes); - if (redoStack.isEmpty()) { - buttonRedo.setEnabled(false); - } - buttonUndo.setEnabled(true); - hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize); - currentUndoSet = null; // should be anyway - correctExits(); - } - setFrameTitle(); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_RAISE)) { - boardChangeLevel(); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_CLEAR)) { - boardClear(); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_FLOOD)) { - boardFlood(); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_WATER)) { - boardRemoveTerrain(WATER, WATER_FLUFF); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_ROADS)) { - boardRemoveTerrain(ROAD, ROAD_FLUFF, BRIDGE, BRIDGE_CF, BRIDGE_ELEV); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_FORESTS)) { - boardRemoveTerrain(WOODS, JUNGLE, FOLIAGE_ELEV); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_REMOVE_BUILDINGS)) { - boardRemoveTerrain(BUILDING, BLDG_ARMOR, BLDG_CF, BLDG_CLASS, - BLDG_FLUFF, BLDG_BASE_COLLAPSED, BLDG_BASEMENT_TYPE, BLDG_ELEV, - FUEL_TANK, FUEL_TANK_CF, FUEL_TANK_ELEV, FUEL_TANK_MAGN); - } else if (ae.getActionCommand().equals(ClientGUI.BOARD_FLATTEN)) { - boardFlatten(); - } else if (ae.getActionCommand().equals(ClientGUI.VIEW_RESET_WINDOW_POSITIONS)) { - minimapW.setBounds(0, 0, minimapW.getWidth(), minimapW.getHeight()); - } - } - - /** Flattens the board, setting all hexes to level 0. */ - private void boardFlatten() { - for (int x = 0; x < board.getWidth(); x++) { - for (int y = 0; y < board.getHeight(); y++) { - Coords c = new Coords(x, y); - if (board.getHex(c).getLevel() != 0) { - saveToUndo(c); - Hex newHex = board.getHex(c).duplicate(); - newHex.setLevel(0); - board.setHex(c, newHex); - } - } - } - correctExits(); - endCurrentUndoSet(); - } - - /** Removes the given terrain type(s) from the board. */ - private void boardRemoveTerrain(int type, int... types) { - for (int x = 0; x < board.getWidth(); x++) { - for (int y = 0; y < board.getHeight(); y++) { - Coords c = new Coords(x, y); - if (board.getHex(c).containsTerrain(type) || board.getHex(c).containsAnyTerrainOf(types)) { - saveToUndo(c); - Hex newHex = board.getHex(c).duplicate(); - newHex.removeTerrain(type); - for (int additional : types) { - newHex.removeTerrain(additional); - } - board.setHex(c, newHex); - } - } - } - correctExits(); - endCurrentUndoSet(); - } - - /** - * Asks for confirmation and clears the whole board (sets all hexes to clear - * level 0). - */ - private void boardClear() { - if (!MMConfirmDialog.confirm(frame, - Messages.getString("BoardEditor.clearTitle"), Messages.getString("BoardEditor.clearMsg"))) { - return; - } - board.resetStoredElevation(); - for (int x = 0; x < board.getWidth(); x++) { - for (int y = 0; y < board.getHeight(); y++) { - Coords c = new Coords(x, y); - saveToUndo(c); - board.setHex(c, new Hex(0)); - } - } - correctExits(); - endCurrentUndoSet(); - } - - /** - * "Pushes" the current set of undoable hexes as a package to the stack, meaning - * that a - * paint or other action is finished. - */ - private void endCurrentUndoSet() { - if ((currentUndoSet != null) && !currentUndoSet.isEmpty()) { - undoStack.push(currentUndoSet); - currentUndoSet = null; - buttonUndo.setEnabled(true); - // Drawing something disables any redo actions - redoStack.clear(); - buttonRedo.setEnabled(false); - // When Undo (without Redo) has been used after saving - // and the user draws on the board, then it can - // no longer know if it's been returned to the saved state - // and it will always be treated as changed. - if (savedUndoStackSize > undoStack.size()) { - canReturnToSaved = false; - } - hasChanges = !canReturnToSaved || (undoStack.size() != savedUndoStackSize); - } - } - - /** - * Asks for a level delta and changes the level of all the board's hexes by that - * delta. - */ - private void boardChangeLevel() { - var dlg = new LevelChangeDialog(frame); - dlg.setVisible(true); - if (!dlg.getResult().isConfirmed() || (dlg.getLevelChange() == 0)) { - return; - } - - board.resetStoredElevation(); - for (int x = 0; x < board.getWidth(); x++) { - for (int y = 0; y < board.getHeight(); y++) { - Coords c = new Coords(x, y); - saveToUndo(c); - Hex newHex = board.getHex(c).duplicate(); - newHex.setLevel(newHex.getLevel() + dlg.getLevelChange()); - board.setHex(c, newHex); - } - } - correctExits(); - endCurrentUndoSet(); - } - - /** - * Asks for flooding info and then floods the whole board with water up to a - * level. - */ - private void boardFlood() { - var dlg = new FloodDialog(frame); - dlg.setVisible(true); - if (!dlg.getResult().isConfirmed()) { - return; - } - - int surface = dlg.getLevelChange(); - board.resetStoredElevation(); - for (int x = 0; x < board.getWidth(); x++) { - for (int y = 0; y < board.getHeight(); y++) { - Coords c = new Coords(x, y); - Hex hex = board.getHex(c); - if (hex.getLevel() < surface) { - saveToUndo(c); - Hex newHex = hex.duplicate(); - int presentDepth = hex.containsTerrain(Terrains.WATER) ? hex.terrainLevel(Terrains.WATER) : 0; - if (dlg.getRemoveTerrain()) { - newHex.removeAllTerrains(); - // Restore bridges if they're above the water - if (hex.containsTerrain(BRIDGE) - && (hex.getLevel() + hex.getTerrain(BRIDGE_ELEV).getLevel() >= surface)) { - newHex.addTerrain(hex.getTerrain(BRIDGE)); - newHex.addTerrain(new Terrain(BRIDGE_ELEV, - hex.getLevel() + hex.getTerrain(BRIDGE_ELEV).getLevel() - surface)); - newHex.addTerrain(hex.getTerrain(BRIDGE_CF)); - } - } - int addedWater = surface - hex.getLevel(); - newHex.addTerrain(new Terrain(Terrains.WATER, addedWater + presentDepth)); - newHex.setLevel(newHex.getLevel() + addedWater); - board.setHex(c, newHex); - } - } - } - correctExits(); - endCurrentUndoSet(); - } - - private void setConvenientTerrain(ActionEvent event, Terrain... terrains) { - if (terrains.length == 0) { - return; - } - if ((event.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { - curHex.removeAllTerrains(); - } - buttonUpDn.setSelected(false); - for (var terrain : terrains) { - curHex.addTerrain(terrain); - } - refreshTerrainList(); - repaintWorkingHex(); - selectTerrain(terrains[0]); - } - - /** - * Selects the given terrain in the terrain list, if possible. All but terrain - * type is ignored. - */ - private void selectTerrain(Terrain terrain) { - for (int i = 0; i < lisTerrain.getModel().getSize(); i++) { - Terrain listEntry = lisTerrain.getModel().getElementAt(i).getTerrain(); - if (listEntry.getType() == terrain.getType()) { - lisTerrain.setSelectedIndex(i); - return; - } - } - } - - /** - * Sets the "Use Exits" checkbox to newState and adapts the coloring of the - * textfield accordingly. - * Use this instead of setting the checkbox state directly. - */ - private void setExitsState(boolean newState) { - cheTerrExitSpecified.setSelected(newState); - if (cheTerrExitSpecified.isSelected()) { - texTerrExits.setForeground(null); - } else { - texTerrExits.setForeground(UIUtil.uiGray()); - } - } - - @Override - public void valueChanged(ListSelectionEvent event) { - if (event.getValueIsAdjusting()) { - return; - } - if (event.getSource().equals(lisTerrain) && !noTextFieldUpdate) { - refreshTerrainFromList(); - } - } - - /** - * Displays the currently selected hex picture, in component form - */ - private class HexCanvas extends JPanel { - - /** Returns list or an empty list when list is null. */ - private List safeList(List list) { - return list == null ? Collections.emptyList() : list; - } - - StringDrawer invalidString = new StringDrawer(Messages.getString("BoardEditor.INVALID")) - .at(HexTileset.HEX_W / 2, HexTileset.HEX_H / 2).color(guip.getWarningColor()) - .outline(Color.WHITE, 1).font(FontHandler.notoFont().deriveFont(Font.BOLD)).center(); - - @Override - public void paintComponent(Graphics g) { - super.paintComponent(g); - if (curHex != null) { - // draw the terrain images - TilesetManager tm = bv.getTilesetManager(); - g.drawImage(tm.baseFor(curHex), 0, 0, HexTileset.HEX_W, HexTileset.HEX_H, this); - for (final Image newVar : safeList(tm.supersFor(curHex))) { - g.drawImage(newVar, 0, 0, this); - } - for (final Image newVar : safeList(tm.orthoFor(curHex))) { - g.drawImage(newVar, 0, 0, this); - } - UIUtil.setHighQualityRendering(g); - // add level and INVALID if necessary - g.setColor(getForeground()); - g.setFont(new Font(MMConstants.FONT_SANS_SERIF, Font.PLAIN, 9)); - g.drawString(Messages.getString("BoardEditor.LEVEL") + curHex.getLevel(), 24, 70); - List errors = new ArrayList<>(); - if (!curHex.isValid(errors)) { - invalidString.draw(g); - String tooltip = Messages.getString("BoardEditor.invalidHex") + String.join("
", errors); - setToolTipText(tooltip); - } else { - setToolTipText(null); - } - } else { - g.clearRect(0, 0, 72, 72); - } - } - - // Make the hex stubborn when resizing the frame - @Override - public Dimension getPreferredSize() { - return new Dimension(90, 90); - } - - @Override - public Dimension getMinimumSize() { - return new Dimension(90, 90); - } - } - - /** - * @return the frame this is displayed in - */ - public JFrame getFrame() { - return frame; - } - - /** - * Returns true if a dialog is visible on top of the ClientGUI. - * For example, the MegaMekController should ignore hotkeys - * if there is a dialog, like the CommonSettingsDialog, open. - * - * @return whether hot keys should be ignored or not - */ - public boolean shouldIgnoreHotKeys() { - return ignoreHotKeys - || UIUtil.isModalDialogDisplayed() - || ((help != null) && help.isVisible()) - || ((setdlg != null) && setdlg.isVisible()) - || texElev.hasFocus() || texTerrainLevel.hasFocus() || texTerrExits.hasFocus(); - } - - private void setDialogSize(JFileChooser dialog) { - int width = guip.getBoardEditLoadWidth(); - int height = guip.getBoardEditLoadHeight(); - dialog.setPreferredSize(new Dimension(width, height)); - } - - private void saveDialogSize(JComponent dialog) { - guip.setBoardEditLoadHeight(dialog.getHeight()); - guip.setBoardEditLoadWidth(dialog.getWidth()); - } - - /** - * Sets the Board Editor frame title, adding the current file name if any - * and a "*" if the board has unsaved changes. - */ - private void setFrameTitle() { - String title = (curBoardFile == null) ? Messages.getString("BoardEditor.title") - : Messages.getString("BoardEditor.title0", curBoardFile); - frame.setTitle(title + (hasChanges ? "*" : "")); - } - - private void copyWorkingHexToClipboard() { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - clipboard.setContents(new StringSelection(curHex.getClipboardString()), null); - } - - private void pasteFromClipboard() { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - Transferable contents = clipboard.getContents(null); - if ((contents != null) && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { - try { - String clipboardString = (String) contents.getTransferData(DataFlavor.stringFlavor); - Hex pastedHex = Hex.parseClipboardString(clipboardString); - if (pastedHex != null) { - setCurrentHex(pastedHex); - } - } catch (Exception ex) { - logger.error(ex, "pasteFromClipboard"); - } - } - } - - /** - * Specialized field for the BoardEditor that supports - * MouseWheel changes. - * - * @author Simon - */ - public static class EditorTextField extends JTextField { - private int minValue = Integer.MIN_VALUE; - private int maxValue = Integer.MAX_VALUE; - - /** - * Creates an EditorTextField based on JTextField. This is a - * specialized field for the BoardEditor that supports - * MouseWheel changes. - * - * @param text the initial text - * @param columns as in JTextField - * - * @see JTextField#JTextField(String, int) - */ - public EditorTextField(String text, int columns) { - super(text, columns); - // Automatically select all text when clicking the text field - addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - selectAll(); - } - }); - addMouseWheelListener(e -> { - if (e.getWheelRotation() < 0) { - incValue(); - } else { - decValue(); - } - }); - setMargin(new Insets(1, 1, 1, 1)); - setHorizontalAlignment(JTextField.CENTER); - setFont(new Font(MMConstants.FONT_SANS_SERIF, Font.BOLD, 20)); - setCursor(Cursor.getDefaultCursor()); - } - - /** - * Creates an EditorTextField based on JTextField. This is a - * specialized field for the BoardEditor that supports - * MouseWheel changes. - * - * @param text the initial text - * @param columns as in JTextField - * @param minimum a minimum value that the EditorTextField - * will generally adhere to when its own methods are used - * to change its value. - * - * @see JTextField#JTextField(String, int) - * - * @author Simon/Juliez - */ - public EditorTextField(String text, int columns, int minimum) { - this(text, columns); - minValue = minimum; - } - - /** - * Creates an EditorTextField based on JTextField. This is a - * specialized field for the BoardEditor that supports - * MouseWheel changes. - * - * @param text the initial text - * @param columns as in JTextField - * @param minimum a minimum value that the EditorTextField - * will generally adhere to when its own methods are used - * to change its value. - * @param maximum a maximum value that the EditorTextField - * will generally adhere to when its own methods are used - * to change its value. - * - * @see JTextField#JTextField(String, int) - * - * @author Simon/Juliez - */ - public EditorTextField(String text, int columns, int minimum, int maximum) { - this(text, columns); - minValue = minimum; - maxValue = maximum; - } - - /** - * Increases the EditorTextField's number by one, if a number - * is present. - */ - public void incValue() { - int newValue = getNumber() + 1; - setNumber(newValue); - } - - /** - * Lowers the EditorTextField's number by one, if a number - * is present and if that number is higher than the minimum - * value. - */ - public void decValue() { - setNumber(getNumber() - 1); - } - - /** - * Sets the text to newValue. If newValue is lower - * than the EditorTextField's minimum value, the minimum value will - * be set instead. - * - * @param newValue the value to be set - */ - public void setNumber(int newValue) { - int value = Math.max(newValue, minValue); - value = Math.min(value, maxValue); - setText(Integer.toString(value)); - } - - /** - * Returns the text in the EditorTextField's as an int. - * Returns 0 when no parsable number (only letters) are present. - */ - public int getNumber() { - try { - return Integer.parseInt(getText()); - } catch (NumberFormatException ex) { - return 0; - } - } - } - - /** - * A specialized JButton that only shows an icon but scales that icon according - * to the current GUI scaling when its rescale() method is called. - */ - private static class ScalingIconButton extends JButton { - - private final Image baseImage; - private Image baseRolloverImage; - private Image baseDisabledImage; - private final int baseWidth; - - ScalingIconButton(Image image, int width) { - super(); - Objects.requireNonNull(image); - baseImage = image; - baseWidth = width; - rescale(); - } - - /** Adapts all images of this button to the current gui scale. */ - void rescale() { - int realWidth = UIUtil.scaleForGUI(baseWidth); - int realHeight = baseImage.getHeight(null) * realWidth / baseImage.getWidth(null); - setIcon(new ImageIcon(ImageUtil.getScaledImage(baseImage, realWidth, realHeight))); - - if (baseRolloverImage != null) { - realHeight = baseRolloverImage.getHeight(null) * realWidth / baseRolloverImage.getWidth(null); - setRolloverIcon(new ImageIcon(ImageUtil.getScaledImage(baseRolloverImage, realWidth, realHeight))); - } else { - setRolloverIcon(null); - } - - if (baseDisabledImage != null) { - realHeight = baseDisabledImage.getHeight(null) * realWidth / baseDisabledImage.getWidth(null); - setDisabledIcon(new ImageIcon(ImageUtil.getScaledImage(baseDisabledImage, realWidth, realHeight))); - } else { - setDisabledIcon(null); - } - } - - /** - * Sets the unscaled base image to use as a mouse hover image for the button. - * image may be null. Passing null disables the hover image. - */ - void setRolloverImage(@Nullable Image image) { - baseRolloverImage = image; - } - - /** - * Sets the unscaled base image to use as a button disabled image for the - * button. - * image may be null. Passing null disables the button disabled image. - */ - void setDisabledImage(@Nullable Image image) { - baseDisabledImage = image; - } - } - - /** - * A specialized JToggleButton that only shows an icon but scales that icon - * according - * to the current GUI scaling when its rescale() method is called. - */ - private static class ScalingIconToggleButton extends JToggleButton { - - private final Image baseImage; - private Image baseRolloverImage; - private Image baseSelectedImage; - private final int baseWidth; - - ScalingIconToggleButton(Image image, int width) { - super(); - Objects.requireNonNull(image); - baseImage = image; - baseWidth = width; - rescale(); - } - - /** Adapts all images of this button to the current gui scale. */ - void rescale() { - int realWidth = UIUtil.scaleForGUI(baseWidth); - int realHeight = baseImage.getHeight(null) * realWidth / baseImage.getWidth(null); - setIcon(new ImageIcon(ImageUtil.getScaledImage(baseImage, realWidth, realHeight))); - - if (baseRolloverImage != null) { - realHeight = baseRolloverImage.getHeight(null) * realWidth / baseRolloverImage.getWidth(null); - setRolloverIcon(new ImageIcon(ImageUtil.getScaledImage(baseRolloverImage, realWidth, realHeight))); - } else { - setRolloverIcon(null); - } - - if (baseSelectedImage != null) { - realHeight = baseSelectedImage.getHeight(null) * realWidth / baseSelectedImage.getWidth(null); - setSelectedIcon(new ImageIcon(ImageUtil.getScaledImage(baseSelectedImage, realWidth, realHeight))); - } else { - setSelectedIcon(null); - } - } - - /** - * Sets the unscaled base image to use as a mouse hover image for the button. - * image may be null. Passing null disables the hover image. - */ - void setRolloverImage(@Nullable Image image) { - baseRolloverImage = image; - } - - /** - * Sets the unscaled base image to use as a "toggle button is selected" image - * for the button. - * image may be null. Passing null disables the "is selected" image. - */ - void setSelectedImage(@Nullable Image image) { - baseSelectedImage = image; - } - } -} From 268be6fc272b25f8973bd89cef9f2e7b60ef7612 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sun, 29 Dec 2024 00:16:12 -0300 Subject: [PATCH 08/16] feat: AI Editor is half-way there --- .../src/megamek/ai/utility/Consideration.java | 13 +- .../ai/utility/DecisionScoreEvaluator.java | 3 +- .../src/megamek/ai/utility/DefaultCurve.java | 2 +- .../ai/utility/tw/TWUtilityAIRepository.java | 26 +- .../tw/considerations/MyUnitArmor.java | 3 + .../tw/considerations/MyUnitRoleIs.java | 49 ++ .../considerations/TargetUnitsHaveRole.java | 51 +++ .../tw/decision/TWDecisionScoreEvaluator.java | 1 + .../ai/utility/tw/profile/TWProfile.java | 5 + .../client/ui/swing/CommonMenuBar.java | 13 +- .../megamek/client/ui/swing/MegaMekGUI.java | 1 - .../ui/swing/ai/editor/AiProfileEditor.form | 45 +- .../ui/swing/ai/editor/AiProfileEditor.java | 421 +++++++++++++----- .../ui/swing/ai/editor/ConsiderationPane.java | 20 + .../client/ui/swing/ai/editor/CurvePane.java | 4 + .../ai/editor/DecisionScoreEvaluatorPane.java | 44 +- .../editor/DecisionScoreEvaluatorTable.java | 33 +- .../swing/ai/editor/DecisionTableModel.java | 16 +- 18 files changed, 594 insertions(+), 156 deletions(-) create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java index c5ed311bb75..653afd89e4a 100644 --- a/megamek/src/megamek/ai/utility/Consideration.java +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import megamek.client.bot.duchess.ai.utility.tw.considerations.*; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.StringJoiner; @@ -33,7 +34,9 @@ @JsonSubTypes.Type(value = MyUnitArmor.class, name = "MyUnitArmor"), @JsonSubTypes.Type(value = TargetWithinOptimalRange.class, name = "TargetWithinOptimalRange"), @JsonSubTypes.Type(value = TargetWithinRange.class, name = "TargetWithinRange"), - @JsonSubTypes.Type(value = MyUnitUnderThreat.class, name = "MyUnitUnderThreat") + @JsonSubTypes.Type(value = MyUnitUnderThreat.class, name = "MyUnitUnderThreat"), + @JsonSubTypes.Type(value = MyUnitRoleIs.class, name = "MyUnitRoleIs"), + @JsonSubTypes.Type(value = TargetUnitsHaveRole.class, name = "TargetUnitsHaveRole"), }) @JsonIgnoreProperties(ignoreUnknown = true) public abstract class Consideration implements NamedObject { @@ -42,7 +45,7 @@ public abstract class Consideration implements Named @JsonProperty("curve") private Curve curve; @JsonProperty("parameters") - private Map parameters; + protected Map parameters = Collections.emptyMap(); public Consideration() { } @@ -52,7 +55,7 @@ public Consideration(String name) { } public Consideration(String name, Curve curve) { - this(name, curve, new HashMap<>()); + this(name, curve, Collections.emptyMap()); } public Consideration(String name, Curve curve, Map parameters) { @@ -103,6 +106,10 @@ protected long getLongParameter(String key) { return (long) parameters.get(key); } + protected boolean hasParameter(String key) { + return parameters.containsKey(key); + } + public double computeResponseCurve(double score) { return curve.evaluate(score); } diff --git a/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java b/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java index 0067cff7e16..7dbcd0b3071 100644 --- a/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java +++ b/megamek/src/megamek/ai/utility/DecisionScoreEvaluator.java @@ -83,9 +83,8 @@ public List> getConsiderations() { return considerations; } - public DecisionScoreEvaluator addConsideration(Consideration consideration) { + public void addConsideration(Consideration consideration) { considerations.add(consideration); - return this; } public String getDescription() { diff --git a/megamek/src/megamek/ai/utility/DefaultCurve.java b/megamek/src/megamek/ai/utility/DefaultCurve.java index 10aff611dee..f12b55f9444 100644 --- a/megamek/src/megamek/ai/utility/DefaultCurve.java +++ b/megamek/src/megamek/ai/utility/DefaultCurve.java @@ -50,6 +50,6 @@ public static DefaultCurve fromCurve(Curve curve) { } public Curve getCurve() { - return curve; + return curve.copy(); } } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java index 768dee1cf43..a24c9de3233 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -22,7 +22,6 @@ import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; import megamek.common.Configuration; -import megamek.common.util.fileUtils.MegaMekFile; import megamek.logging.MMLogger; import java.io.File; @@ -62,16 +61,27 @@ private TWUtilityAIRepository() { } private void initialize() { -// persistData(); - loadConsiderations(new MegaMekFile(Configuration.twAiDir(), CONSIDERATIONS).getFile()) + loadRepository(); + loadUserDataRepository(); + } + + private void loadRepository() { + loadData(Configuration.twAiDir()); + } + + private void loadUserDataRepository() { + loadData(Configuration.userDataAiTwDir()); + } + + private void loadData(File directory) { + loadConsiderations(new File(directory, CONSIDERATIONS)) .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); - loadDecisionScoreEvaluators(new MegaMekFile(Configuration.twAiDir(), EVALUATORS).getFile()).forEach( + loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach( twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); - loadDecisions(new MegaMekFile(Configuration.twAiDir(), DECISIONS).getFile()).forEach( + loadDecisions(new File(directory, DECISIONS)).forEach( twDecision -> decisions.put(twDecision.getName(), twDecision)); - loadProfiles(new MegaMekFile(Configuration.twAiDir(), PROFILES).getFile()).forEach( + loadProfiles(new File(directory, PROFILES)).forEach( twProfile -> profiles.put(twProfile.getName(), twProfile)); - persistDataToUserData(); } public void reloadRepository() { @@ -198,7 +208,7 @@ private List objectsFromDirectory(File inputFile, Class clazz) { objects.addAll(objectsFromFile(clazz, file)); } } - return objects; + return objects.stream().filter(Objects::nonNull).toList(); } private List objectsFromFile(Class clazz, File file) { diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java index a7aae3dbdbd..588b20d24a8 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java @@ -16,9 +16,12 @@ package megamek.client.bot.duchess.ai.utility.tw.considerations; import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.Curve; import megamek.ai.utility.DecisionContext; import megamek.common.Entity; +import java.util.Map; + import static megamek.codeUtilities.MathUtility.clamp01; /** diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java new file mode 100644 index 00000000000..3564b2cfca6 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; +import megamek.common.UnitRole; + +import java.util.Map; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if a target is an easy target. + */ +@JsonTypeName("MyUnitRoleIs") +public class MyUnitRoleIs extends TWConsideration { + + public MyUnitRoleIs() { + parameters = Map.of("role", UnitRole.AMBUSHER.name()); + } + + @Override + public double score(DecisionContext context) { + if (!hasParameter("role")) { + return 0d; + } + + var currentUnit = context.getCurrentUnit().orElseThrow(); + var role = UnitRole.valueOf(getStringParameter("role")); + + return currentUnit.getRole().equals(role) ? 1d : 0d; + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java new file mode 100644 index 00000000000..1210b22893d --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; +import megamek.common.UnitRole; + +import java.util.Map; + +/** + * This consideration is used to determine if a specific role is present in between the targets. + */ +@JsonTypeName("TargetUnitsHaveRole") +public class TargetUnitsHaveRole extends TWConsideration { + + public TargetUnitsHaveRole() { + parameters = Map.of("role", UnitRole.AMBUSHER.name()); + } + + @Override + public double score(DecisionContext context) { + if (!hasParameter("role")) { + return 0d; + } + + for (var target : context.getTargets()) { + var role = UnitRole.valueOf(getStringParameter("role")); + if (target.getRole().equals(role)) { + return 1d; + } + } + + return 0d; + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java index 3366aca4898..a61886ba4e3 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/decision/TWDecisionScoreEvaluator.java @@ -21,6 +21,7 @@ import megamek.ai.utility.DecisionScoreEvaluator; import megamek.common.Entity; +import java.util.ArrayList; import java.util.List; @JsonTypeName("TWDecisionScoreEvaluator") diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java index 3f9b0017a0e..eddb47e44af 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/profile/TWProfile.java @@ -38,4 +38,9 @@ public TWProfile( { super(id, name, description, decisions); } + + @Override + public String toString() { + return this.getName(); + } } diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java index b3d6164c0f7..0fe6ad9e047 100644 --- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java +++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java @@ -385,25 +385,30 @@ public CommonMenuBar() { menu = new JMenu(Messages.getString("CommonMenuBar.AIEditorMenu")); menu.setMnemonic(VK_A); add(menu); + initMenuItem(aiEditorNew, menu, AI_EDITOR_NEW); initMenuItem(aiEditorOpen, menu, AI_EDITOR_OPEN); initMenuItem(aiEditorRecentProfile, menu, AI_EDITOR_RECENT_PROFILE); initializeRecentAiProfilesMenu(); menu.addSeparator(); + initMenuItem(aiEditorSave, menu, AI_EDITOR_SAVE); initMenuItem(aiEditorSaveAs, menu, AI_EDITOR_SAVE_AS); initMenuItem(aiEditorReloadFromDisk, menu, AI_EDITOR_RELOAD_FROM_DISK); menu.addSeparator(); + initMenuItem(aiEditorUndo, menu, AI_EDITOR_UNDO); initMenuItem(aiEditorRedo, menu, AI_EDITOR_REDO); menu.addSeparator(); - initMenuItem(aiEditorNewDecision, menu, AI_EDITOR_NEW_DECISION); - aiEditorNewDecision.setSelected(GUIP.getShowSensorRange()); + initMenuItem(aiEditorNewConsideration, menu, AI_EDITOR_NEW_CONSIDERATION); - aiEditorNewConsideration.setSelected(GUIP.getShowSensorRange()); + aiEditorNewConsideration.setMnemonic(VK_U); initMenuItem(aiEditorNewDecisionScoreEvaluator, menu, AI_EDITOR_NEW_DECISION_SCORE_EVALUATOR); - aiEditorNewDecisionScoreEvaluator.setSelected(GUIP.getShowSensorRange()); + aiEditorNewDecisionScoreEvaluator.setMnemonic(VK_I); + initMenuItem(aiEditorNewDecision, menu, AI_EDITOR_NEW_DECISION); + aiEditorNewDecision.setMnemonic(VK_O); menu.addSeparator(); + initMenuItem(aiEditorExport, menu, AI_EDITOR_EXPORT); initMenuItem(aiEditorImport, menu, AI_EDITOR_IMPORT); diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index c45d4792bde..e545c78c731 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -32,7 +32,6 @@ import megamek.client.ui.dialogs.helpDialogs.MMReadMeHelpDialog; import megamek.client.ui.enums.DialogResult; import megamek.client.ui.swing.ai.editor.AiProfileEditor; -import megamek.client.ui.swing.ai.editor.UtilityAiEditor; import megamek.client.ui.swing.dialog.MainMenuUnitBrowserDialog; import megamek.client.ui.swing.gameConnectionDialogs.ConnectDialog; import megamek.client.ui.swing.gameConnectionDialogs.HostDialog; diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form index 195aebb7973..e7453982416 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form @@ -17,7 +17,7 @@ - + @@ -25,33 +25,45 @@ - + - - + + + + + + + + - + - + - - - + - + - + - - - + - + + + + + + + + + + + @@ -83,6 +95,7 @@
+ @@ -91,7 +104,9 @@ + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 18b02151c44..53a60fe57e0 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -19,6 +19,7 @@ import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.uiDesigner.core.Spacer; import megamek.ai.utility.Action; +import megamek.ai.utility.Decision; import megamek.ai.utility.NamedObject; import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; @@ -30,32 +31,34 @@ import megamek.client.ui.swing.CommonMenuBar; import megamek.client.ui.swing.GUIPreferences; import megamek.client.ui.swing.util.MegaMekController; +import megamek.common.Entity; +import megamek.logging.MMLogger; import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; +import java.awt.event.*; import java.lang.reflect.Method; import java.util.List; +import java.util.Random; import java.util.ResourceBundle; -public class AiProfileEditor extends JFrame { +import static megamek.client.ui.swing.ClientGUI.*; + +public class AiProfileEditor extends JFrame implements ActionListener { + private static final MMLogger logger = MMLogger.create(AiProfileEditor.class); + private final TWUtilityAIRepository sharedData = TWUtilityAIRepository.getInstance(); private final GUIPreferences guip = GUIPreferences.getInstance(); private final MegaMekController controller; - private JButton newDecisionButton; private JTree repositoryViewer; private JTabbedPane mainEditorTabbedPane; private JPanel dseTabPane; private JTextField descriptionTextField; private JTextField profileNameTextField; - private JButton newConsiderationButton; private JPanel profileTabPane; private JTable profileDecisionTable; private JPanel decisionTabPane; @@ -67,11 +70,21 @@ public class AiProfileEditor extends JFrame { private JPanel dsePane; private JPanel considerationTabPane; private JPanel considerationEditorPanel; + private JButton saveProfileButton; + private JButton saveDseButton; + private JButton saveConsiderationButton; + private JButton saveDecisionButton; + private ConsiderationPane considerationPane; private final CommonMenuBar menuBar = CommonMenuBar.getMenuBarForAiEditor(); + private int profileId = -1; - private boolean hasChanges = true; + private boolean hasDecisionChanges = false; + private boolean hasProfileChanges = false; + private boolean hasDseChanges = false; + private boolean hasConsiderationChanges = false; + private boolean hasChangesToSave = false; private boolean ignoreHotKeys = false; public AiProfileEditor(MegaMekController controller) { @@ -84,6 +97,10 @@ public AiProfileEditor(MegaMekController controller) { setVisible(true); } + private boolean hasChanges() { + return hasDecisionChanges || hasProfileChanges || hasDseChanges || hasConsiderationChanges || hasChangesToSave; + } + private void initialize() { GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; @@ -96,31 +113,12 @@ private void initialize() { considerationPane = new ConsiderationPane(); considerationPane.setMinimumSize(new Dimension(considerationEditorPanel.getWidth(), considerationEditorPanel.getHeight())); considerationEditorPanel.add(considerationPane, gbc); - - newDecisionButton.addActionListener(e -> { - var action = (Action) actionComboBox.getSelectedItem(); - var weight = (double) weightSpinner.getValue(); - var dse = new TWDecision(action, weight); - var model = profileDecisionTable.getModel(); - //noinspection unchecked - ((DecisionTableModel) model).addRow(dse); - }); - - newConsiderationButton.addActionListener(e -> { - var action = (Action) actionComboBox.getSelectedItem(); - var weight = (double) weightSpinner.getValue(); - var dse = new TWDecision(action, weight); - var model = profileDecisionTable.getModel(); - //noinspection unchecked - ((DecisionTableModel) model).addRow(dse); - }); - this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { // When the board has changes, ask the user - if (!hasChanges || (showSavePrompt() != DialogResult.CANCELLED)) { + if (!hasChanges() || (showSavePrompt() != DialogResult.CANCELLED)) { if (controller != null) { controller.removeAllActions(); controller.aiEditor = null; @@ -131,17 +129,26 @@ public void windowClosing(WindowEvent e) { } }); - // Add mouse listener for double-click events repositoryViewer.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { + if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) { + TreePath path = repositoryViewer.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + if (node.isLeaf()) { + handleOpenNodeAction(node); + } + } + } else if (e.getButton() != MouseEvent.BUTTON1) { TreePath path = repositoryViewer.getPathForLocation(e.getX(), e.getY()); if (path != null) { + repositoryViewer.setSelectionPath(path); DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (node.isLeaf()) { - handleNodeAction(node); + JPopupMenu contextMenu = createContextMenu(node); + contextMenu.show(repositoryViewer, e.getX(), e.getY()); } } } @@ -153,37 +160,99 @@ public void mouseClicked(MouseEvent e) { if (path != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (node.isLeaf()) { - handleNodeAction(node); + handleOpenNodeAction(node); } } }); - - menuBar.addActionListener(e -> { - if (!ignoreHotKeys) { - switch (e.getActionCommand()) { - case "Save": - persistProfile(); - break; - case "Close": - if (!hasChanges || (showSavePrompt() != DialogResult.CANCELLED)) { - if (controller != null) { - controller.removeAllActions(); - controller.aiEditor = null; - } - getFrame().dispose(); - } - break; - case "Help": -// controller.showHelp("aiEditor"); - break; - } + getFrame().setJMenuBar(menuBar); + menuBar.addActionListener(this); + saveProfileButton.addActionListener(e -> { + try { + persistProfile(); + } catch (IllegalArgumentException ex) { + logger.formattedErrorDialog("Error saving profile", + "One or more fields are empty or invalid in the Profile tab. Please correct the errors and try again."); } }); - getFrame().setJMenuBar(menuBar); + saveDseButton.addActionListener(e -> { + try { + persistDecisionScoreEvaluator(); + } catch (IllegalArgumentException ex) { + logger.formattedErrorDialog("Error saving decision score evaluator", + "One or more fields are empty or invalid in the Decision Score Evaluator tab. Please correct the errors and try again."); + } + }); + saveConsiderationButton.addActionListener(e -> { + try { + persistConsideration(); + } catch (IllegalArgumentException ex) { + logger.formattedErrorDialog("Error saving consideration", + "One or more fields are empty or invalid in the Consideration tab. Please correct the errors and try again."); + } + }); + saveDecisionButton.addActionListener(e -> { + try { + persistDecision(); + } catch (IllegalArgumentException ex) { + logger.formattedErrorDialog("Error saving decision", + "One or more fields are empty or invalid in the Decision tab. Please correct the errors and try again."); + } + }); + saveProfileButton.setVisible(true); + saveDseButton.setVisible(false); + saveConsiderationButton.setVisible(false); + saveDecisionButton.setVisible(false); + mainEditorTabbedPane.addChangeListener(e -> { + if (mainEditorTabbedPane.getSelectedComponent() == profileTabPane) { + saveProfileButton.setVisible(true); + saveDseButton.setVisible(false); + saveConsiderationButton.setVisible(false); + saveDecisionButton.setVisible(false); + } else if (mainEditorTabbedPane.getSelectedComponent() == dseTabPane) { + saveProfileButton.setVisible(false); + saveDseButton.setVisible(true); + saveConsiderationButton.setVisible(false); + saveDecisionButton.setVisible(false); + } else if (mainEditorTabbedPane.getSelectedComponent() == considerationTabPane) { + saveProfileButton.setVisible(false); + saveDseButton.setVisible(false); + saveConsiderationButton.setVisible(true); + saveDecisionButton.setVisible(false); + } else if (mainEditorTabbedPane.getSelectedComponent() == decisionTabPane) { + saveProfileButton.setVisible(false); + saveDseButton.setVisible(false); + saveConsiderationButton.setVisible(false); + saveDecisionButton.setVisible(true); + } + }); + + } + + private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { + // Create a popup menu + JPopupMenu contextMenu = new JPopupMenu(); + + // Example menu item #1 + JMenuItem menuItemAction = new JMenuItem("Open"); + menuItemAction.addActionListener(evt -> { + handleOpenNodeAction(node); + }); + contextMenu.add(menuItemAction); + + // Example menu item #2 + JMenuItem menuItemOther = new JMenuItem("Delete"); + menuItemOther.addActionListener(evt -> { + // Another action + handleDeleteNodeAction(node); + }); + contextMenu.add(menuItemOther); + + return contextMenu; } - private void handleNodeAction(DefaultMutableTreeNode node) { + + private void handleOpenNodeAction(DefaultMutableTreeNode node) { var obj = node.getUserObject(); if (obj instanceof TWDecision twDecision) { openDecision(twDecision); @@ -196,31 +265,49 @@ private void handleNodeAction(DefaultMutableTreeNode node) { } } -// decisionScoreEvaluatorTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); -// decisionTabDsePanel = new DecisionScoreEvaluatorPane(); -// dsePane = new DecisionScoreEvaluatorPane(); + + private void handleDeleteNodeAction(DefaultMutableTreeNode node) { + var obj = node.getUserObject(); + if (obj instanceof TWDecision twDecision) { + sharedData.removeDecision(twDecision); + hasDecisionChanges = true; + } else if (obj instanceof TWProfile twProfile) { + sharedData.removeProfile(twProfile); + hasProfileChanges = true; + } else if (obj instanceof TWDecisionScoreEvaluator twDse) { + sharedData.removeDecisionScoreEvaluator(twDse); + hasDseChanges = true; + } else if (obj instanceof TWConsideration twConsideration) { + sharedData.removeConsideration(twConsideration); + hasConsiderationChanges = true; + } + } private void openConsideration(TWConsideration twConsideration) { considerationPane.setConsideration(twConsideration); mainEditorTabbedPane.setSelectedComponent(considerationTabPane); + hasConsiderationChanges = true; } private void openDecision(TWDecision twDecision) { ((DecisionScoreEvaluatorPane) decisionTabDsePanel).setDecisionScoreEvaluator(twDecision.getDecisionScoreEvaluator()); mainEditorTabbedPane.setSelectedComponent(decisionTabPane); + hasDecisionChanges = true; } private void openProfile(TWProfile twProfile) { - // profileTab.setProfile(twProfile); + profileId = twProfile.getId(); profileNameTextField.setText(twProfile.getName()); descriptionTextField.setText(twProfile.getDescription()); profileDecisionTable.setModel(new DecisionTableModel<>(twProfile.getDecisions())); mainEditorTabbedPane.setSelectedComponent(profileTabPane); + hasProfileChanges = true; } private void openDecisionScoreEvaluator(TWDecisionScoreEvaluator twDse) { ((DecisionScoreEvaluatorPane) dsePane).setDecisionScoreEvaluator(twDse); mainEditorTabbedPane.setSelectedComponent(dseTabPane); + hasDseChanges = true; } private DialogResult showSavePrompt() { @@ -232,36 +319,156 @@ private DialogResult showSavePrompt() { JOptionPane.WARNING_MESSAGE); ignoreHotKeys = false; // When the user cancels or did not actually save the board, don't load anything - if (((savePrompt == JOptionPane.YES_OPTION) && !hasChanges) + if (((savePrompt == JOptionPane.YES_OPTION) && !hasChanges()) || (savePrompt == JOptionPane.CANCEL_OPTION) || (savePrompt == JOptionPane.CLOSED_OPTION)) { return DialogResult.CANCELLED; } else { - persistProfile(); - return DialogResult.CONFIRMED; + if (saveEverything()) { + return DialogResult.CONFIRMED; + } else { + return DialogResult.CANCELLED; + } + } + } + + private boolean saveEverything() { + try { + if (hasProfileChanges) { + persistProfile(); + } + if (hasDecisionChanges) { + persistDecision(); + } + if (hasDseChanges) { + persistDecisionScoreEvaluator(); + } + if (hasConsiderationChanges) { + persistConsideration(); + } + if (hasChangesToSave) { + sharedData.persistDataToUserData(); + hasChangesToSave = false; + } + return true; + } catch (IllegalArgumentException ex) { + logger.formattedErrorDialog("Error saving data", + "One or more fields are empty or invalid. Please correct the errors and try again."); } + return false; + } + + private void persistConsideration() { + var consideration = considerationPane.getConsideration(); + sharedData.addConsideration(consideration); + hasConsiderationChanges = false; + hasChangesToSave = true; + loadDataRepoViewer(); + } + + private void persistDecision() { + var dse = ((DecisionScoreEvaluatorPane) decisionTabDsePanel).getDecisionScoreEvaluator(); + var decision = new TWDecision((Action) actionComboBox.getSelectedItem(), (double) weightSpinner.getValue(), dse); + sharedData.addDecision(decision); + hasDecisionChanges = false; + hasChangesToSave = true; + loadDataRepoViewer(); + } + + private void persistDecisionScoreEvaluator() { + var dse = ((DecisionScoreEvaluatorPane) dsePane).getDecisionScoreEvaluator(); + sharedData.addDecisionScoreEvaluator(dse); + hasDseChanges = false; + hasChangesToSave = true; + loadDataRepoViewer(); } private void persistProfile() { //noinspection unchecked var model = (DecisionTableModel) profileDecisionTable.getModel(); - var updatedList = model.getDecisions(); - System.out.println("== Updated DecisionScoreEvaluator List =="); - for (int i = 0; i < updatedList.size(); i++) { - var dse = updatedList.get(i); - System.out.printf("Row %d -> Decision: %s, Evaluator: %s%n", - i, - dse.getAction().getActionName(), - dse.getDecisionScoreEvaluator().getName()); - sharedData.addDecision(dse); + if (profileId <= 0) { + var maxId = sharedData.getProfiles().stream().map(TWProfile::getId).max(Integer::compareTo).orElse(0); + profileId = new Random().nextInt(maxId + 1, Integer.MAX_VALUE); } - sharedData.persistDataToUserData(); + var decisions = model.getDecisions().stream().map(e -> (Decision) e).toList(); + sharedData.addProfile(new TWProfile(profileId, profileNameTextField.getText(), descriptionTextField.getText(), decisions)); + hasProfileChanges = false; + hasChangesToSave = true; + loadDataRepoViewer(); } public JFrame getFrame() { return this; } + @Override + public void actionPerformed(ActionEvent e) { + switch (e.getActionCommand()) { + case AI_EDITOR_NEW: + createNewProfile(); + break; + case AI_EDITOR_OPEN: + break; + case AI_EDITOR_RECENT_PROFILE: + break; + case AI_EDITOR_SAVE: + saveEverything(); + break; + case AI_EDITOR_SAVE_AS: + // not implemented + break; + case AI_EDITOR_RELOAD_FROM_DISK: + + break; + case AI_EDITOR_UNDO: + break; + case AI_EDITOR_REDO: + break; + case AI_EDITOR_NEW_DECISION: + createNewDecision(); + break; + case AI_EDITOR_NEW_CONSIDERATION: + addNewConsideration(); + break; + case AI_EDITOR_NEW_DECISION_SCORE_EVALUATOR: + createNewDecisionScoreEvaluator(); + break; + case AI_EDITOR_EXPORT: + break; + case AI_EDITOR_IMPORT: + break; + } + } + + private void createNewProfile() { + profileId = -1; + profileNameTextField.setText("New Profile"); + descriptionTextField.setText(""); + initializeProfileUI(); + mainEditorTabbedPane.setSelectedComponent(profileTabPane); + profileTabPane.updateUI(); + } + + private void createNewDecisionScoreEvaluator() { + ((DecisionScoreEvaluatorPane) dsePane).reset(); + mainEditorTabbedPane.setSelectedComponent(dseTabPane); + dsePane.updateUI(); + } + + private void addNewConsideration() { + ((DecisionScoreEvaluatorPane) dsePane).addEmptyConsideration(); + dsePane.updateUI(); + } + + private void createNewDecision() { + var action = (Action) actionComboBox.getSelectedItem(); + var weight = (double) weightSpinner.getValue(); + var dse = new TWDecision(action, weight); + var model = profileDecisionTable.getModel(); + //noinspection unchecked + ((DecisionTableModel) model).addRow(dse); + } + private enum TreeViewHelper { PROFILES("Profiles"), DECISIONS("Decisions"), @@ -282,6 +489,19 @@ public String getName() { private void createUIComponents() { weightSpinner = new JSpinner(new SpinnerNumberModel(1d, 0d, 4d, 0.01d)); + loadDataRepoViewer(); + actionComboBox = new JComboBox<>(Action.values()); + initializeProfileUI(); + decisionTabDsePanel = new DecisionScoreEvaluatorPane(); + dsePane = new DecisionScoreEvaluatorPane(); + } + + private void initializeProfileUI() { + var model = new DecisionTableModel<>(sharedData.getDecisions()); + profileDecisionTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); + } + + private void loadDataRepoViewer() { var root = new DefaultMutableTreeNode(Messages.getString("aiEditor.tree.title")); addToMutableTreeNode(root, TreeViewHelper.PROFILES.getName(), sharedData.getProfiles()); addToMutableTreeNode(root, TreeViewHelper.DECISIONS.getName(), sharedData.getDecisions()); @@ -290,12 +510,7 @@ private void createUIComponents() { DefaultTreeModel treeModel = new DefaultTreeModel(root); repositoryViewer = new JTree(treeModel); - actionComboBox = new JComboBox<>(Action.values()); - var model = new DecisionTableModel<>(sharedData.getDecisions()); - profileDecisionTable = new DecisionScoreEvaluatorTable<>(model, Action.values(), sharedData.getDecisionScoreEvaluators()); - decisionTabDsePanel = new DecisionScoreEvaluatorPane(); - dsePane = new DecisionScoreEvaluatorPane(); - + repositoryViewer.updateUI(); } private void addToMutableTreeNode(DefaultMutableTreeNode root, String nodeName, List items) { @@ -321,15 +536,21 @@ private void addToMutableTreeNode(DefaultMutableTreeNode final JSplitPane splitPane1 = new JSplitPane(); uAiEditorPanel.add(splitPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(200, 200), null, 0, false)); final JPanel panel1 = new JPanel(); - panel1.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); + panel1.setLayout(new GridLayoutManager(5, 1, new Insets(0, 0, 0, 0), -1, -1)); splitPane1.setLeftComponent(panel1); - newDecisionButton = new JButton(); - this.$$$loadButtonText$$$(newDecisionButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.newDecision")); - panel1.add(newDecisionButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, 1, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(233, 34), null, 0, false)); - newConsiderationButton = new JButton(); - this.$$$loadButtonText$$$(newConsiderationButton, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.newConsideration")); - panel1.add(newConsiderationButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, 1, null, new Dimension(233, 34), null, 0, false)); - panel1.add(repositoryViewer, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(233, 50), null, 0, false)); + panel1.add(repositoryViewer, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(233, 50), null, 0, false)); + saveProfileButton = new JButton(); + saveProfileButton.setText("Save"); + panel1.add(saveProfileButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + saveDseButton = new JButton(); + saveDseButton.setText("Save"); + panel1.add(saveDseButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + saveConsiderationButton = new JButton(); + saveConsiderationButton.setText("Save"); + panel1.add(saveConsiderationButton, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + saveDecisionButton = new JButton(); + saveDecisionButton.setText("Save"); + panel1.add(saveDecisionButton, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JPanel panel2 = new JPanel(); panel2.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); splitPane1.setRightComponent(panel2); @@ -339,10 +560,13 @@ private void addToMutableTreeNode(DefaultMutableTreeNode profileTabPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile"), profileTabPane); profileScrollPane = new JScrollPane(); + profileScrollPane.setDoubleBuffered(false); profileScrollPane.setWheelScrollingEnabled(true); profileTabPane.add(profileScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); profileDecisionTable.setColumnSelectionAllowed(false); + profileDecisionTable.setDragEnabled(true); profileDecisionTable.setFillsViewportHeight(true); + profileDecisionTable.setInheritsPopupMenu(true); profileDecisionTable.setMinimumSize(new Dimension(150, 32)); profileDecisionTable.setPreferredScrollableViewportSize(new Dimension(150, 32)); profileScrollPane.setViewportView(profileDecisionTable); @@ -444,33 +668,6 @@ private void addToMutableTreeNode(DefaultMutableTreeNode } } - /** - * @noinspection ALL - */ - private void $$$loadButtonText$$$(AbstractButton component, String text) { - StringBuffer result = new StringBuffer(); - boolean haveMnemonic = false; - char mnemonic = '\0'; - int mnemonicIndex = -1; - for (int i = 0; i < text.length(); i++) { - if (text.charAt(i) == '&') { - i++; - if (i == text.length()) break; - if (!haveMnemonic && text.charAt(i) != '&') { - haveMnemonic = true; - mnemonic = text.charAt(i); - mnemonicIndex = result.length(); - } - } - result.append(text.charAt(i)); - } - component.setText(result.toString()); - if (haveMnemonic) { - component.setMnemonic(mnemonic); - component.setDisplayedMnemonicIndex(mnemonicIndex); - } - } - /** * @noinspection ALL */ diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index ea670b355b0..5513d884586 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -19,6 +19,7 @@ import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.uiDesigner.core.Spacer; import megamek.ai.utility.Consideration; +import megamek.ai.utility.DefaultCurve; import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; import megamek.common.Entity; @@ -28,6 +29,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.reflect.Method; +import java.util.Collections; import java.util.ResourceBundle; public class ConsiderationPane extends JPanel { @@ -59,10 +61,28 @@ public void setConsideration(Consideration consideration) { ((CurvePane) curveContainer).setCurve(consideration.getCurve()); } + public void setEmptyConsideration() { + considerationComboBox.setSelectedItem(null); + considerationName.setText(""); + ((ParametersTableModel) parametersTable.getModel()).setParameters(Collections.emptyMap()); + ((CurvePane) curveContainer).setCurve(DefaultCurve.Logit.getCurve()); + } + public void setHoverStateModel(HoverStateModel model) { ((CurvePane) curveContainer).setHoverStateModel(model); } + public TWConsideration getConsideration() { + TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); + if (consideration == null) { + throw new IllegalStateException("No consideration selected"); + } + consideration.setName(considerationName.getText()); + consideration.setParameters(((ParametersTableModel) parametersTable.getModel()).getParameters()); + consideration.setCurve(((CurvePane) curveContainer).getCurve().copy()); + return consideration; + } + private void createUIComponents() { parametersTable = new JTable(new ParametersTableModel()); parametersTable.setModel(new ParametersTableModel()); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index fe645758a2b..cbca8f85afb 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -48,6 +48,10 @@ public void setCurve(Curve curve) { updateCurveDataUI(); } + public Curve getCurve() { + return selectedCurve.get(); + } + public void setHoverStateModel(HoverStateModel model) { ((CurveGraph) this.curveGraph).setHoverStateModel(model); } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index 99464a285c1..34af41a0a8b 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -19,11 +19,12 @@ import com.intellij.uiDesigner.core.GridLayoutManager; import megamek.ai.utility.DecisionScoreEvaluator; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; -import megamek.common.Entity; import javax.swing.*; + import java.awt.*; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; @@ -34,27 +35,64 @@ public class DecisionScoreEvaluatorPane extends JPanel { private JPanel decisionScoreEvaluatorPane; private JPanel considerationsPane; private final HoverStateModel hoverStateModel; - + private final List considerationPaneList = new ArrayList<>(); public DecisionScoreEvaluatorPane() { $$$setupUI$$$(); add(decisionScoreEvaluatorPane, BorderLayout.WEST); hoverStateModel = new HoverStateModel(); } + public TWDecisionScoreEvaluator getDecisionScoreEvaluator() { + var dse = new TWDecisionScoreEvaluator(); + dse.setName(nameField.getText()); + dse.setDescription(descriptionField.getText()); + dse.setNotes(notesField.getText()); + for (var considerationPane : considerationPaneList) { + dse.addConsideration(considerationPane.getConsideration()); + } + return dse; + } + + public void addEmptyConsideration() { + considerationsPane.removeAll(); + considerationsPane.setLayout(new GridLayoutManager((considerationPaneList.size()+1) * 2, 1, new Insets(0, 0, 0, 0), -1, -1)); + int row = 0; + var emptyConsideration = new ConsiderationPane(); + emptyConsideration.setEmptyConsideration(); + emptyConsideration.setHoverStateModel(hoverStateModel); + considerationsPane.add(emptyConsideration, new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); + considerationsPane.add(new JSeparator(), new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); + for (var c : considerationPaneList) { + considerationsPane.add(c, new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); + considerationsPane.add(new JSeparator(), new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); + } + considerationPaneList.add(0, emptyConsideration); + } + + public void reset() { + nameField.setText(""); + descriptionField.setText(""); + notesField.setText(""); + considerationsPane.removeAll(); + considerationPaneList.clear(); + } + public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { nameField.setText(dse.getName()); descriptionField.setText(dse.getDescription()); notesField.setText(dse.getNotes()); considerationsPane.removeAll(); + var considerations = dse.getConsiderations(); considerationsPane.setLayout(new GridLayoutManager(considerations.size() * 2, 1, new Insets(0, 0, 0, 0), -1, -1)); + considerationPaneList.clear(); int row = 0; for (var consideration : considerations) { var c = new ConsiderationPane(); c.setConsideration(consideration); c.setHoverStateModel(hoverStateModel); - + considerationPaneList.add(c); considerationsPane.add(c, new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); considerationsPane.add(new JSeparator(), new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java index 9444a573e0a..02522ae7135 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java @@ -21,11 +21,13 @@ import javax.swing.*; import javax.swing.table.TableCellEditor; +import java.awt.*; import java.util.List; public class DecisionScoreEvaluatorTable, DSE extends DecisionScoreEvaluator> extends JTable { private final Action[] actionList; + private final List dse; public DecisionScoreEvaluatorTable( @@ -40,6 +42,7 @@ public void createUIComponents() { } @Override + @SuppressWarnings("unchecked") public DecisionTableModel getModel() { return (DecisionTableModel) super.getModel(); } @@ -48,13 +51,13 @@ public DecisionTableModel getModel() { public TableCellEditor getCellEditor(int row, int column) { // Decision is column 1, Evaluator is column 2 if (column == 1) { - // Create a combo box for Decisions JComboBox cb = new JComboBox<>( actionList ); return new DefaultCellEditor(cb); } else if (column == 2) { - // Create a combo box for Evaluators + return new SpinnerCellEditor(1d, 0d, 5d, 0.1d); + } else if (column == 3) { var cb = new JComboBox<>( dse.toArray(new DecisionScoreEvaluator[0]) ); @@ -63,4 +66,30 @@ public TableCellEditor getCellEditor(int row, int column) { return super.getCellEditor(row, column); } + + public static class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { + private final JSpinner spinner; + + public SpinnerCellEditor(double defaultValue, double min, double max, double step) { + spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); + spinner.setValue(defaultValue); + JComponent editor = spinner.getEditor(); + if (editor instanceof JSpinner.DefaultEditor) { + JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); + textField.setHorizontalAlignment(JFormattedTextField.LEFT); + } + } + + @Override + public Object getCellEditorValue() { + return spinner.getValue(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + spinner.setValue(value); + return spinner; + } + } + } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java index 0281690ef5d..a62390e0928 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java @@ -22,12 +22,14 @@ import javax.swing.table.AbstractTableModel; import java.util.ArrayList; import java.util.List; +import java.util.Set; -public class DecisionTableModel> extends AbstractTableModel { +public class DecisionTableModel> extends AbstractTableModel { private final List rows; - private final String[] columnNames = { "ID", "Decision", "Evaluator" }; + private final String[] columnNames = { "ID", "Decision", "Weight", "Evaluator" }; + private final Set editableColumns = Set.of(1, 2, 3); public DecisionTableModel(List initialRows) { this.rows = new ArrayList<>(initialRows); @@ -59,15 +61,15 @@ public Object getValueAt(int rowIndex, int columnIndex) { return switch (columnIndex) { case 0 -> rowIndex; // or some ID from dse case 1 -> dse.getAction().getActionName(); - case 2 -> dse.getDecisionScoreEvaluator().getName(); + case 2 -> dse.getWeight(); + case 3 -> dse.getDecisionScoreEvaluator().getName(); default -> null; }; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { - // Let's keep ID read-only, but allow editing for Decision & Evaluator - return columnIndex == 1 || columnIndex == 2; + return editableColumns.contains(columnIndex); } @Override @@ -81,6 +83,10 @@ public void setValueAt(Object aValue, int rowIndex, int columnIndex) { } break; case 2: + if (aValue instanceof Number weight) { + dse.setWeight((Double) weight); + } + case 3: if (aValue instanceof DecisionScoreEvaluator decisionScoreEvaluator) { dse.setDecisionScoreEvaluator(decisionScoreEvaluator); } From 6771ceb54fe923d773fecda2b2ffc12ca01768f6 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sun, 29 Dec 2024 00:39:34 -0300 Subject: [PATCH 09/16] chore: update user data readme --- megamek/userdata/README.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/megamek/userdata/README.md b/megamek/userdata/README.md index c03f835609c..7e524f4da4c 100644 --- a/megamek/userdata/README.md +++ b/megamek/userdata/README.md @@ -1,6 +1,6 @@ # User Data Folder -Written 11-JUN-2021 -MekHQ version 0.49.2 + +MekHQ version 0.50.03 @ 29-DEZ-2024 This is a list of all supported files in the userdata directory. Any file not listed here has not been checked to ensure it works properly, and thus the userdata folder is not supported for them. Directories are especially likely to not work if you place them here. @@ -8,17 +8,35 @@ The default file implementation is the userdata file overriding the core data fi ## General Suite Directories/Files: ### data/names/ -factions/: This subdirectory holds faction-specific name generation files. The individual faction files are line-based merge implemented, so that you may override any line of the default file by writing the historical ethnic code in the userdata folder. -callsigns.csv: This file contains weighted callsigns used in the random callsign generator. This file is merge implemented, with callsigns duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Callsign,Weight". -femaleGivenNames.csv: This file contains weighted historical ethnic code organized name used in the random name generator. This file is merge implemented, with names duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Ethnic Code,Name,Weight". -historicalEthnicity.csv: This file contains historical ethnic codes and their names. This file is merge implemented, with duplicated codes overwritten by the value in the userdata file. -maleGivenNames.csv: This file contains weighted historical ethnic code organized names used in the random name generator. This file is merge implemented, with names duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Ethnic Code,Name,Weight". -surnames.csv: This file contains weighted historical ethnic code organized surnames used in the random name generator. This file is merge implemented, with names duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Ethnic Code,Name,Weight". +**factions/**: This subdirectory holds faction-specific name generation files. The individual faction files are line-based merge implemented, so that you may override any line of the default file by writing the historical ethnic code in the userdata folder. + +**callsigns.csv**: This file contains weighted callsigns used in the random callsign generator. This file is merge implemented, with callsigns duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Callsign,Weight". + +**femaleGivenNames.csv**: This file contains weighted historical ethnic code organized name used in the random name generator. This file is merge implemented, with names duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Ethnic Code,Name,Weight". + +**historicalEthnicity.csv**: This file contains historical ethnic codes and their names. This file is merge implemented, with duplicated codes overwritten by the value in the userdata file. + +**maleGivenNames.csv**: This file contains weighted historical ethnic code organized names used in the random name generator. This file is merge implemented, with names duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Ethnic Code,Name,Weight". + +**surnames.csv**: This file contains weighted historical ethnic code organized surnames used in the random name generator. This file is merge implemented, with names duplicated in the userdata folder having the userdata weight instead of the default weight. The first line of this file must be the standard header of "Ethnic Code,Name,Weight". ## MegaMek-specific Folders/Files: +### userdata/ai/tw/ +This directory contains the AI files for the Tactical Warfare module. +The files are separated between considerations, decisions, evaluators and profiles, with the userdata ai files taking +priority over the default file. -## MegaMekLab-specific Directories/Files: +Default files that are generated by the AI Editor when saving: + +- userdata/ai/tw/considerations/custom_considerations.yaml +- userdata/ai/tw/decisions/custom_decisions.yaml +- userdata/ai/tw/evaluators/custom_decision_evaluators.yaml +- userdata/ai/tw/profiles/custom_profiles.yaml ## MekHQ-specific Directories/Files: ### data/universe/ ranks.xml: This file contains custom rank systems. This is merge implemented, so that each rank system will be handled akin to the default MekHQ rank systems, with the default rank systems taking primary key priority on load. + +## MegaMekLab-specific Directories/Files: + +Left empty \ No newline at end of file From b4f52315d0dd484cdecd4972d0869fe63c9ec21d Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sun, 29 Dec 2024 00:55:33 -0300 Subject: [PATCH 10/16] fix: more random profile id --- .../megamek/client/ui/swing/ai/editor/AiProfileEditor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 53a60fe57e0..6a8a245de06 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Random; import java.util.ResourceBundle; +import java.util.stream.Collectors; import static megamek.client.ui.swing.ClientGUI.*; @@ -387,8 +388,10 @@ private void persistProfile() { //noinspection unchecked var model = (DecisionTableModel) profileDecisionTable.getModel(); if (profileId <= 0) { - var maxId = sharedData.getProfiles().stream().map(TWProfile::getId).max(Integer::compareTo).orElse(0); - profileId = new Random().nextInt(maxId + 1, Integer.MAX_VALUE); + var ids = sharedData.getProfiles().stream().map(TWProfile::getId).collect(Collectors.toSet()); + while (ids.contains(profileId) || profileId <= 0) { + profileId = new Random().nextInt(1, Integer.MAX_VALUE); + } } var decisions = model.getDecisions().stream().map(e -> (Decision) e).toList(); sharedData.addProfile(new TWProfile(profileId, profileNameTextField.getText(), descriptionTextField.getText(), decisions)); From 572a2a21e88b2524903847e73cd10616dc720d89 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sun, 29 Dec 2024 23:29:23 -0300 Subject: [PATCH 11/16] feat: added new curves, added reflection based ui controls --- .../considerations/proto_considerations.yaml | 4 +- .../data/ai/tw/decisions/proto_decisions.yaml | 4 +- megamek/data/ai/tw/evaluators/proto_dse.yaml | 8 +- .../megamek/ai/utility/BandFilterCurve.java | 106 ++++++++++++++++ .../src/megamek/ai/utility/BandPassCurve.java | 106 ++++++++++++++++ .../src/megamek/ai/utility/Consideration.java | 51 ++++++-- megamek/src/megamek/ai/utility/Curve.java | 20 ++- .../src/megamek/ai/utility/DefaultCurve.java | 9 +- .../src/megamek/ai/utility/LinearCurve.java | 2 + .../src/megamek/ai/utility/LogisticCurve.java | 4 + .../src/megamek/ai/utility/LogitCurve.java | 4 + .../megamek/ai/utility/ParabolicCurve.java | 3 + .../ai/utility/tw/TWUtilityAIRepository.java | 76 ++++++------ .../tw/considerations/MyUnitArmor.java | 2 +- .../tw/considerations/MyUnitIsCrippled.java | 39 ++++++ .../tw/considerations/MyUnitRoleIs.java | 5 +- .../tw/considerations/TargetUnitsArmor.java | 64 ++++++++++ .../considerations/TargetUnitsHaveRole.java | 3 +- .../ui/swing/ai/editor/AiProfileEditor.java | 93 ++++++++++++-- .../ui/swing/ai/editor/ConsiderationPane.java | 66 ++++++---- .../editor/ConsiderationParametersTable.java | 115 ++++++++++++++++++ .../client/ui/swing/ai/editor/CurvePane.java | 78 +++++++----- .../ai/editor/DecisionScoreEvaluatorPane.java | 16 ++- .../editor/DecisionScoreEvaluatorTable.java | 27 ---- .../swing/ai/editor/ParametersTableModel.java | 63 ++++------ .../ui/swing/ai/editor/SpinnerCellEditor.java | 45 +++++++ .../swing/ai/editor/TWConsiderationClass.java | 50 ++++++++ 27 files changed, 859 insertions(+), 204 deletions(-) create mode 100644 megamek/src/megamek/ai/utility/BandFilterCurve.java create mode 100644 megamek/src/megamek/ai/utility/BandPassCurve.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java diff --git a/megamek/data/ai/tw/considerations/proto_considerations.yaml b/megamek/data/ai/tw/considerations/proto_considerations.yaml index 939fb9e1bd2..7152f98239b 100644 --- a/megamek/data/ai/tw/considerations/proto_considerations.yaml +++ b/megamek/data/ai/tw/considerations/proto_considerations.yaml @@ -3,9 +3,7 @@ name: "MyUnitArmor" curve: ! m: 0.5 b: 0.3 -parameters: - minValue: 0 - maxValue: 10 +parameters: {} --- ! name: "TargetWithinOptimalRange" curve: ! diff --git a/megamek/data/ai/tw/decisions/proto_decisions.yaml b/megamek/data/ai/tw/decisions/proto_decisions.yaml index ab04fef9e2b..f6dac7b3e47 100644 --- a/megamek/data/ai/tw/decisions/proto_decisions.yaml +++ b/megamek/data/ai/tw/decisions/proto_decisions.yaml @@ -13,9 +13,7 @@ decisionScoreEvaluator: ! curve: ! m: 0.5 b: 0.3 - parameters: - minValue: 0 - maxValue: 10 + parameters: {} - ! name: "TargetWithinOptimalRange" curve: ! diff --git a/megamek/data/ai/tw/evaluators/proto_dse.yaml b/megamek/data/ai/tw/evaluators/proto_dse.yaml index 092c3027257..5ac8befb1a2 100644 --- a/megamek/data/ai/tw/evaluators/proto_dse.yaml +++ b/megamek/data/ai/tw/evaluators/proto_dse.yaml @@ -9,9 +9,7 @@ considerations: curve: ! m: 0.5 b: 0.3 - parameters: - minValue: 0 - maxValue: 10 + parameters: {} - ! name: "TargetWithinOptimalRange" curve: ! @@ -33,9 +31,7 @@ considerations: curveType: "LinearCurve" m: 0.5 b: 0.3 - parameters: - minValue: 0 - maxValue: 10 + parameters: {} - ! name: "TargetWithinOptimalRange" curve: diff --git a/megamek/src/megamek/ai/utility/BandFilterCurve.java b/megamek/src/megamek/ai/utility/BandFilterCurve.java new file mode 100644 index 00000000000..2309ed756d7 --- /dev/null +++ b/megamek/src/megamek/ai/utility/BandFilterCurve.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.StringJoiner; + +import static megamek.codeUtilities.MathUtility.clamp01; + +@JsonTypeName("BandFilterCurve") +public class BandFilterCurve implements Curve { + private double m; + private double b; + private double k; + private double c; + + @JsonCreator + public BandFilterCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k, + @JsonProperty("c") double c) { + this.m = m; + this.b = b; + this.k = k; + this.c = c; + } + + @Override + public BandFilterCurve copy() { + return new BandFilterCurve(m, b, k, c); + } + + public double evaluate(double x) { + var bandStart = m - b / 2; + var bandEnd = m + b / 2; + + return clamp01(x < bandStart ? 1d + c : x > bandEnd ? 1d + c : 0d + k); + } + + @Override + public double getC() { + return c; + } + + @Override + public void setC(double c) { + this.c = c; + } + + @Override + public double getM() { + return m; + } + + @Override + public double getB() { + return b; + } + + @Override + public double getK() { + return k; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public String toString() { + return new StringJoiner(", ", BandFilterCurve.class.getSimpleName() + " [", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .add("c=" + c) + .toString(); + } +} diff --git a/megamek/src/megamek/ai/utility/BandPassCurve.java b/megamek/src/megamek/ai/utility/BandPassCurve.java new file mode 100644 index 00000000000..6dbb07b7017 --- /dev/null +++ b/megamek/src/megamek/ai/utility/BandPassCurve.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.StringJoiner; + +import static megamek.codeUtilities.MathUtility.clamp01; + +@JsonTypeName("BandPassCurve") +public class BandPassCurve implements Curve { + private double m; + private double b; + private double k; + private double c; + + @JsonCreator + public BandPassCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k, + @JsonProperty("c") double c) { + this.m = m; + this.b = b; + this.k = k; + this.c = c; + } + + @Override + public BandPassCurve copy() { + return new BandPassCurve(m, b, k, c); + } + + public double evaluate(double x) { + var bandStart = m - b / 2; + var bandEnd = m + b / 2; + + return clamp01(x < bandStart ? 0d + k : x > bandEnd ? 0d + k : 1d + c); + } + + @Override + public double getK() { + return k; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public double getC() { + return c; + } + + @Override + public void setC(double c) { + this.c = c; + } + + @Override + public double getM() { + return m; + } + + @Override + public double getB() { + return b; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public String toString() { + return new StringJoiner(", ", BandPassCurve.class.getSimpleName() + " [", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .add("c=" + c) + .toString(); + } +} diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java index 653afd89e4a..abffd2d5c47 100644 --- a/megamek/src/megamek/ai/utility/Consideration.java +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -47,6 +47,8 @@ public abstract class Consideration implements Named @JsonProperty("parameters") protected Map parameters = Collections.emptyMap(); + protected transient Map> parameterTypes = Collections.emptyMap(); + public Consideration() { } @@ -78,35 +80,58 @@ public Map getParameters() { return Map.copyOf(parameters); } + public Map> getParameterTypes() { + return Map.copyOf(parameterTypes); + } + + public Class getParameterType(String key) { + return parameterTypes.get(key); + } + public void setParameters(Map parameters) { + var params = new HashMap(); + for (var entry : parameters.entrySet()) { + var clazz = parameterTypes.get(entry.getKey()); + if (clazz == null) { + throw new IllegalArgumentException("Unknown parameter: " + entry.getKey()); + } + if (clazz.isAssignableFrom(entry.getValue().getClass())) { + throw new IllegalArgumentException("Invalid parameter type for " + entry.getKey() + ": " + entry.getValue().getClass()); + } + params.put(entry.getKey(), entry.getValue()); + } this.parameters = Map.copyOf(parameters); } - protected double getDoubleParameter(String key) { - return (double) parameters.get(key); + public double getDoubleParameter(String key) { + return (double) getParameter(key); + } + + public int getIntParameter(String key) { + return (int) getParameter(key); } - protected int getIntParameter(String key) { - return (int) parameters.get(key); + public boolean getBooleanParameter(String key) { + return (boolean) getParameter(key); } - protected boolean getBooleanParameter(String key) { - return (boolean) parameters.get(key); + public String getStringParameter(String key) { + return (String) getParameter(key); } - protected String getStringParameter(String key) { - return (String) parameters.get(key); + public float getFloatParameter(String key) { + return (float) getParameter(key); } - protected float getFloatParameter(String key) { - return (float) parameters.get(key); + public long getLongParameter(String key) { + return (long) getParameter(key); } - protected long getLongParameter(String key) { - return (long) parameters.get(key); + public Object getParameter(String key) { + return parameters.get(key); } - protected boolean hasParameter(String key) { + public boolean hasParameter(String key) { return parameters.containsKey(key); } diff --git a/megamek/src/megamek/ai/utility/Curve.java b/megamek/src/megamek/ai/utility/Curve.java index 3f7d6f12606..ad4c64500cf 100644 --- a/megamek/src/megamek/ai/utility/Curve.java +++ b/megamek/src/megamek/ai/utility/Curve.java @@ -31,7 +31,9 @@ @JsonSubTypes.Type(value = LinearCurve.class, name = "LinearCurve"), @JsonSubTypes.Type(value = LogisticCurve.class, name = "LogisticCurve"), @JsonSubTypes.Type(value = LogitCurve.class, name = "LogitCurve"), - @JsonSubTypes.Type(value = ParabolicCurve.class, name = "ParabolicCurve") + @JsonSubTypes.Type(value = ParabolicCurve.class, name = "ParabolicCurve"), + @JsonSubTypes.Type(value = BandPassCurve.class, name = "BandPassCurve"), + @JsonSubTypes.Type(value = BandFilterCurve.class, name = "BandFilterCurve"), }) public interface Curve { double evaluate(double x); @@ -127,4 +129,20 @@ default void setK(double k) { default void setC(double c) { // } + + default double getM() { + return 0.0; + } + + default double getB() { + return 0.0; + } + + default double getK() { + return 0.0; + } + + default double getC() { + return 0.0; + } } diff --git a/megamek/src/megamek/ai/utility/DefaultCurve.java b/megamek/src/megamek/ai/utility/DefaultCurve.java index f12b55f9444..283d5d18ba4 100644 --- a/megamek/src/megamek/ai/utility/DefaultCurve.java +++ b/megamek/src/megamek/ai/utility/DefaultCurve.java @@ -27,7 +27,10 @@ public enum DefaultCurve { LogisticDecreasing(new LogisticCurve(1.0, 0.5, -10.0, 0.0)), Logit(new LogitCurve(1.0, 0.5, -15.0, 0.0)), - LogitDecreasing(new LogitCurve(1.0, 0.5, 15.0, 0.0)); + LogitDecreasing(new LogitCurve(1.0, 0.5, 15.0, 0.0)), + + BandPass(new BandPassCurve(0.5, 0.2, 0, 0)), + BandFilter(new BandFilterCurve(0.5, 0.2, 0, 0)); private final Curve curve; @@ -44,6 +47,10 @@ public static DefaultCurve fromCurve(Curve curve) { return Logistic; } else if (curve instanceof LogitCurve) { return Logit; + } else if (curve instanceof BandPassCurve) { + return BandPass; + } else if (curve instanceof BandFilterCurve) { + return BandFilter; } // Return Linear as default return Linear; diff --git a/megamek/src/megamek/ai/utility/LinearCurve.java b/megamek/src/megamek/ai/utility/LinearCurve.java index 9a3b5eae258..744846ab37b 100644 --- a/megamek/src/megamek/ai/utility/LinearCurve.java +++ b/megamek/src/megamek/ai/utility/LinearCurve.java @@ -46,10 +46,12 @@ public double evaluate(double x) { return clamp01(m * x + b); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } diff --git a/megamek/src/megamek/ai/utility/LogisticCurve.java b/megamek/src/megamek/ai/utility/LogisticCurve.java index 71174b93acb..c963a1283d3 100644 --- a/megamek/src/megamek/ai/utility/LogisticCurve.java +++ b/megamek/src/megamek/ai/utility/LogisticCurve.java @@ -53,18 +53,22 @@ public double evaluate(double x) { return clamp01(m * (1 / (1 + Math.exp(-k * (x - b)))) + c); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } + @Override public double getK() { return k; } + @Override public double getC() { return c; } diff --git a/megamek/src/megamek/ai/utility/LogitCurve.java b/megamek/src/megamek/ai/utility/LogitCurve.java index 0b608d6dc39..b1c95abf120 100644 --- a/megamek/src/megamek/ai/utility/LogitCurve.java +++ b/megamek/src/megamek/ai/utility/LogitCurve.java @@ -58,18 +58,22 @@ public double evaluate(double x) { return clamp01(b + (1 / k) * Math.log((m - (x - c)) / (x - c))); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } + @Override public double getK() { return k; } + @Override public double getC() { return c; } diff --git a/megamek/src/megamek/ai/utility/ParabolicCurve.java b/megamek/src/megamek/ai/utility/ParabolicCurve.java index b102b2fd018..4c142d80ec4 100644 --- a/megamek/src/megamek/ai/utility/ParabolicCurve.java +++ b/megamek/src/megamek/ai/utility/ParabolicCurve.java @@ -50,14 +50,17 @@ public double evaluate(double x) { return clamp01(-m * Math.pow(x - b, 2) + k); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } + @Override public double getK() { return k; } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java index a24c9de3233..f6e1e5a0019 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -17,7 +17,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; +import megamek.client.bot.duchess.ai.utility.tw.considerations.*; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; @@ -65,25 +65,6 @@ private void initialize() { loadUserDataRepository(); } - private void loadRepository() { - loadData(Configuration.twAiDir()); - } - - private void loadUserDataRepository() { - loadData(Configuration.userDataAiTwDir()); - } - - private void loadData(File directory) { - loadConsiderations(new File(directory, CONSIDERATIONS)) - .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); - loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach( - twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); - loadDecisions(new File(directory, DECISIONS)).forEach( - twDecision -> decisions.put(twDecision.getName(), twDecision)); - loadProfiles(new File(directory, PROFILES)).forEach( - twProfile -> profiles.put(twProfile.getName(), twProfile)); - } - public void reloadRepository() { decisionScoreEvaluators.clear(); considerations.clear(); @@ -92,6 +73,24 @@ public void reloadRepository() { initialize(); } + public void persistData() { + var twAiDir = Configuration.twAiDir(); + createDirectoryStructureIfMissing(twAiDir); + persistToFile(new File(twAiDir, EVALUATORS + File.separator + "decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(twAiDir, CONSIDERATIONS + File.separator + "considerations.yaml"), considerations.values()); + persistToFile(new File(twAiDir, DECISIONS + File.separator + "decisions.yaml"), decisions.values()); + persistToFile(new File(twAiDir, PROFILES + File.separator + "profiles.yaml"), profiles.values()); + } + + public void persistDataToUserData() { + var userDataAiTwDir = Configuration.userDataAiTwDir(); + createDirectoryStructureIfMissing(userDataAiTwDir); + persistToFile(new File(userDataAiTwDir, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(userDataAiTwDir, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values()); + persistToFile(new File(userDataAiTwDir, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values()); + persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); + } + public List getDecisions() { return List.copyOf(decisions.values()); } @@ -168,6 +167,25 @@ public void addProfile(TWProfile profile) { profiles.put(profile.getName(), profile); } + private void loadRepository() { + loadData(Configuration.twAiDir()); + } + + private void loadUserDataRepository() { + loadData(Configuration.userDataAiTwDir()); + } + + private void loadData(File directory) { + loadConsiderations(new File(directory, CONSIDERATIONS)) + .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); + loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach( + twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); + loadDecisions(new File(directory, DECISIONS)).forEach( + twDecision -> decisions.put(twDecision.getName(), twDecision)); + loadProfiles(new File(directory, PROFILES)).forEach( + twProfile -> profiles.put(twProfile.getName(), twProfile)); + } + private List loadDecisionScoreEvaluators(File inputFile) { return loadObjects(inputFile, TWDecisionScoreEvaluator.class); } @@ -224,24 +242,6 @@ private List objectsFromFile(Class clazz, File file) { return Collections.emptyList(); } - public void persistData() { - var twAiDir = Configuration.twAiDir(); - createDirectoryStructureIfMissing(twAiDir); - persistToFile(new File(twAiDir, EVALUATORS + File.separator + "decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); - persistToFile(new File(twAiDir, CONSIDERATIONS + File.separator + "considerations.yaml"), considerations.values()); - persistToFile(new File(twAiDir, DECISIONS + File.separator + "decisions.yaml"), decisions.values()); - persistToFile(new File(twAiDir, PROFILES + File.separator + "profiles.yaml"), profiles.values()); - } - - public void persistDataToUserData() { - var userDataAiTwDir = Configuration.userDataAiTwDir(); - createDirectoryStructureIfMissing(userDataAiTwDir); - persistToFile(new File(userDataAiTwDir, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); - persistToFile(new File(userDataAiTwDir, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values()); - persistToFile(new File(userDataAiTwDir, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values()); - persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); - } - private void persistToFile(File outputFile, Collection objects) { if (objects.isEmpty()) { return; diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java index 588b20d24a8..26c806ddc53 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java @@ -25,7 +25,7 @@ import static megamek.codeUtilities.MathUtility.clamp01; /** - * This consideration is used to determine if a target is an easy target. + * This consideration is used to determine the armor percent. */ @JsonTypeName("MyUnitArmor") public class MyUnitArmor extends TWConsideration { diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java new file mode 100644 index 00000000000..e06c3b6c901 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if the unit is crippled or not. + */ +@JsonTypeName("MyUnitIsCrippled") +public class MyUnitIsCrippled extends TWConsideration { + + public MyUnitIsCrippled() { + } + + @Override + public double score(DecisionContext context) { + var currentUnit = context.getCurrentUnit().orElseThrow(); + return currentUnit.isCrippled(true) ? 1d : 0d; + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java index 3564b2cfca6..2d5ba236f68 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java @@ -22,8 +22,6 @@ import java.util.Map; -import static megamek.codeUtilities.MathUtility.clamp01; - /** * This consideration is used to determine if a target is an easy target. */ @@ -31,7 +29,8 @@ public class MyUnitRoleIs extends TWConsideration { public MyUnitRoleIs() { - parameters = Map.of("role", UnitRole.AMBUSHER.name()); + parameters = Map.of("role", UnitRole.AMBUSHER); + parameterTypes = Map.of("role", UnitRole.class); } @Override diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java new file mode 100644 index 00000000000..663aa2ac1e8 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; + +import java.util.Map; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine the armor percent of enemies. + */ +@JsonTypeName("TargetUnitsArmor") +public class TargetUnitsArmor extends TWConsideration { + + public enum Aggregation { + AVERAGE, + MIN, + MAX + } + + public TargetUnitsArmor() { + parameters = Map.of("aggregation", Aggregation.AVERAGE); + parameterTypes = Map.of("aggregation", Aggregation.class); + } + + @Override + public double score(DecisionContext context) { + var targets = context.getTargets(); + if (targets.isEmpty()) { + return 0d; + } + var armorPercent = 0d; + for (var target : targets) { + armorPercent += target.getArmorRemainingPercent(); + } + if (getBooleanParameter("average")) { + return clamp01(armorPercent / targets.size()); + } else if (getBooleanParameter("min")) { + return clamp01(targets.stream().mapToDouble(Entity::getArmorRemainingPercent).min().orElse(0d)); + } else if (getBooleanParameter("max")) { + return clamp01(targets.stream().mapToDouble(Entity::getArmorRemainingPercent).max().orElse(0d)); + } + + return clamp01(armorPercent); + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java index 1210b22893d..da7892f7339 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java @@ -29,7 +29,8 @@ public class TargetUnitsHaveRole extends TWConsideration { public TargetUnitsHaveRole() { - parameters = Map.of("role", UnitRole.AMBUSHER.name()); + parameters = Map.of("role", UnitRole.AMBUSHER); + parameterTypes = Map.of("role", UnitRole.class); } @Override diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 6a8a245de06..1e3dad06eed 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -156,15 +156,15 @@ public void mouseClicked(MouseEvent e) { } }); - repositoryViewer.addTreeSelectionListener(e -> { - TreePath path = e.getPath(); - if (path != null) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.isLeaf()) { - handleOpenNodeAction(node); - } - } - }); +// repositoryViewer.addTreeSelectionListener(e -> { +// TreePath path = e.getPath(); +// if (path != null) { +// DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); +// if (node.isLeaf()) { +// handleOpenNodeAction(node); +// } +// } +// }); getFrame().setJMenuBar(menuBar); menuBar.addActionListener(this); saveProfileButton.addActionListener(e -> { @@ -232,7 +232,7 @@ public void mouseClicked(MouseEvent e) { private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { // Create a popup menu JPopupMenu contextMenu = new JPopupMenu(); - + var obj = node.getUserObject(); // Example menu item #1 JMenuItem menuItemAction = new JMenuItem("Open"); menuItemAction.addActionListener(evt -> { @@ -240,17 +240,79 @@ private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { }); contextMenu.add(menuItemAction); + if (obj instanceof TWDecision twDecision) { + var action = new JMenuItem("Add to current Profile"); + action.addActionListener(evt -> { + var model = profileDecisionTable.getModel(); + //noinspection unchecked + ((DecisionTableModel) model).addRow(twDecision); + }); + contextMenu.add(action); + } else if (obj instanceof TWProfile) { + var action = getCopyProfileMenuItem((TWProfile) obj); + contextMenu.add(action); + } else if (obj instanceof TWDecisionScoreEvaluator twDse) { + var action = new JMenuItem("New Decision Score Evaluator"); + action.addActionListener(evt -> { + createNewDecisionScoreEvaluator(); + }); + contextMenu.add(action); + } else if (obj instanceof TWConsideration twConsideration) { + var action = new JMenuItem("New Consideration"); + action.addActionListener(evt -> { + addNewConsideration(); + }); + contextMenu.add(action); + // if the tab is a DSE, add the consideration to the DSE + if (mainEditorTabbedPane.getSelectedComponent() == dseTabPane) { + var action1 = new JMenuItem("Add to current Decision Score Evaluator"); + action1.addActionListener(evt -> { + var dse = ((DecisionScoreEvaluatorPane) dsePane).getDecisionScoreEvaluator(); + dse.addConsideration(twConsideration); + ((DecisionScoreEvaluatorPane) dsePane).setDecisionScoreEvaluator(dse); + }); + contextMenu.add(action1); + } else if (mainEditorTabbedPane.getSelectedComponent() == decisionTabPane) { + var action1 = new JMenuItem("Add to current Decision"); + action1.addActionListener(evt -> { + var dse = ((DecisionScoreEvaluatorPane)decisionTabDsePanel).getDecisionScoreEvaluator(); + dse.addConsideration(twConsideration); + ((DecisionScoreEvaluatorPane) decisionTabDsePanel).setDecisionScoreEvaluator(dse); + }); + contextMenu.add(action1); + } + } + // Example menu item #2 JMenuItem menuItemOther = new JMenuItem("Delete"); menuItemOther.addActionListener(evt -> { // Another action - handleDeleteNodeAction(node); + int deletePrompt = JOptionPane.showConfirmDialog(null, + Messages.getString("aiEditor.deleteNodePrompt"), + Messages.getString("BoardEditor.deleteNodeTitle"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + if (deletePrompt == JOptionPane.YES_OPTION) { + handleDeleteNodeAction(node); + } }); contextMenu.add(menuItemOther); return contextMenu; } + private JMenuItem getCopyProfileMenuItem(TWProfile obj) { + var action = new JMenuItem("Copy Profile"); + action.addActionListener(evt -> { + profileId = -1; + profileNameTextField.setText(obj.getName() + " (Copy)"); + descriptionTextField.setText(obj.getDescription()); + profileDecisionTable.setModel(new DecisionTableModel<>(obj.getDecisions())); + mainEditorTabbedPane.setSelectedComponent(profileTabPane); + hasProfileChanges = true; + }); + return action; + } private void handleOpenNodeAction(DefaultMutableTreeNode node) { @@ -282,6 +344,7 @@ private void handleDeleteNodeAction(DefaultMutableTreeNode node) { sharedData.removeConsideration(twConsideration); hasConsiderationChanges = true; } + ((DefaultTreeModel) repositoryViewer.getModel()).removeNodeFromParent(node); } private void openConsideration(TWConsideration twConsideration) { @@ -512,8 +575,12 @@ private void loadDataRepoViewer() { addToMutableTreeNode(root, TreeViewHelper.CONSIDERATIONS.getName(), sharedData.getConsiderations()); DefaultTreeModel treeModel = new DefaultTreeModel(root); - repositoryViewer = new JTree(treeModel); - repositoryViewer.updateUI(); + if (repositoryViewer == null) { + repositoryViewer = new JTree(treeModel); + } else { + repositoryViewer.setModel(treeModel); + repositoryViewer.updateUI(); + } } private void addToMutableTreeNode(DefaultMutableTreeNode root, String nodeName, List items) { diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index 5513d884586..75c51685301 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -17,24 +17,18 @@ import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; -import com.intellij.uiDesigner.core.Spacer; import megamek.ai.utility.Consideration; import megamek.ai.utility.DefaultCurve; -import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; -import megamek.common.Entity; import javax.swing.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.lang.reflect.Method; -import java.util.Collections; import java.util.ResourceBundle; public class ConsiderationPane extends JPanel { private JTextField considerationName; - private JComboBox considerationComboBox; + private JComboBox considerationComboBox; private JPanel curveContainer; private JTable parametersTable; private JPanel considerationPane; @@ -45,26 +39,46 @@ public ConsiderationPane() { add(considerationPane); considerationComboBox.addActionListener(e -> { - TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); - if (consideration != null) { - considerationName.setText(consideration.getName()); - ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); - ((CurvePane) (curveContainer)).setCurve(consideration.getCurve()); + if (considerationComboBox.getSelectedItem() == null) { + return; } + + var selectedClass = ((TWConsiderationClass) considerationComboBox.getSelectedItem()).getConsiderationClass(); + if (selectedClass == null) { + return; + } + + try { + // Create a new instance via reflection + var newInstance = (TWConsideration) selectedClass.getDeclaredConstructor().newInstance(); + + // Populate your fields (likely blank or default if no-arg constructor doesn't do much) + considerationName.setText(selectedClass.getSimpleName()); + ((ParametersTableModel) parametersTable.getModel()).setParameters(newInstance); + ((CurvePane) curveContainer).setCurve(DefaultCurve.Linear.getCurve()); + } catch (Exception ex) { + JOptionPane.showMessageDialog( + this, + "Failed to instantiate " + selectedClass.getSimpleName() + ": " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + }); } public void setConsideration(Consideration consideration) { - considerationComboBox.setSelectedItem(consideration); + considerationComboBox.setSelectedItem(TWConsiderationClass.fromClass(consideration.getClass())); considerationName.setText(consideration.getName()); - ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); + ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration); ((CurvePane) curveContainer).setCurve(consideration.getCurve()); } public void setEmptyConsideration() { considerationComboBox.setSelectedItem(null); considerationName.setText(""); - ((ParametersTableModel) parametersTable.getModel()).setParameters(Collections.emptyMap()); + ((ParametersTableModel) parametersTable.getModel()).setEmptyParameters(); ((CurvePane) curveContainer).setCurve(DefaultCurve.Logit.getCurve()); } @@ -73,21 +87,27 @@ public void setHoverStateModel(HoverStateModel model) { } public TWConsideration getConsideration() { - TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); - if (consideration == null) { + var selectedItem = (TWConsiderationClass) considerationComboBox.getSelectedItem(); + if (selectedItem == null) { throw new IllegalStateException("No consideration selected"); } - consideration.setName(considerationName.getText()); - consideration.setParameters(((ParametersTableModel) parametersTable.getModel()).getParameters()); - consideration.setCurve(((CurvePane) curveContainer).getCurve().copy()); - return consideration; + + try { + var consideration = (TWConsideration) selectedItem.getConsiderationClass().getDeclaredConstructor().newInstance(); + consideration.setName(considerationName.getText()); + consideration.setParameters(((ParametersTableModel) parametersTable.getModel()).getParameters()); + consideration.setCurve(((CurvePane) curveContainer).getCurve().copy()); + return consideration; + } catch (Exception ex) { + throw new IllegalStateException("Failed to instantiate " + selectedItem.getConsiderationClass().getSimpleName(), ex); + } } private void createUIComponents() { - parametersTable = new JTable(new ParametersTableModel()); + parametersTable = new ConsiderationParametersTable(new ParametersTableModel()); parametersTable.setModel(new ParametersTableModel()); - considerationComboBox = new JComboBox<>(TWUtilityAIRepository.getInstance().getConsiderations().toArray(new TWConsideration[0])); + considerationComboBox = new JComboBox<>(TWConsiderationClass.values()); considerationComboBox.setSelectedItem(null); curveContainer = new CurvePane(); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java new file mode 100644 index 00000000000..8abacd04a07 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; +import java.awt.*; + +public class ConsiderationParametersTable extends JTable { + + public ConsiderationParametersTable( + ParametersTableModel model) { + super(model); + } + + public void createUIComponents() { + // + } + + @Override + @SuppressWarnings("unchecked") + public ParametersTableModel getModel() { + return (ParametersTableModel) super.getModel(); + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + if (column == 1) { + var clazz = getModel().getParameterValueAt(row); + var value = getModel().getValueAt(row, column); + if (clazz == null) { + // Should actually throw an error here... + return super.getCellEditor(row, column); + } + if (clazz.equals(Boolean.class)) { + return new DefaultCellEditor(new JCheckBox()); + } else if (clazz.equals(Double.class)) { + return new SpinnerCellEditor( + (double) (value == null ? 0d : value), + Double.MIN_VALUE, + Double.MAX_VALUE, + 0.1d + ); + } else if (clazz.equals(Float.class)) { + return new SpinnerCellEditor( + (float) (value == null ? 0f : value), + Float.MIN_VALUE, + Float.MAX_VALUE, + 0.1f + ); + } else if (clazz.equals(Integer.class)) { + return new SpinnerCellEditor( + (int) (value == null ? 0 : value), + Integer.MIN_VALUE, + Integer.MAX_VALUE, + 1 + ); + } else if (clazz.equals(Long.class)) { + return new SpinnerCellEditor( + (long) (value == null ? 0 : value), + Long.MIN_VALUE, + Long.MAX_VALUE, + 1 + ); + } else if (clazz.equals(String.class)) { + return new DefaultCellEditor(new JTextField()); + } else if (clazz.isEnum()) { + var cb = new JComboBox<>( + clazz.getEnumConstants() + ); + return new DefaultCellEditor(cb); + } + } + return super.getCellEditor(row, column); + } + + public static class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { + private final JSpinner spinner; + + public SpinnerCellEditor(double defaultValue, double min, double max, double step) { + spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); + spinner.setValue(defaultValue); + JComponent editor = spinner.getEditor(); + if (editor instanceof JSpinner.DefaultEditor) { + JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); + textField.setHorizontalAlignment(JFormattedTextField.LEFT); + } + } + + @Override + public Object getCellEditorValue() { + return spinner.getValue(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + spinner.setValue(value); + return spinner; + } + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index cbca8f85afb..a9b996040f7 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -15,9 +15,8 @@ package megamek.client.ui.swing.ai.editor; -import com.intellij.uiDesigner.core.GridConstraints; -import com.intellij.uiDesigner.core.GridLayoutManager; -import megamek.ai.utility.*; +import megamek.ai.utility.Curve; +import megamek.ai.utility.DefaultCurve; import javax.swing.*; import java.awt.*; @@ -249,31 +248,54 @@ private void createUIComponents() { private void updateCurveDataUI() { curveGraph.repaint(); var curve = selectedCurve.get(); - if (curve instanceof LinearCurve) { - bParamSpinner.setValue(((LinearCurve) curve).getB()); - mParamSpinner.setValue(((LinearCurve) curve).getM()); - kParamSpinner.setEnabled(false); - cParamSpinner.setEnabled(false); - } else if (curve instanceof ParabolicCurve) { - bParamSpinner.setValue(((ParabolicCurve) curve).getB()); - mParamSpinner.setValue(((ParabolicCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((ParabolicCurve) curve).getK()); - cParamSpinner.setEnabled(false); - } else if (curve instanceof LogitCurve) { - bParamSpinner.setValue(((LogitCurve) curve).getB()); - mParamSpinner.setValue(((LogitCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((LogitCurve) curve).getK()); - cParamSpinner.setEnabled(true); - cParamSpinner.setValue(((LogitCurve) curve).getC()); - } else if (curve instanceof LogisticCurve) { - bParamSpinner.setValue(((LogisticCurve) curve).getB()); - mParamSpinner.setValue(((LogisticCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((LogisticCurve) curve).getK()); - cParamSpinner.setEnabled(true); - cParamSpinner.setValue(((LogisticCurve) curve).getC()); + Class curveClass = curve.getClass(); + + // B + boolean hasB = isMethodOverridden(curveClass, Curve.class, "getB"); + bParamSpinner.setEnabled(hasB); + if (hasB) { + bParamSpinner.setValue(curve.getB()); + } + + // M + boolean hasM = isMethodOverridden(curveClass, Curve.class, "getM"); + mParamSpinner.setEnabled(hasM); + if (hasM) { + mParamSpinner.setValue(curve.getM()); + } + + // K + boolean hasK = isMethodOverridden(curveClass, Curve.class, "getK"); + kParamSpinner.setEnabled(hasK); + if (hasK) { + kParamSpinner.setValue(curve.getK()); + } + + // C + boolean hasC = isMethodOverridden(curveClass, Curve.class, "getC"); + cParamSpinner.setEnabled(hasC); + if (hasC) { + cParamSpinner.setValue(curve.getC()); + } + } + + public static boolean isMethodOverridden( + Class clazz, + Class interfaceClass, + String methodName, + Class... paramTypes + ) { + try { + // The method from the interface + var interfaceMethod = interfaceClass.getMethod(methodName, paramTypes); + // The method as found on the concrete class + var classMethod = clazz.getMethod(methodName, paramTypes); + + // If the declaring class is different, the method is overridden + return !classMethod.getDeclaringClass().equals(interfaceMethod.getDeclaringClass()); + } catch (NoSuchMethodException e) { + // If the class or interface does not declare the method at all + return false; } } } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index 34af41a0a8b..a8b82c994e4 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -21,8 +21,9 @@ import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; import javax.swing.*; - import java.awt.*; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -40,6 +41,18 @@ public DecisionScoreEvaluatorPane() { $$$setupUI$$$(); add(decisionScoreEvaluatorPane, BorderLayout.WEST); hoverStateModel = new HoverStateModel(); + + // Add a MouseWheelListener to forward the event to the parent JScrollPane + this.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (getParent() instanceof JViewport viewport) { + if (viewport.getParent() instanceof JScrollPane scrollPane) { + scrollPane.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, scrollPane)); + } + } + } + }); } public TWDecisionScoreEvaluator getDecisionScoreEvaluator() { @@ -96,6 +109,7 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { considerationsPane.add(c, new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); considerationsPane.add(new JSeparator(), new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); } + this.updateUI(); } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java index 02522ae7135..4e492d5bc6e 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java @@ -21,7 +21,6 @@ import javax.swing.*; import javax.swing.table.TableCellEditor; -import java.awt.*; import java.util.List; public class DecisionScoreEvaluatorTable, DSE extends DecisionScoreEvaluator> extends JTable { @@ -66,30 +65,4 @@ public TableCellEditor getCellEditor(int row, int column) { return super.getCellEditor(row, column); } - - public static class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { - private final JSpinner spinner; - - public SpinnerCellEditor(double defaultValue, double min, double max, double step) { - spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); - spinner.setValue(defaultValue); - JComponent editor = spinner.getEditor(); - if (editor instanceof JSpinner.DefaultEditor) { - JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); - textField.setHorizontalAlignment(JFormattedTextField.LEFT); - } - } - - @Override - public Object getCellEditorValue() { - return spinner.getValue(); - } - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - spinner.setValue(value); - return spinner; - } - } - } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java index 92e576a30c8..fdb55469276 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java @@ -15,6 +15,7 @@ package megamek.client.ui.swing.ai.editor; +import megamek.ai.utility.Consideration; import megamek.logging.MMLogger; import javax.swing.table.AbstractTableModel; @@ -25,52 +26,32 @@ public class ParametersTableModel extends AbstractTableModel { private static final MMLogger logger = MMLogger.create(ParametersTableModel.class); - private final Map hashRows = new HashMap<>(); private final List rowValues = new ArrayList<>(); private final String[] columnNames = { "Name", "Value" }; - private final Class[] columnClasses = { String.class, Object.class }; - private record Row(String name, Object value) {} + private final Class[] columnClasses = { String.class, String.class }; + private record Row(String name, Object value, Class clazz) {} public ParametersTableModel() { } - public ParametersTableModel(Map parameters) { - this.hashRows.putAll(parameters); - for (Map.Entry entry : parameters.entrySet()) { - rowValues.add(new Row(entry.getKey(), entry.getValue())); - } + public void setParameters(Consideration consideration) { + setParameters(consideration.getParameters(), consideration.getParameterTypes()); } - public void setParameters(Map parameters) { - hashRows.clear(); + public void setEmptyParameters() { rowValues.clear(); - for (Map.Entry entry : parameters.entrySet()) { - hashRows.put(entry.getKey(), entry.getValue()); - rowValues.add(new Row(entry.getKey(), entry.getValue())); - } fireTableDataChanged(); } - public void addRow(String parameterName, Object value) { - if (hashRows.containsKey(parameterName)) { - logger.formattedErrorDialog("Parameter already exists", - "Could not add parameter {}, another parameters with the same name is already present.", parameterName); - return; - } - hashRows.put(parameterName, value); - rowValues.add(new Row(parameterName, value)); - fireTableRowsInserted(rowValues.size() - 1, rowValues.size() - 1); - } - - public String newParameterName() { - int i = 0; - while (hashRows.containsKey("Parameter " + i)) { - i++; - } - return "Parameter " + i; + private void setParameters(Map parameters, Map> parameterTypes) { + rowValues.clear(); + parameters.forEach((k, v) -> rowValues.add(new Row(k, v, parameterTypes.get(k)))); + fireTableDataChanged(); } public Map getParameters() { + Map hashRows = new HashMap<>(); + rowValues.forEach(row -> hashRows.put(row.name, row.value)); return hashRows; } @@ -96,7 +77,7 @@ public Class getColumnClass(int columnIndex) { @Override public boolean isCellEditable(int rowIndex, int columnIndex) { - return true; + return columnIndex == 1; } @Override @@ -108,21 +89,19 @@ public Object getValueAt(int rowIndex, int columnIndex) { return row.value; } + public Class getParameterValueAt(int rowIndex) { + return rowValues.get(rowIndex).clazz; + } + @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { var row = rowValues.get(rowIndex); if (columnIndex == 1) { - rowValues.set(rowIndex, new Row(row.name, aValue)); - hashRows.put(row.name, aValue); - } else { - if (hashRows.containsKey((String) aValue)) { - logger.formattedErrorDialog("Parameter already exists", - "Could not rename parameter %s, another parameters with the same name is already present.", aValue); - return; + if (aValue.getClass().equals(row.clazz)) { + rowValues.set(rowIndex, new Row(row.name, aValue, row.clazz)); + } else { + logger.error("Invalid value type: " + aValue.getClass() + " for " + row.clazz, "Invalid value type"); } - rowValues.set(rowIndex, new Row((String) aValue, row.value)); - hashRows.remove(row.name); - hashRows.put((String) aValue, row.value); } } } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java new file mode 100644 index 00000000000..285ab049708 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; +import java.awt.*; + +public class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { + private final JSpinner spinner; + + public SpinnerCellEditor(double defaultValue, double min, double max, double step) { + spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); + spinner.setValue(defaultValue); + JComponent editor = spinner.getEditor(); + if (editor instanceof JSpinner.DefaultEditor) { + JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); + textField.setHorizontalAlignment(JFormattedTextField.LEFT); + } + } + + @Override + public Object getCellEditorValue() { + return spinner.getValue(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + spinner.setValue(value); + return spinner; + } +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java b/megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java new file mode 100644 index 00000000000..7e2ef2e0487 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.ai.utility.Consideration; +import megamek.client.bot.duchess.ai.utility.tw.considerations.*; + +public enum TWConsiderationClass { + + MyUnitArmor(MyUnitArmor.class), + MyUnitIsCrippled(MyUnitIsCrippled.class), + MyUnitUnderThreat(MyUnitUnderThreat.class), + TargetUnitsArmor(TargetUnitsArmor.class), + TargetUnitsHaveRole(TargetUnitsHaveRole.class), + MyUnitRoleIs(MyUnitRoleIs.class), + TargetWithinRange(TargetWithinRange.class), + TargetWithinOptimalRange(TargetWithinOptimalRange.class); + + private final Class> considerationClass; + + TWConsiderationClass(Class> considerationClass) { + this.considerationClass = considerationClass; + } + + public Class> getConsiderationClass() { + return considerationClass; + } + + public static TWConsiderationClass fromClass(Class considerationClass) { + for (TWConsiderationClass twConsiderationClass : values()) { + if (twConsiderationClass.considerationClass.equals(considerationClass)) { + return twConsiderationClass; + } + } + return null; + } +} From ae16e42d0481e311c446c752fb2919a09c52582a Mon Sep 17 00:00:00 2001 From: Scoppio Date: Mon, 30 Dec 2024 22:22:54 -0300 Subject: [PATCH 12/16] feat: import and export ai files --- .../data/ai/tw/decisions/proto_decisions.yaml | 3 +- megamek/data/ai/tw/evaluators/proto_dse.yaml | 6 +- .../i18n/megamek/client/messages.properties | 28 ++- .../src/megamek/ai/utility/Consideration.java | 27 +-- megamek/src/megamek/ai/utility/Decision.java | 3 + .../ai/utility/tw/TWUtilityAIRepository.java | 166 +++++++++++++++++- .../client/ui/swing/CommonMenuBar.java | 2 +- .../ui/swing/ai/editor/AiProfileEditor.java | 93 +++++++--- 8 files changed, 265 insertions(+), 63 deletions(-) diff --git a/megamek/data/ai/tw/decisions/proto_decisions.yaml b/megamek/data/ai/tw/decisions/proto_decisions.yaml index f6dac7b3e47..de3361324cd 100644 --- a/megamek/data/ai/tw/decisions/proto_decisions.yaml +++ b/megamek/data/ai/tw/decisions/proto_decisions.yaml @@ -1,5 +1,4 @@ ---- -! +--- ! action: AttackUnit weight: 1.0 decisionScoreEvaluator: ! diff --git a/megamek/data/ai/tw/evaluators/proto_dse.yaml b/megamek/data/ai/tw/evaluators/proto_dse.yaml index 5ac8befb1a2..49e74178f79 100644 --- a/megamek/data/ai/tw/evaluators/proto_dse.yaml +++ b/megamek/data/ai/tw/evaluators/proto_dse.yaml @@ -1,5 +1,4 @@ ---- -! +--- ! name: "AttackEnemyInRange" description: "Evaluate if the enemy is optimal for attack." notes: "File for testing the load of the evaluator system." @@ -18,8 +17,7 @@ considerations: k: 10.0 c: 0.0 parameters: {} ---- -! +--- ! name: "Foobar" description: "Spam Spam" notes: "File for testing the load of the evaluator system." diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 55b387606a7..f95326a1618 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4825,7 +4825,7 @@ Bot.commands.aggression=Aggression Bot.commands.bravery=Bravery Bot.commands.avoid=Self-Preservation Bot.commands.caution=Piloting Caution -aiEditor.tree.title=Princess Data + #### TacOps movement and damage descriptions TacOps.leaping.leg_damage=leaping (leg damage) @@ -4836,17 +4836,27 @@ CommonMenuBar.AIEditorMenu=AI Editor CommonMenuBar.aiEditor.New=New Profile CommonMenuBar.aiEditor.Open=Open Profile CommonMenuBar.aiEditor.RecentProfile=Recent Profiles -CommonMenuBar.aiEditor.Save=Save Profile -CommonMenuBar.aiEditor.SaveAs=Save Profile As +CommonMenuBar.aiEditor.Save=Save All Changes +CommonMenuBar.aiEditor.SaveAs=Save As CommonMenuBar.aiEditor.ReloadFromDisk=Reload Profile from Disk CommonMenuBar.aiEditor.Undo=Undo CommonMenuBar.aiEditor.Redo=Redo CommonMenuBar.aiEditor.NewDecision=New Decision CommonMenuBar.aiEditor.NewConsideration=New Consideration CommonMenuBar.aiEditor.NewDecisionScoreEvaluator=New Decision Score Evaluator -CommonMenuBar.aiEditor.Export=Export Profile -CommonMenuBar.aiEditor.ExportConsiderations=Export Considerations -CommonMenuBar.aiEditor.ExportDSE=Export Decision Score Evaluators -CommonMenuBar.aiEditor.Import=Import Profile -CommonMenuBar.aiEditor.ImportConsiderations=Import Considerations -CommonMenuBar.aiEditor.ImportDSE=Import Decision Score Evaluators +CommonMenuBar.aiEditor.Export=Export... +CommonMenuBar.aiEditor.Import=Import... +aiEditor.Profiles=Profiles +aiEditor.Decisions=Decisions +aiEditor.DecisionScoreEvaluators=Decision Score Evaluators +aiEditor.Considerations=Considerations +aiEditor.tree.title=Princess Data +aiEditor.save.title=Save +aiEditor.export.title=Export AI configurations +aiEditor.save.filenameExtension=Megamek Utility AI (.uai) +aiEditor.export.error.title=Error exporting AI profile +aiEditor.export.error.message=An error occurred while saving the AI configuration. +aiEditor.import.title=Import AI configurations + +aiEditor.import.error.title=Error importing AI configuration +aiEditor.import.error.message=An error occurred while importing the AI configuration. diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java index abffd2d5c47..80216150785 100644 --- a/megamek/src/megamek/ai/utility/Consideration.java +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -13,16 +13,15 @@ */ package megamek.ai.utility; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.*; import megamek.client.bot.duchess.ai.utility.tw.considerations.*; +import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.StringJoiner; +import java.util.function.Function; +import java.util.stream.Collectors; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -46,7 +45,7 @@ public abstract class Consideration implements Named private Curve curve; @JsonProperty("parameters") protected Map parameters = Collections.emptyMap(); - + @JsonIgnore protected transient Map> parameterTypes = Collections.emptyMap(); public Consideration() { @@ -84,21 +83,23 @@ public Map> getParameterTypes() { return Map.copyOf(parameterTypes); } - public Class getParameterType(String key) { - return parameterTypes.get(key); - } - public void setParameters(Map parameters) { - var params = new HashMap(); for (var entry : parameters.entrySet()) { var clazz = parameterTypes.get(entry.getKey()); if (clazz == null) { throw new IllegalArgumentException("Unknown parameter: " + entry.getKey()); } - if (clazz.isAssignableFrom(entry.getValue().getClass())) { + if (clazz.isEnum() && entry.getValue() instanceof String value) { + var enumValues = ((Class) clazz).getEnumConstants(); + for (var anEnum : enumValues) { + if (anEnum.toString().equalsIgnoreCase(value)) { + parameters.put(entry.getKey(), anEnum); + break; + } + } + } else if (!clazz.isAssignableFrom(entry.getValue().getClass())) { throw new IllegalArgumentException("Invalid parameter type for " + entry.getKey() + ": " + entry.getValue().getClass()); } - params.put(entry.getKey(), entry.getValue()); } this.parameters = Map.copyOf(parameters); } diff --git a/megamek/src/megamek/ai/utility/Decision.java b/megamek/src/megamek/ai/utility/Decision.java index d55a3ab02dd..d54fd93aa06 100644 --- a/megamek/src/megamek/ai/utility/Decision.java +++ b/megamek/src/megamek/ai/utility/Decision.java @@ -15,6 +15,7 @@ package megamek.ai.utility; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -37,7 +38,9 @@ public class Decision implements NamedObject{ private Action action; private double weight; private DecisionScoreEvaluator decisionScoreEvaluator; + @JsonIgnore private transient double score; + @JsonIgnore private transient DecisionContext decisionContext; public Decision() { diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java index f6e1e5a0019..f0f9f163402 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -24,9 +24,13 @@ import megamek.common.Configuration; import megamek.logging.MMLogger; -import java.io.File; -import java.io.IOException; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; public class TWUtilityAIRepository { private static final MMLogger logger = MMLogger.create(TWUtilityAIRepository.class); @@ -91,20 +95,168 @@ public void persistDataToUserData() { persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); } + public void exportAiData(File zipOutput) throws IOException { + var tempFolder = Files.createTempDirectory("my-ai-export"); + var tempFile = tempFolder.toFile(); + + createDirectoryStructureIfMissing(tempFile); + persistToFile(new File(tempFile, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(tempFile, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values()); + persistToFile(new File(tempFile, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values()); + persistToFile(new File(tempFile, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); + + zipDirectory(tempFolder, zipOutput.toPath()); + deleteRecursively(tempFolder); + } + + public void importAiData(File zipInput) { + try { + unzipDirectory(zipInput.toPath()); + loadUserDataRepository(); + persistDataToUserData(); + deleteUserTempFiles(); + } catch (IOException e) { + logger.error(e, "Could not load data from file: {}", zipInput); + } + } + + private void deleteUserTempFiles() throws IOException { + var userFolder = Configuration.userDataAiTwDir(); + + Files.walk(userFolder.toPath()) + .sorted(Comparator.reverseOrder()) + .filter(p -> p.toFile().getName().startsWith("temp_")) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private void deleteRecursively(Path path) throws IOException { + // Walk the directory in reverse, so we delete children before parent + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + /** + * Create a new file, ensuring that it is within the destination directory. + * This covers against the vulnerability for zip slip attacks + * @param destinationDir + * @param zipEntry + * @return the new file + * @throws IOException + */ + public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException { + File destFile = new File(destinationDir, zipEntry.getName()); + + String destDirPath = destinationDir.getCanonicalPath(); + String destFilePath = destFile.getCanonicalPath(); + + if (!destFilePath.startsWith(destDirPath + File.separator)) { + throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); + } + + return destFile; + } + + private void unzipDirectory(Path zipFile) throws IOException { + var destDir = Configuration.userDataAiTwDir(); + + byte[] buffer = new byte[1024]; + var zis = new ZipInputStream(new FileInputStream(zipFile.toFile())); + ZipEntry zipEntry = zis.getNextEntry(); + + while (zipEntry != null) { + File newFile = newFile(destDir, zipEntry); + if (zipEntry.isDirectory()) { + if (!newFile.isDirectory() && !newFile.mkdirs()) { + throw new IOException("Failed to create directory " + newFile); + } + } else { + // fix for Windows-created archives + File parent = newFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Failed to create directory " + parent); + } + if (newFile.exists()) { + // rewrite the filename with current timestamp at the end before the extension + var newName = newFile.getName(); + newName = "temp_" + newName; + newFile = new File(parent, newName); + } + + // write file content + FileOutputStream fos = new FileOutputStream(newFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + } + zipEntry = zis.getNextEntry(); + } + + zis.closeEntry(); + zis.close(); + } + + private void zipDirectory(Path sourceDir, Path zipFile) throws IOException { + // Try-with-resources to ensure ZipOutputStream is closed + try (ZipOutputStream zs = new ZipOutputStream(Files.newOutputStream(zipFile))) { + // Walk the directory tree + Files.walk(sourceDir) + // Only zip up files, not directories themselves + .filter(path -> !Files.isDirectory(path)) + .forEach(path -> { + // Create a zip entry with a relative path + ZipEntry zipEntry = new ZipEntry(sourceDir.relativize(path).toString()); + try { + zs.putNextEntry(zipEntry); + Files.copy(path, zs); + zs.closeEntry(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + } + + public List getDecisions() { - return List.copyOf(decisions.values()); + var orderedList = new ArrayList<>(decisions.values()); + orderedList.sort(Comparator.comparing(TWDecision::getName)); + + return List.copyOf(orderedList); } public List getConsiderations() { - return List.copyOf(considerations.values()); + var orderedList = new ArrayList<>(considerations.values()); + orderedList.sort(Comparator.comparing(TWConsideration::getName)); + + return List.copyOf(orderedList); } public List getDecisionScoreEvaluators() { - return List.copyOf(decisionScoreEvaluators.values()); + var orderedList = new ArrayList<>(decisionScoreEvaluators.values()); + orderedList.sort(Comparator.comparing(TWDecisionScoreEvaluator::getName)); + + return List.copyOf(orderedList); } public List getProfiles() { - return List.copyOf(profiles.values()); + var orderedList = new ArrayList<>(profiles.values()); + orderedList.sort(Comparator.comparing(TWProfile::getName)); + return List.copyOf(orderedList); } public boolean hasDecision(String name) { @@ -177,7 +329,7 @@ private void loadUserDataRepository() { private void loadData(File directory) { loadConsiderations(new File(directory, CONSIDERATIONS)) - .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); + .forEach(twConsideration -> considerations.put(twConsideration.getName(), twConsideration)); loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach( twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); loadDecisions(new File(directory, DECISIONS)).forEach( diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java index 0fe6ad9e047..192f6b2d5dd 100644 --- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java +++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java @@ -393,7 +393,7 @@ public CommonMenuBar() { menu.addSeparator(); initMenuItem(aiEditorSave, menu, AI_EDITOR_SAVE); - initMenuItem(aiEditorSaveAs, menu, AI_EDITOR_SAVE_AS); +// initMenuItem(aiEditorSaveAs, menu, AI_EDITOR_SAVE_AS); initMenuItem(aiEditorReloadFromDisk, menu, AI_EDITOR_RELOAD_FROM_DISK); menu.addSeparator(); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 1e3dad06eed..ab6e1733c6d 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -35,11 +35,13 @@ import megamek.logging.MMLogger; import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.*; +import java.io.File; import java.lang.reflect.Method; import java.util.List; import java.util.Random; @@ -146,10 +148,23 @@ public void mouseClicked(MouseEvent e) { TreePath path = repositoryViewer.getPathForLocation(e.getX(), e.getY()); if (path != null) { repositoryViewer.setSelectionPath(path); + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + JPopupMenu contextMenu = createContextMenu(node); + contextMenu.show(repositoryViewer, e.getX(), e.getY()); + } + } + } + }); + + repositoryViewer.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + TreePath path = repositoryViewer.getSelectionPath(); + if (path != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (node.isLeaf()) { - JPopupMenu contextMenu = createContextMenu(node); - contextMenu.show(repositoryViewer, e.getX(), e.getY()); + handleOpenNodeAction(node); } } } @@ -233,6 +248,14 @@ private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { // Create a popup menu JPopupMenu contextMenu = new JPopupMenu(); var obj = node.getUserObject(); + if (obj instanceof String) { + JMenuItem menuItemAction = new JMenuItem("New " + obj); + menuItemAction.addActionListener(evt -> { + handleOpenNodeAction(node); + }); + contextMenu.add(menuItemAction); + } + // Example menu item #1 JMenuItem menuItemAction = new JMenuItem("Open"); menuItemAction.addActionListener(evt -> { @@ -474,6 +497,7 @@ public void actionPerformed(ActionEvent e) { createNewProfile(); break; case AI_EDITOR_OPEN: + // not implemented??? break; case AI_EDITOR_RECENT_PROFILE: break; @@ -484,7 +508,7 @@ public void actionPerformed(ActionEvent e) { // not implemented break; case AI_EDITOR_RELOAD_FROM_DISK: - + loadDataRepoViewer(); break; case AI_EDITOR_UNDO: break; @@ -499,10 +523,42 @@ public void actionPerformed(ActionEvent e) { case AI_EDITOR_NEW_DECISION_SCORE_EVALUATOR: createNewDecisionScoreEvaluator(); break; - case AI_EDITOR_EXPORT: - break; - case AI_EDITOR_IMPORT: + case AI_EDITOR_EXPORT: { + var fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(Messages.getString("aiEditor.export.title")); + fileChooser.setFileFilter(new FileNameExtensionFilter(Messages.getString("aiEditor.save.filenameExtension"), "uai")); + fileChooser.setAcceptAllFileFilterUsed(false); + int userSelection = fileChooser.showSaveDialog(this); + if (userSelection == JFileChooser.APPROVE_OPTION) { + File fileToSave = fileChooser.getSelectedFile(); + if (!fileToSave.getName().toLowerCase().endsWith(".uai")) { + fileToSave = new File(fileToSave + ".uai"); + } + try { + sharedData.exportAiData(fileToSave); + } catch (Exception ex) { + logger.formattedErrorDialog(Messages.getString("aiEditor.export.error.title"), + Messages.getString("aiEditor.export.error.message")); + } + } break; + } + case AI_EDITOR_IMPORT: { + var fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(Messages.getString("aiEditor.import.title")); + fileChooser.setFileFilter(new FileNameExtensionFilter(Messages.getString("aiEditor.save.filenameExtension"), "uai")); + fileChooser.setAcceptAllFileFilterUsed(false); + int userSelection = fileChooser.showOpenDialog(this); + if (userSelection == JFileChooser.APPROVE_OPTION) { + File fileToLoad = fileChooser.getSelectedFile(); + try { + sharedData.importAiData(fileToLoad); + } catch (Exception ex) { + logger.formattedErrorDialog(Messages.getString("aiEditor.import.error.title"), + Messages.getString("aiEditor.import.error.message")); + } + } + } } } @@ -535,23 +591,6 @@ private void createNewDecision() { ((DecisionTableModel) model).addRow(dse); } - private enum TreeViewHelper { - PROFILES("Profiles"), - DECISIONS("Decisions"), - DSE("Decision Score Evaluators (DSE)"), - CONSIDERATIONS("Considerations"); - - private final String name; - - TreeViewHelper(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - private void createUIComponents() { weightSpinner = new JSpinner(new SpinnerNumberModel(1d, 0d, 4d, 0.01d)); @@ -569,10 +608,10 @@ private void initializeProfileUI() { private void loadDataRepoViewer() { var root = new DefaultMutableTreeNode(Messages.getString("aiEditor.tree.title")); - addToMutableTreeNode(root, TreeViewHelper.PROFILES.getName(), sharedData.getProfiles()); - addToMutableTreeNode(root, TreeViewHelper.DECISIONS.getName(), sharedData.getDecisions()); - addToMutableTreeNode(root, TreeViewHelper.DSE.getName(), sharedData.getDecisionScoreEvaluators()); - addToMutableTreeNode(root, TreeViewHelper.CONSIDERATIONS.getName(), sharedData.getConsiderations()); + addToMutableTreeNode(root, Messages.getString("aiEditor.Profiles"), sharedData.getProfiles()); + addToMutableTreeNode(root, Messages.getString("aiEditor.Decisions"), sharedData.getDecisions()); + addToMutableTreeNode(root, Messages.getString("aiEditor.DecisionScoreEvaluators"), sharedData.getDecisionScoreEvaluators()); + addToMutableTreeNode(root, Messages.getString("aiEditor.Considerations"), sharedData.getConsiderations()); DefaultTreeModel treeModel = new DefaultTreeModel(root); if (repositoryViewer == null) { From 448b7925c71a28009b2c515e63a84e9a0d629776 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Mon, 30 Dec 2024 23:25:51 -0300 Subject: [PATCH 13/16] feat: small improvements on internationalization --- .../i18n/megamek/client/messages.properties | 13 ++ .../megamek/client/ui/swing/MegaMekGUI.java | 4 +- .../ui/swing/ai/editor/AiProfileEditor.java | 162 +++++++++--------- 3 files changed, 100 insertions(+), 79 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index f95326a1618..c828535c1c5 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4860,3 +4860,16 @@ aiEditor.import.title=Import AI configurations aiEditor.import.error.title=Error importing AI configuration aiEditor.import.error.message=An error occurred while importing the AI configuration. +aiEditor.new.profile=New Profile +aiEditor.new.decision=New Decision +aiEditor.new.consideration=New Consideration +aiEditor.new.dse=New Decision Score Evaluator + +aiEditor.add.to.profile=Add to Profile +aiEditor.copy.profile=Copy Profile +aiEditor.item.copy=(copy) +aiEditor.contextualMenu.delete=Delete +aiEditor.add.to.dse=Add to Decision Score Evaluator +aiEditor.add.to.decision=Add to Decision +aiEditor.save.error.title=Error saving data +aiEditor.save.error.message=One or more mandatory fields are empty or invalid. Please correct the errors and try again. \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index e545c78c731..3269d1ea790 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -344,10 +344,10 @@ private void showMainMenu() { // c.gridy++; // addBag(connectSBF, gridbag, c); - c.gridy++; - addBag(editAi, gridbag, c); } + c.gridy++; + addBag(editAi, gridbag, c); c.gridy++; c.insets = new Insets(4, 4, 15, 12); addBag(quitB, gridbag, c); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index ab6e1733c6d..12d37119c66 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -171,15 +171,6 @@ public void keyPressed(KeyEvent e) { } }); -// repositoryViewer.addTreeSelectionListener(e -> { -// TreePath path = e.getPath(); -// if (path != null) { -// DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); -// if (node.isLeaf()) { -// handleOpenNodeAction(node); -// } -// } -// }); getFrame().setJMenuBar(menuBar); menuBar.addActionListener(this); saveProfileButton.addActionListener(e -> { @@ -249,86 +240,103 @@ private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { JPopupMenu contextMenu = new JPopupMenu(); var obj = node.getUserObject(); if (obj instanceof String) { - JMenuItem menuItemAction = new JMenuItem("New " + obj); + if (obj.equals(Messages.getString("aiEditor.Profiles"))) { + JMenuItem menuItemAction = new JMenuItem(Messages.getString("aiEditor.new.profile")); + menuItemAction.addActionListener(evt -> { + createNewProfile(); + }); + contextMenu.add(menuItemAction); + } else if (obj.equals(Messages.getString("aiEditor.Decisions"))) { + JMenuItem menuItemAction = new JMenuItem(Messages.getString("aiEditor.new.decision")); + menuItemAction.addActionListener(evt -> { + createNewDecision(); + }); + contextMenu.add(menuItemAction); + } else if (obj.equals(Messages.getString("aiEditor.DecisionScoreEvaluators"))) { + JMenuItem menuItemAction = new JMenuItem(Messages.getString("aiEditor.new.dse")); + menuItemAction.addActionListener(evt -> { + createNewDecisionScoreEvaluator(); + }); + contextMenu.add(menuItemAction); + } else if (obj.equals(Messages.getString("aiEditor.Considerations"))) { + JMenuItem menuItemAction = new JMenuItem(Messages.getString("aiEditor.new.consideration")); + menuItemAction.addActionListener(evt -> { + addNewConsideration(); + }); + contextMenu.add(menuItemAction); + } + } else { + JMenuItem menuItemAction = new JMenuItem("Open"); menuItemAction.addActionListener(evt -> { handleOpenNodeAction(node); }); contextMenu.add(menuItemAction); - } - // Example menu item #1 - JMenuItem menuItemAction = new JMenuItem("Open"); - menuItemAction.addActionListener(evt -> { - handleOpenNodeAction(node); - }); - contextMenu.add(menuItemAction); - - if (obj instanceof TWDecision twDecision) { - var action = new JMenuItem("Add to current Profile"); - action.addActionListener(evt -> { - var model = profileDecisionTable.getModel(); - //noinspection unchecked - ((DecisionTableModel) model).addRow(twDecision); - }); - contextMenu.add(action); - } else if (obj instanceof TWProfile) { - var action = getCopyProfileMenuItem((TWProfile) obj); - contextMenu.add(action); - } else if (obj instanceof TWDecisionScoreEvaluator twDse) { - var action = new JMenuItem("New Decision Score Evaluator"); - action.addActionListener(evt -> { - createNewDecisionScoreEvaluator(); - }); - contextMenu.add(action); - } else if (obj instanceof TWConsideration twConsideration) { - var action = new JMenuItem("New Consideration"); - action.addActionListener(evt -> { - addNewConsideration(); - }); - contextMenu.add(action); - // if the tab is a DSE, add the consideration to the DSE - if (mainEditorTabbedPane.getSelectedComponent() == dseTabPane) { - var action1 = new JMenuItem("Add to current Decision Score Evaluator"); - action1.addActionListener(evt -> { - var dse = ((DecisionScoreEvaluatorPane) dsePane).getDecisionScoreEvaluator(); - dse.addConsideration(twConsideration); - ((DecisionScoreEvaluatorPane) dsePane).setDecisionScoreEvaluator(dse); + if (obj instanceof TWDecision twDecision) { + var action = new JMenuItem(Messages.getString("aiEditor.add.to.profile")); + action.addActionListener(evt -> { + var model = profileDecisionTable.getModel(); + //noinspection unchecked + ((DecisionTableModel) model).addRow(twDecision); }); - contextMenu.add(action1); - } else if (mainEditorTabbedPane.getSelectedComponent() == decisionTabPane) { - var action1 = new JMenuItem("Add to current Decision"); - action1.addActionListener(evt -> { - var dse = ((DecisionScoreEvaluatorPane)decisionTabDsePanel).getDecisionScoreEvaluator(); - dse.addConsideration(twConsideration); - ((DecisionScoreEvaluatorPane) decisionTabDsePanel).setDecisionScoreEvaluator(dse); + contextMenu.add(action); + } else if (obj instanceof TWProfile) { + var action = getCopyProfileMenuItem((TWProfile) obj); + contextMenu.add(action); + } else if (obj instanceof TWDecisionScoreEvaluator) { + var action = new JMenuItem(Messages.getString("aiEditor.new.dse")); + action.addActionListener(evt -> { + createNewDecisionScoreEvaluator(); }); - contextMenu.add(action1); - } - } - - // Example menu item #2 - JMenuItem menuItemOther = new JMenuItem("Delete"); - menuItemOther.addActionListener(evt -> { - // Another action - int deletePrompt = JOptionPane.showConfirmDialog(null, - Messages.getString("aiEditor.deleteNodePrompt"), - Messages.getString("BoardEditor.deleteNodeTitle"), - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - if (deletePrompt == JOptionPane.YES_OPTION) { - handleDeleteNodeAction(node); + contextMenu.add(action); + } else if (obj instanceof TWConsideration twConsideration) { + var action = new JMenuItem(Messages.getString("aiEditor.new.consideration")); + action.addActionListener(evt -> { + addNewConsideration(); + }); + contextMenu.add(action); + // if the tab is a DSE, add the consideration to the DSE + if (mainEditorTabbedPane.getSelectedComponent() == dseTabPane) { + var action1 = new JMenuItem(Messages.getString("aiEditor.add.to.dse")); + action1.addActionListener(evt -> { + var dse = ((DecisionScoreEvaluatorPane) dsePane).getDecisionScoreEvaluator(); + dse.addConsideration(twConsideration); + ((DecisionScoreEvaluatorPane) dsePane).setDecisionScoreEvaluator(dse); + }); + contextMenu.add(action1); + } else if (mainEditorTabbedPane.getSelectedComponent() == decisionTabPane) { + var action1 = new JMenuItem(Messages.getString("aiEditor.add.to.decision")); + action1.addActionListener(evt -> { + var dse = ((DecisionScoreEvaluatorPane) decisionTabDsePanel).getDecisionScoreEvaluator(); + dse.addConsideration(twConsideration); + ((DecisionScoreEvaluatorPane) decisionTabDsePanel).setDecisionScoreEvaluator(dse); + }); + contextMenu.add(action1); + } } - }); - contextMenu.add(menuItemOther); + JMenuItem menuItemOther = new JMenuItem(Messages.getString("aiEditor.contextualMenu.delete")); + menuItemOther.addActionListener(evt -> { + // Another action + int deletePrompt = JOptionPane.showConfirmDialog(null, + Messages.getString("aiEditor.deleteNodePrompt"), + Messages.getString("aiEditor.deleteNode.title"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + if (deletePrompt == JOptionPane.YES_OPTION) { + handleDeleteNodeAction(node); + } + }); + contextMenu.add(menuItemOther); + } return contextMenu; } private JMenuItem getCopyProfileMenuItem(TWProfile obj) { - var action = new JMenuItem("Copy Profile"); + var action = new JMenuItem(Messages.getString("aiEditor.copy.profile")); action.addActionListener(evt -> { profileId = -1; - profileNameTextField.setText(obj.getName() + " (Copy)"); + profileNameTextField.setText(obj.getName() + " " + Messages.getString("aiEditor.item.copy")); descriptionTextField.setText(obj.getDescription()); profileDecisionTable.setModel(new DecisionTableModel<>(obj.getDecisions())); mainEditorTabbedPane.setSelectedComponent(profileTabPane); @@ -439,8 +447,8 @@ private boolean saveEverything() { } return true; } catch (IllegalArgumentException ex) { - logger.formattedErrorDialog("Error saving data", - "One or more fields are empty or invalid. Please correct the errors and try again."); + logger.formattedErrorDialog(Messages.getString("aiEditor.save.error.title"), + Messages.getString("aiEditor.save.error.message") + ": " + ex.getMessage()); } return false; } @@ -564,7 +572,7 @@ public void actionPerformed(ActionEvent e) { private void createNewProfile() { profileId = -1; - profileNameTextField.setText("New Profile"); + profileNameTextField.setText(Messages.getString("aiEditor.new.profile")); descriptionTextField.setText(""); initializeProfileUI(); mainEditorTabbedPane.setSelectedComponent(profileTabPane); From f4fc0da755882fdca4e9af2f6f9c9e963f4ff320 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 1 Jan 2025 01:32:18 -0300 Subject: [PATCH 14/16] fix: fixed error where property k would show up twice for the deserializer --- megamek/src/megamek/ai/utility/LogitCurve.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/src/megamek/ai/utility/LogitCurve.java b/megamek/src/megamek/ai/utility/LogitCurve.java index b1c95abf120..4c6bcbb1e14 100644 --- a/megamek/src/megamek/ai/utility/LogitCurve.java +++ b/megamek/src/megamek/ai/utility/LogitCurve.java @@ -36,7 +36,7 @@ public LogitCurve( @JsonProperty("m") double m, @JsonProperty("b") double b, @JsonProperty("k") double k, - @JsonProperty("k") double c) { + @JsonProperty("c") double c) { this.m = m; this.b = b; this.k = k; From 6454c6db646a0ab895101f4c560bbca937f43666 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Thu, 2 Jan 2025 09:25:44 -0300 Subject: [PATCH 15/16] feat: adding toolbar to DSE and Profile --- .../i18n/megamek/client/messages.properties | 13 ++++++++-- .../ui/swing/ai/editor/AiProfileEditor.form | 14 +++++++++-- .../ui/swing/ai/editor/AiProfileEditor.java | 25 ++++++++++++++++--- .../ai/editor/DecisionScoreEvaluatorPane.form | 14 +++++++++-- .../ai/editor/DecisionScoreEvaluatorPane.java | 22 +++++++++++++--- .../swing/ai/editor/DecisionTableModel.java | 4 +++ 6 files changed, 80 insertions(+), 12 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index c828535c1c5..fff794ca3ce 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4861,14 +4861,23 @@ aiEditor.import.title=Import AI configurations aiEditor.import.error.title=Error importing AI configuration aiEditor.import.error.message=An error occurred while importing the AI configuration. aiEditor.new.profile=New Profile +aiEditor.new.profile.description=Created at %s aiEditor.new.decision=New Decision -aiEditor.new.consideration=New Consideration +aiEditor.edit.decision=Edit Decision +aiEditor.delete.decision=Remove Decision +aiEditor.copy.decision=Duplicate Decision + aiEditor.new.dse=New Decision Score Evaluator +aiEditor.new.consideration=New Consideration +aiEditor.edit.consideration=Edit Consideration +aiEditor.delete.consideration=Remove Consideration +aiEditor.copy.consideration=Duplicate Consideration + aiEditor.add.to.profile=Add to Profile aiEditor.copy.profile=Copy Profile aiEditor.item.copy=(copy) -aiEditor.contextualMenu.delete=Delete +aiEditor.contextualMenu.delete=Remove aiEditor.add.to.dse=Add to Decision Score Evaluator aiEditor.add.to.decision=Add to Decision aiEditor.save.error.title=Error saving data diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form index e7453982416..509d79430fe 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.form @@ -82,7 +82,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -155,6 +155,16 @@ + + + + + + + + + +
diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 12d37119c66..b2ee7b55334 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -43,6 +43,7 @@ import java.awt.event.*; import java.io.File; import java.lang.reflect.Method; +import java.util.Date; import java.util.List; import java.util.Random; import java.util.ResourceBundle; @@ -77,6 +78,7 @@ public class AiProfileEditor extends JFrame implements ActionListener { private JButton saveDseButton; private JButton saveConsiderationButton; private JButton saveDecisionButton; + private JToolBar profileTools; private ConsiderationPane considerationPane; @@ -116,6 +118,20 @@ private void initialize() { considerationPane = new ConsiderationPane(); considerationPane.setMinimumSize(new Dimension(considerationEditorPanel.getWidth(), considerationEditorPanel.getHeight())); considerationEditorPanel.add(considerationPane, gbc); + + // Profile toolbar + + var newDecisionBtn = new JButton(Messages.getString("aiEditor.new.decision")); + var copyDecisionBtn = new JButton(Messages.getString("aiEditor.copy.decision")); + var editDecisionBtn = new JButton(Messages.getString("aiEditor.edit.decision")); + var deleteDecisionBtn = new JButton(Messages.getString("aiEditor.delete.decision")); + + profileTools.add(newDecisionBtn); + profileTools.add(copyDecisionBtn); + profileTools.add(editDecisionBtn); + profileTools.add(deleteDecisionBtn); + + // Setup window frame behaviors this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); this.addWindowListener(new WindowAdapter() { @Override @@ -573,7 +589,8 @@ public void actionPerformed(ActionEvent e) { private void createNewProfile() { profileId = -1; profileNameTextField.setText(Messages.getString("aiEditor.new.profile")); - descriptionTextField.setText(""); + descriptionTextField.setText(Messages.getString("aiEditor.new.profile.description", new Date())); + profileDecisionTable.setModel(new DecisionTableModel<>()); initializeProfileUI(); mainEditorTabbedPane.setSelectedComponent(profileTabPane); profileTabPane.updateUI(); @@ -674,12 +691,12 @@ private void addToMutableTreeNode(DefaultMutableTreeNode mainEditorTabbedPane = new JTabbedPane(); panel2.add(mainEditorTabbedPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); profileTabPane = new JPanel(); - profileTabPane.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); + profileTabPane.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.profile"), profileTabPane); profileScrollPane = new JScrollPane(); profileScrollPane.setDoubleBuffered(false); profileScrollPane.setWheelScrollingEnabled(true); - profileTabPane.add(profileScrollPane, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + profileTabPane.add(profileScrollPane, new GridConstraints(2, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); profileDecisionTable.setColumnSelectionAllowed(false); profileDecisionTable.setDragEnabled(true); profileDecisionTable.setFillsViewportHeight(true); @@ -700,6 +717,8 @@ private void addToMutableTreeNode(DefaultMutableTreeNode final JLabel label2 = new JLabel(); this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "AiEditor.description")); panel3.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + profileTools = new JToolBar(); + profileTabPane.add(profileTools, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(-1, 20), null, 0, false)); decisionTabPane = new JPanel(); decisionTabPane.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); mainEditorTabbedPane.addTab(this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.tab.decision"), decisionTabPane); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form index 67da208fd11..002c54a2005 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.form @@ -1,6 +1,6 @@ - + @@ -34,7 +34,7 @@ - + @@ -92,6 +92,16 @@ + + + + + + + + + + diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index a8b82c994e4..9f2c221f80d 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -19,6 +19,7 @@ import com.intellij.uiDesigner.core.GridLayoutManager; import megamek.ai.utility.DecisionScoreEvaluator; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; +import megamek.client.ui.Messages; import javax.swing.*; import java.awt.*; @@ -35,13 +36,26 @@ public class DecisionScoreEvaluatorPane extends JPanel { private JTextField notesField; private JPanel decisionScoreEvaluatorPane; private JPanel considerationsPane; + private JToolBar considerationsToolbar; private final HoverStateModel hoverStateModel; private final List considerationPaneList = new ArrayList<>(); + public DecisionScoreEvaluatorPane() { $$$setupUI$$$(); add(decisionScoreEvaluatorPane, BorderLayout.WEST); hoverStateModel = new HoverStateModel(); + // Considerations Toolbar + var newConsiderationBtn = new JButton(Messages.getString("aiEditor.new.consideration")); + var copyConsiderationBtn = new JButton(Messages.getString("aiEditor.copy.consideration")); + var editConsiderationBtn = new JButton(Messages.getString("aiEditor.edit.consideration")); + var deleteConsiderationBtn = new JButton(Messages.getString("aiEditor.delete.consideration")); + + considerationsToolbar.add(newConsiderationBtn); + considerationsToolbar.add(copyConsiderationBtn); + considerationsToolbar.add(editConsiderationBtn); + considerationsToolbar.add(deleteConsiderationBtn); + // Add a MouseWheelListener to forward the event to the parent JScrollPane this.addMouseWheelListener(new MouseWheelListener() { @Override @@ -68,7 +82,7 @@ public TWDecisionScoreEvaluator getDecisionScoreEvaluator() { public void addEmptyConsideration() { considerationsPane.removeAll(); - considerationsPane.setLayout(new GridLayoutManager((considerationPaneList.size()+1) * 2, 1, new Insets(0, 0, 0, 0), -1, -1)); + considerationsPane.setLayout(new GridLayoutManager((considerationPaneList.size() + 1) * 2, 1, new Insets(0, 0, 0, 0), -1, -1)); int row = 0; var emptyConsideration = new ConsiderationPane(); emptyConsideration.setEmptyConsideration(); @@ -121,7 +135,7 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { */ private void $$$setupUI$$$() { decisionScoreEvaluatorPane = new JPanel(); - decisionScoreEvaluatorPane.setLayout(new GridLayoutManager(9, 1, new Insets(0, 0, 0, 0), -1, -1)); + decisionScoreEvaluatorPane.setLayout(new GridLayoutManager(10, 1, new Insets(0, 0, 0, 0), -1, -1)); nameField = new JTextField(); decisionScoreEvaluatorPane.add(nameField, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); descriptionField = new JTextField(); @@ -133,7 +147,7 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { scrollPane1.setMaximumSize(new Dimension(800, 32767)); scrollPane1.setMinimumSize(new Dimension(800, 600)); scrollPane1.setWheelScrollingEnabled(true); - decisionScoreEvaluatorPane.add(scrollPane1, new GridConstraints(7, 0, 2, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + decisionScoreEvaluatorPane.add(scrollPane1, new GridConstraints(8, 0, 2, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); considerationsPane = new JPanel(); considerationsPane.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); considerationsPane.setMaximumSize(new Dimension(800, 2147483647)); @@ -151,6 +165,8 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { final JLabel label4 = new JLabel(); this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("megamek/common/options/messages", "aiEditor.name")); decisionScoreEvaluatorPane.add(label4, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + considerationsToolbar = new JToolBar(); + decisionScoreEvaluatorPane.add(considerationsToolbar, new GridConstraints(7, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(-1, 20), null, 0, false)); label1.setLabelFor(scrollPane1); label2.setLabelFor(notesField); label3.setLabelFor(descriptionField); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java index a62390e0928..a7763b4f3fc 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionTableModel.java @@ -31,6 +31,10 @@ public class DecisionTableModel> extends Abstract private final String[] columnNames = { "ID", "Decision", "Weight", "Evaluator" }; private final Set editableColumns = Set.of(1, 2, 3); + public DecisionTableModel() { + this.rows = new ArrayList<>(); + } + public DecisionTableModel(List initialRows) { this.rows = new ArrayList<>(initialRows); } From 6778e1bf0faecddf42466b888c84c56518f9fb9d Mon Sep 17 00:00:00 2001 From: Scoppio Date: Thu, 9 Jan 2025 16:58:12 -0300 Subject: [PATCH 16/16] chore: removing comments that borked javadoc --- .../client/ui/swing/ai/editor/AiProfileEditor.java | 7 ------- .../client/ui/swing/ai/editor/ConsiderationPane.java | 7 ------- .../src/megamek/client/ui/swing/ai/editor/CurvePane.java | 7 ------- .../ui/swing/ai/editor/DecisionScoreEvaluatorPane.java | 8 +------- 4 files changed, 1 insertion(+), 28 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index b2ee7b55334..10d51d6d114 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -661,7 +661,6 @@ private void addToMutableTreeNode(DefaultMutableTreeNode * >>> IMPORTANT!! <<< * DO NOT edit this method OR call it in your code! * - * @noinspection ALL */ private void $$$setupUI$$$() { createUIComponents(); @@ -777,9 +776,6 @@ private void addToMutableTreeNode(DefaultMutableTreeNode return bundle.getString(key); } - /** - * @noinspection ALL - */ private void $$$loadLabelText$$$(JLabel component, String text) { StringBuffer result = new StringBuffer(); boolean haveMnemonic = false; @@ -804,9 +800,6 @@ private void addToMutableTreeNode(DefaultMutableTreeNode } } - /** - * @noinspection ALL - */ public JComponent $$$getRootComponent$$$() { return uAiEditorPanel; } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index 75c51685301..b8ad0f32daa 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -118,7 +118,6 @@ private void createUIComponents() { * >>> IMPORTANT!! <<< * DO NOT edit this method OR call it in your code! * - * @noinspection ALL */ private void $$$setupUI$$$() { createUIComponents(); @@ -171,9 +170,6 @@ private void createUIComponents() { return bundle.getString(key); } - /** - * @noinspection ALL - */ private void $$$loadLabelText$$$(JLabel component, String text) { StringBuffer result = new StringBuffer(); boolean haveMnemonic = false; @@ -198,9 +194,6 @@ private void createUIComponents() { } } - /** - * @noinspection ALL - */ public JComponent $$$getRootComponent$$$() { return considerationPane; } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index a9b996040f7..33999e6b77c 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -60,7 +60,6 @@ public void setHoverStateModel(HoverStateModel model) { * >>> IMPORTANT!! <<< * DO NOT edit this method OR call it in your code! * - * @noinspection ALL */ private void $$$setupUI$$$() { createUIComponents(); @@ -166,9 +165,6 @@ public void setHoverStateModel(HoverStateModel model) { return bundle.getString(key); } - /** - * @noinspection ALL - */ private void $$$loadLabelText$$$(JLabel component, String text) { StringBuffer result = new StringBuffer(); boolean haveMnemonic = false; @@ -193,9 +189,6 @@ public void setHoverStateModel(HoverStateModel model) { } } - /** - * @noinspection ALL - */ public JComponent $$$getRootComponent$$$() { return basePane; } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index 9f2c221f80d..2b504b0a7d4 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -131,7 +131,7 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { * >>> IMPORTANT!! <<< * DO NOT edit this method OR call it in your code! * - * @noinspection ALL + * */ private void $$$setupUI$$$() { decisionScoreEvaluatorPane = new JPanel(); @@ -190,9 +190,6 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { return bundle.getString(key); } - /** - * @noinspection ALL - */ private void $$$loadLabelText$$$(JLabel component, String text) { StringBuffer result = new StringBuffer(); boolean haveMnemonic = false; @@ -217,9 +214,6 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { } } - /** - * @noinspection ALL - */ public JComponent $$$getRootComponent$$$() { return decisionScoreEvaluatorPane; }