浏览代码

Merge branch 'equipment_loadout'

Sam Jaffe 8 年之前
父节点
当前提交
b4ab670267
共有 55 个文件被更改,包括 1337 次插入384 次删除
  1. 15 9
      resources/Potato.json
  2. 96 55
      schema/inventory.json
  3. 1 0
      src/main/lombok/org/leumasjaffe/charsheet/config/Constants.java
  4. 96 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/EquipItemController.java
  5. 18 3
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDArmor.java
  6. 32 0
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEnchantableItem.java
  7. 124 0
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEquipment.java
  8. 49 103
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDInventory.java
  9. 21 5
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDItem.java
  10. 21 4
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDWeapon.java
  11. 12 0
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Enchantment.java
  12. 22 0
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Enhancement.java
  13. 17 3
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Money.java
  14. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Area.java
  15. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Duration.java
  16. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Effect.java
  17. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Range.java
  18. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Target.java
  19. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/observer/helper/AbilModStringify.java
  20. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/observer/helper/IntValueStringify.java
  21. 9 1
      src/main/lombok/org/leumasjaffe/charsheet/view/ClassTab.java
  22. 0 2
      src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java
  23. 1 2
      src/main/lombok/org/leumasjaffe/charsheet/view/dev/DeveloperMenu.java
  24. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/builders/DialogBuilder.java
  25. 191 0
      src/main/lombok/org/leumasjaffe/charsheet/view/dialog/BuySellItemDialog.java
  26. 4 4
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipItemPanel.java
  27. 5 5
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ArmorPanel.java
  28. 0 89
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipItemHelper.java
  29. 21 12
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentInfoMenu.java
  30. 29 24
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentPanel.java
  31. 8 13
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/InventoryPanel.java
  32. 91 9
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemInfoMenu.java
  33. 234 0
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemInfoPanel.java
  34. 31 6
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemPanel.java
  35. 74 0
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/LoadoutMenu.java
  36. 5 5
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ShieldPanel.java
  37. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/WeaponPanel.java
  38. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/ChooseSpellsPerDayHeader.java
  39. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectPreparedSpellsPanel.java
  40. 7 0
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPanel.java
  41. 8 0
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellPanel.java
  42. 2 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsKnownHeader.java
  43. 7 0
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java
  44. 8 1
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpDialog.java
  45. 7 1
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpLine.java
  46. 9 1
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLine.java
  47. 8 1
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityBox.java
  48. 14 5
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/ArmorLine.java
  49. 8 1
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AttackLine.java
  50. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/DescriptionPanel.java
  51. 8 1
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/InitiativeLine.java
  52. 9 1
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/ResistanceLine.java
  53. 1 3
      src/main/lombok/org/leumasjaffe/charsheet/util/StringHelper.java
  54. 1 0
      src/main/lombok/org/leumasjaffe/observer/ObserverDispatch.java
  55. 0 1
      src/test/java/org/leumasjaffe/format/StringFormatterTest.java

+ 15 - 9
resources/Potato.json

@@ -110,7 +110,7 @@
   "inventory": {
     "items": [
       {
-        "name": "MWK Quarterstaff",
+        "name": "Quarterstaff",
         "count": 1,
         "countEquipped": 1,
         "value": {"pp": 0, "gp": 600, "sp": 0, "cp": 0},
@@ -118,31 +118,32 @@
         "slot": "TWO_HANDS",
         "weight": 4,
         "weapon": {
-          "attackBonus": 1,
-          "damageBonus": 0,
           "damage": "1d6",
           "secondaryDamage": "1d6",
           "criticalThreat": 20,
           "criticalDamage": 2,
           "range": "Melee",
-          "type": "Bludgeoning"
+          "type": "Bludgeoning",
+          "masterwork": true
         }
       },
       {
-        "name": "+1 Full Plate Armor",
+        "name": "Full Plate Armor",
         "count": 1,
         "countEquipped": 1,
-        "value": {"pp": 0, "gp": 2650, "sp": 0, "cp": 0},
+        "value": {"pp": 0, "gp": 1500, "sp": 0, "cp": 0},
         "page": "PH123",
         "slot": "BODY",
         "weight": 50,
         "armor": {
-          "bonus": 9,
+          "acBonus": 8,
           "type": "Heavy",
           "maxDex": 1,
-          "checkPenalty": -5,
+          "checkPenalty": -6,
           "spellFailure": 35,
-          "speed": 15
+          "speed": 15,
+          "masterwork": true,
+          "bonus": "+1"
         }
       }
     ],
@@ -150,6 +151,11 @@
       "BODY": "+1 Full Plate Armor",
       "MAIN_HAND": "MWK Quarterstaff",
       "OFF_HAND": "MWK Quarterstaff"
+    },
+    "favorites": {
+    },
+    "wealth": {
+      "pp": 0, "gp": 0, "sp": 0, "cp": 0
     }
   }
 }

+ 96 - 55
schema/inventory.json

@@ -85,66 +85,107 @@
         "TWO_HANDS"
       ]
     },
-    "weapon": {
-      "dependencies":{
-        "secondaryCriticalDamage": ["secondaryDamage"]
+    "enchantable": {
+      "bonus": {
+        "type": "string",
+        "enum": [
+          "+1", "+2", "+3", "+4", "+5"
+        ]
       },
       "properties": {
-        "attackBonus": {"type": "integer"},
-        "damageBonus": {"type": "integer"},
-        "damage": {"type": "string"},
-        "secondaryDamage": {"type": "string"},
-        "criticalThreat": {"type": "integer", "maximum": 20},
-        "criticalDamage": {"type": "integer", "minimum": 2},
-        "secondaryCriticalDamage": {"type": "integer", "minimum": 2},
-        "range": {
-          "oneOf": [
-            {"type": "string", "const":"Melee"},
-            {"$ref": "file:common.json#/definitions/distance"}
-          ]
-        },
-        "type": {
-          "type": "string", 
-          "enum": [
-            "Slashing",
-            "Piercing",
-            "Bludgeoning"
-          ]
+        "masterwork": {"type": "boolean"},
+        "bonus": {"$ref": "#/definitions/enchantable/bonus"},
+        "enchantments": {
+          "items": {
+            "oneOf": [
+              {
+                "properties": {
+                  "name": {"type": "string"},
+                  "effectiveBonus": {"$ref": "#/definitions/enchantable/bonus"}
+                },
+                "required": ["name", "effectiveBonus"],
+                "type": "object"
+              }
+              {
+                "properties": {
+                  "name": {"type": "string"},
+                  "adHocValue": {"$ref": "#/definitions/money"}
+                },
+                "required": ["name", "adHocValue"],
+                "type": "object"
+              }
+            ]
+          },
+          "type": "array"
         }
-      },
-      "required": [
-        "damage",
-        "type",
-        "range",
-        "criticalThreat",
-        "criticalDamage"
-      ],
-      "type": "object",
-      "additionalProperties": false
+      }
+    },
+    "weapon": {
+      "allOf": [
+        {"$ref": "#/definitions/enchantable"}
+        {
+          "dependencies":{
+            "secondaryCriticalDamage": ["secondaryDamage"]
+          },
+          "properties": {
+            "damage": {"type": "string"},
+            "secondaryDamage": {"type": "string"},
+            "criticalThreat": {"type": "integer", "maximum": 20},
+            "criticalDamage": {"type": "integer", "minimum": 2},
+            "secondaryCriticalDamage": {"type": "integer", "minimum": 2},
+            "range": {
+              "oneOf": [
+                {"type": "string", "const":"Melee"},
+                {"$ref": "file:common.json#/definitions/distance"}
+              ]
+            },
+            "type": {
+              "type": "string", 
+              "enum": [
+                "Slashing",
+                "Piercing",
+                "Bludgeoning"
+              ]
+            }
+          },
+          "required": [
+            "damage",
+            "type",
+            "range",
+            "criticalThreat",
+            "criticalDamage"
+          ],
+          "type": "object"
+        }
+      ]
     },
     "armor": {
-      "dependencies": {
-        "speed": {"properties": {"type": {"not": {"const": "Shield"}}}}
-      },
-      "properties": {
-        "bonus": {"type": "integer", "minimum": 0},
-        "type": {
-          "type": "string", 
-          "enum": ["Light", "Medium", "Heavy", "Shield"]
-        },
-        "maxDex": {"type": "integer", "minimum": 0},
-        "checkPenalty": {"type": "integer", "maximum": 0},
-        "spellFailure": {"type": "integer", "minimum": 0, "maximum": 100},
-        "speed": {"type": "integer"}
-      },
-      "required": [
-        "bonus",
-        "type",
-        "checkPenalty",
-        "spellFailure"
-      ],
-      "type": "object",
-      "additionalProperties": false
+      "allOf": [
+        {"$ref": "#/definitions/enchantable"}
+        {
+          "dependencies": {
+            "speed": {"properties": {"type": {"not": {"const": "Shield"}}}}
+          },
+          "properties": {
+            "acBonus": {"type": "integer", "minimum": 0},
+            "type": {
+              "type": "string", 
+              "enum": ["Light", "Medium", "Heavy", "Shield"]
+            },
+            "maxDex": {"type": "integer", "minimum": 0},
+            "checkPenalty": {"type": "integer", "maximum": 0},
+            "spellFailure": {"type": "integer", "minimum": 0, "maximum": 100},
+            "speed": {"type": "integer"}
+          },
+          "required": [
+            "bonus",
+            "type",
+            "checkPenalty",
+            "spellFailure"
+          ],
+          "type": "object"
+        }
+      ]
     },
     "money": {
       "properties": {

+ 1 - 0
src/main/lombok/org/leumasjaffe/charsheet/config/Constants.java

@@ -6,6 +6,7 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 @FieldDefaults(makeFinal=true) // level is not correctly handled when delomboking in maven builds
 public final class Constants {
+	public String PREVIOUS_LOADOUT = "$$PREVIOUS";
 	public String NO_FLAT_FOOTED = "Keeps Dexterity When Flat-footed";
 	
 	public String K_DISTANCE = "Distance Measurement Unit";

+ 96 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/EquipItemController.java

@@ -0,0 +1,96 @@
+package org.leumasjaffe.charsheet.controller;
+
+import static org.leumasjaffe.charsheet.model.inventory.EquipmentSlot.*;
+
+import javax.swing.JOptionPane;
+
+import org.leumasjaffe.charsheet.model.inventory.DDEquipment;
+import org.leumasjaffe.charsheet.model.inventory.DDItem;
+import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
+import org.leumasjaffe.charsheet.view.dialog.EquipItemDialog;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.RequiredArgsConstructor;
+
+public class EquipItemController {	
+
+	public static void accept(final DDEquipment inv, final DDItem item) {
+		if (item.getUnequippedCount() == 0) { return; }
+		if (inv.canEquip(item) || new Helper(inv).getReplaceItem(item.getSlot())) {
+			item.adjustCounEquipped(+1);
+			inv.equipNext(item);
+			ObserverDispatch.notifySubscribers(inv, null);
+		}
+	}
+
+	@RequiredArgsConstructor
+	private static class Helper {
+		private static final String QUERY_REPLACE = "Replace Equipped Item";
+		private static final String REPLACE_EITHER = "Which of the following items would you like to replace?";
+		private static final String REPLACE_BOTH = "Do you want to replace both of the following items?";
+		private static final String REPLACE_ONE = "Do you want to replace the following item?";
+		final DDEquipment inv;
+		
+		private boolean getReplaceItem(final EquipmentSlot slot) {
+			switch (slot) {
+			case TWO_HANDS: return selectToReplaceAllOf(TWO_HANDS, MAIN_HAND, OFF_HAND);
+			case ONE_HAND: return selectToReplaceOneOf(MAIN_HAND, OFF_HAND);
+			case RING: return selectToReplaceOneOf(RING1, RING2);
+			default: return selectToReplace(slot);
+			}
+		}
+
+		private boolean selectToReplaceAllOf(final EquipmentSlot base,
+				final EquipmentSlot slot1, final EquipmentSlot slot2) {
+			if (inv.get(slot1).getSlot() == base) {
+				return selectToReplace(slot1);
+			} else if (JOptionPane.showConfirmDialog(null, createDialogTwoSlots(REPLACE_BOTH, slot1, slot2), 
+					QUERY_REPLACE, JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
+				doUnequip(slot1);
+				doUnequip(slot2);
+				return true;
+			}
+			return false;
+		}
+
+		private boolean selectToReplaceOneOf(final EquipmentSlot slot1, 
+				final EquipmentSlot slot2) {
+			final int choice = JOptionPane.showOptionDialog(null, createDialogTwoSlots(REPLACE_EITHER, slot1, slot2), 
+					QUERY_REPLACE, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, 
+					new String[] {"Cancel", slot1.toString(), slot2.toString()}, null);
+			if (choice == JOptionPane.YES_OPTION) {
+				doUnequip(slot1);
+				return true;
+			} else if (choice == JOptionPane.NO_OPTION) {
+				doUnequip(slot2);
+				return true;
+			}
+			return false;
+		}
+
+		private boolean selectToReplace(final EquipmentSlot slot) {
+			if (JOptionPane.showConfirmDialog(null, createDialogOneSlot(slot), QUERY_REPLACE, JOptionPane.YES_NO_OPTION) 
+					== JOptionPane.YES_OPTION) {
+				doUnequip(slot);
+				return true;
+			}
+			return false;
+		}
+
+		private void doUnequip(final EquipmentSlot slot) {
+			inv.get(slot).adjustCounEquipped(-1);
+			inv.unequip(slot);
+		}		
+
+		private EquipItemDialog createDialogOneSlot(final EquipmentSlot slot) {
+			return new EquipItemDialog(REPLACE_ONE, slot, inv.get(slot));
+		}
+		
+		private EquipItemDialog createDialogTwoSlots(final String message, 
+				final EquipmentSlot slot1, final EquipmentSlot slot2) {
+			return new EquipItemDialog(message,
+					new EquipItemDialog.Tuple(slot1, inv.get(slot1)),
+					new EquipItemDialog.Tuple(slot2, inv.get(slot2)));
+		}
+	}
+}

+ 18 - 3
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDArmor.java

@@ -2,12 +2,27 @@ package org.leumasjaffe.charsheet.model.inventory;
 
 import lombok.AccessLevel;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.experimental.FieldDefaults;
 
-@Data
+@Data @EqualsAndHashCode(callSuper=true)
 @FieldDefaults(level=AccessLevel.PRIVATE)
-public class DDArmor {
+public class DDArmor extends DDEnchantableItem {
 	public static enum Type { Light, Medium, Heavy }
-	int bonus, maxDex, speed, spellFailure, checkPenalty;
+	int acBonus, maxDex, speed, spellFailure, checkPenalty;
 	Type type;
+	
+	public int getActualAcBonus() {
+		return acBonus + bonus.value;
+	}
+	
+	public int getActualCheckPenalty() {
+		return isMasterwork() ? checkPenalty + 1 : checkPenalty;
+	}
+	
+	public Money getActualValue() {
+		int gp = isMasterwork ? 150 : 0;
+		gp += 1000 * (bonus.value + getEnchantBonus());
+		return Money.fromCopperToGold(100 * gp + getAdHocPrice());
+	}
 }

+ 32 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEnchantableItem.java

@@ -0,0 +1,32 @@
+package org.leumasjaffe.charsheet.model.inventory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.Data;
+
+@Data
+class DDEnchantableItem {
+	boolean isMasterwork = false;
+	Enhancement bonus = Enhancement.NONE;
+	List<Enchantment> enchantments = new ArrayList<>();
+
+	public String getNameModifier() {
+		return bonus == Enhancement.NONE ? (isMasterwork ? "MWK " : "") : 
+			bonus.toValue() + " " + getEnchantNames();
+	}
+
+	int getAdHocPrice() {
+		return enchantments.stream().map(Enchantment::getAdHocValue)
+				.map(o -> o.orElse(Money.fromCopper(0))).mapToInt(Money::asCopper).sum();
+	}
+
+	String getEnchantNames() {
+		return enchantments.stream().map(Enchantment::getName).reduce("", (l, r) -> l + r + " ");
+	}
+
+	int getEnchantBonus() {
+		return enchantments.stream().map(Enchantment::getEffectiveBonus)
+				.mapToInt(Enhancement::getValue).sum();
+	}
+}

+ 124 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEquipment.java

@@ -0,0 +1,124 @@
+package org.leumasjaffe.charsheet.model.inventory;
+
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+import org.leumasjaffe.observer.Observable;
+
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.NonNull;
+import lombok.ToString;
+import lombok.experimental.FieldDefaults;
+
+@ToString
+@EqualsAndHashCode(callSuper=false)
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class DDEquipment extends Observable {
+	@NonNull Map<EquipmentSlot, DDItem> equipment = new EnumMap<>(EquipmentSlot.class);
+
+	public boolean canEquip(final DDItem item) {
+		return canEquip(item.getSlot());
+	}
+	
+	private boolean canEquip(final EquipmentSlot slot) {
+		switch (slot) {
+		case NONE: return false;
+		case TWO_HANDS: return canEquip(EquipmentSlot.MAIN_HAND) && 
+				canEquip(EquipmentSlot.OFF_HAND);
+		case ONE_HAND: return canEquip(EquipmentSlot.MAIN_HAND) || 
+				canEquip(EquipmentSlot.OFF_HAND);
+		case RING: return canEquip(EquipmentSlot.RING1) || 
+				canEquip(EquipmentSlot.RING2);
+		default:
+			return !equipment.containsKey(slot);
+		}
+	}
+	
+	public void equip(final EquipmentSlot slot, final DDItem item) {
+		switch ( slot ) {
+		case NONE:
+		case ONE_HAND:
+		case RING:
+			throw new IllegalArgumentException("Cannot equip directly to slot:" + slot);
+		case TWO_HANDS: 
+			equip(EquipmentSlot.MAIN_HAND, item); 
+			equip(EquipmentSlot.OFF_HAND, item);
+			break;
+		default:
+			equipment.put(slot, item);
+			break;
+		}
+	}
+	
+	public void unequip(final EquipmentSlot slot) {
+		switch (slot) {
+		case NONE:
+		case ONE_HAND:
+		case RING:
+			throw new IllegalArgumentException("Cannot unequip directly to slot:" + slot);
+		case TWO_HANDS: 
+			unequip(EquipmentSlot.MAIN_HAND); 
+			unequip(EquipmentSlot.OFF_HAND);
+			break;
+		default:
+			equipment.remove(slot);
+			break;
+		}
+	}
+
+	public void equipNext(final DDItem item) {
+		switch (item.getSlot()) {
+		case NONE:
+			throw new IllegalArgumentException("Cannot equip unequippable item");
+		case ONE_HAND:
+			if (canEquip(EquipmentSlot.MAIN_HAND)) { 
+				equip(EquipmentSlot.MAIN_HAND, item);
+			} else {
+				equip(EquipmentSlot.OFF_HAND, item);
+			}
+			break;
+		case RING:
+			if (canEquip(EquipmentSlot.RING1) ) { 
+				equip(EquipmentSlot.RING1, item);
+			} else {
+				equip(EquipmentSlot.RING2, item);
+			}
+			break;
+		default:
+			equip(item.getSlot(), item);
+			break;
+		}
+	}
+	
+	public Set<EquipmentSlot> keySet() {
+		return Collections.unmodifiableSet(equipment.keySet());
+	}
+	
+	public DDItem get(EquipmentSlot slot) {
+		return equipment.get(slot);
+	}
+	
+	public boolean containsKey(EquipmentSlot slot) {
+		return equipment.containsKey(slot);
+	}
+
+	void put(EquipmentSlot slot, DDItem item) {
+		equipment.put(slot, item);
+	}
+
+	void remove(EquipmentSlot slot) {
+		equipment.remove(slot);
+	}
+	
+	void forEach(BiConsumer<? super EquipmentSlot, ? super DDItem> action) {
+		equipment.forEach(action);
+	}
+
+	void clear() {
+		equipment.clear();
+	}
+}

+ 49 - 103
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDInventory.java

@@ -1,29 +1,19 @@
 package org.leumasjaffe.charsheet.model.inventory;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
+import org.leumasjaffe.charsheet.config.Constants;
 import org.leumasjaffe.observer.Observable;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonValue;
 
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.NonNull;
-import lombok.Setter;
-import lombok.ToString;
+import lombok.*;
 import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
 
-@Getter
-@Setter
-@ToString
+@Getter @Setter @ToString
 @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class DDInventory extends Observable {
@@ -31,110 +21,66 @@ public class DDInventory extends Observable {
 	public static class Serializable {
 		@NonNull List<DDItem> items;
 		@NonNull Map<EquipmentSlot, String> equipment;
+		@NonNull Map<String, Map<EquipmentSlot, String>> favorites;
+		@NonNull Money wealth;
 	}
 	
-	@NonNull List<DDItem> items;
-	@NonNull Map<EquipmentSlot, DDItem> equipment;
+	// Serializable properties
+	@NonNull List<DDItem> items = new ArrayList<>();
+	@NonNull DDEquipment equipment = new DDEquipment();
+	@NonNull Map<String, Map<EquipmentSlot, String>> favorites = new TreeMap<>();
+	@NonNull Money wealth = new Money(0, 0, 0, 0);
+	// Transient/Record-Keeping properties
+	@NonNull Map<String, DDItem> named = new HashMap<>();
+	@NonFinal Map<EquipmentSlot, String> previous = null;
 	
-	public DDInventory() {
-		items = new ArrayList<>();
-		equipment = new EnumMap<>(EquipmentSlot.class);
-	}
+	public DDInventory() {}
 	
 	@JsonCreator
-	public DDInventory(DDInventory.Serializable data) {
-		items = new ArrayList<>(data.items);
-		equipment = new EnumMap<>(EquipmentSlot.class);
-		final Map<String, DDItem> named = items.stream().collect(
-				Collectors.toMap(DDItem::getName, i -> i));
-		data.equipment.entrySet().stream().forEach(
-				e -> equipment.put(e.getKey(), named.get(e.getValue())));
+	private DDInventory(DDInventory.Serializable data) {
+		items.addAll(data.items);
+		named.putAll(items.stream().collect(Collectors.toMap(DDItem::getFullName, i -> i)));
+		fromSerializableEquipment(data.equipment);
+		favorites.putAll(data.favorites);
+		wealth.assign(data.wealth);
 	}
-	
+
 	@JsonValue
 	private DDInventory.Serializable getSerial() {
-		final Map<EquipmentSlot, String> m = new EnumMap<>(EquipmentSlot.class);
-		equipment.entrySet().stream().forEach( 
-				e -> m.put(e.getKey(), e.getValue().getName())
-				);
-		return new DDInventory.Serializable(items, m);
-	}
-	
-	public Map<EquipmentSlot, DDItem> getEquipment() {
-		return Collections.unmodifiableMap(equipment);
+		return new DDInventory.Serializable(items, getSerializableEquipment(), favorites, wealth);
 	}
-	
-	public boolean canEquip( final DDItem item ) {
-		return canEquip( item.getSlot() );
+
+	public void load(final String name) {
+		final @NonNull Map<EquipmentSlot, String> chosen;
+		if (name.equals(Constants.PREVIOUS_LOADOUT)) { chosen = previous; }
+		else { chosen = favorites.get(name); }
+		previous = getSerializableEquipment();
+		fromSerializableEquipment(chosen);
 	}
 	
-	private boolean canEquip( final EquipmentSlot slot ) {
-		switch ( slot ) {
-		case NONE: return false;
-		case TWO_HANDS: return canEquip( EquipmentSlot.MAIN_HAND ) && 
-				canEquip( EquipmentSlot.OFF_HAND );
-		case ONE_HAND: return canEquip( EquipmentSlot.MAIN_HAND ) || 
-				canEquip( EquipmentSlot.OFF_HAND );
-		case RING: return canEquip( EquipmentSlot.RING1 ) || 
-				canEquip( EquipmentSlot.RING2 );
-		default:
-			return !equipment.containsKey(slot);
-		}
+	public void save(final String name) {
+		favorites.put(name, getSerializableEquipment());
 	}
 	
-	public void equip( final EquipmentSlot slot, final DDItem item ) {
-		switch ( slot ) {
-		case NONE:
-		case ONE_HAND:
-		case RING:
-			throw new IllegalArgumentException("Cannot equip directly to slot:" + slot);
-		case TWO_HANDS: 
-			equip( EquipmentSlot.MAIN_HAND, item ); 
-			equip( EquipmentSlot.OFF_HAND, item );
-			break;
-		default:
-			equipment.put(slot, item);
-			break;
-		}
+	public Map<String, Map<EquipmentSlot, String>> getFavorites() {
+		return Collections.unmodifiableMap(favorites);
 	}
 	
-	public void unequip( final EquipmentSlot slot ) {
-		switch ( slot ) {
-		case NONE:
-		case ONE_HAND:
-		case RING:
-			throw new IllegalArgumentException("Cannot unequip directly to slot:" + slot);
-		case TWO_HANDS: 
-			unequip( EquipmentSlot.MAIN_HAND ); 
-			unequip( EquipmentSlot.OFF_HAND );
-			break;
-		default:
-			equipment.remove(slot);
-			break;
-		}
+	public boolean hasItem(final String itemName) {
+		return named.containsKey(itemName);
 	}
 
-	public void equipNext( final DDItem item ) {
-		switch ( item.getSlot() ) {
-		case NONE:
-			throw new IllegalArgumentException("Cannot equip unequippable item");
-		case ONE_HAND:
-			if ( canEquip( EquipmentSlot.MAIN_HAND ) ) { 
-				equip( EquipmentSlot.MAIN_HAND, item );
-			} else {
-				equip( EquipmentSlot.OFF_HAND, item );
-			}
-			break;
-		case RING:
-			if ( canEquip( EquipmentSlot.RING1 ) ) { 
-				equip( EquipmentSlot.RING1, item );
-			} else {
-				equip( EquipmentSlot.RING2, item );
-			}
-			break;
-		default:
-			equip( item.getSlot(), item );
-			break;
-		}
+	private Map<EquipmentSlot, String> getSerializableEquipment() {
+		final Map<EquipmentSlot, String> m = new EnumMap<>(EquipmentSlot.class);
+		equipment.forEach((k, v) -> m.put(k, v.getName()));
+		return Collections.unmodifiableMap(m);
 	}
+	
+	private void fromSerializableEquipment(@NonNull Map<EquipmentSlot, String> data) {
+		equipment.clear();
+		data.entrySet().stream().forEach(e -> {
+			@NonNull DDItem item = named.get(e.getValue()); // Throws NPE if item d/n exist any more
+			equipment.put(e.getKey(), item);
+		});
+	}	
 }

+ 21 - 5
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDItem.java

@@ -16,15 +16,14 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.experimental.FieldDefaults;
 
-@AllArgsConstructor
+@AllArgsConstructor @NoArgsConstructor
 @Data
-@NoArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE)
 public class DDItem {
 	String name = "";
 	IntValue count = new IntValue(1);
-	int countEquipped = 0;
-	IntValue weight = new IntValue(0);
+	IntValue countEquipped = new IntValue(0);
+	float weight = 0.f;
 	Money value = new Money(0, 0, 0, 0);
 	StringValue page = new StringValue();
 	EquipmentSlot slot = EquipmentSlot.NONE;
@@ -46,12 +45,29 @@ public class DDItem {
 		properties.put(key, prop);
 	}
 	
+	public String getFullName() {
+		return (isWeapon() ? weapon.getNameModifier() : isArmor() ? armor.getNameModifier() : "") + getName(); 
+	}
+	
+	public Money getActualValue() {
+		return isWeapon() ? value.sum(weapon.getActualValue()) : isArmor() ? value.sum(armor.getActualValue()) : value;
+	}
+	
+	public void adjustCount(int amt) {
+		this.count.value(this.count.value() + amt);
+	}
+	
+	public void adjustCounEquipped(int amt) {
+		this.countEquipped.value(this.countEquipped.value() + amt);
+	}
+	
 	@JsonAnyGetter 
 	private Map<String, Object> getProperties() { 
+		if ( properties == null ) { properties = new HashMap<>(); }
 		return Collections.unmodifiableMap(properties);
 	}
 	
 	public int getUnequippedCount() {
-		return count.value() - countEquipped;
+		return count.value() - countEquipped.value();
 	}
 }

+ 21 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDWeapon.java

@@ -2,22 +2,30 @@ package org.leumasjaffe.charsheet.model.inventory;
 
 import lombok.AccessLevel;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.experimental.FieldDefaults;
 
-@Data
+@Data @EqualsAndHashCode(callSuper=true)
 @FieldDefaults(level=AccessLevel.PRIVATE)
-public class DDWeapon {
+public class DDWeapon extends DDEnchantableItem {
 	public static enum Type { Piercing, Bludgeoning, Slashing }
-	int attackBonus;
+
 	String damage;
 	String secondaryDamage;
-	int damageBonus;
 	int criticalThreat;
 	int criticalDamage;
 	int secondaryCriticalDamage;
+	
 	Range range;
 	Type type;
 	
+	public Money getActualValue() {
+		int gp = isMasterwork ? 300 : 0;
+		if (hasSecondaryAttack()) gp *= 2;
+		gp += 2000 * (bonus.value + getEnchantBonus());
+		return Money.fromCopperToGold(100 * gp + getAdHocPrice());
+	}
+
 	public boolean hasCriticalThreat() {
 		return criticalThreat != 20 && criticalThreat != 0;
 	}
@@ -25,4 +33,13 @@ public class DDWeapon {
 	public boolean hasSecondaryAttack() {
 		return secondaryDamage != null && !secondaryDamage.isEmpty();
 	}
+	
+	public int getDamageBonus() {
+		return bonus.getValue();
+	}
+
+	public int getAttackBonus() {
+		return bonus.getValue() == 0 && isMasterwork ? 1 : bonus.getValue();
+	}
+
 }

+ 12 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Enchantment.java

@@ -0,0 +1,12 @@
+package org.leumasjaffe.charsheet.model.inventory;
+
+import java.util.Optional;
+
+import lombok.Getter;
+
+@Getter
+public class Enchantment {
+	Enhancement effectiveBonus;
+	String name;
+	Optional<Money> adHocValue;
+}

+ 22 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Enhancement.java

@@ -0,0 +1,22 @@
+package org.leumasjaffe.charsheet.model.inventory;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+public enum Enhancement {
+	NONE(0), P1(1), P2(2), P3(3), P4(4), P5(5);
+	
+	@Getter final int value;
+	
+	@JsonCreator Enhancement of(String name) {
+		return name.equals("") ? NONE : Enhancement.valueOf(name.replace('+', 'P'));
+	}
+	
+	@JsonValue String toValue() {
+		return this == NONE ? "" : name().replace('P', '+');
+	}
+}

+ 17 - 3
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Money.java

@@ -11,12 +11,20 @@ import lombok.experimental.FieldDefaults;
 public class Money implements Comparable<Money> {
 	int pp, gp, sp, cp;
 	
-	Money sum( final Money other ) {
+	public Money assign(final Money other) {
+		setPp(other.getPp());
+		setGp(other.getGp());
+		setSp(other.getSp());
+		setCp(other.getCp());
+		return this;
+	}
+	
+	public Money sum( final Money other ) {
 		return new Money(pp + other.pp, gp + other.gp, sp + other.sp, cp + other.cp);
 	}
 	
-	Money subtract( final Money other ) {
-		final Money tmp = new Money(pp + other.pp, gp + other.gp, sp + other.sp, cp + other.cp);
+	public Money difference( final Money other ) {
+		final Money tmp = new Money(pp - other.pp, gp - other.gp, sp - other.sp, cp - other.cp);
 		tmp.rebalance( );
 		return tmp;
 	}
@@ -50,6 +58,12 @@ public class Money implements Comparable<Money> {
 		final int sp = cp / 10; cp -= sp * 10;
 		return new Money(pp, gp, sp, cp);
 	}
+	
+	public static Money fromCopperToGold(int cp) {
+		final int gp = cp / 100; cp -= gp * 100;
+		final int sp = cp / 10; cp -= sp * 10;
+		return new Money(0, gp, sp, cp);
+	}
 
 	public int asCopper() {
 		return cp + sp * 10 + gp * 100 + pp * 1000;

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Area.java

@@ -1,6 +1,6 @@
 package org.leumasjaffe.charsheet.model.magic.dimension;
 
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonSubTypes.Type;

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Duration.java

@@ -4,7 +4,7 @@ import static org.leumasjaffe.charsheet.config.Constants.*;
 import static org.leumasjaffe.charsheet.config.Constants.DurationMeasurement.*;
 
 import org.leumasjaffe.charsheet.config.Config;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Effect.java

@@ -1,7 +1,7 @@
 package org.leumasjaffe.charsheet.model.magic.dimension;
 
-import org.leumasjaffe.charsheet.util.StringHelper;
 import org.leumasjaffe.format.Named;
+import org.leumasjaffe.format.StringHelper;
 
 import lombok.AccessLevel;
 import lombok.Getter;

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Range.java

@@ -1,7 +1,7 @@
 package org.leumasjaffe.charsheet.model.magic.dimension;
 
 import org.leumasjaffe.charsheet.config.Config;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Target.java

@@ -1,7 +1,7 @@
 package org.leumasjaffe.charsheet.model.magic.dimension;
 
-import org.leumasjaffe.charsheet.util.StringHelper;
 import org.leumasjaffe.format.Named;
+import org.leumasjaffe.format.StringHelper;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/observer/helper/AbilModStringify.java

@@ -5,7 +5,7 @@ import java.util.function.BiConsumer;
 import javax.swing.text.JTextComponent;
 
 import org.leumasjaffe.charsheet.model.Ability;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 public class AbilModStringify implements BiConsumer<JTextComponent, Ability.Scores> {
 

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/observer/helper/IntValueStringify.java

@@ -5,7 +5,7 @@ import java.util.function.BiConsumer;
 import javax.swing.text.JTextComponent;
 
 import org.leumasjaffe.charsheet.model.observable.IntValue;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import lombok.experimental.UtilityClass;
 

+ 9 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/ClassTab.java

@@ -10,9 +10,10 @@ import javax.swing.JTextField;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.observable.IntValue;
-import org.leumasjaffe.charsheet.util.StringHelper;
 import org.leumasjaffe.charsheet.view.magic.SpellPanel;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
@@ -185,4 +186,11 @@ public class ClassTab extends JPanel {
 	public String getName() {
 		return title;
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(levelListener);
+		ObserverDispatch.unsubscribeAll(expListener);
+	}
 }

+ 0 - 2
src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java

@@ -24,8 +24,6 @@ import javax.swing.UIManager;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.observer.ObserverHelper;
-import org.leumasjaffe.charsheet.view.builders.DialogBuilder;
-import org.leumasjaffe.charsheet.view.dev.DeveloperMenu;
 
 import java.awt.event.KeyEvent;
 import java.awt.event.WindowEvent;

+ 1 - 2
src/main/lombok/org/leumasjaffe/charsheet/view/dev/DeveloperMenu.java

@@ -1,4 +1,4 @@
-package org.leumasjaffe.charsheet.view.dev;
+package org.leumasjaffe.charsheet.view;
 
 import java.util.stream.Collectors;
 
@@ -9,7 +9,6 @@ import javax.swing.JOptionPane;
 
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
-import org.leumasjaffe.charsheet.view.builders.DialogBuilder;
 
 public class DeveloperMenu extends JMenu {
 	/**

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/builders/DialogBuilder.java

@@ -1,4 +1,4 @@
-package org.leumasjaffe.charsheet.view.builders;
+package org.leumasjaffe.charsheet.view;
 
 import java.awt.Dialog.ModalityType;
 

+ 191 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/dialog/BuySellItemDialog.java

@@ -0,0 +1,191 @@
+package org.leumasjaffe.charsheet.view.dialog;
+
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+
+import org.leumasjaffe.charsheet.model.inventory.Money;
+import org.leumasjaffe.format.StringHelper;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+import javax.swing.Box;
+import javax.swing.JLabel;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import javax.swing.SwingConstants;
+import javax.swing.JTextField;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class BuySellItemDialog extends JPanel {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	
+	JSpinner spinner;
+
+	public BuySellItemDialog(final String itemName, final int rawValue, int cap) {
+		spinner = new JSpinner(new SpinnerNumberModel(1, 1, cap, 1));
+
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{120, 0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+
+		JLabel lblName = new JLabel("ITEM NAME");
+		lblName.setOpaque(true);
+		lblName.setHorizontalAlignment(SwingConstants.CENTER);
+		lblName.setForeground(Color.WHITE);
+		lblName.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblName.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblName = new GridBagConstraints();
+		gbc_lblName.gridheight = 2;
+		gbc_lblName.fill = GridBagConstraints.BOTH;
+		gbc_lblName.insets = new Insets(0, 0, 0, 1);
+		gbc_lblName.gridx = 0;
+		gbc_lblName.gridy = 0;
+		add(lblName, gbc_lblName);
+
+		JTextField txtName = new JTextField(itemName);
+		txtName.setEditable(false);
+		GridBagConstraints gbc_txtName = new GridBagConstraints();
+		gbc_txtName.insets = new Insets(0, 0, 0, 0);
+		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtName.gridx = 0;
+		gbc_txtName.gridy = 2;
+		add(txtName, gbc_txtName);
+
+		Component verticalStrut = Box.createVerticalStrut(20);
+		verticalStrut.setMinimumSize(new Dimension(0, 10));
+		verticalStrut.setMaximumSize(new Dimension(32767, 10));
+		verticalStrut.setPreferredSize(new Dimension(0, 10));
+		GridBagConstraints gbc_verticalStrut = new GridBagConstraints();
+		gbc_verticalStrut.insets = new Insets(0, 0, 0, 1);
+		gbc_verticalStrut.fill = GridBagConstraints.HORIZONTAL;
+		gbc_verticalStrut.gridx = 1;
+		gbc_verticalStrut.gridy = 0;
+		add(verticalStrut, gbc_verticalStrut);
+		
+		JLabel lblPp = new JLabel("pp");
+		lblPp.setOpaque(true);
+		lblPp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblPp.setForeground(Color.WHITE);
+		lblPp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblPp.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblPp = new GridBagConstraints();
+		gbc_lblPp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblPp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblPp.gridx = 1;
+		gbc_lblPp.gridy = 1;
+		add(lblPp, gbc_lblPp);
+		
+		JLabel lblGp = new JLabel("gp");
+		lblGp.setOpaque(true);
+		lblGp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblGp.setForeground(Color.WHITE);
+		lblGp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblGp.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblGp = new GridBagConstraints();
+		gbc_lblGp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblGp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblGp.gridx = 2;
+		gbc_lblGp.gridy = 1;
+		add(lblGp, gbc_lblGp);
+		
+		JLabel lblSp = new JLabel("sp");
+		lblSp.setOpaque(true);
+		lblSp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblSp.setForeground(Color.WHITE);
+		lblSp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblSp.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblSp = new GridBagConstraints();
+		gbc_lblSp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblSp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblSp.gridx = 3;
+		gbc_lblSp.gridy = 1;
+		add(lblSp, gbc_lblSp);
+		
+		JLabel lblCp = new JLabel("cp");
+		lblCp.setOpaque(true);
+		lblCp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblCp.setForeground(Color.WHITE);
+		lblCp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblCp.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblCp = new GridBagConstraints();
+		gbc_lblCp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblCp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblCp.gridx = 4;
+		gbc_lblCp.gridy = 1;
+		add(lblCp, gbc_lblCp);
+		
+		JTextField txtPp = new JTextField();
+		txtPp.setEditable(false);
+		txtPp.setColumns(6);
+		GridBagConstraints gbc_txtPp = new GridBagConstraints();
+		gbc_txtPp.insets = new Insets(0, 0, 0, 0);
+		gbc_txtPp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtPp.gridx = 1;
+		gbc_txtPp.gridy = 2;
+		add(txtPp, gbc_txtPp);
+		
+		JTextField txtGp = new JTextField();
+		txtGp.setEditable(false);
+		txtGp.setColumns(3);
+		GridBagConstraints gbc_txtGp = new GridBagConstraints();
+		gbc_txtGp.insets = new Insets(0, 0, 0, 0);
+		gbc_txtGp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtGp.gridx = 2;
+		gbc_txtGp.gridy = 2;
+		add(txtGp, gbc_txtGp);
+		
+		JTextField txtSp = new JTextField();
+		txtSp.setEditable(false);
+		txtSp.setColumns(3);
+		GridBagConstraints gbc_txtSp = new GridBagConstraints();
+		gbc_txtSp.insets = new Insets(0, 0, 0, 0);
+		gbc_txtSp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtSp.gridx = 3;
+		gbc_txtSp.gridy = 2;
+		add(txtSp, gbc_txtSp);
+		
+		JTextField txtCp = new JTextField();
+		txtCp.setEditable(false);
+		txtCp.setColumns(3);
+		GridBagConstraints gbc_txtCp = new GridBagConstraints();
+		gbc_txtCp.insets = new Insets(0, 0, 0, 0);
+		gbc_txtCp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtCp.gridx = 4;
+		gbc_txtCp.gridy = 2;
+		add(txtCp, gbc_txtCp);
+		
+		GridBagConstraints gbc_spinner = new GridBagConstraints();
+		gbc_spinner.fill = GridBagConstraints.HORIZONTAL;
+		gbc_spinner.insets = new Insets(0, 0, 0, 0);
+		gbc_spinner.gridwidth = 5;
+		gbc_spinner.gridx = 0;
+		gbc_spinner.gridy = 3;
+		add(spinner, gbc_spinner);
+		spinner.addChangeListener(e -> {
+			final Money value = Money.fromCopper(rawValue * getSelectedNumber());
+			txtPp.setText(StringHelper.toString(value.getPp()));
+			txtGp.setText(StringHelper.toString(value.getGp()));
+			txtSp.setText(StringHelper.toString(value.getSp()));
+			txtCp.setText(StringHelper.toString(value.getCp()));
+		});
+		spinner.getChangeListeners()[0].stateChanged(null);
+	}
+	
+	public int getSelectedNumber() {
+		return (Integer) spinner.getModel().getValue();
+	}
+}

+ 4 - 4
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipItemPanel.java

@@ -1,4 +1,4 @@
-package org.leumasjaffe.charsheet.view.inventory;
+package org.leumasjaffe.charsheet.view.dialog;
 
 import javax.swing.JPanel;
 import java.awt.GridBagLayout;
@@ -17,18 +17,18 @@ import lombok.AllArgsConstructor;
 
 import java.awt.Font;
 
-public class EquipItemPanel extends JPanel {
+public class EquipItemDialog extends JPanel {
 	@AllArgsConstructor
 	public static class Tuple { EquipmentSlot slot; DDItem item; }
 	/**
 	 * 
 	 */
 	private static final long serialVersionUID = 1L;
-	public EquipItemPanel(final String title, final EquipmentSlot slot, final DDItem item) {
+	public EquipItemDialog(final String title, final EquipmentSlot slot, final DDItem item) {
 		this(title, new Tuple(slot, item));
 	}
 	
-	public EquipItemPanel(final String title, final Tuple... tuples) {
+	public EquipItemDialog(final String title, final Tuple... tuples) {
 		setOpaque(false);
 		setLayout(new VerticalLayout(5));
 

+ 5 - 5
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ArmorPanel.java

@@ -14,7 +14,7 @@ import javax.swing.SwingConstants;
 
 import org.leumasjaffe.charsheet.model.inventory.DDArmor;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import java.awt.Component;
 import javax.swing.Box;
@@ -113,7 +113,7 @@ public class ArmorPanel extends JPanel {
 		gbc_lblMaxDex.gridy = 1;
 		panel.add(lblMaxDex, gbc_lblMaxDex);
 		
-		JTextField armorNameField = new JTextField(item.getName());
+		JTextField armorNameField = new JTextField(item.getFullName());
 		GridBagConstraints gbc_armorNameField = new GridBagConstraints();
 		gbc_armorNameField.insets = new Insets(0, 0, 0, 0);
 		gbc_armorNameField.fill = GridBagConstraints.HORIZONTAL;
@@ -131,7 +131,7 @@ public class ArmorPanel extends JPanel {
 		panel.add(armorTypeField, gbc_armorTypeField);
 		armorTypeField.setColumns(10);
 		
-		JTextField armorBonusField = new JTextField(StringHelper.toSignedString(item.getArmor().getBonus()));
+		JTextField armorBonusField = new JTextField(StringHelper.toSignedString(item.getArmor().getActualAcBonus()));
 		armorBonusField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_armorBonusField = new GridBagConstraints();
 		gbc_armorBonusField.insets = new Insets(0, 0, 0, 0);
@@ -228,7 +228,7 @@ public class ArmorPanel extends JPanel {
 		gbc_lblSpecialProperties.gridy = 0;
 		panel_1.add(lblSpecialProperties, gbc_lblSpecialProperties);
 		
-		JTextField checkField = new JTextField(StringHelper.toString(armor.getCheckPenalty()));
+		JTextField checkField = new JTextField(StringHelper.toString(armor.getActualCheckPenalty()));
 		checkField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_checkField = new GridBagConstraints();
 		gbc_checkField.insets = new Insets(0, 0, 0, 0);
@@ -258,7 +258,7 @@ public class ArmorPanel extends JPanel {
 		panel_1.add(speedField, gbc_speedField);
 		speedField.setColumns(10);
 		
-		JTextField weightField = new JTextField(StringHelper.toString(item.getWeight().value()) + " lb.");
+		JTextField weightField = new JTextField(StringHelper.toString(item.getWeight()) + " lb.");
 		GridBagConstraints gbc_weightField = new GridBagConstraints();
 		gbc_weightField.insets = new Insets(0, 0, 0, 0);
 		gbc_weightField.fill = GridBagConstraints.HORIZONTAL;

+ 0 - 89
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipItemHelper.java

@@ -1,89 +0,0 @@
-package org.leumasjaffe.charsheet.view.inventory;
-
-import static org.leumasjaffe.charsheet.model.inventory.EquipmentSlot.*;
-
-import javax.swing.JOptionPane;
-
-import org.leumasjaffe.charsheet.model.inventory.DDInventory;
-import org.leumasjaffe.charsheet.model.inventory.DDItem;
-import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
-import org.leumasjaffe.function.VoidVoidFunction;
-import org.leumasjaffe.observer.ObserverDispatch;
-
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-public class EquipItemHelper implements VoidVoidFunction {
-	final InventoryPanel panel;
-	final DDInventory inv;
-	final DDItem item;
-	
-	public void apply() {
-		if ( item.getUnequippedCount() == 0 ) { return; }
-		if ( inv.canEquip( item ) || getReplaceItem( item.getSlot() ) ) {
-			item.setCountEquipped(item.getCountEquipped() + 1);
-			inv.equipNext( item );
-			ObserverDispatch.notifySubscribers(inv, panel);
-		}
-	}
-
-	private boolean getReplaceItem(final EquipmentSlot slot) {
-		switch ( slot ) {
-		case TWO_HANDS: return selectToReplaceAllOf( TWO_HANDS, MAIN_HAND, OFF_HAND );
-		case ONE_HAND: return selectToReplaceOneOf( MAIN_HAND, OFF_HAND );
-		case RING: return selectToReplaceOneOf( RING1, RING2 );
-		default: return selectToReplace( slot );
-		}
-	}
-
-	private boolean selectToReplaceAllOf(final EquipmentSlot base,
-			final EquipmentSlot slot1, final EquipmentSlot slot2) {
-		final EquipItemPanel panel;
-		if ( inv.getEquipment().get(slot1).getSlot() == base ) {
-			panel = new EquipItemPanel("Do you want to replace the following item?", 
-					slot1, inv.getEquipment().get(slot1));
-		} else {
-			panel = new EquipItemPanel("Do you want to replace both of the following items?", 
-					new EquipItemPanel.Tuple(slot1, inv.getEquipment().get(slot1)),
-					new EquipItemPanel.Tuple(slot2, inv.getEquipment().get(slot2)));
-		}
-		if ( JOptionPane.showConfirmDialog(null, 
-				panel, "Replace Equipped Item", JOptionPane.YES_NO_OPTION) 
-				== JOptionPane.YES_OPTION ) {
-			inv.unequip( slot1 );
-			inv.unequip( slot2 );
-			return true;
-		}
-		return false;
-	}
-
-	private boolean selectToReplaceOneOf(final EquipmentSlot slot1, 
-			final EquipmentSlot slot2) {
-		final int choice = JOptionPane.showOptionDialog(null, 
-				new EquipItemPanel("Which of the following items would you like to replace?", 
-						new EquipItemPanel.Tuple(slot1, inv.getEquipment().get(slot1)),
-						new EquipItemPanel.Tuple(slot2, inv.getEquipment().get(slot2))), 
-				"Replace Equipped Item", JOptionPane.YES_NO_CANCEL_OPTION,
-				JOptionPane.QUESTION_MESSAGE, null, 
-				new String[] {"Cancel", slot1.toString(), slot2.toString()}, null );
-		if ( choice == JOptionPane.YES_OPTION ) {
-			inv.unequip( slot1 );
-			return true;
-		} else if ( choice == JOptionPane.NO_OPTION ) {
-			inv.unequip( slot2 );
-			return true;
-		}
-		return false;
-	}
-
-	private boolean selectToReplace(final EquipmentSlot slot) {
-		if ( JOptionPane.showConfirmDialog(null, 
-				new EquipItemPanel("Do you want to replace the following item?", slot, inv.getEquipment().get(slot)), 
-				"Replace Equipped Item", JOptionPane.YES_NO_OPTION) 
-				== JOptionPane.YES_OPTION ) {
-			inv.unequip( slot );
-			return true;
-		}
-		return false;
-	}
-}

+ 21 - 12
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentInfoMenu.java

@@ -2,11 +2,12 @@ package org.leumasjaffe.charsheet.view.inventory;
 
 import javax.swing.JPopupMenu;
 
+import org.leumasjaffe.charsheet.model.inventory.DDEquipment;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
-import org.leumasjaffe.function.VoidVoidFunction;
-
-import java.util.function.Consumer;
+import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
+import org.leumasjaffe.observer.ObserverDispatch;
 
+import javax.swing.JFrame;
 import javax.swing.JMenuItem;
 import javax.swing.JOptionPane;
 
@@ -15,23 +16,31 @@ class EquipmentInfoMenu extends JPopupMenu {
 	 * 
 	 */
 	private static final long serialVersionUID = 1L;
-	public Consumer<Void> example;
 	
-	public EquipmentInfoMenu(DDItem item, VoidVoidFunction unequipAction) {
+	public EquipmentInfoMenu(DDEquipment equip, DDItem item,
+			EquipmentSlot slot) {
 		
 		JMenuItem mntmInfo = new JMenuItem("Info");
 		add(mntmInfo);
-		
+		mntmInfo.addActionListener(e -> {
+			JFrame frame = new JFrame(item.getName());
+			frame.add(new ItemInfoPanel(item));
+			frame.pack();
+			frame.setVisible(true);
+		});
+
 		JMenuItem mntmEquip = new JMenuItem("Unequip");
-		mntmEquip.addActionListener( e -> {
-			if ( JOptionPane.showConfirmDialog(null, 
+		mntmEquip.addActionListener(e -> {
+			if (JOptionPane.showConfirmDialog(null, 
 					"Are you sure you want to unequip your " + item.getName(), 
 					"Unequip", JOptionPane.YES_NO_OPTION) 
-					== JOptionPane.YES_OPTION ) {
-				unequipAction.apply();
-				System.out.println("Unequiping " + item.getName());
+					== JOptionPane.YES_OPTION) {
+				item.adjustCounEquipped(-1);
+				equip.unequip(slot);
+				ObserverDispatch.notifySubscribers(equip, null);
+				ObserverDispatch.notifySubscribers(item.getCountEquipped(), null);
 			}
-		} );
+		});
 		add(mntmEquip);
 	}
 

+ 29 - 24
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentPanel.java

@@ -8,7 +8,7 @@ import javax.swing.border.MatteBorder;
 
 import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.charsheet.model.DDCharacter;
-import org.leumasjaffe.charsheet.model.inventory.DDInventory;
+import org.leumasjaffe.charsheet.model.inventory.DDEquipment;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
 import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
 import org.leumasjaffe.event.PopClickListener;
@@ -32,13 +32,15 @@ import static org.leumasjaffe.charsheet.model.inventory.EquipmentSlot.*;
 
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class EquipmentPanel extends JPanel {
+	
 	/**
 	 * 
 	 */
 	private static final long serialVersionUID = 1L;
 	JPanel equipment;
+	LoadoutMenu loadoutMenu = new LoadoutMenu();
 	
-	ObservableListener<EquipmentPanel, DDInventory> equipmentObserver;
+	ObservableListener<EquipmentPanel, DDEquipment> equipmentObserver;
 
 	public EquipmentPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
@@ -64,6 +66,7 @@ public class EquipmentPanel extends JPanel {
 		lblEquipment.setBackground(Color.BLACK);
 		lblEquipment.setOpaque(true);
 		lblEquipment.setForeground(Color.WHITE);
+		lblEquipment.addMouseListener(new PopClickListener(this.loadoutMenu));
 		scrollPane.setColumnHeaderView(lblEquipment);
 
 		equipment = new JPanel();
@@ -72,24 +75,25 @@ public class EquipmentPanel extends JPanel {
 		equipment.setLayout(new VerticalLayout(5));
 		
 		equipmentObserver = new ObservableListener<>(this, 
-				(self, inv) -> self.updateModel(inv) );
+				(self, equip) -> self.updateModel(equip) );
 	}
 	
 	public void setModel(DDCharacter model) {
-		equipmentObserver.setObserved(model.getInventory());
+		equipmentObserver.setObserved(model.getInventory().getEquipment());
+		loadoutMenu.setModel(model.getInventory());
 	}
 
-	private void updateModel(final DDInventory inv) {
+	private void updateModel(final DDEquipment equip) {
 		final Set<EquipmentSlot> manual = EnumSet.noneOf(EquipmentSlot.class);
 		equipment.removeAll();	
-		final DDItem armor = inv.getEquipment().get(BODY);
-		if ( armor != null && armor.isArmor() ) {
+		final DDItem armor = equip.get(BODY);
+		if (armor != null && armor.isArmor()) {
 			manual.add(BODY);
-			createWithRightClickMenu(ArmorPanel::new, inv, BODY, BODY);
+			createWithRightClickMenu(ArmorPanel::new, equip, BODY, BODY);
 		}
 		
-		final DDItem main = inv.getEquipment().get(MAIN_HAND);
-		final DDItem off = inv.getEquipment().get(OFF_HAND);
+		final DDItem main = equip.get(MAIN_HAND);
+		final DDItem off = equip.get(OFF_HAND);
 	
 		final Optional<Function<DDItem, JPanel>> makeMain = getEquipmentRightClickPanelFactory(main);
 		final Optional<Function<DDItem, JPanel>> makeOff = getEquipmentRightClickPanelFactory(off);
@@ -98,23 +102,24 @@ public class EquipmentPanel extends JPanel {
 			makeMain.ifPresent(f -> {
 				manual.add(MAIN_HAND);
 				manual.add(OFF_HAND);
-				createWithRightClickMenu(f, inv, TWO_HANDS, MAIN_HAND);
+				createWithRightClickMenu(f, equip, TWO_HANDS, MAIN_HAND);
 			});
 		} else {
 			makeMain.ifPresent(f -> {
 				manual.add(MAIN_HAND);
-				createWithRightClickMenu(f, inv, MAIN_HAND, MAIN_HAND);
+				createWithRightClickMenu(f, equip, MAIN_HAND, MAIN_HAND);
 			});
 			makeOff.ifPresent(f -> {
 				manual.add(OFF_HAND);
-				createWithRightClickMenu(f, inv, OFF_HAND, OFF_HAND);
+				createWithRightClickMenu(f, equip, OFF_HAND, OFF_HAND);
 			});
 		}
 		
-		inv.getEquipment().keySet().stream().filter( slot -> ! manual.contains(slot) )
+		equip.keySet().stream().filter( slot -> equip.get(slot) != null && !manual.contains(slot) )
 		.forEach( slot -> {
-			createWithRightClickMenu(null, inv, slot, slot);
+			createWithRightClickMenu(null, equip, slot, slot);
 		});
+		repaint();
 	}
 
 	private Optional<Function<DDItem, JPanel>> getEquipmentRightClickPanelFactory(final DDItem item) {
@@ -129,17 +134,17 @@ public class EquipmentPanel extends JPanel {
 	}
 	
 	private void createWithRightClickMenu(final Function<DDItem, JPanel> make,
-			final DDInventory inv, final EquipmentSlot slot, EquipmentSlot get) {
-		final DDItem item = inv.getEquipment().get(get);
+			final DDEquipment equip, final EquipmentSlot slot, EquipmentSlot get) {
+		final DDItem item = equip.get(get);
 		final JPanel panel = make.apply(item);
 		equipment.add(panel);
 		panel.addMouseListener(new PopClickListener(
-				new EquipmentInfoMenu(item, () -> {
-					equipment.remove(panel);
-					item.setCountEquipped(item.getCountEquipped() - 1);
-					inv.unequip(slot);
-					repaint();
-					ObserverDispatch.notifySubscribers(inv, this);
-				})));
+				new EquipmentInfoMenu(equip, item, slot)));
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(equipmentObserver);
 	}
 }

+ 8 - 13
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/InventoryPanel.java

@@ -10,9 +10,8 @@ import javax.swing.border.MatteBorder;
 import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.inventory.DDInventory;
-import org.leumasjaffe.charsheet.model.inventory.DDItem;
-import org.leumasjaffe.event.PopClickListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -30,7 +29,7 @@ public class InventoryPanel extends JPanel {
 	private static final long serialVersionUID = 1L;
 	JComponent inventory;
 	ObservableListener<InventoryPanel, DDInventory> inventoryObserver;
-
+	
 	public InventoryPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0};
@@ -67,20 +66,16 @@ public class InventoryPanel extends JPanel {
 
 	private void updateModel(DDInventory inv) {
 		inventory.removeAll();
-		
-		inv.getItems().stream().forEach( 
-				item -> createWithRightClickMenu(inv, item) );
+		inv.getItems().stream().forEach(item -> inventory.add(new ItemPanel(inv, item)));
 	}
 
 	public void setModel(DDCharacter model) {
 		inventoryObserver.setObserved(model.getInventory());
 	}
-
-	private void createWithRightClickMenu(final DDInventory inv, final DDItem item) {
-		final ItemPanel panel = new ItemPanel(item);
-		panel.addMouseListener(new PopClickListener(
-				new ItemInfoMenu(item, new EquipItemHelper(this, inv, item))));
-		inventory.add(panel);
-	}
 	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(inventoryObserver);
+	}
 }

+ 91 - 9
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemInfoMenu.java

@@ -2,31 +2,113 @@ package org.leumasjaffe.charsheet.view.inventory;
 
 import javax.swing.JPopupMenu;
 
+import org.leumasjaffe.charsheet.controller.EquipItemController;
+import org.leumasjaffe.charsheet.model.inventory.DDInventory;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
 import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
-import org.leumasjaffe.function.VoidVoidFunction;
+import org.leumasjaffe.charsheet.model.inventory.Money;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.view.dialog.BuySellItemDialog;
+import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+import javax.swing.JFrame;
 import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
 
 class ItemInfoMenu extends JPopupMenu {
 	/**
 	 * 
 	 */
 	private static final long serialVersionUID = 1L;
+	
+	IndirectObservableListener<JMenuItem, IntValue> sellListener, equipListener = null;
+	ObservableListener<JMenuItem, DDInventory> buyListener;
+
+	public ItemInfoMenu(final DDInventory inv, final DDItem item) {
+		final int rawValue = item.getValue().asCopper();
+		final BuySellAction doTxn = new BuySellAction(inv, item);
+		final BuySellDialogHelper dlg = new BuySellDialogHelper(item.getFullName());
 
-	public ItemInfoMenu(final DDItem item, final VoidVoidFunction cons) {
-		
 		JMenuItem mntmInfo = new JMenuItem("Info");
+		mntmInfo.addActionListener(e -> {
+			JFrame frame = new JFrame(item.getFullName());
+			frame.add(new ItemInfoPanel(item));
+			frame.pack();
+			frame.setVisible(true);
+		});
 		add(mntmInfo);
-		
-		if ( item.getSlot() != EquipmentSlot.NONE && item.getUnequippedCount() > 0 ) {
+
+		JMenuItem mntmBuy = new JMenuItem("Purchase");
+		mntmBuy.addActionListener(e -> {
+			doTxn.applyTransaction(dlg.getNumUnits("Purchase", rawValue, inv.getWealth().asCopper() / rawValue), rawValue);
+		});
+		buyListener = new ObservableListener<>(mntmBuy, (c, v) -> {
+			c.setEnabled(inv.getWealth().asCopper() >= item.getValue().asCopper());
+		});
+		buyListener.setObserved(inv);
+		add(mntmBuy);
+
+		JMenuItem mntmSell = new JMenuItem("Sell");	
+		mntmSell.addActionListener(e -> {
+			doTxn.applyTransaction(-dlg.getNumUnits("Sell", rawValue / 2, item.getCount().value()), rawValue / 2);
+		});
+		sellListener = new IndirectObservableListener<>(mntmSell, (c, v) -> {
+			c.setEnabled(item.getUnequippedCount() > 0);
+		});
+		sellListener.setObserved(item.getCount(), item.getCountEquipped());
+		add(mntmSell);
+
+		if (item.getSlot() != EquipmentSlot.NONE) {
 			JMenuItem mntmEquip = new JMenuItem("Equip");
-			mntmEquip.addActionListener(e -> cons.apply());
+			mntmEquip.addActionListener(e -> EquipItemController.accept(inv.getEquipment(), item));
+			equipListener = new IndirectObservableListener<>(mntmEquip, (c, v) -> {
+				c.setEnabled(item.getUnequippedCount() > 0);
+			});
+			equipListener.setObserved(item.getCount(), item.getCountEquipped());
 			add(mntmEquip);
 		}
-		
-		JMenuItem mntmRemove = new JMenuItem("Remove");
-		add(mntmRemove);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(buyListener);
+		ObserverDispatch.unsubscribeAll(sellListener);
+		ObserverDispatch.unsubscribeAll(equipListener);
 	}
 
+	@AllArgsConstructor
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	private static final class BuySellDialogHelper {
+		String name;
+		public int getNumUnits(String word, int txnPrice, int maxUnits) {
+			final BuySellItemDialog dialog = new BuySellItemDialog(name, txnPrice, maxUnits);
+			if (JOptionPane.showConfirmDialog(null, dialog, word + " How Many " + name + "?", 
+					JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) {
+				return dialog.getSelectedNumber();
+			}
+			return 0;
+		}
+	}
+	
+	@AllArgsConstructor
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	private static final class BuySellAction {
+		DDInventory inv;
+		DDItem item;
+		
+		public void applyTransaction(int selected, int txnPrice) {
+			if (selected == 0) return;
+			item.getCount().value(item.getCount().value() + selected);
+			final Money wealth = inv.getWealth();
+			wealth.assign(wealth.difference(Money.fromCopper(selected * txnPrice)));
+			ObserverDispatch.notifySubscribers(item.getCount(), null);
+		}
+	}
 }

+ 234 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemInfoPanel.java

@@ -0,0 +1,234 @@
+package org.leumasjaffe.charsheet.view.inventory;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.inventory.DDItem;
+import org.leumasjaffe.format.StringHelper;
+
+import java.awt.Insets;
+import javax.swing.JLabel;
+import java.awt.Color;
+import java.awt.Font;
+import javax.swing.SwingConstants;
+import java.awt.Component;
+import java.awt.Dimension;
+
+import javax.swing.Box;
+import javax.swing.JTextField;
+
+class ItemInfoPanel extends JPanel {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	public ItemInfoPanel(final DDItem item) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.insets = new Insets(0, 0, 0, 0);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{120, 0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0, 0, 0};
+		gbl_panel.columnWeights = new double[]{1.0, 0.0, 0.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblName = new JLabel("ITEM NAME");
+		lblName.setOpaque(true);
+		lblName.setHorizontalAlignment(SwingConstants.CENTER);
+		lblName.setForeground(Color.WHITE);
+		lblName.setFont(new Font("Tahoma", Font.BOLD, 10));
+		lblName.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblName = new GridBagConstraints();
+		gbc_lblName.fill = GridBagConstraints.BOTH;
+		gbc_lblName.gridheight = 2;
+		gbc_lblName.insets = new Insets(0, 0, 0, 1);
+		gbc_lblName.gridx = 0;
+		gbc_lblName.gridy = 0;
+		panel.add(lblName, gbc_lblName);
+		
+		Component verticalStrut = Box.createVerticalStrut(20);
+		verticalStrut.setMinimumSize(new Dimension(0, 10));
+		verticalStrut.setMaximumSize(new Dimension(32767, 10));
+		verticalStrut.setPreferredSize(new Dimension(0, 10));
+		GridBagConstraints gbc_verticalStrut = new GridBagConstraints();
+		gbc_verticalStrut.insets = new Insets(0, 0, 0, 1);
+		gbc_verticalStrut.fill = GridBagConstraints.HORIZONTAL;
+		gbc_verticalStrut.gridx = 1;
+		gbc_verticalStrut.gridy = 0;
+		panel.add(verticalStrut, gbc_verticalStrut);
+		
+		JLabel lblWeight = new JLabel("WEIGHT");
+		lblWeight.setOpaque(true);
+		lblWeight.setHorizontalAlignment(SwingConstants.CENTER);
+		lblWeight.setForeground(Color.WHITE);
+		lblWeight.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblWeight.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblWeight = new GridBagConstraints();
+		gbc_lblWeight.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblWeight.insets = new Insets(0, 0, 0, 1);
+		gbc_lblWeight.gridx = 1;
+		gbc_lblWeight.gridy = 1;
+		panel.add(lblWeight, gbc_lblWeight);
+		
+		JLabel lblPage = new JLabel("PAGE");
+		lblPage.setOpaque(true);
+		lblPage.setHorizontalAlignment(SwingConstants.CENTER);
+		lblPage.setForeground(Color.WHITE);
+		lblPage.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblPage.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblPage = new GridBagConstraints();
+		gbc_lblPage.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblPage.insets = new Insets(0, 0, 1, 0);
+		gbc_lblPage.gridx = 2;
+		gbc_lblPage.gridy = 1;
+		panel.add(lblPage, gbc_lblPage);
+		
+		JTextField txtName = new JTextField(item.getName());
+		txtName.setEditable(false);
+		txtName.setColumns(10);
+		GridBagConstraints gbc_txtName = new GridBagConstraints();
+		gbc_txtName.insets = new Insets(0, 0, 0, 0);
+		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtName.gridx = 0;
+		gbc_txtName.gridy = 2;
+		panel.add(txtName, gbc_txtName);
+				
+		JTextField txtWeight = new JTextField(StringHelper.toString(item.getWeight()) + " lb.");
+		txtWeight.setEditable(false);
+		txtWeight.setColumns(10);
+		GridBagConstraints gbc_txtWeight = new GridBagConstraints();
+		gbc_txtWeight.insets = new Insets(0, 0, 0, 0);
+		gbc_txtWeight.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtWeight.gridx = 1;
+		gbc_txtWeight.gridy = 2;
+		panel.add(txtWeight, gbc_txtWeight);
+		
+		JTextField txtPage = new JTextField(StringHelper.toString(item.getPage()));
+		txtPage.setEditable(false);
+		txtPage.setColumns(10);
+		GridBagConstraints gbc_txtPage = new GridBagConstraints();
+		gbc_txtPage.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtPage.gridx = 2;
+		gbc_txtPage.gridy = 2;
+		panel.add(txtPage, gbc_txtPage);
+		
+		JPanel panel_1 = new JPanel();
+		GridBagConstraints gbc_panel_1 = new GridBagConstraints();
+		gbc_panel_1.fill = GridBagConstraints.BOTH;
+		gbc_panel_1.gridx = 0;
+		gbc_panel_1.gridy = 1;
+		add(panel_1, gbc_panel_1);
+		GridBagLayout gbl_panel_1 = new GridBagLayout();
+		gbl_panel_1.columnWidths = new int[]{0, 0, 0, 0, 0};
+		gbl_panel_1.rowHeights = new int[]{0, 0, 0};
+		gbl_panel_1.columnWeights = new double[]{1.0, 1.0, 1.0, 1.0, Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+		panel_1.setLayout(gbl_panel_1);
+		
+		JLabel lblPp = new JLabel("pp");
+		GridBagConstraints gbc_lblPp = new GridBagConstraints();
+		gbc_lblPp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblPp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblPp.gridx = 0;
+		gbc_lblPp.gridy = 0;
+		panel_1.add(lblPp, gbc_lblPp);
+		lblPp.setOpaque(true);
+		lblPp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblPp.setForeground(Color.WHITE);
+		lblPp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblPp.setBackground(Color.BLACK);
+		
+		JLabel lblGp = new JLabel("gp");
+		GridBagConstraints gbc_lblGp = new GridBagConstraints();
+		gbc_lblGp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblGp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblGp.gridx = 1;
+		gbc_lblGp.gridy = 0;
+		panel_1.add(lblGp, gbc_lblGp);
+		lblGp.setOpaque(true);
+		lblGp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblGp.setForeground(Color.WHITE);
+		lblGp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblGp.setBackground(Color.BLACK);
+		
+		JLabel lblSp = new JLabel("sp");
+		GridBagConstraints gbc_lblSp = new GridBagConstraints();
+		gbc_lblSp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblSp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblSp.gridx = 2;
+		gbc_lblSp.gridy = 0;
+		panel_1.add(lblSp, gbc_lblSp);
+		lblSp.setOpaque(true);
+		lblSp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblSp.setForeground(Color.WHITE);
+		lblSp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblSp.setBackground(Color.BLACK);
+		
+		JLabel lblCp = new JLabel("cp");
+		GridBagConstraints gbc_lblCp = new GridBagConstraints();
+		gbc_lblCp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblCp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblCp.gridx = 3;
+		gbc_lblCp.gridy = 0;
+		panel_1.add(lblCp, gbc_lblCp);
+		lblCp.setOpaque(true);
+		lblCp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblCp.setForeground(Color.WHITE);
+		lblCp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblCp.setBackground(Color.BLACK);
+		
+		JTextField txtPP = new JTextField(StringHelper.toString(item.getValue().getPp()));
+		GridBagConstraints gbc_txtPP = new GridBagConstraints();
+		gbc_txtPP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtPP.insets = new Insets(0, 0, 0, 0);
+		gbc_txtPP.gridx = 0;
+		gbc_txtPP.gridy = 1;
+		panel_1.add(txtPP, gbc_txtPP);
+		txtPP.setEditable(false);
+		txtPP.setColumns(5);
+		
+		JTextField txtGP = new JTextField(StringHelper.toString(item.getValue().getGp()));
+		GridBagConstraints gbc_txtGP = new GridBagConstraints();
+		gbc_txtGP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtGP.insets = new Insets(0, 0, 0, 0);
+		gbc_txtGP.gridx = 1;
+		gbc_txtGP.gridy = 1;
+		panel_1.add(txtGP, gbc_txtGP);
+		txtGP.setEditable(false);
+		txtGP.setColumns(5);
+		
+		JTextField txtSP = new JTextField(StringHelper.toString(item.getValue().getSp()));
+		GridBagConstraints gbc_txtSP = new GridBagConstraints();
+		gbc_txtSP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtSP.insets = new Insets(0, 0, 0, 0);
+		gbc_txtSP.gridx = 2;
+		gbc_txtSP.gridy = 1;
+		panel_1.add(txtSP, gbc_txtSP);
+		txtSP.setEditable(false);
+		txtSP.setColumns(5);
+		
+		JTextField txtCP = new JTextField(StringHelper.toString(item.getValue().getCp()));
+		GridBagConstraints gbc_txtCP = new GridBagConstraints();
+		gbc_txtCP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtCP.gridx = 3;
+		gbc_txtCP.gridy = 1;
+		panel_1.add(txtCP, gbc_txtCP);
+		txtCP.setEditable(false);
+		txtCP.setColumns(5);
+	}
+}

+ 31 - 6
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemPanel.java

@@ -12,10 +12,17 @@ import java.awt.Font;
 import java.awt.Color;
 import javax.swing.SwingConstants;
 
+import org.leumasjaffe.charsheet.model.inventory.DDInventory;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.event.PopClickListener;
+import org.leumasjaffe.format.StringHelper;
+import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.Component;
+import java.awt.Container;
+
 import javax.swing.Box;
 
 public class ItemPanel extends JPanel {
@@ -23,8 +30,10 @@ public class ItemPanel extends JPanel {
 	 * 
 	 */
 	private static final long serialVersionUID = 1L;
+	
+	IndirectObservableListener<JTextField, IntValue> countListener;
 
-	public ItemPanel(DDItem item) {
+	public ItemPanel(DDInventory inv, DDItem item) {
 		setPreferredSize(new Dimension(280, 40));
 		GridBagLayout gbl_panel = new GridBagLayout();
 		gbl_panel.columnWidths = new int[]{120, 0, 0, 0, 0};
@@ -96,7 +105,7 @@ public class ItemPanel extends JPanel {
 		gbc_lblValue.gridy = 1;
 		add(lblValue, gbc_lblValue);
 		
-		JTextField nameField = new JTextField(item.getName());
+		JTextField nameField = new JTextField(item.getFullName());
 		GridBagConstraints gbc_nameField = new GridBagConstraints();
 		gbc_nameField.insets = new Insets(0, 0, 0, 0);
 		gbc_nameField.fill = GridBagConstraints.HORIZONTAL;
@@ -105,7 +114,7 @@ public class ItemPanel extends JPanel {
 		add(nameField, gbc_nameField);
 		nameField.setColumns(10);
 		
-		JTextField countField = new JTextField(Integer.toString(item.getCount().value()));
+		JTextField countField = new JTextField();
 		countField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_countField = new GridBagConstraints();
 		gbc_countField.insets = new Insets(0, 0, 0, 0);
@@ -115,7 +124,7 @@ public class ItemPanel extends JPanel {
 		add(countField, gbc_countField);
 		countField.setColumns(10);
 		
-		JTextField weightField = new JTextField(StringHelper.toString(item.getWeight().value()) + " lb.");
+		JTextField weightField = new JTextField(StringHelper.toString(item.getWeight()) + " lb.");
 		weightField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_weightField = new GridBagConstraints();
 		gbc_weightField.insets = new Insets(0, 0, 0, 0);
@@ -125,7 +134,7 @@ public class ItemPanel extends JPanel {
 		add(weightField, gbc_weightField);
 		weightField.setColumns(10);
 		
-		JTextField valueField = new JTextField(item.getValue().toString());
+		JTextField valueField = new JTextField(item.getActualValue().toString());
 		valueField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_valueField = new GridBagConstraints();
 		gbc_valueField.insets = new Insets(0, 0, 0, 0);
@@ -134,6 +143,22 @@ public class ItemPanel extends JPanel {
 		gbc_valueField.gridy = 2;
 		add(valueField, gbc_valueField);
 		valueField.setColumns(10);
+		
+		countListener = new IndirectObservableListener<>(countField, (c, v) -> {
+			c.setText(StringHelper.toString(v.value()));
+			if (v.value() == 0) {
+				Container comp = getParent();
+				comp.remove(this);
+				comp.repaint();
+			}
+		});
+		countListener.setObserved(item.getCount(), item.getCountEquipped());
+		addMouseListener(new PopClickListener(new ItemInfoMenu(inv, item)));
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(countListener);
+	}
 }

+ 74 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/LoadoutMenu.java

@@ -0,0 +1,74 @@
+package org.leumasjaffe.charsheet.view.inventory;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import javax.swing.JComboBox;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPopupMenu;
+
+import org.leumasjaffe.charsheet.config.Constants;
+import org.leumasjaffe.charsheet.model.inventory.DDInventory;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+
+class LoadoutMenu extends JPopupMenu {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	
+	@Getter(AccessLevel.PRIVATE) @Setter DDInventory model = null;
+	
+	public LoadoutMenu() {
+		JMenuItem mntmSaveAs = new JMenuItem("Save As");
+		add(mntmSaveAs);
+		mntmSaveAs.addActionListener(e -> {
+			showSaveDialog().ifPresent(name -> getModel().save(name));
+		});
+		
+		JMenuItem mntmLoad = new JMenuItem("Load");
+		add(mntmLoad);
+		mntmLoad.addActionListener(e -> {
+			showLoadDialog().ifPresent(name -> {
+				try {
+					getModel().load(name);
+					ObserverDispatch.notifySubscribers(getModel(), null);
+				} catch (NullPointerException except) {
+					JOptionPane.showMessageDialog(this, "Unable to load equipment, some items are missing", 
+							"Error", JOptionPane.ERROR_MESSAGE);
+				}
+			});
+		});
+		
+		JMenuItem mntmViewLoadouts = new JMenuItem("View Loadouts");
+		add(mntmViewLoadouts);
+		mntmViewLoadouts.addActionListener(e -> {
+			
+		});
+	}
+	
+	private Optional<String> showSaveDialog() {
+		return showSaveLoadDialog("Save Equipment Loadout As...", Stream.empty(), true);
+	}
+	
+	private Optional<String> showLoadDialog() {
+		return showSaveLoadDialog("Select Equipment Loadout", Stream.of(Constants.PREVIOUS_LOADOUT), false);
+	}
+	
+	private Optional<String> showSaveLoadDialog(String message, Stream<String> base, boolean edit) {
+		final String[] options = Stream.concat(base, getModel().getFavorites().keySet().stream()).toArray(String[]::new);
+		final JComboBox<String> comboBox = new JComboBox<>(options);
+		comboBox.setEditable(edit);
+		if (JOptionPane.showConfirmDialog(this, comboBox, message, 
+				JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION) {
+			return Optional.of(comboBox.getSelectedItem().toString());
+		}
+		return Optional.empty();
+	}
+}

+ 5 - 5
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ShieldPanel.java

@@ -14,7 +14,7 @@ import javax.swing.SwingConstants;
 
 import org.leumasjaffe.charsheet.model.inventory.DDArmor;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import java.awt.Component;
 import javax.swing.Box;
@@ -113,7 +113,7 @@ public class ShieldPanel extends JPanel {
 		lblCheckPenalty.setBackground(Color.BLACK);
 		lblCheckPenalty.setFont(new Font("Tahoma", Font.BOLD, 8));
 		
-		JTextField nameField = new JTextField(item.getName());
+		JTextField nameField = new JTextField(item.getFullName());
 		GridBagConstraints gbc_nameField = new GridBagConstraints();
 		gbc_nameField.insets = new Insets(0, 0, 0, 0);
 		gbc_nameField.fill = GridBagConstraints.HORIZONTAL;
@@ -122,7 +122,7 @@ public class ShieldPanel extends JPanel {
 		panel.add(nameField, gbc_nameField);
 		nameField.setColumns(10);
 		
-		JTextField armorBonusField = new JTextField(StringHelper.toSignedString(armor.getBonus()));
+		JTextField armorBonusField = new JTextField(StringHelper.toSignedString(armor.getActualAcBonus()));
 		armorBonusField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_armorBonusField = new GridBagConstraints();
 		gbc_armorBonusField.insets = new Insets(0, 0, 0, 0);
@@ -132,7 +132,7 @@ public class ShieldPanel extends JPanel {
 		panel.add(armorBonusField, gbc_armorBonusField);
 		armorBonusField.setColumns(10);
 		
-		JTextField weightField = new JTextField(StringHelper.toString(item.getWeight().value()) + " lb.");
+		JTextField weightField = new JTextField(StringHelper.toString(item.getWeight()) + " lb.");
 		weightField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_weightField = new GridBagConstraints();
 		gbc_weightField.insets = new Insets(0, 0, 0, 0);
@@ -142,7 +142,7 @@ public class ShieldPanel extends JPanel {
 		panel.add(weightField, gbc_weightField);
 		weightField.setColumns(10);
 		
-		JTextField checkField = new JTextField(StringHelper.toString(armor.getCheckPenalty()));
+		JTextField checkField = new JTextField(StringHelper.toString(armor.getActualCheckPenalty()));
 		checkField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_checkField = new GridBagConstraints();
 		gbc_checkField.fill = GridBagConstraints.HORIZONTAL;

+ 2 - 2
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/WeaponPanel.java

@@ -14,7 +14,7 @@ import javax.swing.SwingConstants;
 
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
 import org.leumasjaffe.charsheet.model.inventory.DDWeapon;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import java.awt.Component;
 import javax.swing.Box;
@@ -113,7 +113,7 @@ public class WeaponPanel extends JPanel {
 		lblCritical.setBackground(Color.BLACK);
 		lblCritical.setFont(new Font("Tahoma", Font.BOLD, 8));
 		
-		JTextField nameField = new JTextField(item.getName());
+		JTextField nameField = new JTextField(item.getFullName());
 		GridBagConstraints gbc_nameField = new GridBagConstraints();
 		gbc_nameField.insets = new Insets(0, 0, 0, 0);
 		gbc_nameField.fill = GridBagConstraints.HORIZONTAL;

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/magic/ChooseSpellsPerDayHeader.java

@@ -10,7 +10,7 @@ import javax.swing.JTextField;
 
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import java.awt.Dimension;
 

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectPreparedSpellsPanel.java

@@ -13,8 +13,8 @@ import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.impl.Prepared;
-import org.leumasjaffe.charsheet.util.StringHelper;
 import org.leumasjaffe.event.SelectTableRowPopupMenuListener;
+import org.leumasjaffe.format.StringHelper;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;

+ 7 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPanel.java

@@ -13,6 +13,7 @@ import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -72,4 +73,10 @@ class SpellLevelPanel extends JPanel {
 	}
 	
 	public boolean isCastableFromHere() { return false; }
+
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
 }

+ 8 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellPanel.java

@@ -15,6 +15,7 @@ import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
 import org.leumasjaffe.function.TriFunction;
 import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
@@ -82,4 +83,11 @@ public class SpellPanel extends JPanel {
 			previousHighestSpellLevel = dclass.getHighestSpellLevel();
 		}
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listenerKnown);
+		ObserverDispatch.unsubscribeAll(listenerPerDay);
+	}
 }

+ 2 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsKnownHeader.java

@@ -10,7 +10,8 @@ import javax.swing.JTextField;
 
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
+
 import java.awt.Dimension;
 
 class SpellsKnownHeader extends JPanel {

+ 7 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java

@@ -11,6 +11,7 @@ import javax.swing.JTextField;
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.Dimension;
 
@@ -106,4 +107,10 @@ class SpellsPerDayHeader extends JPanel {
 		});
 		listener.setObserved(model);
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
 }

+ 8 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpDialog.java

@@ -15,6 +15,7 @@ import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.charsheet.model.skill.DDSkills;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
@@ -33,6 +34,7 @@ public class SkillLevelUpDialog extends JPanel {
 	 * 
 	 */
 	private static final long serialVersionUID = 1L;
+	private ObservableListener<JTextField, IntValue> purchaseListener;
 	
 	public SkillLevelUpDialog(final DDCharacter chara, final DDCharacterClass cclass) {
 		final IntValue pointsAvaliable = new IntValue(Math.max(1, cclass.getSkillPoints() + 
@@ -104,7 +106,7 @@ public class SkillLevelUpDialog extends JPanel {
 			lines.add(line);
 		});
 		
-		ObservableListener<JTextField, IntValue> purchaseListener = new ObservableListener<>(pointsRemaining, (c, v) -> {
+		purchaseListener = new ObservableListener<>(pointsRemaining, (c, v) -> {
 			btnSubmitSkillChange.setEnabled(v.value() == 0);
 			c.setText(Integer.toString(v.value()));
 		});
@@ -118,4 +120,9 @@ public class SkillLevelUpDialog extends JPanel {
 		});
 	}
 	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(purchaseListener);
+	}
 }

+ 7 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpLine.java

@@ -8,7 +8,7 @@ import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.charsheet.model.skill.DDSkill;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 
@@ -235,4 +235,10 @@ class SkillLevelUpLine extends JPanel {
 		skill.spendPoints(current.value(), !isClassSkill);
 		ObserverDispatch.notifySubscribers(skill.getRanks(), this);
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(totalListener);
+	}
 }

+ 9 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLine.java

@@ -7,9 +7,10 @@ import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.charsheet.model.skill.DDSkill;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -188,4 +189,11 @@ public class SkillLine extends JPanel {
 		skillListener.setObserved(skill.getRanks());
 	}
 	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(totalListener);
+		ObserverDispatch.unsubscribeAll(modifierListener);
+		ObserverDispatch.unsubscribeAll(skillListener);
+	}
 }

+ 8 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityBox.java

@@ -17,10 +17,11 @@ import javax.swing.border.LineBorder;
 import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.charsheet.observer.helper.IntValueHelper;
 import org.leumasjaffe.charsheet.observer.helper.IntValueStringify;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.graphics.NumberTextField;
 import org.leumasjaffe.observer.ObservableController;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -96,4 +97,10 @@ public class AbilityBox extends JPanel {
 		modListener.setObserved(value);
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(valueListener);
+		ObserverDispatch.unsubscribeAll(modListener);
+	}
 }

+ 14 - 5
src/main/lombok/org/leumasjaffe/charsheet/view/summary/ArmorLine.java

@@ -12,9 +12,10 @@ import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.inventory.DDInventory;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
 import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -367,14 +368,14 @@ public class ArmorLine extends JPanel {
 			{
 				final DDItem body = inv.getEquipment().get(EquipmentSlot.BODY);
 				if ( body != null && body.isArmor() ) {
-					iarmor = body.getArmor().getBonus();
+					iarmor = body.getArmor().getActualAcBonus();
 					dex = Math.min(dex, body.getArmor().getMaxDex());
 				}
 			}
 			{
 				final DDItem offHand = inv.getEquipment().get(EquipmentSlot.OFF_HAND);
 				if ( offHand != null && offHand.isArmor() ) {
-					ishield = offHand.getArmor().getBonus();
+					ishield = offHand.getArmor().getActualAcBonus();
 				}
 			}
 			c.setText(StringHelper.toString(10 + iarmor + ishield + 
@@ -385,7 +386,7 @@ public class ArmorLine extends JPanel {
 			int iarmor = 0;
 			final DDItem body = v.getEquipment().get(EquipmentSlot.BODY);
 			if ( body != null && body.isArmor() ) {
-				iarmor = body.getArmor().getBonus();
+				iarmor = body.getArmor().getActualAcBonus();
 			}
 			c.setText(StringHelper.toString(iarmor));
 		});
@@ -394,7 +395,7 @@ public class ArmorLine extends JPanel {
 			int iarmor = 0;
 			final DDItem offHand = v.getEquipment().get(EquipmentSlot.OFF_HAND);
 			if ( offHand != null && offHand.isArmor() ) {
-				iarmor = offHand.getArmor().getBonus();
+				iarmor = offHand.getArmor().getActualAcBonus();
 			}
 			c.setText(StringHelper.toString(iarmor));
 		});
@@ -421,4 +422,12 @@ public class ArmorLine extends JPanel {
 		armorDexObserver.setObserved(model, inv, dex);
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(armorTotalObserver);
+		ObserverDispatch.unsubscribeAll(armorArmorObserver);
+		ObserverDispatch.unsubscribeAll(armorShieldObserver);
+		ObserverDispatch.unsubscribeAll(armorDexObserver);
+	}
 }

+ 8 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/summary/AttackLine.java

@@ -10,9 +10,10 @@ import javax.swing.border.LineBorder;
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.observer.helper.AbilModStringify;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -247,4 +248,10 @@ public class AttackLine extends JPanel {
 		this.grappleMisc.setText(StringHelper.toString(misc));
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(gTtlObserver);
+		ObserverDispatch.unsubscribeAll(gStrObserver);
+	}
 }

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/summary/DescriptionPanel.java

@@ -8,7 +8,7 @@ import javax.swing.JTextField;
 import javax.swing.border.TitledBorder;
 
 import org.leumasjaffe.charsheet.model.DDCharacter;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;

+ 8 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/summary/InitiativeLine.java

@@ -11,9 +11,10 @@ import javax.swing.border.LineBorder;
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.observer.helper.AbilModStringify;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -138,4 +139,10 @@ public class InitiativeLine extends JPanel {
 		dexObserver.setObserved(dex);
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(ttlObserver);
+		ObserverDispatch.unsubscribeAll(dexObserver);
+	}
 }

+ 9 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/summary/ResistanceLine.java

@@ -10,10 +10,11 @@ import javax.swing.border.LineBorder;
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.observer.helper.AbilModStringify;
-import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.graphics.NumberTextField;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -240,4 +241,11 @@ public class ResistanceLine extends JPanel {
 		abilObserver.setObserved(abil);
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(totalObserver);
+		ObserverDispatch.unsubscribeAll(baseObserver);
+		ObserverDispatch.unsubscribeAll(abilObserver);
+	}
 }

+ 1 - 3
src/main/lombok/org/leumasjaffe/charsheet/util/StringHelper.java

@@ -1,6 +1,4 @@
-package org.leumasjaffe.charsheet.util;
-
-import org.leumasjaffe.format.StringFormatter;
+package org.leumasjaffe.format;
 
 import lombok.experimental.UtilityClass;
 

+ 1 - 0
src/main/lombok/org/leumasjaffe/observer/ObserverDispatch.java

@@ -28,6 +28,7 @@ public class ObserverDispatch {
 	}
 	
 	public void unsubscribeAll(Object src) {
+		if (src == null) return;
 		observers.entries().removeIf( e -> e.getValue().obj == src );
 	}
 	

+ 0 - 1
src/test/java/org/leumasjaffe/format/StringFormatterTest.java

@@ -3,7 +3,6 @@ package org.leumasjaffe.format;
 import static org.junit.Assert.*;
 
 import org.junit.Test;
-import org.leumasjaffe.charsheet.util.StringHelper;
 
 public class StringFormatterTest {