Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Equipment/Unit modifiers (salvage quality, partial repair, scenario effect) #6229

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions megamek/docs/Scenarios/ScenarioV2 HowTo.mms
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,41 @@ factions:
# motive: 1
# firecontrol: 1

# quality, partial repair modifiers
modifiers:
# the locations are not a list
LA:
# the slots must always be a list!
- slot: 4
modifiers:
# the modifiers can be a list or a single entry
# misfire
- type: misfire
# The roll results to misfire on, must always be a list, not a single number
on: [ 2, 3 ]
# heat adds the given delta to the weapon heat
- type: heat
delta: 2
# damage obviously adds the given delta to the weapon's damage
- type: damage
delta: -1
# tohit changes the tohit target number by the given delta
- type: tohit
delta: 1

RA:
- slot: 1
modifier:
type: heat
delta: 2

# ammo types and reduced amount
# this usually requires looking up the unit file and possibly AmmoType.java for the type designations
ammo:
LA:
slot: 5
shots: 2
# the slots must always be a list!
- slot: 5
shots: 2
# type: xyz (TODO)

# Optional: give details of the crew/pilot - currently only for single pilots (TODO)
Expand Down
3 changes: 3 additions & 0 deletions megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,9 @@ MekView.Pod=Pod
MekView.Fixed=Fixed
MekView.Quirks=Quirks
MekView.WeaponQuirks=Weapon Quirks
MekView.WeaponMods=Weapon Modifiers
MekView.SalvageQuality=Salvage Quality
MekView.PartialRepair=Partial Repair
MekView.InvalidReasons=Invalid because:
MekView.InvalidButIllegalQuirk=Illegal Design Quirk, would be invalid because:

Expand Down
30 changes: 15 additions & 15 deletions megamek/src/megamek/client/ui/swing/unitDisplay/WeaponPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
Expand All @@ -37,7 +36,6 @@
import javax.swing.event.ListSelectionListener;
import javax.swing.event.MouseInputAdapter;

import megamek.MMConstants;
import megamek.client.Client;
import megamek.client.event.MekDisplayEvent;
import megamek.client.ui.GBC;
Expand All @@ -48,7 +46,6 @@
import megamek.client.ui.swing.GUIPreferences;
import megamek.client.ui.swing.TargetingPhaseDisplay;
import megamek.client.ui.swing.tooltip.UnitToolTip;
import megamek.client.ui.swing.util.UIUtil;
import megamek.client.ui.swing.widget.BackGroundDrawer;
import megamek.client.ui.swing.widget.PMUtil;
import megamek.client.ui.swing.widget.PicMap;
Expand All @@ -59,6 +56,7 @@
import megamek.common.enums.WeaponSortOrder;
import megamek.common.equipment.AmmoMounted;
import megamek.common.equipment.WeaponMounted;
import megamek.common.modifiers.DamageModifier;
import megamek.common.options.OptionsConstants;
import megamek.common.preference.IPreferenceChangeListener;
import megamek.common.preference.PreferenceChangeEvent;
Expand Down Expand Up @@ -1854,21 +1852,15 @@ private void displaySelected() {
&& (unitDisplay.getClientGUI() != null)
&& unitDisplay.getClientGUI().getClient().getGame().getOptions().booleanOption(
OptionsConstants.ADVCOMBAT_TACOPS_ENERGY_WEAPONS)) {
int damage = Compute.dialDownDamage(mounted, wtype);
if (mounted.hasChargedCapacitor() != 0) {
if (mounted.hasChargedCapacitor() == 1) {
wDamR.setText(Integer.toString(Compute.dialDownDamage(
mounted, wtype) + 5));
}
if (mounted.hasChargedCapacitor() == 2) {
wDamR.setText(Integer.toString(Compute.dialDownDamage(
mounted, wtype) + 10));
}
} else {
wDamR.setText(Integer.toString(Compute.dialDownDamage(
mounted, wtype)));
damage += mounted.hasChargedCapacitor() * 5;
}
damage = Math.max(0, damage + damageModification(mounted));
wDamR.setText(Integer.toString(damage));
} else {
wDamR.setText(Integer.toString(wtype.getDamage()));
int damage = Math.max(0, wtype.getDamage() + damageModification(mounted));
wDamR.setText(Integer.toString(damage));
}

// update range
Expand Down Expand Up @@ -2107,6 +2099,14 @@ private String formatAmmo(Mounted<?> m) {
return sb.toString();
}

private int damageModification(WeaponMounted weapon) {
return weapon.getModifiers().stream()
.filter(m -> m instanceof DamageModifier)
.map(m -> (DamageModifier) m)
.mapToInt(DamageModifier::getDeltaDamage)
.sum();
}

private String formatBayWeapon(WeaponMounted m) {
StringBuffer sb = new StringBuffer(64);
sb.append(m.getDesc());
Expand Down
106 changes: 72 additions & 34 deletions megamek/src/megamek/common/MekView.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import megamek.common.equipment.WeaponMounted;
import megamek.common.eras.Era;
import megamek.common.eras.Eras;
import megamek.common.modifiers.*;
import megamek.common.options.IOption;
import megamek.common.options.OptionsConstants;
import megamek.common.options.PilotOptions;
Expand Down Expand Up @@ -528,10 +529,11 @@ public MekView(final Entity entity, final boolean showDetail, final boolean useA

Game game = entity.getGame();

// Unit and Weapon Quirks
if ((game == null) || game.getOptions().booleanOption(OptionsConstants.ADVANCED_STRATOPS_QUIRKS)) {
List<String> activeUnitQuirksNames = entity.getQuirks().activeQuirks().stream()
.map(IOption::getDisplayableNameWithValue)
.collect(Collectors.toList());
.toList();

if (!activeUnitQuirksNames.isEmpty()) {
sQuirks.add(new SingleLine());
Expand All @@ -543,8 +545,8 @@ public MekView(final Entity entity, final boolean showDetail, final boolean useA
List<String> wpQuirksList = new ArrayList<>();
for (Mounted<?> weapon : entity.getWeaponList()) {
List<String> activeWeaponQuirksNames = weapon.getQuirks().activeQuirks().stream()
.map(IOption::getDisplayableNameWithValue)
.collect(Collectors.toList());
.map(IOption::getDisplayableNameWithValue)
.toList();
if (!activeWeaponQuirksNames.isEmpty()) {
String wq = weapon.getDesc() + " (" + entity.getLocationAbbr(weapon.getLocation()) + "): ";
wq += String.join(", ", activeWeaponQuirksNames);
Expand All @@ -558,6 +560,24 @@ public MekView(final Entity entity, final boolean showDetail, final boolean useA
sQuirks.add(list);
}
}

// Equipment and Unit modifiers (salvage quality etc)
List<String> weaponModifierList = new ArrayList<>();
for (WeaponMounted weapon : entity.getWeaponList()) {
List<String> weaponModifiers = getEquipmentModifiers(weapon);
if (!weaponModifiers.isEmpty()) {
String wq = weapon.getDesc() + " (" + entity.getLocationAbbr(weapon.getLocation()) + "): ";
wq += String.join(", ", weaponModifiers);
weaponModifierList.add(wq);
}
}
if (!weaponModifierList.isEmpty()) {
sQuirks.add(new SingleLine());
ItemList list = new ItemList(Messages.getString("MekView.WeaponMods"));
weaponModifierList.forEach(list::addItem);
sQuirks.add(list);
}

sFluff.addAll(sQuirks);
if (!entity.getFluff().getOverview().isEmpty()) {
sFluff.add(new SingleLine());
Expand Down Expand Up @@ -594,6 +614,34 @@ public MekView(final Entity entity, final boolean showDetail, final boolean useA
}
}

private List<String> getEquipmentModifiers(Mounted<?> equipment) {
return equipment.getModifiers().stream().map(m -> "%s (%s)".formatted(modifierReason(m), modifierText(m))).toList();
}

private String modifierReason(EquipmentModifier modifier) {
return switch (modifier.reason()) {
case SALVAGE_QUALITY -> Messages.getString("MekView.SalvageQuality");
case PARTIAL_REPAIR -> Messages.getString("MekView.PartialRepair");
case UNKNOWN -> "Unknown";
};
}

private String modifierText(EquipmentModifier modifier) {
if (modifier instanceof WeaponHeatModifier heatModifier) {
return heatModifier.formattedDeltaHeat() + " heat";
} else if (modifier instanceof ToHitModifier toHitModifier) {
return toHitModifier.formattedToHitModifier() + " to-hit";
} else if (modifier instanceof DamageModifier damageModifier) {
return damageModifier.formattedDamageModifier() + " damage";
} else if (modifier instanceof WeaponMisfireModifier) {
return "weapon may misfire";
} else if (modifier instanceof WeaponJamModifier) {
return "weapon may jam";
} else {
return "unknown";
}
}

/** @return True when the unit requires an ammo block. */
private boolean showAmmoBlock(boolean showDetail) {
return (!entity.usesWeaponBays() || !showDetail) && !entity.getAmmo().stream().allMatch(this::hideAmmo);
Expand Down Expand Up @@ -927,48 +975,33 @@ private List<ViewElement> getWeapons(boolean showDetail) {
}

TableElement wpnTable = new TableElement(4);
wpnTable.setColNames("Weapons ", " Loc ", " Heat ", entity.isOmni() ? " Omni " : "");
wpnTable.setJustification(TableElement.JUSTIFIED_LEFT, TableElement.JUSTIFIED_CENTER,
TableElement.JUSTIFIED_CENTER, TableElement.JUSTIFIED_CENTER);
wpnTable.setColNames("Weapons ", " Loc ", entity.isOmni() ? " Omni " : "");
wpnTable.setJustification(TableElement.JUSTIFIED_LEFT, TableElement.JUSTIFIED_CENTER, TableElement.JUSTIFIED_CENTER);
for (WeaponMounted mounted : entity.getWeaponList()) {
String[] row = { mounted.getDesc() + quirkMarker(mounted),
String[] row = { mounted.getDesc() + quirkAndModMarker(mounted),
entity.joinLocationAbbr(mounted.allLocations(), 3), "", "" };
WeaponType wtype = mounted.getType();

if (entity.isClan()
&& (mounted.getType().getTechBase() == ITechnology.TECH_BASE_IS)) {
if (entity.isClan() && (mounted.getType().getTechBase() == ITechnology.TECH_BASE_IS)) {
row[0] += Messages.getString("MekView.IS");
}
if (!entity.isClan()
&& (mounted.getType().getTechBase() == ITechnology.TECH_BASE_CLAN)) {
if (!entity.isClan() && (mounted.getType().getTechBase() == ITechnology.TECH_BASE_CLAN)) {
row[0] += Messages.getString("MekView.Clan");
}
/*
* TODO: this should probably go in the ammo table somewhere if
* (wtype.hasFlag(WeaponType.F_ONESHOT)) { sWeapons.append(" [")
* .append(mounted.getLinked().getDesc()).append("]");
* }
*/

int heat = wtype.getHeat();
int bWeapDamaged = 0;
int damagedWeaponsInBay = 0;
if (wtype instanceof BayWeapon) {
// loop through weapons in bay and add up heat
heat = 0;
for (WeaponMounted m : mounted.getBayWeapons()) {
heat = heat + m.getType().getHeat();
if (m.isDestroyed()) {
bWeapDamaged++;
damagedWeaponsInBay++;
}
}
}
row[2] = String.valueOf(heat);

if (entity.isOmni()) {
row[3] = Messages.getString(mounted.isOmniPodMounted() ? "MekView.Pod" : "MekView.Fixed");
} else if (wtype instanceof BayWeapon && bWeapDamaged > 0 && !showDetail) {
row[3] = warningStart() + Messages.getString("MekView.WeaponDamage")
+ ")" + warningEnd();
row[2] = Messages.getString(mounted.isOmniPodMounted() ? "MekView.Pod" : "MekView.Fixed");
} else if (damagedWeaponsInBay > 0 && !showDetail) {
row[2] = warningStart() + Messages.getString("MekView.WeaponDamage") + ")" + warningEnd();
}
if (mounted.isDestroyed()) {
if (mounted.isRepairable()) {
Expand All @@ -985,12 +1018,10 @@ private List<ViewElement> getWeapons(boolean showDetail) {
for (WeaponMounted m : mounted.getBayWeapons()) {
row = new String[] { m.getDesc(), "", "", "" };

if (entity.isClan()
&& (mounted.getType().getTechBase() == ITechnology.TECH_BASE_IS)) {
if (entity.isClan() && (mounted.getType().getTechBase() == ITechnology.TECH_BASE_IS)) {
row[0] += Messages.getString("MekView.IS");
}
if (!entity.isClan()
&& (mounted.getType().getTechBase() == ITechnology.TECH_BASE_CLAN)) {
if (!entity.isClan() && (mounted.getType().getTechBase() == ITechnology.TECH_BASE_CLAN)) {
row[0] += Messages.getString("MekView.Clan");
}
if (m.isDestroyed()) {
Expand Down Expand Up @@ -1026,8 +1057,15 @@ private List<ViewElement> getWeapons(boolean showDetail) {
return retVal;
}

private String quirkMarker(Mounted<?> mounted) {
return (mounted.countQuirks() > 0) ? " (Q)" : "";
private String quirkAndModMarker(Mounted<?> mounted) {
List<String> markers = new ArrayList<>();
if (mounted.countQuirks() > 0) {
markers.add("Q");
}
if (mounted.isModified()) {
markers.add("M");
}
return markers.isEmpty() ? "" : " (%s)".formatted(String.join(", ", markers));
}

private boolean hideAmmo(Mounted<?> mounted) {
Expand Down
83 changes: 83 additions & 0 deletions megamek/src/megamek/common/Modifiable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package megamek.common;

import megamek.common.modifiers.EquipmentModifier;

import java.util.List;

/**
* This interface is implemented by equipment, parts, units or other things that may have modifications such as a to-hit modifier or a heat
* modifier.
*
* @see EquipmentModifier
*/
public interface Modifiable {

/**
* Returns all modifiers of this equipment or unit.
*
* This method must be overridden by implementing classes to provide a non-null (albeit often empty) return value. The returned list
* must be the list kept by the object, in other words it must not be unmodifiable nor a copy, as some default methods of this interface
* access that list.
*
* @see EquipmentModifier
*/
List<EquipmentModifier> getModifiers();

/**
* Adds the given modifier to this equipment or unit (even if it is already present).
*
* This method usually needs no override.
*
* @param modifier The modifier to add
*/
default void addEquipmentModifier(EquipmentModifier modifier) {
getModifiers().add(modifier);
}

/**
* Removes the given modifier from this equipment or unit. Does nothing if the modifier is not present.
*
* This method usually needs no override.
*
* @param modifier The modifier to remove
*/
default void removeEquipmentModifier(EquipmentModifier modifier) {
getModifiers().remove(modifier);
}

/**
* Removes all modifiers from this equipment or unit.
*
* This method usually needs no override.
*/
default void clearEquipmentModifiers() {
getModifiers().clear();
}

/**
* Returns true when there is a modifier present for this unit or equipment, false otherwise.
*
* This method usually needs no override.
*/
default boolean isModified() {
return !getModifiers().isEmpty();
}
}
Loading