/* * Initiative - A role playing utility to track turns * Copyright (C) 2002 Devon D Jones * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * AttackDialog.java * * Created on Oct 8, 2003 */ package plugin.initiative.gui; import gmgen.GMGenSystem; import gmgen.plugin.PcgCombatant; import plugin.initiative.AttackModel; import javax.swing.*; import javax.swing.event.TableModelEvent; import javax.swing.table.DefaultTableModel; import javax.swing.text.DefaultFormatter; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.Vector; import pcgen.core.RollingMethods; /** * @author Ross M. Lodge * * <p>This class resolves an attack as described by AttackModel.</p> * <p>The dialog presents a table which holds the details of the attacks. Certain cells * are editable (notably fudge bonus, range value, hit/crit checkboxes, damage dice).</p> * <p>User can enter an armor class and roll attacks. If a vector of combatants is passed the * dialog displays a {@code JComboBox} which displays attackable combatants. Changing * selections changes the AC value and re-calculates the attack rolls.</p> * */ public class AttackDialog extends JDialog { /** <p>List of resulting damage values, one for each successful attack.</p> */ private List<Integer> m_damageList = null; /** * <p>List of targets, one for each successful attack. Each one matches a damage value in * m_damagelist</p> */ private List m_targetList = null; /** <p>This dialog's attack model; that is, the attack object this dialog will resolve.</p> */ private AttackModel m_attack = null; /** <p>Instance of {@code AttackTableModel}.</p> */ private AttackTableModel m_tableModel = null; /** <p>{@code JComboBox} for Armor class types.</p> */ private JComboBox m_acTypeCombo; /** * Cell editor for target column */ private JComboBox m_targets = null; /** <p>{@code JComboBox} for combatants.</p> */ private JComboBox m_targetsCombo; /** <p>Text field for the armor class</p> */ private JFormattedTextField m_field; /** <p>Label to hold the total damage information.</p> */ private JLabel m_totalDamageLabel; /** <p>The dialog's {@code JTable}; holds all attack information for display</p> */ private JTable m_table = null; /** <p>Vector of combatants that are valid targets.</p> */ private Vector m_combatants = null; /** <p>{@code boolean}; whether or not damage is subdual.</p> */ private boolean m_subdual; /** <p>Total damage for all successful attacks.</p> */ private int m_totalDmg; /** * <p>Initializes the dialog with the specified model.</p> * * @param model Attack model for this dlg. */ public AttackDialog(AttackModel model) { super(GMGenSystem.inst); m_attack = model; initComponents(); } /** * <p>Initializes the dialog with the specified model and the specified * list of valid targets. This class ignores combatants that are not * instances of {@code PcgCombatant}. A null or empty vector * will cause the dialgo to display as if {@code AttackDialog(AttackModel model)} * had been called.</p> * * @param model Attack model for this dlg. * @param combatants Vector of combatants */ public AttackDialog(AttackModel model, Vector combatants) { super(GMGenSystem.inst); m_attack = model; if (combatants != null) { m_combatants = (Vector) combatants.clone(); } initComponents(); } /** * Returns the damage list (may be null or 0-length). * * @return Damage list */ public List<Integer> getDamageList() { return m_damageList; } /** * Returns the combatants for the successful attacks. * * @return Chosen combatant. */ public List getDamagedCombatants() { return m_targetList; } /** * @return {@code true} if damage is subdual */ public boolean isSubdual() { return m_subdual; } /** * Returns the total damage (may be 0) * * @return Total damage */ public int getTotalDmg() { return m_totalDmg; } /** * Handles actions from {@code m_acTypeCombo}; changes how armor * class is recalculated * * @param e Event which fired this handler */ protected void handleAcTypeAction(ActionEvent e) { m_field.setValue(( (PcgCombatant) m_targetsCombo .getSelectedItem() ).getPC().getDisplay().calcACOfType( m_acTypeCombo.getSelectedItem().toString())); m_tableModel.setAcType(m_acTypeCombo.getSelectedItem().toString()); } /** * <p>Hides the dialog and makes sure all damage/target lists are null.</p> */ protected void handleCancel() { m_damageList = null; m_targetList = null; setVisible(false); } /** * Handles actions from the Ok button. Sets the damage list and hides the dialog. * */ protected void handleOk() { m_damageList = new ArrayList<>(m_tableModel.getRowCount()); m_targetList = new ArrayList(m_tableModel.getRowCount()); for (int i = 0; i < m_table.getRowCount(); i++) { int dmg = m_tableModel.getIntAt(i, m_tableModel .columnFromKey(AttackTableModel.COLUMN_KEY_DMGTOT)); if (dmg > 0) { m_damageList.add(dmg); Object target = m_tableModel.getValueAt(i, m_tableModel .columnFromKey(AttackTableModel.COLUMN_KEY_TARGET)); if ((target != null) && target instanceof PcgCombatant) { m_targetList.add(target); } } } setVisible(false); } /** * <p>Handles actions on the subdual checkbox (saving data back to m_subdual).</p> * @param e * Event which fired this handler. */ protected void handleSubdualAction(ActionEvent e) { if (e.getSource() instanceof JCheckBox) { m_subdual = ((JCheckBox) e.getSource()).isSelected(); } } /** * Handles table updates. If damage column has been updated in any way, re-calculates * total damage and resets text of the total damage label. * * @param e Model event */ protected void handleTableUpdate(TableModelEvent e) { int dmgColumn = m_tableModel.columnFromKey(AttackTableModel.COLUMN_KEY_DMGTOT); if ((dmgColumn == e.getColumn()) || (e.getColumn() == TableModelEvent.ALL_COLUMNS) || (e.getType() == TableModelEvent.DELETE) || (e.getType() == TableModelEvent.INSERT)) { m_totalDmg = 0; for (int i = 0; i < m_table.getRowCount(); i++) { m_totalDmg += m_tableModel.getIntAt(i, dmgColumn); } m_totalDamageLabel.setText("<html>Total Damage: <b>" + m_totalDmg + "</b></html>"); } } /** * Handles actions from <code>m_targetsCombo</code>; sets chosen combatant * and value of armor class. * * @param e Event which fired this handler */ protected void handleTargetAction(ActionEvent e) { if ((m_targetsCombo != null) && (m_targetsCombo.getSelectedItem() != null) && m_targetsCombo.getSelectedItem() instanceof PcgCombatant) { PcgCombatant combatant = (PcgCombatant) m_targetsCombo.getSelectedItem(); m_field.setValue(combatant.getPC().getDisplay().calcACOfType( m_acTypeCombo.getSelectedItem().toString())); m_tableModel.setTarget(combatant); } } /** * Handles actions from the Roll button; calls table model's <code>rollAttacks</code> * method with the value of <code>m_field</code> */ protected void performRoll() { m_tableModel.rollAttacks(); } /** * <p>Initiaizes the dialog components.</p> */ private void initComponents() { setTitle("Attack: " + m_attack.toString()); //Set Layout getContentPane().setLayout(new BorderLayout()); //Build the center panel with JTable and scroll pane m_attack.getBonusList(); JPanel center = new JPanel(new BorderLayout()); getContentPane().add(center, BorderLayout.CENTER); //This column model auto-sizes the columns based on contents. AutoSizingColumnModel columns = new AutoSizingColumnModel(); m_tableModel = new AttackTableModel(); m_tableModel.addTableModelListener(this::handleTableUpdate); m_table = new JTable(m_tableModel, columns); columns.referenceTable(m_table); m_table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); m_table.setAutoCreateColumnsFromModel(true); m_table.setPreferredScrollableViewportSize(new Dimension(columns .getTotalPreferredWidth(), m_table.getRowHeight() * m_table.getRowCount())); center.add(new JScrollPane(m_table), BorderLayout.CENTER); //Build panel to contain buttons, controls at bottom JPanel bottom = new JPanel(); bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS)); bottom.add(Box.createRigidArea(new Dimension(10, 0))); m_totalDamageLabel = new JLabel("<html>Total Damage: <b>0</b></html>"); bottom.add(m_totalDamageLabel); bottom.add(Box.createRigidArea(new Dimension(10, 0))); JCheckBox checkbox = new JCheckBox("Damage is subdual?"); checkbox.addActionListener(this::handleSubdualAction); bottom.add(checkbox); bottom.add(Box.createRigidArea(new Dimension(10, 0))); JButton button = null; button = new JButton(new AbstractAction("Roll") { @Override public void actionPerformed(ActionEvent e) { performRoll(); } }); bottom.add(button); bottom.add(Box.createRigidArea(new Dimension(10, 0))); button = new JButton(new AbstractAction("Ok") { @Override public void actionPerformed(ActionEvent e) { handleOk(); } }); bottom.add(button); bottom.add(Box.createRigidArea(new Dimension(10, 0))); button = new JButton(new AbstractAction("Cancel") { @Override public void actionPerformed(ActionEvent e) { handleCancel(); } }); bottom.add(button); getContentPane().add(bottom, BorderLayout.SOUTH); JPanel top = new JPanel(); top.setLayout(new BoxLayout(top, BoxLayout.X_AXIS)); if ((m_combatants != null) && (!m_combatants.isEmpty())) { m_targets = new JComboBox(m_combatants); m_table.setDefaultEditor(PcgCombatant.class, new DefaultCellEditor( m_targets)); //If we have combatants, initialize the top panel and populate //the JComboBox m_targetsCombo = new JComboBox(m_combatants); m_targetsCombo.addActionListener(this::handleTargetAction); top.add(new JLabel("Attack Character: ")); top.add(m_targetsCombo); m_acTypeCombo = new JComboBox(); m_acTypeCombo.addItem("Total"); m_acTypeCombo.addItem("Flatfooted"); m_acTypeCombo.addItem("Touch"); m_acTypeCombo.addActionListener(this::handleAcTypeAction); top.add(new JLabel("Use AC Type: ")); top.add(m_acTypeCombo); } DefaultFormatter formatter = new DefaultFormatter(); formatter.setValueClass(Integer.class); formatter.setCommitsOnValidEdit(true); m_field = new JFormattedTextField(formatter); m_field.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT); m_field.setPreferredSize(new Dimension(40, m_field.getPreferredSize().height)); m_field.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if ((evt.getPropertyName() != null) && evt.getPropertyName().equals("value")) { m_tableModel.setArmorClass(((Integer) m_field.getValue()) .intValue()); } } }); top.add(new JLabel("AC")); top.add(m_field); m_field.setValue(15); handleTargetAction(null); getContentPane().add(top, BorderLayout.NORTH); //Pack and locate the dialog pack(); setLocationRelativeTo(GMGenSystem.inst); } /** * @author Ross M. Lodge * * <p>A table model for the dialog. Defines columns, column data types, editable values, etc.</p> * <p>Provides methods for rolling attacks and re-calculating data values.</p> */ public class AttackTableModel extends DefaultTableModel { /** Key strings for accessing column data. (Allows column names to be loaded from resource files * if necessary) */ static final String COLUMN_KEY_BONUS = "BONUS"; static final String COLUMN_KEY_FUDGE = "FUDGE"; static final String COLUMN_KEY_INCREMENT = "INCREMENT"; static final String COLUMN_KEY_RANGE = "RANGE"; static final String COLUMN_KEY_ROLL = "ROLL"; static final String COLUMN_KEY_TOTAL = "TOTAL"; static final String COLUMN_KEY_CRITROLL = "CRITROLL"; static final String COLUMN_KEY_CRITTOTAL = "CRITTOTAL"; static final String COLUMN_KEY_HIT = "HIT"; static final String COLUMN_KEY_CRIT = "CRIT"; static final String COLUMN_KEY_DMG = "DMG"; static final String COLUMN_KEY_DMGTOT = "DMGTOT"; static final String COLUMN_KEY_TARGET = "TARGET"; static final String COLUMN_KEY_AC = "ARMORCLASS"; /** * Integers for use when accessing values in the columns array */ static final int COLUMN_INDEX_NAME = 0; static final int COLUMN_INDEX_CLASS = 1; static final int COLUMN_INDEX_DEFAULT = 2; static final int COLUMN_INDEX_EDITABLE = 3; static final int COLUMN_INDEX_KEY = 4; /** AC Type string */ private String m_acType = "Total"; /** * Provides a way to access column data without repetitive hard-coding. Probably not * efficient but saves me a lot of typing. Use fields COLUMN_INDEX_XYZ as the second index. * You can use columnFromKey(COLUMN_KEY_XYZ) to get the first (column number) index. * This array is used to initialize the names of the columns and for returning values from * <code>getColumClass</code> and <code>isCellEditable</code>. */ /* * CONSIDER Could this be a List<Blah> where Blah is a type-safe immutable object? * Seems that might be a way to clean this up to be more understandable - would also * prevent some object use (Boolean) - thpr 10/27/06 */ private Object[][] columns = { {"Bonus", Integer.class, null, Boolean.FALSE, COLUMN_KEY_BONUS}, {"Fudge", Integer.class, 0, Boolean.TRUE, COLUMN_KEY_FUDGE}, {"Increment", Integer.class, m_attack.getRangeAsInt(), Boolean.FALSE, COLUMN_KEY_INCREMENT}, {"Range", Integer.class, null, Boolean.TRUE, COLUMN_KEY_RANGE}, {"Roll", Integer.class, null, Boolean.FALSE, COLUMN_KEY_ROLL}, {"Total", Integer.class, null, Boolean.FALSE, COLUMN_KEY_TOTAL}, {"Target", PcgCombatant.class, null, Boolean.TRUE, COLUMN_KEY_TARGET}, {"AC", Integer.class, null, Boolean.TRUE, COLUMN_KEY_AC}, {"Crit Roll", Integer.class, null, Boolean.FALSE, COLUMN_KEY_CRITROLL}, {"Crit Total", Integer.class, null, Boolean.FALSE, COLUMN_KEY_CRITTOTAL}, {"Hit", Boolean.class, Boolean.FALSE, Boolean.TRUE, COLUMN_KEY_HIT}, {"Crit", Boolean.class, Boolean.FALSE, Boolean.TRUE, COLUMN_KEY_CRIT}, {"Dmg", String.class, null, Boolean.TRUE, COLUMN_KEY_DMG}, {"Dmg Tot", Integer.class, null, Boolean.TRUE, COLUMN_KEY_DMGTOT}}; /** * Constructor. Builds values based on the enclosing class's m_attack member. */ public AttackTableModel() { super(); int[] attacks = m_attack.getBonusList(); Vector<Object> values = new Vector<>(columns.length); values.setSize(values.capacity()); for (int i = 0; i < columns.length; i++) { addColumn(columns[i][COLUMN_INDEX_NAME]); values.add(i, columns[i][COLUMN_INDEX_DEFAULT]); } for (int i = 0; i < attacks.length; i++) { values.set(columnFromKey(COLUMN_KEY_BONUS), attacks[i]); values .set(columnFromKey(COLUMN_KEY_DMG), m_attack.getDamage(i)); addRow((Vector) values.clone()); } } /** * @param string */ public void setAcType(String string) { m_acType = string; for (int row = 0; row < getRowCount(); row++) { recalcRow(row, columnFromKey(COLUMN_KEY_TARGET)); } } /** * Sets the armor class and recalculates the rows in the model * without re-rolling the attacks. * * @param ac Armor class */ public void setArmorClass(int ac) { for (int i = 0; i < getRowCount(); i++) { setValueAt(ac, i, columnFromKey(COLUMN_KEY_AC)); } } /* (non-Javadoc) * @see javax.swing.table.TableModel#isCellEditable(int, int) */ @Override public boolean isCellEditable(int row, int column) { return ((Boolean) columns[column][COLUMN_INDEX_EDITABLE]) .booleanValue(); } /* (non-Javadoc) * @see javax.swing.table.TableModel#getColumnClass(int) */ @Override public Class getColumnClass(int columnIndex) { return (Class) columns[columnIndex][COLUMN_INDEX_CLASS]; } /** * Sets the target and recalculates the rows in the model * without re-rolling the attacks. * * @param target Target */ public void setTarget(PcgCombatant target) { for (int i = 0; i < getRowCount(); i++) { setValueAt(target, i, columnFromKey(COLUMN_KEY_TARGET)); } } /* (non-Javadoc) * @see javax.swing.table.TableModel#setValueAt(java.lang.Object, int, int) * * In addition to setting the value this method also calls recalcRow. */ @Override public void setValueAt(Object aValue, int row, int column) { super.setValueAt(aValue, row, column); recalcRow(row, column); } /** * Iterates through all rows in the table and calls <code>rollAttack(row)</code>. */ public void rollAttacks() { for (int i = 0; i < getRowCount(); i++) { rollAttack(i); } } /** * Shortcut to get the int value for a column with an Integer data type. * * @param row The row to request a value for * @param column The column to request a value for * @return The integer value at the specified column, or 0 if class of that column not Integer */ private int getIntAt(int row, int column) { int returnValue = 0; if ((getValueAt(row, column) != null) && getValueAt(row, column) instanceof Integer) { returnValue = ((Integer) getValueAt(row, column)).intValue(); } return returnValue; } /** * Shortcut to get the index of a column based on the string * key value. * * @param key The key string (a COLUMN_KEY_XYZ value) * @return The integer index of the column */ private int columnFromKey(String key) { int returnValue = -1; for (int i = 0; i < columns.length; i++) { if (columns[i][COLUMN_INDEX_KEY].equals(key)) { returnValue = i; break; } } return returnValue; } /** * Recalculates the data values on the requested row, based on changes to the * column index supplied. Be aware that this does itself call <code>setValueAt</code>, * so care must be taken to avoid an infinite recursive call. The <code>if</code> blocks * should all make sure they don't set columns that they in turn react to. * * @param row Row that has changed. * @param column Column that has changed */ private void recalcRow(int row, int column) { //COLUMN_KEY_TOTAL int attTotal = 0; int attackModifier = getIntAt(row, columnFromKey(COLUMN_KEY_BONUS)) + getIntAt(row, columnFromKey(COLUMN_KEY_FUDGE)); if ((getIntAt(row, columnFromKey(COLUMN_KEY_INCREMENT)) > 0) && (getIntAt(row, columnFromKey(COLUMN_KEY_RANGE)) > 0)) { attackModifier += ((int) Math.floor((double) getIntAt(row, columnFromKey(COLUMN_KEY_RANGE)) / (double) getIntAt(row, columnFromKey(COLUMN_KEY_INCREMENT))) * -2); } int result = getIntAt(row, columnFromKey(COLUMN_KEY_ROLL)); if ((result == 1) || (result == 0)) { attTotal = result; } else { attTotal = result + attackModifier; } //COLUMN_KEY_TOTAL if ((column == columnFromKey(COLUMN_KEY_BONUS)) || (column == columnFromKey(COLUMN_KEY_FUDGE)) || (column == columnFromKey(COLUMN_KEY_INCREMENT)) || (column == columnFromKey(COLUMN_KEY_RANGE)) || (column == columnFromKey(COLUMN_KEY_ROLL))) { setValueAt(attTotal, row, columnFromKey(COLUMN_KEY_TOTAL)); } //COLUMN_KEY_AC if (column == columnFromKey(COLUMN_KEY_TARGET)) { if (getValueAt(row, columnFromKey(COLUMN_KEY_TARGET)) instanceof PcgCombatant) { setValueAt(( (PcgCombatant) getValueAt(row, columnFromKey(COLUMN_KEY_TARGET)) ).getPC().getDisplay() .calcACOfType(m_acType), row, columnFromKey(COLUMN_KEY_AC)); } } //COLUMN_KEY_HIT if ((column == columnFromKey(COLUMN_KEY_TOTAL)) || (column == columnFromKey(COLUMN_KEY_AC))) { if ((attTotal >= getIntAt(row, columnFromKey(COLUMN_KEY_AC))) || (getIntAt(row, columnFromKey(COLUMN_KEY_ROLL)) >= 20)) { setValueAt(Boolean.TRUE, row, columnFromKey(COLUMN_KEY_HIT)); } else { setValueAt(Boolean.FALSE, row, columnFromKey(COLUMN_KEY_HIT)); } } //COLUMN_KEY_BONUS //COLUMN_KEY_FUDGE //COLUMN_KEY_INCREMENT //COLUMN_KEY_RANGE //COLUMN_KEY_ROLL //COLUMN_KEY_TOTAL //COLUMN_KEY_CRITROLL //COLUMN_KEY_CRITTOTAL if ((column == columnFromKey(COLUMN_KEY_CRITROLL)) || (column == columnFromKey(COLUMN_KEY_AC))) { int critTotal = 0; if (getIntAt(row, columnFromKey(COLUMN_KEY_CRITROLL)) > 1) { critTotal = getIntAt(row, columnFromKey(COLUMN_KEY_CRITROLL)) + attackModifier; setValueAt(critTotal, row, columnFromKey(COLUMN_KEY_CRITTOTAL)); } else { setValueAt(null, row, columnFromKey(COLUMN_KEY_CRITTOTAL)); } //COLUMN_KEY_CRIT if ((critTotal > getIntAt(row, columnFromKey(COLUMN_KEY_AC))) || (getIntAt(row, columnFromKey(COLUMN_KEY_CRITROLL)) >= 20)) { setValueAt(Boolean.TRUE, row, columnFromKey(COLUMN_KEY_CRIT)); } else { setValueAt(Boolean.FALSE, row, columnFromKey(COLUMN_KEY_CRIT)); } } //COLUMN_KEY_DMG //COLUMN_KEY_DMGTOT if ((column == columnFromKey(COLUMN_KEY_HIT)) || (column == columnFromKey(COLUMN_KEY_CRIT))) { if (((Boolean) getValueAt(row, columnFromKey(COLUMN_KEY_HIT))) .booleanValue()) { int numberOfRolls = 1; String damageString = (String) getValueAt(row, columnFromKey(COLUMN_KEY_DMG)); if (damageString.indexOf("/") >= 0) { StringTokenizer tok = new StringTokenizer(damageString, "/"); String[] heads = new String[tok.countTokens()]; for (int i = 0; tok.hasMoreTokens(); i++) { heads[i] = tok.nextToken(); } damageString = (String) JOptionPane.showInputDialog( AttackDialog.this, "This weapon appears to have more than one possible damage " + "die listed. Please choose one:", "Multiple Damage Dice", JOptionPane.QUESTION_MESSAGE, null, heads, heads[1]); setValueAt(damageString, row, columnFromKey(COLUMN_KEY_DMG)); } if (((Boolean) getValueAt(row, columnFromKey(COLUMN_KEY_CRIT))).booleanValue()) { numberOfRolls = Integer.parseInt(m_attack.getCritMultiple(row)); } int dmg = 0; for (int i = 0; i < numberOfRolls; i++) { dmg += RollingMethods.roll(damageString); } setValueAt(dmg, row, columnFromKey(COLUMN_KEY_DMGTOT)); } else { setValueAt(null, row, columnFromKey(COLUMN_KEY_DMGTOT)); } } } /** * Calculates to-hit and damage rolls for the requested row, rolls critical * if necessary. * * @param row The row to roll an attack on. */ private void rollAttack(int row) { setValueAt(RollingMethods.roll("1d20"), row, columnFromKey(COLUMN_KEY_ROLL)); if ((getIntAt(row, columnFromKey(COLUMN_KEY_ROLL)) >= m_attack .getCritRangeMin(row)) && ((Boolean) getValueAt(row, columnFromKey(COLUMN_KEY_HIT))) .booleanValue()) { setValueAt(RollingMethods.roll("1d20"), row, columnFromKey(COLUMN_KEY_CRITROLL)); } else { setValueAt(null, row, columnFromKey(COLUMN_KEY_CRITROLL)); } } } }