瀏覽代碼

Merge branch 'master' into support_domain

* master: (36 commits)
  Adding HitDice panel and update to level up. Adding Ability update to level up commit.
  Fix bug in spell selection
  Make BoolGate much cleaner in design
  Add hit dice to each class's JSON def
  Making Spontaneous spell book properly clone-able through Object -> JSON -> Object cloning.
  Move updating features to own panel definition. This panel will contain: - Hit Dice - Current+New Features (partial) - Ability Score (complete) - Feats
  Update summary page correctly on Level Up
  Updating to observer:0.3
  Apply level up to class correctly. Don't completely regenerate object. TODO: update summary and such?
  Move experience computation to ExperienceDialog
  Fixing cloning to actually be a "Deep copy"
  Fixing some jsonization
  Remove duplicate of getProto()
  Allow the application of Skill Ranks. Allow the learning of spells.
  Begin work to commit changes on level up
  Creating a handle wrapper, so that BoolGate objects don't need to extend beyond their creation scope.
  Converting to using BoolGate
  Use observable instead of property listener for Level Up Spells
  Allow cascading of spell tabs when a class can both Prepare and Learn spells on level up.
  Add handling for Wizard spell book rules
  ...

# Conflicts:
#	resources/spells/default.json
#	src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java
#	src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellbook.java
#	src/main/lombok/org/leumasjaffe/charsheet/observer/ObserverHelper.java
#	src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java
#	src/main/lombok/org/leumasjaffe/charsheet/view/DialogBuilder.java
#	src/main/lombok/org/leumasjaffe/charsheet/view/magic/PrepareSpellsDialog.java
#	src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectPreparedSpellsPanel.java
#	src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellMenu.java
Sam Jaffe 8 年之前
父節點
當前提交
b483fa67f5
共有 63 個文件被更改,包括 2352 次插入500 次删除
  1. 1 1
      pom.xml
  2. 9 4
      resources/classes/Bard.json
  3. 5 3
      resources/classes/Cleric.json
  4. 71 0
      resources/classes/Druid.json
  5. 37 0
      resources/classes/Wizard.json
  6. 0 0
      resources/spells/default.json
  7. 5 0
      src/main/lombok/org/leumasjaffe/charsheet/config/Constants.java
  8. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/controller/EquipItemController.java
  9. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/Ability.java
  10. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacter.java
  11. 25 12
      src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java
  12. 28 4
      src/main/lombok/org/leumasjaffe/charsheet/model/DDClass.java
  13. 2 0
      src/main/lombok/org/leumasjaffe/charsheet/model/features/DDProperty.java
  14. 2 0
      src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/Flat.java
  15. 2 0
      src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/PerSpellLevel.java
  16. 3 1
      src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/Simple.java
  17. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEquipment.java
  18. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDInventory.java
  19. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Money.java
  20. 5 0
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpell.java
  21. 6 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellList.java
  22. 12 4
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellbook.java
  23. 12 4
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Inspired.java
  24. 49 6
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Researched.java
  25. 19 4
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Spontaneous.java
  26. 91 0
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/BoolGate.java
  27. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/IntValue.java
  28. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/ObjectValue.java
  29. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/StringValue.java
  30. 8 6
      src/main/lombok/org/leumasjaffe/charsheet/observer/ObserverHelper.java
  31. 3 2
      src/main/lombok/org/leumasjaffe/charsheet/observer/helper/IntValueHelper.java
  32. 3 2
      src/main/lombok/org/leumasjaffe/charsheet/observer/helper/StringValueHelper.java
  33. 5 4
      src/main/lombok/org/leumasjaffe/charsheet/view/ClassTab.java
  34. 81 32
      src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java
  35. 14 8
      src/main/lombok/org/leumasjaffe/charsheet/view/DeveloperMenu.java
  36. 23 3
      src/main/lombok/org/leumasjaffe/charsheet/view/DialogBuilder.java
  37. 77 0
      src/main/lombok/org/leumasjaffe/charsheet/view/config/ConfigEnumPanel.java
  38. 79 0
      src/main/lombok/org/leumasjaffe/charsheet/view/config/ConfigPanel.java
  39. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentInfoMenu.java
  40. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemInfoMenu.java
  41. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/LoadoutMenu.java
  42. 98 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/ChooseClassLevelUpDialog.java
  43. 44 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/ExperienceDialog.java
  44. 193 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_AbilityPanel.java
  45. 108 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_FeaturesPanel.java
  46. 244 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_HitDicePanel.java
  47. 19 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpClassInfo.java
  48. 99 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpDialog.java
  49. 148 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpSpellPanel.java
  50. 222 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/UpdateClassWithLevelPanel.java
  51. 14 13
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/PrepareSpellsDialog.java
  52. 12 219
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectPreparedSpellsPanel.java
  53. 284 0
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectSpellsPanel.java
  54. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellMenu.java
  55. 13 103
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpDialog.java
  56. 14 7
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpLine.java
  57. 115 0
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpPanel.java
  58. 1 4
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityBox.java
  59. 1 4
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityLine.java
  60. 1 4
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityPanel.java
  61. 15 15
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/ArmorLine.java
  62. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AttackLine.java
  63. 14 14
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/ResistanceLine.java

+ 1 - 1
pom.xml

@@ -171,7 +171,7 @@
     <dependency>
       <groupId>org.leumasjaffe</groupId>
       <artifactId>observer</artifactId>
-      <version>0.1</version>
+      <version>0.3</version>
     </dependency>
     <dependency>
       <groupId>org.leumasjaffe</groupId>

+ 9 - 4
resources/classes/Bard.json

@@ -1,5 +1,6 @@
 {
   "name":"Bard",
+  "hitDice":6,
   "bab":"AVERAGE",
   "fort":"POOR",
   "ref":"GOOD",
@@ -43,18 +44,22 @@
     "Use Magic Device"
   ],
   "spells":{
+    "spellBookTypeName":".impl.Spontaneous",
     "group":"ARCANE",
     "ability":"CHA",
     "known":[
       [4],
-      [5, 2]
+      [5, 2],
+      [6, 3]
     ],
     "perDay":[
       [2],
-      [3, 0]
+      [3, 0],
+      [3, 1]
     ],
     "spellList":[
-      ["Know Direction"]
+      ["Know Direction"],
+      ["Cure Light Wounds"]
     ]
   }
-}
+}

+ 5 - 3
resources/classes/Cleric.json

@@ -1,5 +1,6 @@
 {
   "name":"Cleric",
+  "hitDice":8,
   "bab":"AVERAGE",
   "fort":"GOOD",
   "ref":"POOR",
@@ -26,9 +27,9 @@
     "Spellcraft"
   ],
   "spells":{
+    "spellBookTypeName":"{\"@c\":\".impl.Inspired\",\"classRef\":\"Cleric\",\"spellInfo\":{}}",
     "group":"DIVINE",
     "ability":"WIS",
-    "known":[ ],
     "perDay":[
       [3, 1],
       [4, 2],
@@ -53,7 +54,8 @@
     ],
     "spellList":[
       ["Create Water"],
-      ["Cure Light Wounds"]
+      ["Cure Light Wounds", "Magic Weapon"],
+      ["Magic Vestment"]
     ]
   }
-}
+}

+ 71 - 0
resources/classes/Druid.json

@@ -0,0 +1,71 @@
+{
+  "name":"Druid",
+  "hitDice":8,
+  "bab":"AVERAGE",
+  "fort":"GOOD",
+  "ref":"POOR",
+  "will":"GOOD",
+  "features":[
+    [
+      {
+        "@c":".impl.Simple",
+        "name":"Animal Companion"
+      },
+      {
+        "@c":".impl.Simple",
+        "name":"Nature Sense"
+      },
+      {
+        "@c":".impl.Simple",
+        "name":"Wild Empathy"
+      }
+    ]
+  ],
+  "skillPoints":4,
+  "skills":[
+    "Concentration",
+    "Craft (*)",
+    "Diplomacy",
+    "Handle Animal",
+    "Heal",
+    "Knowledge (nature)",
+    "Listen",
+    "Profession (*)",
+    "Ride",
+    "Spellcraft",
+    "Spot",
+    "Survival",
+    "Swim"
+  ],
+  "spells":{
+    "spellBookTypeName":"{\"@c\":\".impl.Inspired\",\"classRef\":\"Druid\",\"spellInfo\":{}}",
+    "group":"DIVINE",
+    "ability":"WIS",
+    "perDay":[
+      [3, 1],
+      [4, 2],
+      [4, 2, 1],
+      [5, 3, 2],
+      [5, 3, 2, 1],
+      [5, 3, 3, 2],
+      [6, 4, 3, 2, 1],
+      [6, 4, 3, 3, 2],
+      [6, 4, 4, 3, 2, 1],
+      [6, 4, 4, 3, 3, 2],
+      [6, 5, 4, 4, 3, 2, 1],
+      [6, 5, 4, 4, 3, 3, 2],
+      [6, 5, 5, 4, 4, 3, 2, 1],
+      [6, 5, 5, 4, 4, 3, 3, 2],
+      [6, 5, 5, 5, 4, 4, 3, 2, 1],
+      [6, 5, 5, 5, 4, 4, 4, 3, 2],
+      [6, 5, 5, 5, 5, 4, 4, 3, 2, 1],
+      [6, 5, 5, 5, 5, 4, 4, 3, 3, 2],
+      [6, 5, 5, 5, 5, 5, 4, 4, 3, 3],
+      [6, 5, 5, 5, 5, 5, 4, 4, 4, 4]
+    ],
+    "spellList":[
+      ["Create Water"],
+      ["Cure Light Wounds"]
+    ]
+  }
+}

+ 37 - 0
resources/classes/Wizard.json

@@ -0,0 +1,37 @@
+{
+  "name":"Wizard",
+  "hitDice":4,
+  "bab":"POOR",
+  "fort":"POOR",
+  "ref":"POOR",
+  "will":"GOOD",
+  "features":[
+    [
+      {"@c":".impl.Simple", "name":"Summon Familiar"}
+    ]
+  ],
+  "skillPoints":2,
+  "skills":[
+    "Concentration",
+    "Craft (*)",
+    "Decipher Script",
+    "Knowledge (*)",
+    "Profession (*)",
+    "Spellcraft"
+  ],
+  "spells":{
+    "spellBookTypeName":"{\"@c\":\".impl.Researched\",\"classRef\":\"Wizard\",\"freeSpellsPerLevel\":2,\"spellInfo\":{}}",
+    "group":"ARCANE",
+    "ability":"INT",
+    "known":[
+      [-1, 3]
+    ],
+    "perDay":[
+      [3, 1]
+    ],
+    "spellList":[
+      ["Create Water"],
+      ["Magic Weapon"]
+    ]
+  }
+}

文件差異過大導致無法顯示
+ 0 - 0
resources/spells/default.json


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

@@ -19,4 +19,9 @@ public final class Constants {
 	public enum DurationMeasurement {
 		NATURAL, ROUNDS
 	}
+	
+	public String K_LEVELUP = "Applies Level-Up";
+	public enum LevelUpStyle {
+		IMMEDIATELY, DURING_REST
+	}
 }

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

@@ -19,7 +19,7 @@ public class EquipItemController {
 		if (inv.canEquip(item) || new Helper(inv).getReplaceItem(item.getSlot())) {
 			item.adjustCounEquipped(+1);
 			inv.equipNext(item);
-			ObserverDispatch.notifySubscribers(inv, null);
+			ObserverDispatch.notifySubscribers(inv);
 		}
 	}
 

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/Ability.java

@@ -39,7 +39,7 @@ public class Ability {
 	@AllArgsConstructor
 	@EqualsAndHashCode(callSuper=false)
 	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-	public static class Scores extends Observable {
+	public static class Scores extends Observable.Instance {
 		IntValue base, temp;
 		
 		@JsonCreator

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacter.java

@@ -32,7 +32,7 @@ import lombok.experimental.FieldDefaults;
 @Data @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE)
 @JsonIgnoreProperties(ignoreUnknown=true)
-public class DDCharacter extends Observable {
+public class DDCharacter extends Observable.Instance {
 	@NonNull String name = "";
 	
 	@NonNull String player = "";

+ 25 - 12
src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java

@@ -12,18 +12,20 @@ import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.observer.Observable;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 
 import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.experimental.Delegate;
 import lombok.experimental.FieldDefaults;
 
-@Data
+@Data @AllArgsConstructor
 @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class DDCharacterClass extends Observable implements Comparable<DDCharacterClass> {
+public class DDCharacterClass extends Observable.Instance implements Comparable<DDCharacterClass> {
 	private static final class Reference {
 		DDClass base;
 		
@@ -43,7 +45,6 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 			return base.toString();
 		}
 
-		@SuppressWarnings("unused")
 		public Reference(final String name) {
 			this.base = DDClass.getFromResource(name);
 		}
@@ -74,27 +75,33 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 	
 	Optional<DDSpellbookWrapper> spellBook;
 	
+	public DDCharacterClass(String name) {
+		this.level = new IntValue(0);
+		this.name = new Reference(name);
+		this.spellBook = getProto().createNewSpellBook();
+	}
+	
 	public String toString() {
 		return getName() + " " + getLevel();
 	}
 	
-	public int getSkillPoints() {
+	@JsonIgnore public int getSkillPoints() {
 		return name.base.getSkillPoints();
 	}
 
-	public int getBab() {
+	@JsonIgnore public int getBab() {
 		return name.base.getBab().getBonus(level.value());
 	}
 	
-	public int getFort() {
+	@JsonIgnore public int getFort() {
 		return name.base.getFort().getBonus(level.value());
 	}
 	
-	public int getRef() {
+	@JsonIgnore public int getRef() {
 		return name.base.getRef().getBonus(level.value());
 	}
 	
-	public int getWill() {
+	@JsonIgnore public int getWill() {
 		return name.base.getWill().getBonus(level.value());
 	}
 	
@@ -102,18 +109,24 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 		return name.base.isClassSkill(skill);
 	}
 
-	public DDClass getProto() {
+	@JsonIgnore public DDClass getProto() {
 		return name.base;
 	}
 	
-	public int getHighestSpellLevel() {
+	@JsonIgnore public int getHighestSpellLevel() {
+		return getHighestSpellLevel(getLevel().value());
+	}
+	
+	public int getHighestSpellLevel(int level) {
 		// TODO: Bonus levels to spellsKnown/spellsPerDay?
 		// TODO: Bonus spellsPerDay for high ability scores
-		final List<Integer> list = getProto().getSpells().get().getPerDay().get(getLevel().value()-1);
-		final int level = list.size() - 1;
+		if (level == 0) { return -1; }
+		final List<Integer> list = getProto().getSpells().get().getPerDay().get(level-1);
+		level = list.size() - 1;
 		return list.get(level) == 0 ? level : level + 1;
 	}
 
+
 	@Override
 	public int compareTo(DDCharacterClass o) {
 		return getName().compareTo(o.getName());

+ 28 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/DDClass.java

@@ -1,6 +1,7 @@
 package org.leumasjaffe.charsheet.model;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -13,8 +14,12 @@ import org.leumasjaffe.charsheet.model.features.DDProperty;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.DDSpellList;
 import org.leumasjaffe.charsheet.model.magic.DDSpellList.SpellList;
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.format.StringFormatter;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 
@@ -29,9 +34,15 @@ import lombok.experimental.FieldDefaults;
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @JsonIgnoreProperties(ignoreUnknown=true)
 public class DDClass {
+	static ObjectMapper mapper = new ObjectMapper();
+	static {
+		mapper.registerModule(new Jdk8Module());
+	}
+
 	@NonNull String name;
 	
 	int skillPoints;
+	int hitDice;
 	@NonNull AttackQuality bab;
 	@NonNull SaveQuality fort;
 	@NonNull SaveQuality ref;
@@ -41,6 +52,7 @@ public class DDClass {
 	
 	@NonNull Set<String> skills;
 	
+	String spellBookType;
 	@NonNull Optional<DDSpellList> spells;
 	
 	static Map<String, DDClass> store = new HashMap<>();
@@ -48,9 +60,8 @@ public class DDClass {
 	@SneakyThrows
 	public static DDClass getFromResource(final String name) {
 		if (!store.containsKey(name)) {
-			final ObjectMapper mapper = new ObjectMapper();
-			mapper.registerModule(new Jdk8Module());
-			store.put(name, mapper.readValue(new File("resources/classes/" + name + ".json"), DDClass.class));
+			store.put(name, mapper.readValue(new File("resources/classes/" + name + ".json"), 
+					DDClass.class));
 		}
 		return store.get(name);
 	}
@@ -68,6 +79,19 @@ public class DDClass {
 	}
 
 	public List<DDProperty> getFeatures(int level) {
-		return features.size() > level ? Collections.unmodifiableList(features.get(level)) : Collections.emptyList();
+		return features.size() < level ? Collections.emptyList() :
+			Collections.unmodifiableList(features.get(level-1));
+	}
+
+	public Optional<DDSpellbook> createNewSpellBook()  {
+		return spells.flatMap(sl -> sl.getSpellBookTypeName()).map(st -> createSpellbookImpl(st));
+	}
+
+	@SneakyThrows({JsonMappingException.class, JsonParseException.class, IOException.class})
+	private DDSpellbook createSpellbookImpl(String st) {
+		if (!st.startsWith("{")) {
+			st = new StringFormatter("{{\"@c\":\"{}\"}}").format(st);
+		}
+		return mapper.readValue(st.getBytes(), DDSpellbook.class);
 	}
 }

+ 2 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/features/DDProperty.java

@@ -10,6 +10,8 @@ public interface DDProperty {
 	public enum Group {
 		NONE
 	}
+	String getName();
+	String getDescription();
 	boolean appliesTo(Object key);
 	@Deprecated <T> T value();
 	void accumulate(Map<String, Object> props);

+ 2 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/Flat.java

@@ -6,9 +6,11 @@ import org.leumasjaffe.charsheet.model.features.DDFeaturePredicate;
 import org.leumasjaffe.charsheet.model.features.DDProperty;
 
 import lombok.AllArgsConstructor;
+import lombok.Getter;
 
 @AllArgsConstructor
 public class Flat implements DDProperty {
+	@Getter String name, description;
 	DDFeaturePredicate applies;
 	Group group;
 	int value;

+ 2 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/PerSpellLevel.java

@@ -7,9 +7,11 @@ import org.leumasjaffe.charsheet.model.features.DDFeaturePredicate;
 import org.leumasjaffe.charsheet.model.features.DDProperty;
 
 import lombok.AllArgsConstructor;
+import lombok.Getter;
 
 @AllArgsConstructor
 public class PerSpellLevel implements DDProperty {
+	@Getter String name, description;
 	DDFeaturePredicate applies;
 	Group group;
 	String type;

+ 3 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/Simple.java

@@ -5,10 +5,12 @@ import java.util.Map;
 import org.leumasjaffe.charsheet.model.features.DDProperty;
 
 import lombok.AllArgsConstructor;
+import lombok.Getter;
 
-@AllArgsConstructor
+@AllArgsConstructor @Getter
 public class Simple implements DDProperty {
 	String name;
+	String description;
 	@Override public boolean appliesTo(Object key) { return false; }
 	@Override public <T> T value() { return null; }
 	@Override public void accumulate(Map<String, Object> props) {}

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

@@ -18,7 +18,7 @@ import lombok.experimental.FieldDefaults;
 @ToString
 @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class DDEquipment extends Observable {
+public class DDEquipment extends Observable.Instance {
 	@NonNull Map<EquipmentSlot, DDItem> equipment = new EnumMap<>(EquipmentSlot.class);
 
 	public boolean canEquip(final DDItem item) {

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

@@ -16,7 +16,7 @@ import lombok.experimental.NonFinal;
 @Getter @Setter @ToString
 @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class DDInventory extends Observable {
+public class DDInventory extends Observable.Instance {
 	@AllArgsConstructor
 	public static class Serializable {
 		@NonNull List<DDItem> items;

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

@@ -11,7 +11,7 @@ import lombok.experimental.FieldDefaults;
 @AllArgsConstructor
 @Data @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE)
-public class Money extends Observable implements Comparable<Money> {
+public class Money extends Observable.Instance implements Comparable<Money> {
 	int pp, gp, sp, cp;
 	
 	public Money assign(final Money other) {

+ 5 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpell.java

@@ -14,6 +14,7 @@ import org.leumasjaffe.charsheet.model.magic.dimension.Target;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonValue;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
@@ -104,6 +105,10 @@ public class DDSpell {
 		return new SpellClassInfo("", -1);
 	}
 	
+	@JsonValue public String getName() {
+		return this.name;
+	}
+	
 	@JsonCreator public static DDSpell fromString(String str) { 
 		return DDSpellFactory.loadSpell(str);
 	}

+ 6 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellList.java

@@ -1,10 +1,13 @@
 package org.leumasjaffe.charsheet.model.magic;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
 
 import lombok.AccessLevel;
 import lombok.Data;
@@ -15,10 +18,12 @@ import lombok.experimental.FieldDefaults;
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @JsonIgnoreProperties(ignoreUnknown=true)
 public class DDSpellList {
+	Optional<String> spellBookTypeName;
 	@NonNull Source group;
 	@NonNull String ability;
 	
-	@NonNull List<List<Integer>> known;
+	@JsonInclude(JsonInclude.Include.NON_NULL)
+	@NonNull List<List<Integer>> known = Collections.emptyList();
 	@NonNull List<List<Integer>> perDay;
 
 	@NonNull List<SpellList> spellList;

+ 12 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellbook.java

@@ -3,8 +3,10 @@ package org.leumasjaffe.charsheet.model.magic;
 import java.util.Collection;
 import java.util.List;
 
+import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.observer.Observable;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
 
@@ -13,7 +15,7 @@ import lombok.NonNull;
 import lombok.Setter;
 
 @JsonTypeInfo(use = Id.MINIMAL_CLASS)
-public abstract class DDSpellbook extends Observable {
+public abstract class DDSpellbook extends Observable.Instance {
 	public interface Secondary {
 		DDSpellbook getMainSpellbook();
 		void setMainSpellbook(DDSpellbook spellBook);
@@ -25,8 +27,11 @@ public abstract class DDSpellbook extends Observable {
 	
 	@NonNull public List<DDSpell> getSpellsPreparedPreviouslyForLevel(int level) { return spellsPreparedAtLevel(level); }	
 
-
+	public boolean learnsSpells() { return false; }
 	public boolean preparesSpells() { return false; }
+	@JsonIgnore public IntValue getSharedAllowedSlots() {
+		return new IntValue(-1);
+	}
 	
 	public int numSpellsKnownAtLevel( int level ) {
 		return spellsKnownAtLevel( level ).size();
@@ -40,9 +45,12 @@ public abstract class DDSpellbook extends Observable {
 	
 	public abstract void castSpell( int level, final DDSpell spell );
 	
-	public abstract void prepareSpells(int level, List<DDSpell> spells);
-
 	public String getSingleName() {
 		return getName();
 	}
+
+	public void learnSpells(int level, Collection<DDSpell> known) {
+		throw new UnsupportedOperationException("This class does not have a list of known spells to edit");
+	}
+	public abstract void prepareSpells(int level, Collection<DDSpell> collection);
 }

+ 12 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Inspired.java

@@ -8,18 +8,22 @@ import java.util.Map;
 import org.leumasjaffe.charsheet.model.DDClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 
+import com.fasterxml.jackson.annotation.JsonValue;
+
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import lombok.Getter;
 import lombok.NonNull;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.NonFinal;
 
-@AllArgsConstructor
+@AllArgsConstructor @Getter
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class Inspired extends Prepared {
-	@AllArgsConstructor
+	@AllArgsConstructor @Getter
 	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	private static class Level {
+		Level() { this(Collections.emptyList(), Collections.emptyList(), 0); }
 		@NonNull List<DDSpell> spellsPrepared, spellsPreparedPreviously;
 		@NonFinal int spellsPerDay;
 	}
@@ -30,6 +34,10 @@ public class Inspired extends Prepared {
 		public ClassReference(String name) {
 			this.ref = DDClass.getFromResource(name);
 		}
+		
+		@JsonValue public String getName() {
+			return ref.getName();
+		}
 	}
 	
 	@NonNull Map<Integer, Inspired.Level> spellInfo;
@@ -69,7 +77,7 @@ public class Inspired extends Prepared {
 	}
 	
 	private Level get(int level) {
-		return spellInfo.getOrDefault(level, new Level(Collections.emptyList(), Collections.emptyList(), 0));
+		return spellInfo.getOrDefault(level, new Level());
 	}
 
 	@Override
@@ -78,7 +86,7 @@ public class Inspired extends Prepared {
 	}
 
 	@Override
-	public void prepareSpells(int level, List<DDSpell> spells) {
+	public void prepareSpells(int level, Collection<DDSpell> spells) {
 		final Level lInfo = get(level);
 		lInfo.spellsPrepared.clear();
 		lInfo.spellsPreparedPreviously.clear();

+ 49 - 6
src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Researched.java

@@ -1,30 +1,61 @@
 package org.leumasjaffe.charsheet.model.magic.impl;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import org.leumasjaffe.charsheet.model.DDClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonValue;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import lombok.Getter;
 import lombok.NonNull;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.NonFinal;
 
-@AllArgsConstructor
+@AllArgsConstructor @Getter
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class Researched extends Prepared {
-	@AllArgsConstructor
+	@AllArgsConstructor @Getter
 	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	private static class Level {
 		@NonNull Collection<DDSpell> spellsKnown;
 		@NonNull List<DDSpell> spellsPrepared, spellsPreparedPreviously;
 		@NonFinal int spellsPerDay;
 	}
-	
+
+	private static class ClassReference {
+		DDClass ref;
+		@SuppressWarnings("unused")
+		public ClassReference(String name) {
+			this.ref = DDClass.getFromResource(name);
+		}
+		
+		@JsonValue public String getName() {
+			return ref.getName();
+		}
+	}
+
+	int freeSpellsPerLevel;
 	@NonNull Map<Integer, Researched.Level> spellInfo;
+	@NonNull ClassReference classRef;
+
+	@Override
+	@JsonIgnore public IntValue getSharedAllowedSlots() {
+		return new IntValue(freeSpellsPerLevel);
+	}
+
+	@Override
+	public boolean learnsSpells() {
+		return true;
+	}
 
 	@Override
 	public int numSpellsPerDayAtLevel( int level ) {
@@ -33,7 +64,7 @@ public class Researched extends Prepared {
 
 	@Override
 	public Collection<DDSpell> spellsKnownAtLevel(int level) {
-		return Collections.unmodifiableCollection(get(level).spellsKnown);
+		return level == 0 ? classRef.ref.getSpellList(0) : Collections.unmodifiableCollection(get(level).spellsKnown);
 	}
 
 	@Override
@@ -58,13 +89,25 @@ public class Researched extends Prepared {
 	public List<DDSpell> getSpellsPreparedPreviouslyForLevel(int level) {
 		return Collections.unmodifiableList(get(level).spellsPreparedPreviously);
 	}
+	
+	@Override
+	public void learnSpells(int level, Collection<DDSpell> known) {
+		final Level lInfo = spellInfo.getOrDefault(level, new Level(new ArrayList<>(), 
+				new ArrayList<>(), new ArrayList<>(), 0));
+		lInfo.spellsKnown.clear();
+		lInfo.spellsKnown.addAll(known);
+		spellInfo.putIfAbsent(level, lInfo);
+	}
 
 	@Override
-	public void prepareSpells(int level, List<DDSpell> spells) {
+	public void prepareSpells(int level, Collection<DDSpell> spells) {
+		this.spellInfo.putIfAbsent(level, new Level(new ArrayList<>(), 
+				new ArrayList<>(), new ArrayList<>(), 0));
 		final Level lInfo = get(level);
-		if (!lInfo.spellsKnown.containsAll(spells)) {
+		if (!spellsKnownAtLevel(level).containsAll(spells)) {
 			throw new IllegalArgumentException("Attempted to prepare spells that you don't know");
 		}
+		lInfo.spellsPerDay = spells.size();
 		lInfo.spellsPrepared.clear();
 		lInfo.spellsPreparedPreviously.clear();
 		lInfo.spellsPrepared.addAll(spells);

+ 19 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Spontaneous.java

@@ -1,5 +1,6 @@
 package org.leumasjaffe.charsheet.model.magic.impl;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -10,14 +11,15 @@ import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import lombok.Getter;
 import lombok.NonNull;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.NonFinal;
 
-@AllArgsConstructor
+@AllArgsConstructor @Getter
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class Spontaneous extends DDSpellbook {
-	@AllArgsConstructor
+	@AllArgsConstructor @Getter
 	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	private static class Level {
 		@NonNull List<DDSpell> spellsKnown;
@@ -25,6 +27,11 @@ public class Spontaneous extends DDSpellbook {
 	}
 	
 	@NonNull Map<Integer, Spontaneous.Level> spellInfo;
+		
+	@Override
+	public boolean learnsSpells() {
+		return true;
+	}
 	
 	@Override
 	public int numSpellsPerDayAtLevel(int level) {
@@ -63,8 +70,16 @@ public class Spontaneous extends DDSpellbook {
 	}
 
 	@Override
-	public void prepareSpells(int level, List<DDSpell> spells) {
+	public void learnSpells(int level, Collection<DDSpell> known) {
+		final Level lInfo = spellInfo.getOrDefault(level, new Level(new ArrayList<>(), 0, 0));
+		lInfo.spellsKnown.clear();
+		lInfo.spellsKnown.addAll(known);
+		spellInfo.putIfAbsent(level, lInfo);
+	}
+
+	@Override
+	public void prepareSpells(int level, Collection<DDSpell> spells) {
 		final Level lInfo = get(level);
-		lInfo.spellsPerDayRemaining = lInfo.spellsPerDay;
+		lInfo.spellsPerDayRemaining = lInfo.spellsPerDay = spells.size();
 	}
 }

+ 91 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/observable/BoolGate.java

@@ -0,0 +1,91 @@
+package org.leumasjaffe.charsheet.model.observable;
+
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+import org.leumasjaffe.observer.Observable;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class BoolGate extends Observable.Instance {
+	boolean[] data;
+	int size;
+	
+	@AllArgsConstructor(access=AccessLevel.PRIVATE)
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	public class Handle {
+		int index;
+		public boolean get() { return BoolGate.this.get(index); }
+		public void set(boolean bool) {
+			// FIXME
+			if (bool != get()) {
+				BoolGate.this.set(index, bool);
+				ObserverDispatch.notifySubscribers(BoolGate.this);
+			}
+		}
+		
+		public Meta createSubGate(int dim) {
+			return new Meta(this, dim);
+		}
+	}
+	
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	public static class Meta extends BoolGate {
+		BoolGate.Handle callBack;
+		
+		private Meta(BoolGate.Handle callBack, int dim) {
+			super(dim);
+			this.callBack = callBack;
+		}
+		
+		public ObservableListener<Void, Meta> makeListener() {
+			ObservableListener<Void, Meta> obs = new ObservableListener<>(null, Meta::invokeCallback);
+			obs.setObserved(this);
+			return obs;
+		}
+		
+		private static void invokeCallback(Object ignore, Meta self) {
+			self.callBack.set(self.all());
+		}
+	}
+	
+	public BoolGate(int dim) {
+		size = dim;
+		data = new boolean[dim];
+		Arrays.fill(data, false);
+	}
+	
+	public boolean all() {
+		for (int i = 0; i < size; ++i) {
+			if (!data[i]) return false;
+		}
+		return true;
+	}
+	
+	public Handle handle(int idx) {
+		return this.new Handle(idx);
+	}
+	
+	public boolean get(int idx) {
+		return data[idx];
+	}
+	
+	public void set(int idx, boolean bool) {
+		data[idx] = bool;
+	}
+	
+	public boolean some(int...idxs) {
+		return Arrays.stream(idxs).mapToObj(this::get).allMatch(Boolean::booleanValue);
+	}
+	
+	public ObservableListener<Consumer<Boolean>, BoolGate> makeListener(Consumer<Boolean> accepter) {
+		ObservableListener<Consumer<Boolean>, BoolGate> obs = new ObservableListener<>(accepter, (c, v) -> c.accept(v.all()));
+		obs.setObserved(this);
+		return obs;
+	}
+}

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/observable/IntValue.java

@@ -14,7 +14,7 @@ import lombok.experimental.Accessors;
 @Data
 @NoArgsConstructor
 @EqualsAndHashCode(callSuper=false)
-public class IntValue extends Observable {
+public class IntValue extends Observable.Instance {
 	@JsonCreator
 	public IntValue(int v) { this.value = v; }
 	private int value = -1;

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/observable/ObjectValue.java

@@ -13,7 +13,7 @@ import lombok.experimental.Accessors;
 @Accessors(fluent=true)
 @Data
 @EqualsAndHashCode(callSuper=false)
-public class ObjectValue<T> extends Observable {
+public class ObjectValue<T> extends Observable.Instance {
 	@JsonCreator public ObjectValue(T v) { this.value = v; }
 	private @NonNull T value;
 	

+ 1 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/observable/StringValue.java

@@ -15,7 +15,7 @@ import lombok.experimental.Accessors;
 @NoArgsConstructor
 @Data
 @EqualsAndHashCode(callSuper=false)
-public class StringValue extends Observable {
+public class StringValue extends Observable.Instance {
 	@JsonCreator public StringValue(String v) { this.value = v; }
 	private @NonNull String value = "";
 	

+ 8 - 6
src/main/lombok/org/leumasjaffe/charsheet/observer/ObserverHelper.java

@@ -9,16 +9,18 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public class ObserverHelper {
 	public void notifyObservableHierarchy(final DDCharacterClass dclass, final Object src) {
-		ObserverDispatch.notifySubscribers(dclass, src);
+		ObserverDispatch.notifySubscribers(dclass);
+		// FIXME
 		dclass.getSpellBook().ifPresent(wrap -> {
-			ObserverDispatch.notifySubscribers(wrap.getMain(), src);
-			wrap.getSecondary().ifPresent(sb -> ObserverDispatch.notifySubscribers(sb, src));
+			ObserverDispatch.notifySubscribers(wrap.getMain());
+			wrap.getSecondary().ifPresent(sb -> ObserverDispatch.notifySubscribers(sb));
 		});
 	}
 	
 	public void notifyObservableHierarchy(final Ability.Scores abil, final Object src) {
-		ObserverDispatch.notifySubscribers(abil, src);
-		ObserverDispatch.notifySubscribers(abil.getBase(), src);
-		ObserverDispatch.notifySubscribers(abil.getTemp(), src);
+		// FIXME
+		ObserverDispatch.notifySubscribers(abil);
+		ObserverDispatch.notifySubscribers(abil.getBase());
+		ObserverDispatch.notifySubscribers(abil.getTemp());
 	}
 }

+ 3 - 2
src/main/lombok/org/leumasjaffe/charsheet/observer/helper/IntValueHelper.java

@@ -1,9 +1,10 @@
 package org.leumasjaffe.charsheet.observer.helper;
 
+import java.util.function.BiFunction;
+
 import org.leumasjaffe.charsheet.model.observable.IntValue;
-import org.leumasjaffe.observer.helper.Helper;
 
-public class IntValueHelper implements Helper<IntValue> {
+public class IntValueHelper implements BiFunction<String, IntValue, Boolean> {
     public Boolean apply( final String str, final IntValue ref ) {
       if ( ! Character.isDigit(str.charAt(0)) ) return false;
       final int newValue = Integer.parseInt( str );

+ 3 - 2
src/main/lombok/org/leumasjaffe/charsheet/observer/helper/StringValueHelper.java

@@ -1,9 +1,10 @@
 package org.leumasjaffe.charsheet.observer.helper;
 
+import java.util.function.BiFunction;
+
 import org.leumasjaffe.charsheet.model.observable.StringValue;
-import org.leumasjaffe.observer.helper.Helper;
 
-public class StringValueHelper implements Helper<StringValue> {
+public class StringValueHelper implements BiFunction<String, StringValue, Boolean> {
     public Boolean apply( final String str, final StringValue ref ) {
       if ( str.equals( ref.value( ) ) ) return false;
       ref.value( str );

+ 5 - 4
src/main/lombok/org/leumasjaffe/charsheet/view/ClassTab.java

@@ -3,6 +3,7 @@ package org.leumasjaffe.charsheet.view;
 import javax.swing.JPanel;
 
 import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.experimental.FieldDefaults;
 import java.awt.GridBagLayout;
 import javax.swing.JTextField;
@@ -30,13 +31,13 @@ public class ClassTab extends JPanel {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	String title;
-
+	@Getter DDCharacterClass model;
+	
 	ObservableListener<JTextField, IntValue> levelListener;
 	ObservableListener<JTextField, IntValue> expListener;
 	
 	public ClassTab(DDCharacter chara, DDCharacterClass model) {
-		this.title = model.getName();
+		this.model = model;
 		
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0};
@@ -184,7 +185,7 @@ public class ClassTab extends JPanel {
 	
 	@Override
 	public String getName() {
-		return title;
+		return model.getName();
 	}
 	
 	@Override

+ 81 - 32
src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java

@@ -1,8 +1,32 @@
 package org.leumasjaffe.charsheet.view;
 
+import static org.leumasjaffe.charsheet.config.Constants.K_LEVELUP;
+import static org.leumasjaffe.charsheet.config.Constants.LevelUpStyle.IMMEDIATELY;
+
+import java.awt.BorderLayout;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowEvent;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
 import javax.swing.JFileChooser;
 import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
 import javax.swing.JTabbedPane;
+import javax.swing.KeyStroke;
+import javax.swing.UIManager;
+
+import org.leumasjaffe.charsheet.config.Config;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.observer.ObserverHelper;
+import org.leumasjaffe.charsheet.view.level.ExperienceDialog;
+import org.leumasjaffe.observer.ObservableListener;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
@@ -13,32 +37,11 @@ import lombok.SneakyThrows;
 import lombok.experimental.FieldDefaults;
 import lombok.experimental.NonFinal;
 
-import java.awt.BorderLayout;
-import javax.swing.JPanel;
-import javax.swing.JMenuBar;
-import javax.swing.JMenu;
-import javax.swing.JMenuItem;
-import javax.swing.KeyStroke;
-import javax.swing.UIManager;
-
-import org.leumasjaffe.charsheet.model.DDCharacter;
-import org.leumasjaffe.charsheet.model.DDCharacterClass;
-import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
-import org.leumasjaffe.charsheet.observer.ObserverHelper;
-
-import java.awt.event.KeyEvent;
-import java.awt.event.WindowEvent;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.awt.event.InputEvent;
-
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class D20Sheet extends JFrame {
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
+	static int TABS_BEFORE_CLASSES = 1;
+	
 	ObjectMapper mapper = new ObjectMapper();
 	@NonFinal File currentlyOpenFile = null;
 	@NonFinal @NonNull DDCharacter model = new DDCharacter();
@@ -49,7 +52,9 @@ public class D20Sheet extends JFrame {
 	JPanel abilitiesTab;
 	SkillTab skillTab;
 	EquipmentTab equipmentTab;
-	private DeveloperMenu developerMenu;
+	DeveloperMenu developerMenu;
+	
+	ObservableListener<D20Sheet, DDCharacter> listener;
 	
 	public D20Sheet() {
 		UIManager.put("CheckBox.disabledText", UIManager.get("CheckBox.foreground"));
@@ -100,6 +105,11 @@ public class D20Sheet extends JFrame {
 		mntmSaveAs.addActionListener( e -> saveAs(fc) );
 		mntmSaveAs.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
 		mnFile.add(mntmSaveAs);
+		
+		JMenuItem mntmConfig = new JMenuItem("Config");
+		mntmConfig.addActionListener( e -> DialogBuilder.showConfigDialog(this) );
+		mntmConfig.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.ALT_DOWN_MASK));
+		mnFile.add(mntmConfig);
 				
 		JMenuItem mntmExit = new JMenuItem("Exit");
 		mntmExit.addActionListener( e -> { this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); } );
@@ -110,13 +120,21 @@ public class D20Sheet extends JFrame {
 		menuBar.add(mnSession);
 		
 		JMenuItem mntmGainExperience = new JMenuItem("Gain Experience");
+		mntmGainExperience.addActionListener(e -> {
+			ExperienceDialog.gainExperience(this.model);
+			if (Config.get(K_LEVELUP, IMMEDIATELY) == IMMEDIATELY) {
+				DialogBuilder.showLevelUpDialog(this, this.model, 0);
+			}
+		});
 		mnSession.add(mntmGainExperience);
 		
 		JMenuItem mntmTakeRest = new JMenuItem("Take Rest");
 		mntmTakeRest.addActionListener(e -> {
 			// Step 0: Gather information about the rest
 			// Step 0.1: Check if 8-hour or full day
-			// Step 0.2: Check if Long-Term-Care
+			// Step 0.2: Check for level-up
+			DialogBuilder.showLevelUpDialog(this, this.model, 0);
+			// Step 0.3: Check if Long-Term-Care
 			// Step 1: Heal Character according to info
 			// Step 1.1: Heal non-lethal damage
 			// Step 1.2: Heal ability damage
@@ -139,6 +157,7 @@ public class D20Sheet extends JFrame {
 		menuBar.add(developerMenu);
 		
 		// Set up post-GUI dependencies
+		listener = new ObservableListener<>(this, (c, v) -> c.reloadModel());
 		setModel(model);
 	}
 
@@ -165,7 +184,7 @@ public class D20Sheet extends JFrame {
 		tabbedPane.addTab("Skills", null, skillTab, null);
 		tabbedPane.addTab("Gear & Items", null, equipmentTab, null);
 	}
-
+	
 	private void saveAs(final JFileChooser fc) {
 		int rv = fc.showSaveDialog(this);
 		if ( rv == JFileChooser.APPROVE_OPTION ) {
@@ -173,7 +192,7 @@ public class D20Sheet extends JFrame {
 			saveModelResource();
 		}
 	}
-
+	
 	@SneakyThrows
 	private void loadModelResource(File selectedFile) {
 		currentlyOpenFile = selectedFile;
@@ -186,16 +205,46 @@ public class D20Sheet extends JFrame {
 			mapper.writeValue(currentlyOpenFile, model);
 		}
 	}
-		
+	
+	private void reloadModel() {
+		this.summaryTab.setModel(this.model);
+		int i = -1;
+		for (DDCharacterClass cc : this.model.getClasses()) {
+			++i;
+			if (checkIfClassExistsAndOverwrite(cc)) continue;
+			ClassTab ct = new ClassTab(model, cc);
+			classTabs.add(ct);
+			tabbedPane.insertTab(ct.getName(), null, ct, null, TABS_BEFORE_CLASSES + i);
+		}
+		this.listener.notifySubscribers(this.model);
+	}
+	
+	private boolean checkIfClassExistsAndOverwrite(DDCharacterClass cc) {
+		for (int j = 0; j < classTabs.size(); ++j) {
+			final DDCharacterClass dclass = classTabs.get(j).getModel();
+			if (dclass.getName().equals(cc.getName())) {
+				if (dclass.getLevel().value() != cc.getLevel().value()) {
+					ClassTab ct = new ClassTab(model, cc);
+					classTabs.set(j, ct);
+					tabbedPane.setComponentAt(j + TABS_BEFORE_CLASSES, ct);
+				}
+				return true;
+			}
+		}
+		return false;
+	}
+	
 	private void setModel(DDCharacter model) {
-		classTabs.clear();
 		this.model = model;
+		classTabs.clear();
 		summaryTab.setModel(model);
-		model.getClasses().stream().forEach(cc -> classTabs.add(new ClassTab(model, cc)));
+
 		equipmentTab.setModel(model);
 		skillTab.setModel(model);
 		developerMenu.setModel(model);
-		
+	
 		reorderTabs();
+
+		listener.setObserved(this.model);
 	}
 }

+ 14 - 8
src/main/lombok/org/leumasjaffe/charsheet/view/DeveloperMenu.java

@@ -1,7 +1,5 @@
 package org.leumasjaffe.charsheet.view;
 
-import java.util.stream.Collectors;
-
 import javax.swing.JFrame;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
@@ -19,16 +17,24 @@ public class DeveloperMenu extends JMenu {
 	
 	public DeveloperMenu(final JFrame parent) {
 		super("Developer");
-		JMenuItem mntmLevelUp = new JMenuItem("Level Up - Skill");
-		mntmLevelUp.addActionListener( e -> {
-			Object[] choices = model[0].getClasses().stream().map(DDCharacterClass::getName).collect(Collectors.toList()).toArray();
-			String clazz = (String) JOptionPane.showInputDialog(this.getParent(), "Which Class is Leveling Up?", "Level Up - Skill", 
+		JMenuItem mntmLevelUp = new JMenuItem("Level Up - Class");
+		mntmLevelUp.addActionListener(e -> {
+			DialogBuilder.showLevelUpDialog(parent, model[0], 1);
+		});
+		add(mntmLevelUp);
+		JMenuItem mntmLUSkill = new JMenuItem("Level Up - Skill");
+		mntmLUSkill.addActionListener(e -> {
+			Object[] choices = model[0].getClasses().stream().map(DDCharacterClass::getName)
+					.toArray(String[]::new);
+			String clazz = (String) JOptionPane.showInputDialog(this.getParent(), 
+					"Which Class is Leveling Up?", "Level Up - Skill", 
 					JOptionPane.QUESTION_MESSAGE, null, choices, choices[0]);
 			if (clazz != null) {
-				DialogBuilder.createSkillDialog(parent, model[0], model[0].getClasses().stream().filter(c -> c.getName().equals(clazz)).findFirst().get());
+				DialogBuilder.createSkillDialog(parent, model[0], model[0].getClasses().stream()
+						.filter(c -> c.getName().equals(clazz)).findFirst().get());
 			}
 		});
-		add(mntmLevelUp);
+		add(mntmLUSkill);
 	}
 	
 	public void setModel(DDCharacter model) {

+ 23 - 3
src/main/lombok/org/leumasjaffe/charsheet/view/DialogBuilder.java

@@ -9,14 +9,18 @@ import javax.swing.JPanel;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.charsheet.view.config.ConfigPanel;
+import org.leumasjaffe.charsheet.view.level.ExperienceDialog;
+import org.leumasjaffe.charsheet.view.level.LevelUpDialog;
 import org.leumasjaffe.charsheet.view.magic.PrepareSpellsDialog;
 import org.leumasjaffe.charsheet.view.skills.SkillLevelUpDialog;
+import org.leumasjaffe.format.StringFormatter;
 
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
 public class DialogBuilder {
-	private void createDialogue(final JFrame parent, final String title, final JPanel panel) {
+	private void createDialog(final JFrame parent, final String title, final JPanel panel) {
 		final JDialog dialog = new JDialog(parent);
 		dialog.setTitle(title);
 		dialog.setModalityType(ModalityType.DOCUMENT_MODAL);
@@ -27,10 +31,26 @@ public class DialogBuilder {
 	}
 	
 	public void createSkillDialog(final JFrame parent, DDCharacter chara, DDCharacterClass dclass) {
-		createDialogue(parent, "Level Up - Skill Allocation", new SkillLevelUpDialog(chara, dclass));
+		createDialog(parent, "Level Up - Skill Allocation", new SkillLevelUpDialog(chara, dclass));
 	}
 	
 	public void createPrepareSpellsDialog(final JFrame parent, DDCharacter chara, DDCharacterClass dclass, DDSpellbook spellBook) {
-		createDialogue(parent, "Prepare Spells - " + dclass.getName(), new PrepareSpellsDialog(chara, dclass, spellBook));
+		createDialog(parent, "Prepare Spells - " + dclass.getName(), new PrepareSpellsDialog(chara, dclass, spellBook));
+	}
+	
+	public void showConfigDialog(final JFrame parent) {
+		createDialog(parent, "Config", new ConfigPanel());
+	}
+	
+	public void showLevelUpDialog(final JFrame parent, DDCharacter chara, int bonusLevels) {
+		final int levelsGained = ExperienceDialog.computeLevelsNeeded(chara, bonusLevels);
+		if (levelsGained == 1) {
+			createDialog(parent, "Level Up", new LevelUpDialog(chara));
+			return;
+		}
+		for (int i = 0; i < levelsGained; ++i) {
+			createDialog(parent, new StringFormatter("Level Up - {} of {}").format(i, levelsGained),
+					new LevelUpDialog(chara));
+		}
 	}
 }

+ 77 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/config/ConfigEnumPanel.java

@@ -0,0 +1,77 @@
+package org.leumasjaffe.charsheet.view.config;
+
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+
+import org.leumasjaffe.charsheet.config.Config;
+
+import java.awt.GridBagLayout;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JLabel;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.event.ActionListener;
+import java.util.EnumMap;
+import java.awt.FlowLayout;
+
+@SuppressWarnings("serial")
+public class ConfigEnumPanel<E extends Enum<E>> extends JPanel {
+	EnumMap<E, JRadioButton> buttons;
+
+	public ConfigEnumPanel(String name, Class<E> clazz) {
+		buttons = new EnumMap<>(clazz);
+		
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JLabel lblName = new JLabel(name);
+		GridBagConstraints gbc_lblName = new GridBagConstraints();
+		gbc_lblName.insets = new Insets(0, 0, 0, 5);
+		gbc_lblName.gridx = 0;
+		gbc_lblName.gridy = 0;
+		add(lblName, gbc_lblName);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 1;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		panel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
+		
+		for (E e : clazz.getEnumConstants()) {
+			JRadioButton button = new JRadioButton(toTitleCase(e.name()));
+			buttons.put(e, button);
+			panel.add(button);
+		}
+		buttons.get(Config.get(name, clazz.getEnumConstants()[0])).setSelected(true);
+		
+		final ButtonGroup grp = new ButtonGroup();
+		buttons.values().forEach(grp::add);
+		ActionListener listen = e -> Config.set(name, Enum.valueOf(clazz, e.getActionCommand()));
+		buttons.values().forEach(b -> b.addActionListener(listen));
+	}
+
+	public static String toTitleCase(final String input) {
+	    final StringBuilder titleCase = new StringBuilder();
+	    titleCase.ensureCapacity(input.length());
+	    boolean nextTitleCase = true;
+
+	    for (char c : input.replaceAll("_", " ").toCharArray()) {
+	        if (Character.isSpaceChar(c)) {
+	            nextTitleCase = true;
+	        } else if (nextTitleCase) {
+	            c = Character.toTitleCase(c);
+	            nextTitleCase = false;
+	        } else {
+	        	c = Character.toLowerCase(c);
+	        }
+	        titleCase.append(c);
+	    }
+	    return titleCase.toString();
+	}}

+ 79 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/config/ConfigPanel.java

@@ -0,0 +1,79 @@
+package org.leumasjaffe.charsheet.view.config;
+
+import javax.swing.JPanel;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.config.Constants;
+
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import javax.swing.JLabel;
+import java.awt.Insets;
+import java.awt.Component;
+import javax.swing.Box;
+import java.awt.Font;
+
+@SuppressWarnings("serial")
+public class ConfigPanel extends JPanel {
+	public ConfigPanel() {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		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[]{0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0, 0};
+		gbl_panel.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblUnits = new JLabel("Units:");
+		lblUnits.setFont(new Font("Lucida Grande", Font.PLAIN, 16));
+		GridBagConstraints gbc_lblUnits = new GridBagConstraints();
+		gbc_lblUnits.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblUnits.insets = new Insets(5, 0, 5, 0);
+		gbc_lblUnits.gridx = 0;
+		gbc_lblUnits.gridy = 0;
+		panel.add(lblUnits, gbc_lblUnits);
+		
+		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;
+		panel.add(panel_1, gbc_panel_1);
+		GridBagLayout gbl_panel_1 = new GridBagLayout();
+		gbl_panel_1.columnWidths = new int[]{0, 0, 0};
+		gbl_panel_1.rowHeights = new int[]{0, 0};
+		gbl_panel_1.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		panel_1.setLayout(gbl_panel_1);
+		
+		JPanel panel_2 = new JPanel(new VerticalLayout(0));
+		GridBagConstraints gbc_panel_2 = new GridBagConstraints();
+		gbc_panel_2.fill = GridBagConstraints.BOTH;
+		gbc_panel_2.gridx = 1;
+		gbc_panel_2.gridy = 0;
+		panel_1.add(panel_2, gbc_panel_2);
+		
+		Component horizontalStrut = Box.createHorizontalStrut(20);
+		GridBagConstraints gbc_horizontalStrut = new GridBagConstraints();
+		gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5);
+		gbc_horizontalStrut.gridx = 0;
+		gbc_horizontalStrut.gridy = 0;
+		panel_1.add(horizontalStrut, gbc_horizontalStrut);
+		
+		panel_2.add(new ConfigEnumPanel<>(Constants.K_DISTANCE, Constants.DistanceMeasurement.class));
+		panel_2.add(new ConfigEnumPanel<>(Constants.K_DURATION, Constants.DurationMeasurement.class));
+		panel_2.add(new ConfigEnumPanel<>(Constants.K_LEVELUP, Constants.LevelUpStyle.class));
+	}
+}

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

@@ -37,8 +37,8 @@ class EquipmentInfoMenu extends JPopupMenu {
 					== JOptionPane.YES_OPTION) {
 				item.adjustCounEquipped(-1);
 				equip.unequip(slot);
-				ObserverDispatch.notifySubscribers(equip, null);
-				ObserverDispatch.notifySubscribers(item.getCountEquipped(), null);
+				ObserverDispatch.notifySubscribers(equip);
+				ObserverDispatch.notifySubscribers(item.getCountEquipped());
 			}
 		});
 		add(mntmEquip);

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

@@ -110,8 +110,8 @@ class ItemInfoMenu extends JPopupMenu {
 			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);
-			ObserverDispatch.notifySubscribers(wealth, null);
+			ObserverDispatch.notifySubscribers(item.getCount());
+			ObserverDispatch.notifySubscribers(wealth);
 		}
 	}
 }

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

@@ -38,7 +38,7 @@ class LoadoutMenu extends JPopupMenu {
 			showLoadDialog().ifPresent(name -> {
 				try {
 					getModel().load(name);
-					ObserverDispatch.notifySubscribers(getModel(), null);
+					ObserverDispatch.notifySubscribers(getModel());
 				} catch (NullPointerException except) {
 					JOptionPane.showMessageDialog(this, "Unable to load equipment, some items are missing", 
 							"Error", JOptionPane.ERROR_MESSAGE);

+ 98 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/ChooseClassLevelUpDialog.java

@@ -0,0 +1,98 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import lombok.SneakyThrows;
+
+import java.awt.GridBagLayout;
+
+import javax.swing.JComboBox;
+import java.awt.GridBagConstraints;
+import javax.swing.JLabel;
+import java.awt.Insets;
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import javax.swing.JButton;
+
+@SuppressWarnings("serial")
+class ChooseClassLevelUpDialog extends JPanel {
+	public ChooseClassLevelUpDialog(DDCharacter chara, Consumer<LevelUpClassInfo> nexter) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 0.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		
+		JLabel lblClass = new JLabel("Class:");
+		GridBagConstraints gbc_lblClass = new GridBagConstraints();
+		gbc_lblClass.insets = new Insets(0, 0, 5, 5);
+		gbc_lblClass.anchor = GridBagConstraints.EAST;
+		gbc_lblClass.gridx = 1;
+		gbc_lblClass.gridy = 1;
+		add(lblClass, gbc_lblClass);
+
+		String[] choices = chara.getClasses().stream()
+				.map(DDCharacterClass::getName).toArray(String[]::new);
+		JComboBox<String> comboBox = new JComboBox<>(choices);
+		comboBox.setEditable(true);
+		GridBagConstraints gbc_comboBox = new GridBagConstraints();
+		gbc_comboBox.insets = new Insets(0, 0, 5, 0);
+		gbc_comboBox.fill = GridBagConstraints.HORIZONTAL;
+		gbc_comboBox.gridx = 2;
+		gbc_comboBox.gridy = 1;
+		add(comboBox, gbc_comboBox);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.gridwidth = 2;
+		gbc_panel.insets = new Insets(0, 0, 0, 5);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 1;
+		gbc_panel.gridy = 2;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JButton btnNext = new JButton("Next");
+		GridBagConstraints gbc_btnNext = new GridBagConstraints();
+		gbc_btnNext.gridx = 1;
+		gbc_btnNext.gridy = 0;
+		panel.add(btnNext, gbc_btnNext);
+		btnNext.addActionListener(e -> {
+			final String name = (String) comboBox.getSelectedItem();
+			final LevelUpClassInfo info = chara.getClasses().stream()
+					.filter(c -> c.getName().equals(name)).findFirst()
+					.map(dch -> new LevelUpClassInfo(chara, clone(dch), dch.getLevel().value() + 1))
+					.orElseGet(() -> new LevelUpClassInfo(chara, new DDCharacterClass(name)));
+			nexter.accept(info);
+		});
+	}
+
+	static ObjectMapper mapper = new ObjectMapper();
+	static {
+		mapper.registerModule(new Jdk8Module());
+	}
+	
+	@SneakyThrows({JsonParseException.class, JsonMappingException.class, JsonProcessingException.class, IOException.class})
+	private DDCharacterClass clone(DDCharacterClass dch) {
+		final String data = mapper.writeValueAsString(dch);
+//		System.out.println(data);
+		return mapper.readValue(data, DDCharacterClass.class);
+	}
+}

+ 44 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/ExperienceDialog.java

@@ -0,0 +1,44 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import java.util.Arrays;
+
+import javax.swing.JOptionPane;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class ExperienceDialog {
+	private int[] EXPERIENCE_CACHE;
+	static {
+		final int levelsToCalc = 20;
+		EXPERIENCE_CACHE = new int[levelsToCalc];
+		for (int i = 0; i < levelsToCalc; ++i) {
+			EXPERIENCE_CACHE[i] = experienceForLevel(i+1);
+		}
+	}
+
+	public void gainExperience(DDCharacter model) {
+		final String input = JOptionPane.showInputDialog("How much experience");
+		if (input == null || input.isEmpty()) return;
+		final int exp = Integer.parseInt(input);
+		if (exp <= 0) return;
+		final IntValue mdlExp = model.getExperience();
+		mdlExp.value(mdlExp.value()+exp);
+		ObserverDispatch.notifySubscribers(mdlExp);
+	}
+	
+	public static int computeLevelsNeeded(DDCharacter chara, int bonusLevels) {
+		final int exp = chara.getExperience().value();
+		final int currentLevel = chara.getLevel();
+		final int expectedLevel = Math.abs(Arrays.binarySearch(EXPERIENCE_CACHE, exp) + 1);
+		return bonusLevels + expectedLevel - currentLevel;
+	}
+
+	public int experienceForLevel(int level) {
+		return 500 * level * (level - 1);
+	}
+}

+ 193 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_AbilityPanel.java

@@ -0,0 +1,193 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.SwingConstants;
+import javax.swing.border.LineBorder;
+
+import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.view.summary.AbilityBox;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import lombok.AccessLevel;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import lombok.experimental.NonFinal;
+
+@SuppressWarnings("serial")
+public class LU_AbilityPanel extends JPanel {
+	private static final String[] abils = new String[] { "STR", "DEX", "CON", "INT", "WIS", "CHA" };
+	private static final List<Function<Ability, Ability.Scores>> funcs = Arrays.asList( 
+		Ability::getStr, Ability::getDex, Ability::getCon,
+		Ability::getInt, Ability::getWis, Ability::getCha
+		);
+	static ObjectMapper mapper = new ObjectMapper();
+	static {
+		mapper.registerModule(new Jdk8Module());
+	}
+	Ability ability;
+	BoolGate.Handle gate;
+	boolean canGainAbility;
+	@NonFinal @Setter(AccessLevel.PRIVATE) int currentSelection = -1;
+	JPanel abilityGrowth;
+
+	public LU_AbilityPanel(LevelUpClassInfo info, BoolGate.Handle gate) {
+		this.ability = clone(info.ddCharacter.getAbilities());
+		this.gate = gate;
+		this.canGainAbility = info.ddCharacter.getLevel() % 4 == 3;
+		this.gate.set(!canGainAbility);
+		ButtonGroup group = new ButtonGroup();
+
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JLabel lblAbilityScores = new JLabel("Ability Scores");
+		lblAbilityScores.setPreferredSize(new Dimension(86, 22));
+		lblAbilityScores.setToolTipText("When your total character level (Hit Dice) is a multiple of four, " +
+				"you can increase one ability score by one point.");
+		lblAbilityScores.setOpaque(true);
+		lblAbilityScores.setHorizontalAlignment(SwingConstants.CENTER);
+		lblAbilityScores.setForeground(Color.WHITE);
+		lblAbilityScores.setFont(new Font("Tahoma", Font.BOLD, 14));
+		lblAbilityScores.setBorder(new LineBorder(new Color(255, 255, 255), 0));
+		lblAbilityScores.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblAbilityScores = new GridBagConstraints();
+		gbc_lblAbilityScores.fill = GridBagConstraints.BOTH;
+		gbc_lblAbilityScores.insets = new Insets(0, 0, 2, 0);
+		gbc_lblAbilityScores.gridx = 0;
+		gbc_lblAbilityScores.gridy = 0;
+		add(lblAbilityScores, gbc_lblAbilityScores);
+		
+		abilityGrowth = new JPanel();
+		GridBagConstraints gbc_abilGrowth = new GridBagConstraints();
+		gbc_abilGrowth.insets = new Insets(0, 0, 5, 0);
+		gbc_abilGrowth.fill = GridBagConstraints.BOTH;
+		gbc_abilGrowth.gridx = 0;
+		gbc_abilGrowth.gridy = 1;
+		add(abilityGrowth, gbc_abilGrowth);
+		GridBagLayout gbl_abilGrowth = new GridBagLayout();
+		gbl_abilGrowth.columnWidths = new int[]{0, 0, 0, 0};
+		gbl_abilGrowth.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0};
+		gbl_abilGrowth.columnWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gbl_abilGrowth.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		abilityGrowth.setLayout(gbl_abilGrowth);
+		
+		JPanel panel_2 = new JPanel();
+		GridBagConstraints gbc_panel_2 = new GridBagConstraints();
+		gbc_panel_2.insets = new Insets(0, 0, 5, 0);
+		gbc_panel_2.fill = GridBagConstraints.BOTH;
+		gbc_panel_2.gridx = 0;
+		gbc_panel_2.gridy = 2;
+		add(panel_2, gbc_panel_2);
+		GridBagLayout gbl_panel_2 = new GridBagLayout();
+		gbl_panel_2.columnWidths = new int[]{0, 0, 0};
+		gbl_panel_2.rowHeights = new int[]{0, 0};
+		gbl_panel_2.columnWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		gbl_panel_2.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel_2.setLayout(gbl_panel_2);
+		
+		JButton btnClear = new JButton("Clear");
+		GridBagConstraints gbc_btnClear = new GridBagConstraints();
+		gbc_btnClear.gridx = 1;
+		gbc_btnClear.gridy = 0;
+		panel_2.add(btnClear, gbc_btnClear);
+		btnClear.setEnabled(canGainAbility);
+		btnClear.addActionListener(e -> {
+			gate.set(false);
+			group.clearSelection();
+			resetAbility();
+		});
+		
+		for (int i = 0; i < 6; ++i) {
+			createAbilityLine(i, group);
+		}
+	}
+
+	private void createAbilityLine(int y, ButtonGroup group) {
+		JLabel label = new JLabel(abils[y].toUpperCase());
+		label.setPreferredSize(new Dimension(50, 25));
+		label.setOpaque(true);
+		label.setMinimumSize(new Dimension(50, 25));
+		label.setMaximumSize(new Dimension(50, 25));
+		label.setHorizontalAlignment(SwingConstants.CENTER);
+		label.setForeground(Color.WHITE);
+		label.setFont(new Font("Tahoma", Font.BOLD, 18));
+		label.setBorder(new LineBorder(Color.WHITE));
+		label.setBackground(Color.BLACK);
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.gridx = 0;
+		gbc_label.gridy = y;
+		abilityGrowth.add(label, gbc_label);
+		
+		AbilityBox abilityBox = new AbilityBox(abils[y], "Ability");
+		GridBagConstraints gbc_abilityBox = new GridBagConstraints();
+		gbc_abilityBox.fill = GridBagConstraints.BOTH;
+		gbc_abilityBox.gridx = 1;
+		gbc_abilityBox.gridy = y;
+		abilityGrowth.add(abilityBox, gbc_abilityBox);
+		
+		abilityBox.setModel(funcs.get(y).apply(this.ability).getBase());
+		
+		JRadioButton radioButton = new JRadioButton("");
+		GridBagConstraints gbc_radioButton = new GridBagConstraints();
+		gbc_radioButton.gridx = 2;
+		gbc_radioButton.gridy = y;
+		abilityGrowth.add(radioButton, gbc_radioButton);
+		group.add(radioButton);
+		radioButton.addActionListener(e -> {
+			resetAbility();
+			setCurrentSelection(y);
+			updateAbility(this.ability, true);
+			gate.set(true);
+		});
+		radioButton.setEnabled(canGainAbility);
+	}
+
+	private void resetAbility() {
+		updateAbility(this.ability, false);
+	}
+	
+	public void commitChange(Ability ability) {
+		updateAbility(ability, true);
+		this.currentSelection = -1;
+	}
+
+	private void updateAbility(Ability ability, boolean increase) {
+		if (currentSelection != -1) {
+			IntValue val = funcs.get(currentSelection).apply(ability).getBase();
+			val.value(val.value() + (increase ? 1 : -1));
+			ObserverDispatch.notifySubscribers(val);
+		}
+	}
+
+	@SneakyThrows({JsonParseException.class, JsonMappingException.class, JsonProcessingException.class, IOException.class})
+	private Ability clone(Ability abilities) {
+		return mapper.readValue(mapper.writeValueAsBytes(abilities), Ability.class);
+	}
+}

+ 108 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_FeaturesPanel.java

@@ -0,0 +1,108 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+class LU_FeaturesPanel extends JPanel {
+	static int HD_INDEX = 0, ABIL_INDEX = 1, FEAT_INDEX = 2;
+	LevelUpClassInfo info;
+
+	LU_HitDicePanel hdPanel;
+	LU_AbilityPanel abilPanel;
+
+	ObservableListener<Void, BoolGate.Meta> listener;
+
+	public LU_FeaturesPanel(LevelUpClassInfo info, BoolGate.Handle ready) {
+		this.info = info;
+		BoolGate.Meta gate = ready.createSubGate(3);
+		gate.set(HD_INDEX, true);
+		gate.set(FEAT_INDEX, true); // TODO Feats
+
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+				
+		JPanel panel_1 = new JPanel();
+		GridBagConstraints gbc_panel_1 = new GridBagConstraints();
+		gbc_panel_1.insets = new Insets(0, 0, 0, 5);
+		gbc_panel_1.fill = GridBagConstraints.BOTH;
+		gbc_panel_1.gridx = 0;
+		gbc_panel_1.gridy = 0;
+		add(panel_1, gbc_panel_1);
+		GridBagLayout gbl_panel_1 = new GridBagLayout();
+		gbl_panel_1.columnWidths = new int[]{0};
+		gbl_panel_1.rowHeights = new int[]{0, 0};
+		gbl_panel_1.columnWeights = new double[]{Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{0.0, 1.0};
+		panel_1.setLayout(gbl_panel_1);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 1;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{Double.MIN_VALUE, 0.0};
+		panel.setLayout(gbl_panel);
+		
+		hdPanel = new LU_HitDicePanel(info, gate.handle(HD_INDEX));
+		GridBagConstraints gbc_hdPanel = new GridBagConstraints();
+		gbc_hdPanel.insets = new Insets(0, 0, 5, 0);
+		gbc_hdPanel.fill = GridBagConstraints.BOTH;
+		gbc_hdPanel.gridx = 0;
+		gbc_hdPanel.gridy = 0;
+		panel_1.add(hdPanel, gbc_hdPanel);
+
+		JPanel features = new JPanel(new VerticalLayout(2));
+		GridBagConstraints gbc_features = new GridBagConstraints();
+		gbc_features.fill = GridBagConstraints.BOTH;
+		gbc_features.gridx = 0;
+		gbc_features.gridy = 1;
+		panel_1.add(features, gbc_features);
+		info.ddClass.getProto().getFeatures(info.toLevel).forEach(prop -> {
+			features.add(new JLabel(prop.getName()));
+		});
+		
+		abilPanel = new LU_AbilityPanel(info, gate.handle(ABIL_INDEX));
+		GridBagConstraints gbc_abilPanel = new GridBagConstraints();
+		gbc_abilPanel.insets = new Insets(0, 0, 5, 0);
+		gbc_abilPanel.fill = GridBagConstraints.BOTH;
+		gbc_abilPanel.gridx = 0;
+		gbc_abilPanel.gridy = 0;
+		panel.add(abilPanel, gbc_abilPanel);
+		
+		this.listener = gate.makeListener();
+	}
+	
+	public void commitAllChanges() {
+		this.hdPanel.commitChange(info.ddCharacter.getHealth());
+		this.abilPanel.commitChange(info.ddCharacter.getAbilities());
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
+}

+ 244 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_HitDicePanel.java

@@ -0,0 +1,244 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.HitPoints;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.event.AnyActionDocumentListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import java.awt.GridBagLayout;
+import javax.swing.JLabel;
+import java.awt.GridBagConstraints;
+import java.awt.Color;
+import javax.swing.border.LineBorder;
+
+import java.awt.Font;
+import javax.swing.SwingConstants;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.security.SecureRandom;
+
+import javax.swing.JTextField;
+import javax.swing.JButton;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class LU_HitDicePanel extends JPanel {
+	JTextField txtInput;
+	JButton btnRoll;
+
+	public LU_HitDicePanel(LevelUpClassInfo info, BoolGate.Handle handle) {
+		final int maxHealth = info.ddClass.getProto().getHitDice();
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0};
+		setLayout(gridBagLayout);
+		
+		JLabel lblRollHealthIncrease = new JLabel("Roll Health Increase");
+		lblRollHealthIncrease.setPreferredSize(new Dimension(60, 22));
+		lblRollHealthIncrease.setOpaque(true);
+		lblRollHealthIncrease.setMinimumSize(new Dimension(50, 25));
+		lblRollHealthIncrease.setMaximumSize(new Dimension(50, 25));
+		lblRollHealthIncrease.setHorizontalAlignment(SwingConstants.CENTER);
+		lblRollHealthIncrease.setForeground(Color.WHITE);
+		lblRollHealthIncrease.setFont(new Font("Tahoma", Font.BOLD, 14));
+		lblRollHealthIncrease.setBorder(new LineBorder(new Color(255, 255, 255), 0));
+		lblRollHealthIncrease.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblRollHealthIncrease = new GridBagConstraints();
+		gbc_lblRollHealthIncrease.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblRollHealthIncrease.gridwidth = 4;
+		gbc_lblRollHealthIncrease.insets = new Insets(0, 0, 2, 0);
+		gbc_lblRollHealthIncrease.gridx = 0;
+		gbc_lblRollHealthIncrease.gridy = 0;
+		add(lblRollHealthIncrease, gbc_lblRollHealthIncrease);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.insets = new Insets(0, 0, 2, 5);
+		gbc_panel.fill = GridBagConstraints.HORIZONTAL;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 1;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblCurrent = new JLabel("Current");
+		lblCurrent.setPreferredSize(new Dimension(50, 11));
+		lblCurrent.setOpaque(true);
+		lblCurrent.setMinimumSize(new Dimension(50, 25));
+		lblCurrent.setMaximumSize(new Dimension(50, 25));
+		lblCurrent.setHorizontalAlignment(SwingConstants.CENTER);
+		lblCurrent.setForeground(Color.WHITE);
+		lblCurrent.setFont(new Font("Tahoma", Font.BOLD, 10));
+		lblCurrent.setBorder(new LineBorder(new Color(255, 255, 255), 0));
+		lblCurrent.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblCurrent = new GridBagConstraints();
+		gbc_lblCurrent.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblCurrent.gridx = 0;
+		gbc_lblCurrent.gridy = 0;
+		panel.add(lblCurrent, gbc_lblCurrent);
+		
+		JLabel lblHealth = new JLabel("Health");
+		lblHealth.setPreferredSize(new Dimension(50, 11));
+		lblHealth.setOpaque(true);
+		lblHealth.setMinimumSize(new Dimension(50, 25));
+		lblHealth.setMaximumSize(new Dimension(50, 25));
+		lblHealth.setHorizontalAlignment(SwingConstants.CENTER);
+		lblHealth.setForeground(Color.WHITE);
+		lblHealth.setFont(new Font("Tahoma", Font.BOLD, 10));
+		lblHealth.setBorder(new LineBorder(new Color(255, 255, 255), 0));
+		lblHealth.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblHealth = new GridBagConstraints();
+		gbc_lblHealth.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblHealth.gridx = 0;
+		gbc_lblHealth.gridy = 1;
+		panel.add(lblHealth, gbc_lblHealth);
+		
+		JTextField txtCurrenthealth = new JTextField(Integer.toString(info.ddCharacter.getHealth().getTotal().value()));
+		txtCurrenthealth.setHorizontalAlignment(SwingConstants.CENTER);
+		txtCurrenthealth.setEditable(false);
+		txtCurrenthealth.setColumns(3);
+		txtCurrenthealth.setMinimumSize(new Dimension(30, 20));
+		txtCurrenthealth.setMaximumSize(new Dimension(30, 20));
+		txtCurrenthealth.setPreferredSize(new Dimension(30, 20));
+		txtCurrenthealth.setBorder(new LineBorder(Color.BLACK));
+		GridBagConstraints gbc_txtCurrenthealth = new GridBagConstraints();
+		gbc_txtCurrenthealth.insets = new Insets(0, 0, 2, 5);
+		gbc_txtCurrenthealth.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtCurrenthealth.gridx = 1;
+		gbc_txtCurrenthealth.gridy = 1;
+		add(txtCurrenthealth, gbc_txtCurrenthealth);
+		
+		JLabel lblHitDice = new JLabel("Hit Dice");
+		lblHitDice.setPreferredSize(new Dimension(60, 22));
+		lblHitDice.setOpaque(true);
+		lblHitDice.setMinimumSize(new Dimension(50, 25));
+		lblHitDice.setMaximumSize(new Dimension(50, 25));
+		lblHitDice.setHorizontalAlignment(SwingConstants.CENTER);
+		lblHitDice.setForeground(Color.WHITE);
+		lblHitDice.setFont(new Font("Tahoma", Font.BOLD, 14));
+		lblHitDice.setBorder(new LineBorder(new Color(255, 255, 255), 0));
+		lblHitDice.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblHitDice = new GridBagConstraints();
+		gbc_lblHitDice.anchor = GridBagConstraints.EAST;
+		gbc_lblHitDice.insets = new Insets(0, 0, 2, 5);
+		gbc_lblHitDice.gridx = 2;
+		gbc_lblHitDice.gridy = 1;
+		add(lblHitDice, gbc_lblHitDice);
+		
+		JTextField textHitDie = new JTextField("d" + Integer.toString(maxHealth));
+		textHitDie.setHorizontalAlignment(SwingConstants.CENTER);
+		textHitDie.setEditable(false);
+		textHitDie.setColumns(2);
+		textHitDie.setMinimumSize(new Dimension(30, 20));
+		textHitDie.setMaximumSize(new Dimension(30, 20));
+		textHitDie.setPreferredSize(new Dimension(30, 20));
+		textHitDie.setBorder(new LineBorder(Color.BLACK));
+		GridBagConstraints gbc_textHitDie = new GridBagConstraints();
+		gbc_textHitDie.insets = new Insets(0, 0, 2, 0);
+		gbc_textHitDie.fill = GridBagConstraints.HORIZONTAL;
+		gbc_textHitDie.gridx = 3;
+		gbc_textHitDie.gridy = 1;
+		add(textHitDie, gbc_textHitDie);
+		
+		JLabel lblRoll = new JLabel("Roll");
+		lblRoll.setPreferredSize(new Dimension(60, 22));
+		lblRoll.setOpaque(true);
+		lblRoll.setMinimumSize(new Dimension(50, 25));
+		lblRoll.setMaximumSize(new Dimension(50, 25));
+		lblRoll.setHorizontalAlignment(SwingConstants.CENTER);
+		lblRoll.setForeground(Color.WHITE);
+		lblRoll.setFont(new Font("Tahoma", Font.BOLD, 14));
+		lblRoll.setBorder(new LineBorder(new Color(255, 255, 255), 0));
+		lblRoll.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblRoll = new GridBagConstraints();
+		gbc_lblRoll.anchor = GridBagConstraints.EAST;
+		gbc_lblRoll.insets = new Insets(0, 0, 0, 5);
+		gbc_lblRoll.gridx = 0;
+		gbc_lblRoll.gridy = 2;
+		add(lblRoll, gbc_lblRoll);
+		
+		txtInput = new JTextField();
+		txtInput.setPreferredSize(new Dimension(30, 20));
+		txtInput.setMinimumSize(new Dimension(30, 20));
+		txtInput.setMaximumSize(new Dimension(30, 20));
+		txtInput.setHorizontalAlignment(SwingConstants.CENTER);
+		txtInput.setColumns(3);
+		txtInput.setBorder(new LineBorder(Color.BLACK));
+		GridBagConstraints gbc_txtInput = new GridBagConstraints();
+		gbc_txtInput.insets = new Insets(0, 0, 0, 5);
+		gbc_txtInput.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtInput.gridx = 1;
+		gbc_txtInput.gridy = 2;
+		add(txtInput, gbc_txtInput);
+		AnyActionDocumentListener.emptyOrText(txtInput, 
+				evt -> txtInput.setBackground(Color.WHITE),
+				evt -> {
+					final int value = getRoll();
+					final boolean good = value > 0 && value <= maxHealth;
+					txtInput.setBackground(good ? new Color(198,239,206) : new Color(255,199,206));
+					handle.set(good);
+				});
+		
+		JPanel panel_1 = new JPanel();
+		GridBagConstraints gbc_panel_1 = new GridBagConstraints();
+		gbc_panel_1.gridwidth = 2;
+		gbc_panel_1.fill = GridBagConstraints.BOTH;
+		gbc_panel_1.gridx = 2;
+		gbc_panel_1.gridy = 2;
+		add(panel_1, gbc_panel_1);
+		GridBagLayout gbl_panel_1 = new GridBagLayout();
+		gbl_panel_1.columnWidths = new int[]{0, 0};
+		gbl_panel_1.rowHeights = new int[]{0, 0};
+		gbl_panel_1.columnWeights = new double[]{0.0, Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel_1.setLayout(gbl_panel_1);
+		
+		btnRoll = new JButton("Roll");
+		btnRoll.setPreferredSize(new Dimension(75, 24));
+		GridBagConstraints gbc_btnRoll = new GridBagConstraints();
+		gbc_btnRoll.gridx = 0;
+		gbc_btnRoll.gridy = 0;
+		panel_1.add(btnRoll, gbc_btnRoll);
+		btnRoll.addActionListener(e -> {
+			final int result = new SecureRandom().nextInt(maxHealth) + 1;
+			setRoll(result);
+			handle.set(true);
+		});
+		
+		if (info.toLevel == 1 && info.ddCharacter.getLevel() == 0) {
+			setRoll(maxHealth);
+			handle.set(true);
+		}
+	}
+	
+	public void commitChange(HitPoints hp) {
+		final int roll = getRoll();
+		hp.getRolled().value(hp.getRolled().value() + roll);
+		hp.getCurrent().value(hp.getCurrent().value() + roll);
+		hp.getTotal().value(hp.getTotal().value() + roll);
+		ObserverDispatch.notifySubscribers(hp.getRolled());
+		ObserverDispatch.notifySubscribers(hp.getCurrent());
+		ObserverDispatch.notifySubscribers(hp.getTotal());
+	}
+	
+	public int getRoll() {
+		if (!txtInput.getText().matches("^\\d+$")) return -1;
+		return Integer.parseInt(txtInput.getText());
+	}
+	
+	private void setRoll(final int result) {
+		txtInput.setText(Integer.toString(result));
+		txtInput.setEditable(false);
+		btnRoll.setEnabled(false);
+	}
+}

+ 19 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpClassInfo.java

@@ -0,0 +1,19 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PUBLIC)
+class LevelUpClassInfo {
+	@NonNull DDCharacter ddCharacter;
+	@NonNull DDCharacterClass ddClass;
+	int toLevel = 1;
+}

+ 99 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpDialog.java

@@ -0,0 +1,99 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class LevelUpDialog extends JPanel {	
+	ChooseClassLevelUpDialog chooseClass;
+	JButton btnCommit;
+	@NonFinal UpdateClassWithLevelPanel updateClass;
+	private GridBagConstraints gbc_main;
+		
+	public LevelUpDialog(DDCharacter chara) {
+		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, 0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		chooseClass = new ChooseClassLevelUpDialog(chara, this::createLayer2);
+		gbc_main = new GridBagConstraints();
+		gbc_main.insets = new Insets(0, 0, 5, 0);
+		gbc_main.fill = GridBagConstraints.BOTH;
+		gbc_main.gridx = 0;
+		gbc_main.gridy = 0;
+		add(chooseClass, gbc_main);
+				
+		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};
+		gbl_panel_1.columnWeights = new double[]{1.0, 0.0, 0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel_1.setLayout(gbl_panel_1);
+		
+		JButton btnCancel = new JButton("Cancel");
+		GridBagConstraints gbc_btnCancel = new GridBagConstraints();
+		gbc_btnCancel.insets = new Insets(0, 0, 0, 5);
+		gbc_btnCancel.gridx = 1;
+		gbc_btnCancel.gridy = 0;
+		panel_1.add(btnCancel, gbc_btnCancel);
+		btnCancel.addActionListener(e -> closeOut());
+		
+		btnCommit = new JButton("Commit");
+		GridBagConstraints gbc_btnCommit = new GridBagConstraints();
+		gbc_btnCommit.insets = new Insets(0, 0, 0, 5);
+		gbc_btnCommit.gridx = 2;
+		gbc_btnCommit.gridy = 0;
+		panel_1.add(btnCommit, gbc_btnCommit);
+		btnCommit.setEnabled(false);
+		btnCommit.addActionListener(e -> {
+			commitAllChanges();
+			closeOut();
+		});
+	}
+
+	private void commitAllChanges() {
+		updateClass.commitAllChanges();
+	}
+
+	private void closeOut() {
+		((JDialog) this.getParent().getParent().getParent()).dispose();
+	}
+	
+	void dropLayer2() {
+		remove(updateClass);
+		add(chooseClass, gbc_main);
+		repaint();
+		revalidate();
+		((JDialog) this.getParent().getParent().getParent()).pack();
+	}
+	
+	void createLayer2(LevelUpClassInfo info) {
+		remove(chooseClass);
+		updateClass = new UpdateClassWithLevelPanel(info, this::dropLayer2, b -> btnCommit.setEnabled(b));
+		add(updateClass, gbc_main);
+		revalidate();
+		repaint();
+		((JDialog) this.getParent().getParent().getParent()).pack();
+	}	
+}

+ 148 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpSpellPanel.java

@@ -0,0 +1,148 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JPanel;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.view.magic.SelectSpellsPanel;
+import org.leumasjaffe.charsheet.view.magic.SelectSpellsPanel.Info;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
+import java.awt.GridBagLayout;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.swing.JScrollPane;
+import java.awt.GridBagConstraints;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
+class LevelUpSpellPanel extends JPanel {
+	public static interface SpellPicker {
+		public List<List<Integer>> getSpellCounts(SelectSpellsPanel.Info info);
+		public Collection<DDSpell> getAvailableSpells(SelectSpellsPanel.Info info, int i);
+	}
+	public static enum SpellPickType implements SpellPicker {
+		LEARN {
+			@Override
+			public List<List<Integer>> getSpellCounts(SelectSpellsPanel.Info info) {
+				return info.dclass.getProto().getSpells().get().getKnown();
+			}
+			@Override
+			public Collection<DDSpell> getAvailableSpells(Info info, int i) {
+				Collection<DDSpell> spells = new ArrayList<>(info.dclass.getProto().getSpellList(i));
+				spells.removeAll(info.dclass.getSpellBook().get().spellsKnownAtLevel(i));
+				return spells;
+			}
+
+		}, PREPARE {
+			@Override
+			public List<List<Integer>> getSpellCounts(SelectSpellsPanel.Info info) {
+				// TODO: Bonus spells for high ability scores
+				return info.dclass.getProto().getSpells().get().getPerDay();
+			}
+			@Override
+			public Collection<DDSpell> getAvailableSpells(Info info, int i) {
+				return info.dclass.getSpellBook().get().spellsKnownAtLevel(i);
+			}
+		};		
+	}
+
+	int[] ready = {0};
+	@NonFinal int spellLevelsGrown = 0;
+	int oldHighestSpellLevel, newHighestSpellLevel, toLevel;
+	SpellPicker pick;
+	SelectSpellsPanel.Info info;
+	@Getter List<SelectSpellsPanel> panels;
+	ObservableListener<Void, BoolGate.Meta> allReady;
+
+	public LevelUpSpellPanel(SpellPicker pick, SelectSpellsPanel.Info info, 
+			BoolGate.Handle readyCount) {
+		this.pick = pick;
+		this.info = info;
+		this.toLevel = info.dclass.getLevel().value();
+		newHighestSpellLevel = info.dclass.getHighestSpellLevel(toLevel);
+		oldHighestSpellLevel = info.dclass.getHighestSpellLevel(toLevel-1);
+
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+
+		JScrollPane scrollPane = new JScrollPane();
+		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
+		gbc_scrollPane.fill = GridBagConstraints.BOTH;
+		gbc_scrollPane.gridx = 0;
+		gbc_scrollPane.gridy = 0;
+		add(scrollPane, gbc_scrollPane);
+
+		JPanel panel = new JPanel(new VerticalLayout(5));
+		scrollPane.setViewportView(panel);
+
+		panels = new ArrayList<>();
+
+		final IntValue val = getSharedAllowedSlots(info);
+		final Map<Integer, Integer> spells = getNewSpells(val);
+		final int sharedSlots = val.value();
+		BoolGate.Meta gate = readyCount.createSubGate(newHighestSpellLevel);
+		for (int i = 0; i < newHighestSpellLevel; ++i) {
+			if (spells.get(i) < 0) { gate.set(i, true); panels.add(null); continue; }
+			++spellLevelsGrown;
+			SelectSpellsPanel lvl = new SelectSpellsPanel(info, gate.handle(i), i,
+					new LinkedHashSet<>(), Math.max(spells.get(i), sharedSlots), 
+					pick.getAvailableSpells(info, i),
+					pick != SpellPickType.LEARN, val);
+			panels.add(lvl);
+			panel.add(lvl);
+		}
+		allReady = gate.makeListener();
+	}
+	
+	private Map<Integer, Integer> getNewSpells(IntValue sharedSpellCountLimit) {
+		final Map<Integer, Integer> map = new TreeMap<>();
+		final List<List<Integer>> spellList = pick.getSpellCounts(info);
+		final List<Integer> spellsAtPreviousLevel = toLevel == 1 ? Collections.emptyList() :
+			spellList.get(toLevel-2);
+		final List<Integer> spellsAtCurrentLevel = spellList.get(toLevel-1);
+		for (int i = 0; i < newHighestSpellLevel; ++i) {
+			map.put(i, diff(spellsAtCurrentLevel, spellsAtPreviousLevel, i,
+					isNewSpellCircle(i)));
+		}
+		if (!map.isEmpty()) sharedSpellCountLimit.value(-1);
+		return map;
+	}
+
+	private IntValue getSharedAllowedSlots(Info info) {
+		return info.dclass.getSpellBook().get().getSharedAllowedSlots();
+	}
+
+	private boolean isNewSpellCircle(int i) {
+		return i == (newHighestSpellLevel - 1) && oldHighestSpellLevel != newHighestSpellLevel;
+	}
+
+	private int diff(List<Integer> current, List<Integer> previous, int level, boolean newSpellLevel) {
+		if (current.size() <= level) return 0;
+		else if (newSpellLevel || previous.size() <= level) return current.get(level);
+		else return current.get(level) - previous.get(level);
+	}
+
+	@Override
+	public void removeNotify() {
+		ObserverDispatch.unsubscribeAll(allReady);
+	}
+}

+ 222 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/UpdateClassWithLevelPanel.java

@@ -0,0 +1,222 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import static org.leumasjaffe.charsheet.view.level.LevelUpSpellPanel.SpellPickType.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.function.Consumer;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.view.magic.SelectSpellsPanel;
+import org.leumasjaffe.charsheet.view.magic.SelectSpellsPanel.Info;
+import org.leumasjaffe.charsheet.view.skills.SkillLevelUpPanel;
+import org.leumasjaffe.function.VoidVoidFunction;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
+import java.awt.GridBagLayout;
+import javax.swing.JTabbedPane;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.Component;
+import javax.swing.Box;
+import javax.swing.JButton;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+class UpdateClassWithLevelPanel extends JPanel {
+	static int FEATURE_INDEX = 0, CHOOSE_SKILL_INDEX = 1, LEARN_SPELL_INDEX = 2, PREPARE_SPELL_INDEX = 3;
+
+	LevelUpClassInfo levelUpInfo;
+	JTabbedPane tabbedPane;
+	
+	SkillLevelUpPanel skills;
+	@NonFinal Optional<LevelUpSpellPanel> learnSpells = Optional.empty();
+	@NonFinal Optional<LevelUpSpellPanel> prepSpells = Optional.empty();
+	
+	BoolGate readyCount = new BoolGate(4);
+	ObservableListener<Consumer<Boolean>, BoolGate> listener;
+	@NonFinal ObservableListener<UpdateClassWithLevelPanel, BoolGate> learnAndPrepareListener = null;
+
+	LU_FeaturesPanel featurePanel;
+	
+	public UpdateClassWithLevelPanel(LevelUpClassInfo info, VoidVoidFunction back,
+			Consumer<Boolean> setReady) {
+		this.levelUpInfo = info;
+		info.ddClass.getLevel().value(info.toLevel);
+		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, 0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+
+		tabbedPane = new JTabbedPane(JTabbedPane.TOP);
+		GridBagConstraints gbc_tabbedPane = new GridBagConstraints();
+		gbc_tabbedPane.insets = new Insets(0, 0, 5, 0);
+		gbc_tabbedPane.fill = GridBagConstraints.BOTH;
+		gbc_tabbedPane.gridx = 0;
+		gbc_tabbedPane.gridy = 0;
+		add(tabbedPane, gbc_tabbedPane);
+
+		featurePanel = new LU_FeaturesPanel(info, readyCount.handle(FEATURE_INDEX));
+		tabbedPane.addTab("Features", null, featurePanel, null);
+
+		skills = new SkillLevelUpPanel(info.ddCharacter, info.ddClass) {
+			@Override public void setIsReady(boolean b) {
+				readyCount.set(CHOOSE_SKILL_INDEX, b);
+				ObserverDispatch.notifySubscribers(readyCount);
+			}
+		};
+		tabbedPane.addTab("Skills", null, skills, null);
+
+		info.ddClass.getSpellBook().ifPresent(sb -> {
+			readyCount.set(LEARN_SPELL_INDEX, !sb.learnsSpells());
+			readyCount.set(PREPARE_SPELL_INDEX, !sb.preparesSpells());
+			if (sb.learnsSpells()) {
+				createPanelsForLearnSpell(sb);
+			} else if (sb.preparesSpells()) {
+				createPanelForPrepareSpells();
+			}
+		});
+
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 1;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+
+		JButton btnBack = new JButton("Back");
+		GridBagConstraints gbc_btnBack = new GridBagConstraints();
+		gbc_btnBack.insets = new Insets(0, 0, 0, 5);
+		gbc_btnBack.gridx = 0;
+		gbc_btnBack.gridy = 0;
+		panel.add(btnBack, gbc_btnBack);
+		btnBack.addActionListener(e -> back.apply());
+
+		Component horizontalGlue = Box.createHorizontalGlue();
+		GridBagConstraints gbc_horizontalGlue = new GridBagConstraints();
+		gbc_horizontalGlue.gridx = 1;
+		gbc_horizontalGlue.gridy = 0;
+		panel.add(horizontalGlue, gbc_horizontalGlue);
+
+		listener = readyCount.makeListener(setReady);
+	}
+
+	private void createPanelForPrepareSpells() {
+		prepSpells = Optional.of(new LevelUpSpellPanel(PREPARE,
+				new SelectSpellsPanel.Info(levelUpInfo.ddCharacter, levelUpInfo.ddClass),
+				readyCount.handle(PREPARE_SPELL_INDEX)));
+		tabbedPane.addTab("Prepare Spells", null, prepSpells.get(), null);
+	}
+
+	private void createPanelsForLearnSpell(DDSpellbook sb) {
+		learnSpells = Optional.of(new LevelUpSpellPanel(LEARN,
+				new SelectSpellsPanel.Info(levelUpInfo.ddCharacter, levelUpInfo.ddClass),
+				readyCount.handle(LEARN_SPELL_INDEX)));
+		tabbedPane.addTab("Learn Spells", null, learnSpells.get(), null);
+		if (sb.preparesSpells()) {
+			learnAndPrepareListener = new ObservableListener<>(this, (c, v) -> {
+				if (v.get(LEARN_SPELL_INDEX)) {
+					if (!prepSpells.isPresent()) c.createPrepareLearnedSpellPanel(learnSpells.get());
+				} else {
+					prepSpells.ifPresent(c.tabbedPane::remove);
+					c.prepSpells = Optional.empty();
+				}
+			});
+			learnAndPrepareListener.setObserved(readyCount);
+		}
+	}
+
+	private void createPrepareLearnedSpellPanel(LevelUpSpellPanel spells) {
+		LevelUpSpellPanel.SpellPicker pick = new LevelUpSpellPanel.SpellPicker() {
+			@Override
+			public List<List<Integer>> getSpellCounts(Info info) {
+				return PREPARE.getSpellCounts(info);
+			}
+
+			@Override
+			public Collection<DDSpell> getAvailableSpells(Info info, int i) {
+				final Collection<DDSpell> start = new ArrayList<>(PREPARE.getAvailableSpells(info, i));
+				if (spells.getPanels().get(i) != null) {
+					start.addAll(spells.getPanels().get(i).getPrepared());
+				}
+				return start;
+			}
+		};
+
+		prepSpells = Optional.of(new LevelUpSpellPanel(pick,
+				new SelectSpellsPanel.Info(levelUpInfo.ddCharacter, levelUpInfo.ddClass),
+				readyCount.handle(PREPARE_SPELL_INDEX)));
+		tabbedPane.addTab("Prepare Spells", null, prepSpells.get(), null);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+		ObserverDispatch.unsubscribeAll(learnAndPrepareListener);
+	}
+
+	
+	private void commitSpellbook(DDSpellbook book) {
+		learnSpells.ifPresent(pan -> {
+			final List<SelectSpellsPanel> selections = pan.getPanels();
+			for (int i = 0; i < selections.size(); ++i) {
+				if (selections.get(i) == null) continue;
+				List<DDSpell> known = new ArrayList<>(book.spellsKnownAtLevel(i));
+				known.addAll(selections.get(i).getPrepared());
+				book.learnSpells(i, known);
+			}
+		});
+		prepSpells.ifPresent(pan -> {
+			final List<SelectSpellsPanel> selections = pan.getPanels();
+			for (int i = 0; i < selections.size(); ++i) {
+				if (selections.get(i) == null) continue;
+				List<DDSpell> known = new ArrayList<>(book.spellsPreparedAtLevel(i));
+				known.addAll(selections.get(i).getPrepared());
+				book.prepareSpells(i, known);
+			}
+		});
+	}
+	
+	public void commitAllChanges() {
+		final String className = levelUpInfo.ddClass.getName();
+		skills.commitAllChanges();
+		final Optional<DDSpellbook> maybeBook = levelUpInfo.ddClass.getSpellBook();
+		maybeBook.ifPresent(this::commitSpellbook);
+		final SortedSet<DDCharacterClass> classes = new TreeSet<>(levelUpInfo.ddCharacter.getClasses());
+		classes.removeIf(cc -> cc.getName().equals(className));
+		classes.add(levelUpInfo.ddClass);
+		levelUpInfo.ddCharacter.setClasses(classes);
+		this.featurePanel.commitAllChanges();
+		// TODO: Acquire features
+		final IntValue exp = levelUpInfo.ddCharacter.getExperience();
+		final int neededExp = ExperienceDialog.experienceForLevel(levelUpInfo.ddCharacter.getLevel());
+		if (exp.value() < neededExp) { exp.value(neededExp); }
+		ObserverDispatch.notifySubscribers(exp);
+		ObserverDispatch.notifySubscribers(levelUpInfo.ddCharacter);
+	}
+}

+ 14 - 13
src/main/lombok/org/leumasjaffe/charsheet/view/magic/PrepareSpellsDialog.java

@@ -8,7 +8,10 @@ import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.charsheet.model.magic.impl.Prepared;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -20,21 +23,18 @@ import java.awt.GridBagConstraints;
 import java.awt.Insets;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 import javax.swing.JButton;
 import javax.swing.JDialog;
 import javax.swing.ScrollPaneConstants;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class PrepareSpellsDialog extends JPanel {
-
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
-	
+public class PrepareSpellsDialog extends JPanel {	
 	int[] ready = {0};
 	int highestSpellLevel;
+	ObservableListener<Consumer<Boolean>, BoolGate> allReady;
 
 	public PrepareSpellsDialog(DDCharacter chara, DDCharacterClass dclass, DDSpellbook spellBook) {
 		highestSpellLevel = dclass.getHighestSpellLevel();
@@ -82,14 +82,11 @@ public class PrepareSpellsDialog extends JPanel {
 		final Ability.Scores score = AbilityHelper.get(chara, dclass);
 
 		List<SelectPreparedSpellsPanel> panels = new ArrayList<>();
+		final BoolGate gate = new BoolGate(highestSpellLevel);
+		allReady = gate.makeListener(btnPrepareTheseSpells::setEnabled);
 		for (int i = 0; i < highestSpellLevel; ++i) {
-			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, i, dclass, (Prepared) spellBook, score);
+			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, dclass, gate.handle(i), i, (Prepared) spellBook, score);
 			panels.add(lvl);
-			lvl.addPropertyChangeListener(SelectPreparedSpellsPanel.READY, e -> {
-				if ((Boolean) e.getNewValue()) ++ready[0];
-				else --ready[0];
-				btnPrepareTheseSpells.setEnabled(ready[0] == highestSpellLevel);
-			});
 			panel.add(lvl);
 		}
 		
@@ -101,4 +98,8 @@ public class PrepareSpellsDialog extends JPanel {
 		});
 	}
 
+	@Override
+	public void removeNotify() {
+		ObserverDispatch.unsubscribeAll(allReady);
+	}
 }

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

@@ -1,236 +1,29 @@
 package org.leumasjaffe.charsheet.view.magic;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.JTable;
-import javax.swing.ListSelectionModel;
-
-import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.Ability.Scores;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
-import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.impl.Prepared;
-import org.leumasjaffe.event.SelectTableRowPopupMenuListener;
-import org.leumasjaffe.format.StringHelper;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
 
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.experimental.FieldDefaults;
-import java.awt.GridBagLayout;
-import java.awt.Dimension;
-import java.awt.GridBagConstraints;
-import java.awt.Insets;
-
-import javax.swing.border.BevelBorder;
-import javax.swing.border.SoftBevelBorder;
-import javax.swing.table.AbstractTableModel;
-import javax.swing.JButton;
-import javax.swing.JFrame;
-import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
-import javax.swing.JScrollPane;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-class SelectPreparedSpellsPanel extends JPanel {
+class SelectPreparedSpellsPanel extends SelectSpellsPanel {
 	
-	@AllArgsConstructor
-	private static class SelectSpellModel extends AbstractTableModel {
-		/**
-		 * 
-		 */
-		private static final long serialVersionUID = 1L;
-		
-		final Object[] data;
-
-		@Override
-		public int getRowCount() {
-			return data.length;
-		}
-
-		@Override
-		public int getColumnCount() {
-			return 1;
-		}
-
-		@Override
-		public Object getValueAt(int rowIndex, int columnIndex) {
-			if (columnIndex != 0) { throw new IllegalArgumentException("There is only 1 column"); }
-			return data[rowIndex];
-		}
-		
-		@Override
-		public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
-			if (columnIndex != 0) { throw new IllegalArgumentException("There is only 1 column"); }
-			data[rowIndex] = aValue;
-		}
+	private SelectPreparedSpellsPanel(DDCharacter chara, DDCharacterClass dclass, 
+			BoolGate.Handle gate, int level, Prepared prep) {
+		super(new SelectSpellsPanel.Info(chara, dclass), gate, level,
+				prep.getSpellsPreparedPreviouslyForLevel(level),
+				prep.spellsKnownAtLevel(level));
 	}
 
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
-	
-	public static final String READY = "Is Filled Out";
-	
-	@Getter List<DDSpell> prepared;
-	
-	SelectSpellModel modelPrepared, modelKnown;
-
-	public SelectPreparedSpellsPanel(DDCharacter chara, int level, DDCharacterClass dclass, 
-			Prepared spellBook, Ability.Scores score) {
-		putClientProperty(READY, true);
-		this.prepared = new ArrayList<>(spellBook.getSpellsPreparedPreviouslyForLevel(level));
-		final List<DDSpell> known = new ArrayList<>(spellBook.spellsKnownAtLevel(level));
-		this.modelPrepared = new SelectSpellModel(prepared.stream().map(DDSpell::getName).toArray());
-		this.modelKnown = new SelectSpellModel(known.stream().map(DDSpell::getName).toArray());
-		
-		GridBagLayout gridBagLayout = new GridBagLayout();
-		gridBagLayout.columnWidths = new int[]{0, 40, 0, 0};
-		gridBagLayout.rowHeights = new int[]{20, 0, 0};
-		gridBagLayout.columnWeights = new double[]{1.0, 0.0, 1.0, Double.MIN_VALUE};
-		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
-		setLayout(gridBagLayout);
-		
-		JPanel panel = new ChooseSpellsPerDayHeader(level, spellBook, score);
-		GridBagConstraints gbc_panel = new GridBagConstraints();
-		gbc_panel.gridwidth = 3;
-		gbc_panel.insets = new Insets(0, 0, 5, 5);
-		gbc_panel.fill = GridBagConstraints.BOTH;
-		gbc_panel.gridx = 0;
-		gbc_panel.gridy = 0;
-		add(panel, gbc_panel);
-		
-		JScrollPane scrollPane_1 = new JScrollPane();
-		scrollPane_1.setPreferredSize(new Dimension(175, 200));
-		scrollPane_1.setViewportBorder(new SoftBevelBorder(BevelBorder.LOWERED, null, null, null, null));
-		GridBagConstraints gbc_scrollPane_1 = new GridBagConstraints();
-		gbc_scrollPane_1.insets = new Insets(0, 0, 0, 5);
-		gbc_scrollPane_1.fill = GridBagConstraints.BOTH;
-		gbc_scrollPane_1.gridx = 0;
-		gbc_scrollPane_1.gridy = 1;
-		add(scrollPane_1, gbc_scrollPane_1);
-		
-		JTable tablePrepared = new JTable(modelPrepared);
-		tablePrepared.setTableHeader(null);
-		scrollPane_1.setViewportView(tablePrepared);
-		tablePrepared.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
-
-		JPanel panelDivider = new JPanel();
-		GridBagConstraints gbc_panelDivider = new GridBagConstraints();
-		gbc_panelDivider.insets = new Insets(0, 0, 0, 5);
-		gbc_panelDivider.fill = GridBagConstraints.BOTH;
-		gbc_panelDivider.gridx = 1;
-		gbc_panelDivider.gridy = 1;
-		add(panelDivider, gbc_panelDivider);
-		GridBagLayout gbl_panelDivider = new GridBagLayout();
-		gbl_panelDivider.columnWidths = new int[]{0, 0};
-		gbl_panelDivider.rowHeights = new int[]{0, 0, 0, 0, 0};
-		gbl_panelDivider.columnWeights = new double[]{0.0, Double.MIN_VALUE};
-		gbl_panelDivider.rowWeights = new double[]{1.0, 0.0, 0.0, 1.0, Double.MIN_VALUE};
-		panelDivider.setLayout(gbl_panelDivider);		
-		
-		JScrollPane scrollPane = new JScrollPane();
-		scrollPane.setPreferredSize(new Dimension(175, 200));
-		scrollPane.setViewportBorder(new SoftBevelBorder(BevelBorder.LOWERED, null, null, null, null));
-		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
-		gbc_scrollPane.insets = new Insets(0, 0, 0, 5);
-		gbc_scrollPane.fill = GridBagConstraints.BOTH;
-		gbc_scrollPane.gridx = 2;
-		gbc_scrollPane.gridy = 1;
-		add(scrollPane, gbc_scrollPane);
-		
-		JTable tableKnown = new JTable(modelKnown);
-		tableKnown.setTableHeader(null);
-		scrollPane.setViewportView(tableKnown);
-		tableKnown.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
-		
-		JPopupMenu popup = new JPopupMenu();
-		popup.addPopupMenuListener(new SelectTableRowPopupMenuListener(popup, tableKnown));
-		JMenuItem mntmInfo = new JMenuItem("Info");
-		mntmInfo.addActionListener( e -> {
-			DDSpell spell = known.get(tableKnown.getSelectedRow());
-			JFrame frame = new JFrame(spell.getName() +  " (" + dclass.getName() + " " + level + ")");
-			frame.add(new SpellInfoPanel(chara, dclass, spell));
-			frame.pack();
-			frame.setVisible(true);
-		});
-		popup.add(mntmInfo);
-		tableKnown.setComponentPopupMenu(popup);
-		
-		JButton button = new JButton(">>");
-		button.setMargin(new Insets(2, 8, 2, 8));
-		GridBagConstraints gbc_button = new GridBagConstraints();
-		gbc_button.insets = new Insets(0, 0, 5, 0);
-		gbc_button.gridx = 0;
-		gbc_button.gridy = 1;
-		panelDivider.add(button, gbc_button);
-		button.addActionListener(e -> {
-			final int row = tablePrepared.getSelectedRow();
-			if (row != -1) {
-				modelPrepared.setValueAt("<none>", row, 0);
-			}
-			tablePrepared.getSelectionModel().clearSelection();
-			tablePrepared.repaint();
-			if ((Boolean) getClientProperty(READY)) {
-				putClientProperty(READY, false);
-			}
-		});
-		
-		JButton button_1 = new JButton("<<");
-		button_1.setMargin(new Insets(2, 8, 2, 8));
-		GridBagConstraints gbc_button_1 = new GridBagConstraints();
-		gbc_button_1.insets = new Insets(0, 0, 5, 0);
-		gbc_button_1.gridx = 0;
-		gbc_button_1.gridy = 2;
-		panelDivider.add(button_1, gbc_button_1);
-		
-		button_1.addActionListener(e -> {
-			final int[] rows = tableKnown.getSelectedRows();
-			final int[] orows = tablePrepared.getSelectedRows();
-			if (orows.length >= rows.length) {
-				for (int i = 0; i < rows.length; ++i) {
-					modelPrepared.data[orows[i]] = modelKnown.data[rows[i]];
-				}
-			} else if (orows.length == 0 && countNone() >= rows.length) {
-				replace(rows);
-			} else {
-				final String message = StringHelper.format("Unable to assign new spells, more spells were selected ({}) than were avaliable ({})", 
-						rows.length, orows.length == 0 ? countNone() : orows.length);
-				JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE);
-			}
-			tableKnown.getSelectionModel().clearSelection();
-			tablePrepared.getSelectionModel().clearSelection();
-			tablePrepared.repaint();
-			
-			if (!(Boolean) getClientProperty(READY) && !Arrays.asList(modelPrepared.data).contains("<none>")) {
-				this.prepared.clear();
-				for (Object o : modelPrepared.data) {
-					this.prepared.add(DDSpell.fromString((String) o)); // TODO
-				}
-				putClientProperty(READY, true);
-			}
-		});
+	public SelectPreparedSpellsPanel(DDCharacter chara, DDCharacterClass dclass, BoolGate.Handle gate, int level,
+			Prepared spellBook, Scores score) {
+		this(chara, dclass, gate, level, ((Prepared) dclass.getSpellBook().get()));
 	}
 
-	private void replace(int[] rows) {
-		for (int i = 0; i < rows.length; ++i) {
-			for (int j = 0; j < modelPrepared.data.length; ++j) {
-				if (!modelPrepared.data[j].equals("<none>")) continue;
-				modelPrepared.data[j] = modelKnown.data[i];
-			}
-		}
-	}
-	
-	private int countNone() {
-		int cnt = 0;
-		for (Object o : modelPrepared.data) {
-			if (o.equals("<none>")) ++cnt;
-		}
-		return cnt;
-	}
 }

+ 284 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectSpellsPanel.java

@@ -0,0 +1,284 @@
+package org.leumasjaffe.charsheet.view.magic;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.util.AbilityHelper;
+import org.leumasjaffe.event.SelectTableRowPopupMenuListener;
+import org.leumasjaffe.format.StringHelper;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+import java.awt.GridBagLayout;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+import javax.swing.border.BevelBorder;
+import javax.swing.border.SoftBevelBorder;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class SelectSpellsPanel extends JPanel {
+	
+	private static final String NONE = "<none>";
+
+	@AllArgsConstructor
+	private static class SelectSpellModel extends AbstractTableModel {
+		/**
+		 * 
+		 */
+		private static final long serialVersionUID = 1L;
+		
+		final Object[] data;
+
+		@Override
+		public int getRowCount() {
+			return data == null ? 0 : data.length;
+		}
+
+		@Override
+		public int getColumnCount() {
+			return 1;
+		}
+
+		@Override
+		public Object getValueAt(int rowIndex, int columnIndex) {
+			if (columnIndex != 0) { throw new IllegalArgumentException("There is only 1 column"); }
+			return data[rowIndex];
+		}
+		
+		@Override
+		public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+			if (columnIndex != 0) { throw new IllegalArgumentException("There is only 1 column"); }
+			data[rowIndex] = aValue;
+		}
+	}
+
+	@AllArgsConstructor
+	@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
+	public static class Info {
+		DDCharacter chara;
+		DDCharacterClass dclass;
+	}
+	
+	public static final String READY = "Is Filled Out";
+	
+	@Getter Collection<DDSpell> prepared;
+	boolean allowsDuplicates;
+	IntValue sharedValue;
+	SelectSpellModel modelPrepared, modelKnown;
+	
+	public SelectSpellsPanel(Info info, BoolGate.Handle gate, int level, Collection<DDSpell> prepared, int toPrepare,
+			Collection<DDSpell> avail, boolean allowsDuplicates, IntValue sharedValue) {
+		this.allowsDuplicates = allowsDuplicates;
+		this.sharedValue = sharedValue;
+		final DDSpellbook spellBook = info.dclass.getSpellBook().get();
+		this.prepared = new ArrayList<>(prepared);
+		final List<DDSpell> known = new ArrayList<>(avail);
+		this.modelPrepared = new SelectSpellModel(createPrepareModel(prepared, toPrepare));
+		this.modelKnown = new SelectSpellModel(known.stream().map(DDSpell::getName).toArray());
+		putClientProperty(READY, countNone() == 0);
+		sharedValue.value(sharedValue.value() - this.modelPrepared.data.length + countNone());
+		
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 40, 0, 0};
+		gridBagLayout.rowHeights = new int[]{20, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, 0.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panel = new ChooseSpellsPerDayHeader(level, spellBook, AbilityHelper.get(info.chara, info.dclass));
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.gridwidth = 3;
+		gbc_panel.insets = new Insets(0, 0, 5, 5);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		
+		JScrollPane scrollPane_1 = new JScrollPane();
+		scrollPane_1.setPreferredSize(new Dimension(175, 200));
+		scrollPane_1.setViewportBorder(new SoftBevelBorder(BevelBorder.LOWERED, null, null, null, null));
+		GridBagConstraints gbc_scrollPane_1 = new GridBagConstraints();
+		gbc_scrollPane_1.insets = new Insets(0, 0, 0, 5);
+		gbc_scrollPane_1.fill = GridBagConstraints.BOTH;
+		gbc_scrollPane_1.gridx = 0;
+		gbc_scrollPane_1.gridy = 1;
+		add(scrollPane_1, gbc_scrollPane_1);
+		
+		JTable tablePrepared = new JTable(modelPrepared);
+		tablePrepared.setTableHeader(null);
+		scrollPane_1.setViewportView(tablePrepared);
+		tablePrepared.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+
+		JPanel panelDivider = new JPanel();
+		GridBagConstraints gbc_panelDivider = new GridBagConstraints();
+		gbc_panelDivider.insets = new Insets(0, 0, 0, 5);
+		gbc_panelDivider.fill = GridBagConstraints.BOTH;
+		gbc_panelDivider.gridx = 1;
+		gbc_panelDivider.gridy = 1;
+		add(panelDivider, gbc_panelDivider);
+		GridBagLayout gbl_panelDivider = new GridBagLayout();
+		gbl_panelDivider.columnWidths = new int[]{0, 0};
+		gbl_panelDivider.rowHeights = new int[]{0, 0, 0, 0, 0};
+		gbl_panelDivider.columnWeights = new double[]{0.0, Double.MIN_VALUE};
+		gbl_panelDivider.rowWeights = new double[]{1.0, 0.0, 0.0, 1.0, Double.MIN_VALUE};
+		panelDivider.setLayout(gbl_panelDivider);		
+		
+		JScrollPane scrollPane = new JScrollPane();
+		scrollPane.setPreferredSize(new Dimension(175, 200));
+		scrollPane.setViewportBorder(new SoftBevelBorder(BevelBorder.LOWERED, null, null, null, null));
+		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
+		gbc_scrollPane.insets = new Insets(0, 0, 0, 5);
+		gbc_scrollPane.fill = GridBagConstraints.BOTH;
+		gbc_scrollPane.gridx = 2;
+		gbc_scrollPane.gridy = 1;
+		add(scrollPane, gbc_scrollPane);
+		
+		JTable tableKnown = new JTable(modelKnown);
+		tableKnown.setTableHeader(null);
+		scrollPane.setViewportView(tableKnown);
+		tableKnown.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+		
+		JPopupMenu popup = new JPopupMenu();
+		popup.addPopupMenuListener(new SelectTableRowPopupMenuListener(popup, tableKnown));
+		JMenuItem mntmInfo = new JMenuItem("Info");
+		mntmInfo.addActionListener( e -> {
+			DDSpell spell = known.get(tableKnown.getSelectedRow());
+			JFrame frame = new JFrame(spell.getName() +  " (" + info.dclass.getName() + " " + level + ")");
+			frame.add(new SpellInfoPanel(info.chara, info.dclass, spell));
+			frame.pack();
+			frame.setVisible(true);
+		});
+		popup.add(mntmInfo);
+		tableKnown.setComponentPopupMenu(popup);
+		
+		JButton button = new JButton(">>");
+		button.setMargin(new Insets(2, 8, 2, 8));
+		GridBagConstraints gbc_button = new GridBagConstraints();
+		gbc_button.insets = new Insets(0, 0, 5, 0);
+		gbc_button.gridx = 0;
+		gbc_button.gridy = 1;
+		panelDivider.add(button, gbc_button);
+		button.addActionListener(e -> {
+			final int row = tablePrepared.getSelectedRow();
+			if (row != -1 && !modelPrepared.data[row].equals(NONE)) {
+				sharedValue.value(sharedValue.value() + 1);
+				modelPrepared.setValueAt(NONE, row, 0);
+			}
+			tablePrepared.getSelectionModel().clearSelection();
+			tablePrepared.repaint();
+			if ((Boolean) getClientProperty(READY)) {
+				putClientProperty(READY, false);
+			}
+		});
+		
+		JButton button_1 = new JButton("<<");
+		button_1.setMargin(new Insets(2, 8, 2, 8));
+		GridBagConstraints gbc_button_1 = new GridBagConstraints();
+		gbc_button_1.insets = new Insets(0, 0, 5, 0);
+		gbc_button_1.gridx = 0;
+		gbc_button_1.gridy = 2;
+		panelDivider.add(button_1, gbc_button_1);
+		
+		button_1.addActionListener(e -> {
+			final int[] rows = tableKnown.getSelectedRows();
+			final int[] orows = tablePrepared.getSelectedRows();
+			if (sharedValue.value() == 0) {
+				JOptionPane.showMessageDialog(this, "You have exceeded the shared limit on new spells", 
+						"Error", JOptionPane.ERROR_MESSAGE);
+			} else if (orows.length >= rows.length) {
+				for (int i = 0; i < rows.length; ++i) {
+					if (wouldHaveIllegalDuplicate(rows[i])) continue;
+					modelPrepared.data[orows[i]] = modelKnown.data[rows[i]];
+					sharedValue.value(sharedValue.value() - 1);
+				}
+			} else if (orows.length == 0 && countNone() >= rows.length) {
+				replace(rows);
+			} else {
+				final String message = StringHelper.format(
+						"Unable to assign new spells, more spells were selected ({}) than were avaliable ({})", 
+						rows.length, orows.length == 0 ? countNone() : orows.length);
+				JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE);
+			}
+			tableKnown.getSelectionModel().clearSelection();
+			tablePrepared.getSelectionModel().clearSelection();
+			tablePrepared.repaint();
+			
+			if (!gate.get() && !Arrays.asList(modelPrepared.data).contains(NONE)) {
+				this.prepared.clear();
+				for (Object o : modelPrepared.data) {
+					this.prepared.add(DDSpell.fromString((String) o)); // TODO
+				}
+				gate.set(true);
+			}
+		});
+	}
+
+	public SelectSpellsPanel(Info info, BoolGate.Handle gate, int level,
+			Collection<DDSpell> prepared, Collection<DDSpell> avail) {
+		this(info, gate, level, prepared, 0, avail, true, new IntValue(-1));
+	}
+
+	private boolean wouldHaveIllegalDuplicate(int row) {
+		return !this.allowsDuplicates && Arrays.asList(modelPrepared.data).contains(modelKnown.data[row]);
+	}
+
+	private String[] createPrepareModel(Collection<DDSpell> prepared, int toPrepare) {
+		if (toPrepare <= prepared.size()) {
+			return prepared.stream().map(DDSpell::getName).toArray(String[]::new);
+		} else {
+			String[] data = new String[toPrepare];
+			int i = 0;
+			for (DDSpell sp : prepared) {
+				data[i++] = sp.getName();
+			}
+			for (; i < toPrepare; ++i) {
+				data[i] = NONE;
+			}
+			return data;
+		}
+	}
+
+	private void replace(int[] rows) {
+		for (int i = 0; i < rows.length; ++i) {
+			if (wouldHaveIllegalDuplicate(i)) continue;
+			for (int j = 0; j < modelPrepared.data.length; ++j) {
+				if (!modelPrepared.data[j].equals(NONE)) continue;
+				modelPrepared.data[j] = modelKnown.data[rows[i]];
+				sharedValue.value(sharedValue.value() - 1);
+				break;
+			}
+		}
+	}
+	
+	private int countNone() {
+		int cnt = 0;
+		for (Object o : modelPrepared.data) {
+			if (o.equals(NONE)) ++cnt;
+		}
+		return cnt;
+	}
+}

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

@@ -41,7 +41,7 @@ class SpellMenu extends JPopupMenu {
 					return;
 				}
 				book.castSpell(spellLevel.level, spell);
-				ObserverDispatch.notifySubscribers(book, null);
+				ObserverDispatch.notifySubscribers(book);
 			});
 			add(mntmCast);
 		}

+ 13 - 103
src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpDialog.java

@@ -1,128 +1,38 @@
 package org.leumasjaffe.charsheet.view.skills;
 
-import javax.swing.JPanel;
-
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
-import java.awt.GridBagLayout;
-import javax.swing.JScrollPane;
-import javax.swing.JTextField;
 
-import org.jdesktop.swingx.VerticalLayout;
-import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 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;
-import java.util.ArrayList;
-import java.util.List;
 
-import javax.swing.JLabel;
 import javax.swing.JButton;
 import javax.swing.JDialog;
-import java.awt.Dimension;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class SkillLevelUpDialog extends JPanel {
-
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
-	private ObservableListener<JTextField, IntValue> purchaseListener;
+public class SkillLevelUpDialog extends SkillLevelUpPanel {
+	JButton btnSubmitSkillChange;
 	
 	public SkillLevelUpDialog(final DDCharacter chara, final DDCharacterClass cclass) {
-		final IntValue pointsAvaliable = new IntValue(Math.max(1, cclass.getSkillPoints() + 
-				Ability.modifier(chara.getAbilities().getInt().baseModifier())));
-		
-		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[]{0.0, 1.0, Double.MIN_VALUE};
-		setLayout(gridBagLayout);
-		
-		JPanel panel = new JPanel();
-		GridBagConstraints gbc_panel = new GridBagConstraints();
-		gbc_panel.insets = new Insets(0, 0, 5, 0);
-		gbc_panel.fill = GridBagConstraints.HORIZONTAL;
-		gbc_panel.gridx = 0;
-		gbc_panel.gridy = 0;
-		add(panel, gbc_panel);
-		GridBagLayout gbl_panel = new GridBagLayout();
-		gbl_panel.columnWidths = new int[]{0, 0, 0, 0};
-		gbl_panel.rowHeights = new int[]{0, 0};
-		gbl_panel.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
-		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
-		panel.setLayout(gbl_panel);
-		
-		JLabel lblPointsRemaining = new JLabel("Points Remaining:");
-		GridBagConstraints gbc_lblPointsRemaining = new GridBagConstraints();
-		gbc_lblPointsRemaining.insets = new Insets(0, 0, 0, 5);
-		gbc_lblPointsRemaining.anchor = GridBagConstraints.EAST;
-		gbc_lblPointsRemaining.gridx = 0;
-		gbc_lblPointsRemaining.gridy = 0;
-		panel.add(lblPointsRemaining, gbc_lblPointsRemaining);
-		
-		JTextField pointsRemaining = new JTextField();
-		pointsRemaining.setEditable(false);
-		GridBagConstraints gbc_pointsRemaining = new GridBagConstraints();
-		gbc_pointsRemaining.insets = new Insets(0, 0, 0, 5);
-		gbc_pointsRemaining.fill = GridBagConstraints.HORIZONTAL;
-		gbc_pointsRemaining.gridx = 1;
-		gbc_pointsRemaining.gridy = 0;
-		panel.add(pointsRemaining, gbc_pointsRemaining);
-		pointsRemaining.setColumns(10);
-		
-		JScrollPane scrollPane = new JScrollPane();
-		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
-		gbc_scrollPane.fill = GridBagConstraints.BOTH;
-		gbc_scrollPane.gridx = 0;
-		gbc_scrollPane.gridy = 1;
-		add(scrollPane, gbc_scrollPane);
-		
-		JPanel skillPanel = new JPanel();
-		skillPanel.setMinimumSize(new Dimension(300, 200));
-		scrollPane.setViewportView(skillPanel);
-		skillPanel.setLayout(new VerticalLayout());
-		
-		JButton btnSubmitSkillChange = new JButton("Submit Skill Change");
+		super(chara, cclass);
+		btnSubmitSkillChange = new JButton("Submit Skill Change");
 		btnSubmitSkillChange.setEnabled(false);
-		GridBagConstraints gbc_btnSubmitSkillChange = new GridBagConstraints();
-		gbc_btnSubmitSkillChange.gridx = 2;
-		gbc_btnSubmitSkillChange.gridy = 0;
-		panel.add(btnSubmitSkillChange, gbc_btnSubmitSkillChange);
-		
-		final List<SkillLevelUpLine> lines = new ArrayList<>();
-		final DDSkills skills = chara.getSkills();
-		skills.getSkills().stream().forEach( skill -> {
-			SkillLevelUpLine line = new SkillLevelUpLine(chara, cclass, skill, pointsAvaliable);
-			skillPanel.add(line);
-			lines.add(line);
-		});
-		
-		purchaseListener = new ObservableListener<>(pointsRemaining, (c, v) -> {
-			btnSubmitSkillChange.setEnabled(v.value() == 0);
-			c.setText(Integer.toString(v.value()));
-		});
-		purchaseListener.setObserved(pointsAvaliable);
-		
 		btnSubmitSkillChange.addActionListener(e -> {
+			commitAllChanges();
 			((JDialog) this.getParent().getParent().getParent()).dispose();
-			lines.stream().forEach(l -> {
-				l.applyChange();
-			});
 		});
+		GridBagConstraints gbc_btnSubmitSkillChange = new GridBagConstraints();
+		gbc_btnSubmitSkillChange.gridx = 2;
+		gbc_btnSubmitSkillChange.gridy = 0;
+		getPanel().add(btnSubmitSkillChange, gbc_btnSubmitSkillChange);
 	}
 	
 	@Override
-	public void removeNotify() {
-		super.removeNotify();
-		ObserverDispatch.unsubscribeAll(purchaseListener);
+	protected void setIsReady(boolean b) {
+		if (btnSubmitSkillChange == null) return; // Because of the ObservableListener's subscription behavior.
+		btnSubmitSkillChange.setEnabled(b);
 	}
 }

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

@@ -59,11 +59,11 @@ class SkillLevelUpLine extends JPanel {
 		final int maxPoints = (chara.getLevel() + 3) / pointsPerRank - skill.getRanks().value();
 		
 		setBorder(new MatteBorder(0, 0, 1, 0, (Color) new Color(0, 0, 0)));
-		setPreferredSize(new Dimension(475, 22));
+		setPreferredSize(new Dimension(480, 22));
 		GridBagLayout gridBagLayout = new GridBagLayout();
-		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
-		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		
@@ -194,6 +194,7 @@ class SkillLevelUpLine extends JPanel {
 		points.setText("0");
 		points.setEditable(false);
 		GridBagConstraints gbc_points = new GridBagConstraints();
+		gbc_points.insets = new Insets(0, 0, 0, 5);
 		gbc_points.gridx = 11;
 		gbc_points.gridy = 0;
 		add(points, gbc_points);
@@ -203,8 +204,8 @@ class SkillLevelUpLine extends JPanel {
 			pointsAvaliable.value(pointsAvaliable.value() - (value * pointsPerRank));
 			current.value(current.value() + value);
 			points.setText(Integer.toString(current.value()));
-			ObserverDispatch.notifySubscribers(pointsAvaliable, this);
-			ObserverDispatch.notifySubscribers(current, this);
+			ObserverDispatch.notifySubscribers(pointsAvaliable); // TODO (this)?
+			ObserverDispatch.notifySubscribers(current); // TODO (this)?
 			return null;
 		};
 		
@@ -221,6 +222,12 @@ class SkillLevelUpLine extends JPanel {
 					final int mod = p.ability.map(v -> v.baseModifier()).orElse(0);
 					c.setText(StringHelper.toString(skillRanks + mod + p.points.value()));
 				});
+		
+		Component horizontalStrut_1 = Box.createHorizontalStrut(5);
+		GridBagConstraints gbc_horizontalStrut_1 = new GridBagConstraints();
+		gbc_horizontalStrut_1.gridx = 12;
+		gbc_horizontalStrut_1.gridy = 0;
+		add(horizontalStrut_1, gbc_horizontalStrut_1);
 		final Optional<Ability.Scores> ability = getAbility(chara, skill);
 		ability.ifPresent(v -> modifier.setText(StringHelper.toString(v.baseModifier())));
 		totalListener.setObserved(new TotalPacket(ability, skill, current), current);
@@ -231,9 +238,9 @@ class SkillLevelUpLine extends JPanel {
 		else { return Optional.of(AbilityHelper.get(chara, skill)); }
 	}
 
-	void applyChange() {
+	public void applyChange() {
 		skill.spendPoints(current.value(), !isClassSkill);
-		ObserverDispatch.notifySubscribers(skill.getRanks(), this);
+		ObserverDispatch.notifySubscribers(skill.getRanks()); // TODO (this)?
 	}
 	
 	@Override

+ 115 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpPanel.java

@@ -0,0 +1,115 @@
+package org.leumasjaffe.charsheet.view.skills;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+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 lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public abstract class SkillLevelUpPanel extends JPanel {
+	ObservableListener<JTextField, IntValue> purchaseListener;
+	@Getter(AccessLevel.PROTECTED) JPanel panel;
+	List<SkillLevelUpLine> lines;
+
+	public SkillLevelUpPanel(final DDCharacter chara, final DDCharacterClass cclass) {
+		final IntValue pointsAvaliable = new IntValue(Math.max(1, cclass.getSkillPoints() + 
+				chara.getAbilities().getInt().baseModifier()));
+		
+		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[]{0.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.insets = new Insets(0, 0, 5, 0);
+		gbc_panel.fill = GridBagConstraints.HORIZONTAL;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblPointsRemaining = new JLabel("Points Remaining:");
+		GridBagConstraints gbc_lblPointsRemaining = new GridBagConstraints();
+		gbc_lblPointsRemaining.insets = new Insets(0, 0, 0, 5);
+		gbc_lblPointsRemaining.anchor = GridBagConstraints.EAST;
+		gbc_lblPointsRemaining.gridx = 0;
+		gbc_lblPointsRemaining.gridy = 0;
+		panel.add(lblPointsRemaining, gbc_lblPointsRemaining);
+		
+		JTextField pointsRemaining = new JTextField();
+		pointsRemaining.setEditable(false);
+		GridBagConstraints gbc_pointsRemaining = new GridBagConstraints();
+		gbc_pointsRemaining.insets = new Insets(0, 0, 0, 5);
+		gbc_pointsRemaining.fill = GridBagConstraints.HORIZONTAL;
+		gbc_pointsRemaining.gridx = 1;
+		gbc_pointsRemaining.gridy = 0;
+		panel.add(pointsRemaining, gbc_pointsRemaining);
+		pointsRemaining.setColumns(10);
+		
+		JScrollPane scrollPane = new JScrollPane();
+		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
+		gbc_scrollPane.fill = GridBagConstraints.BOTH;
+		gbc_scrollPane.gridx = 0;
+		gbc_scrollPane.gridy = 1;
+		add(scrollPane, gbc_scrollPane);
+		
+		JPanel skillPanel = new JPanel();
+		skillPanel.setMinimumSize(new Dimension(300, 200));
+		scrollPane.setViewportView(skillPanel);
+		skillPanel.setLayout(new VerticalLayout());
+				
+		lines = new ArrayList<>();
+		final DDSkills skills = chara.getSkills();
+		skills.getSkills().stream().forEach( skill -> {
+			SkillLevelUpLine line = new SkillLevelUpLine(chara, cclass, skill, pointsAvaliable);
+			skillPanel.add(line);
+			lines.add(line);
+		});
+		
+		purchaseListener = new ObservableListener<>(pointsRemaining, (c, v) -> {
+			setIsReady(v.value() == 0);
+			c.setText(Integer.toString(v.value()));
+		});
+		purchaseListener.setObserved(pointsAvaliable);
+	}
+	
+	protected abstract void setIsReady(boolean b);
+	
+	public void commitAllChanges() {
+		lines.forEach(SkillLevelUpLine::applyChange);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(purchaseListener);
+	}
+}

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

@@ -28,12 +28,9 @@ import lombok.experimental.FieldDefaults;
 
 import javax.swing.SwingConstants;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class AbilityBox extends JPanel {
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
 	ObservableListener<JTextField, IntValue> valueListener;
 	ObservableListener<JTextField, IntValue> modListener;
 	

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

@@ -17,12 +17,9 @@ import java.awt.GridBagConstraints;
 import java.awt.Dimension;
 import javax.swing.SwingConstants;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class AbilityLine extends JPanel {
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
 	Function<Ability, Ability.Scores> access;
 	AbilityBox ability;
 	AbilityBox temporary;

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

@@ -15,12 +15,9 @@ import org.leumasjaffe.charsheet.model.Ability;
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class AbilityPanel extends JPanel {
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
 	private static final String[] abils = new String[] { "STR", "DEX", "CON", "INT", "WIS", "CHA" };
 	private static final List<Function<Ability, Ability.Scores>> funcs = Arrays.asList( 
 		Ability::getStr, Ability::getDex, Ability::getCon,

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

@@ -95,7 +95,7 @@ public class ArmorLine extends JPanel {
 		total.setColumns(3);
 		total.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_total = new GridBagConstraints();
-		gbc_total.insets = new Insets(0, 0, 0, 4);
+		gbc_total.insets = new Insets(0, 0, 0, 3);
 		gbc_total.fill = GridBagConstraints.HORIZONTAL;
 		gbc_total.gridx = 1;
 		gbc_total.gridy = 0;
@@ -103,7 +103,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label = new JLabel("=");
 		GridBagConstraints gbc_label = new GridBagConstraints();
-		gbc_label.insets = new Insets(0, 0, 0, 4);
+		gbc_label.insets = new Insets(0, 0, 0, 3);
 		gbc_label.gridx = 2;
 		gbc_label.gridy = 0;
 		panel.add(label, gbc_label);
@@ -111,7 +111,7 @@ public class ArmorLine extends JPanel {
 		JLabel label_7 = new JLabel("10+");
 		label_7.setFont(new Font("Tahoma", Font.BOLD, 14));
 		GridBagConstraints gbc_label_7 = new GridBagConstraints();
-		gbc_label_7.insets = new Insets(0, 0, 0, 4);
+		gbc_label_7.insets = new Insets(0, 0, 0, 3);
 		gbc_label_7.anchor = GridBagConstraints.EAST;
 		gbc_label_7.gridx = 3;
 		gbc_label_7.gridy = 0;
@@ -127,7 +127,7 @@ public class ArmorLine extends JPanel {
 		armor.setColumns(3);
 		armor.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_armor = new GridBagConstraints();
-		gbc_armor.insets = new Insets(0, 0, 0, 4);
+		gbc_armor.insets = new Insets(0, 0, 0, 3);
 		gbc_armor.fill = GridBagConstraints.HORIZONTAL;
 		gbc_armor.gridx = 4;
 		gbc_armor.gridy = 0;
@@ -135,7 +135,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label_1 = new JLabel("+");
 		GridBagConstraints gbc_label_1 = new GridBagConstraints();
-		gbc_label_1.insets = new Insets(0, 0, 0, 4);
+		gbc_label_1.insets = new Insets(0, 0, 0, 3);
 		gbc_label_1.anchor = GridBagConstraints.EAST;
 		gbc_label_1.gridx = 5;
 		gbc_label_1.gridy = 0;
@@ -151,7 +151,7 @@ public class ArmorLine extends JPanel {
 		shield.setColumns(3);
 		shield.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_shield = new GridBagConstraints();
-		gbc_shield.insets = new Insets(0, 0, 0, 4);
+		gbc_shield.insets = new Insets(0, 0, 0, 3);
 		gbc_shield.fill = GridBagConstraints.HORIZONTAL;
 		gbc_shield.gridx = 6;
 		gbc_shield.gridy = 0;
@@ -159,7 +159,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label_2 = new JLabel("+");
 		GridBagConstraints gbc_label_2 = new GridBagConstraints();
-		gbc_label_2.insets = new Insets(0, 0, 0, 4);
+		gbc_label_2.insets = new Insets(0, 0, 0, 3);
 		gbc_label_2.anchor = GridBagConstraints.EAST;
 		gbc_label_2.gridx = 7;
 		gbc_label_2.gridy = 0;
@@ -175,7 +175,7 @@ public class ArmorLine extends JPanel {
 		dexterity.setColumns(3);
 		dexterity.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_dexterity = new GridBagConstraints();
-		gbc_dexterity.insets = new Insets(0, 0, 0, 4);
+		gbc_dexterity.insets = new Insets(0, 0, 0, 3);
 		gbc_dexterity.fill = GridBagConstraints.HORIZONTAL;
 		gbc_dexterity.gridx = 8;
 		gbc_dexterity.gridy = 0;
@@ -183,7 +183,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label_3 = new JLabel("+");
 		GridBagConstraints gbc_label_3 = new GridBagConstraints();
-		gbc_label_3.insets = new Insets(0, 0, 0, 4);
+		gbc_label_3.insets = new Insets(0, 0, 0, 3);
 		gbc_label_3.anchor = GridBagConstraints.EAST;
 		gbc_label_3.gridx = 9;
 		gbc_label_3.gridy = 0;
@@ -199,7 +199,7 @@ public class ArmorLine extends JPanel {
 		size.setColumns(3);
 		size.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_size = new GridBagConstraints();
-		gbc_size.insets = new Insets(0, 0, 0, 4);
+		gbc_size.insets = new Insets(0, 0, 0, 3);
 		gbc_size.fill = GridBagConstraints.HORIZONTAL;
 		gbc_size.gridx = 10;
 		gbc_size.gridy = 0;
@@ -207,7 +207,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label_4 = new JLabel("+");
 		GridBagConstraints gbc_label_4 = new GridBagConstraints();
-		gbc_label_4.insets = new Insets(0, 0, 0, 4);
+		gbc_label_4.insets = new Insets(0, 0, 0, 3);
 		gbc_label_4.anchor = GridBagConstraints.EAST;
 		gbc_label_4.gridx = 11;
 		gbc_label_4.gridy = 0;
@@ -223,7 +223,7 @@ public class ArmorLine extends JPanel {
 		natural.setColumns(3);
 		natural.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_natural = new GridBagConstraints();
-		gbc_natural.insets = new Insets(0, 0, 0, 4);
+		gbc_natural.insets = new Insets(0, 0, 0, 3);
 		gbc_natural.fill = GridBagConstraints.HORIZONTAL;
 		gbc_natural.gridx = 12;
 		gbc_natural.gridy = 0;
@@ -231,7 +231,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label_5 = new JLabel("+");
 		GridBagConstraints gbc_label_5 = new GridBagConstraints();
-		gbc_label_5.insets = new Insets(0, 0, 0, 4);
+		gbc_label_5.insets = new Insets(0, 0, 0, 3);
 		gbc_label_5.anchor = GridBagConstraints.EAST;
 		gbc_label_5.gridx = 13;
 		gbc_label_5.gridy = 0;
@@ -247,7 +247,7 @@ public class ArmorLine extends JPanel {
 		deflection.setColumns(3);
 		deflection.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_deflection = new GridBagConstraints();
-		gbc_deflection.insets = new Insets(0, 0, 0, 4);
+		gbc_deflection.insets = new Insets(0, 0, 0, 3);
 		gbc_deflection.fill = GridBagConstraints.HORIZONTAL;
 		gbc_deflection.gridx = 14;
 		gbc_deflection.gridy = 0;
@@ -255,7 +255,7 @@ public class ArmorLine extends JPanel {
 		
 		JLabel label_6 = new JLabel("+");
 		GridBagConstraints gbc_label_6 = new GridBagConstraints();
-		gbc_label_6.insets = new Insets(0, 0, 0, 4);
+		gbc_label_6.insets = new Insets(0, 0, 0, 3);
 		gbc_label_6.anchor = GridBagConstraints.EAST;
 		gbc_label_6.gridx = 15;
 		gbc_label_6.gridy = 0;

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

@@ -242,7 +242,7 @@ public class AttackLine extends JPanel {
 		final int misc = 0;
 		this.baseAttack.setText(StringHelper.toString(bab));
 		final Ability.Scores str = model.getAbilities().getStr();
-		gTtlObserver.setObserved(model, str);
+		gTtlObserver.setObserved(model, model, str);
 		gStrObserver.setObserved(str);
 		this.grappleSize.setText(StringHelper.toString(size));
 		this.grappleMisc.setText(StringHelper.toString(misc));

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

@@ -67,7 +67,7 @@ public class ResistanceLine extends JPanel {
 		label.setBackground(Color.BLACK);
 		GridBagConstraints gbc_label = new GridBagConstraints();
 		gbc_label.fill = GridBagConstraints.HORIZONTAL;
-		gbc_label.insets = new Insets(0, 0, 0, 5);
+		gbc_label.insets = new Insets(0, 0, 0, 3);
 		gbc_label.gridx = 0;
 		gbc_label.gridy = 0;
 		add(label, gbc_label);
@@ -82,7 +82,7 @@ public class ResistanceLine extends JPanel {
 		totalField.setColumns(3);
 		totalField.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_total = new GridBagConstraints();
-		gbc_total.insets = new Insets(0, 0, 0, 5);
+		gbc_total.insets = new Insets(0, 0, 0, 3);
 		gbc_total.fill = GridBagConstraints.HORIZONTAL;
 		gbc_total.gridx = 1;
 		gbc_total.gridy = 0;
@@ -91,7 +91,7 @@ public class ResistanceLine extends JPanel {
 		JLabel label_1 = new JLabel("=");
 		GridBagConstraints gbc_label_1 = new GridBagConstraints();
 		gbc_label_1.anchor = GridBagConstraints.EAST;
-		gbc_label_1.insets = new Insets(0, 0, 0, 5);
+		gbc_label_1.insets = new Insets(0, 0, 0, 3);
 		gbc_label_1.gridx = 2;
 		gbc_label_1.gridy = 0;
 		add(label_1, gbc_label_1);
@@ -106,7 +106,7 @@ public class ResistanceLine extends JPanel {
 		baseSaveField.setColumns(3);
 		baseSaveField.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_baseSave = new GridBagConstraints();
-		gbc_baseSave.insets = new Insets(0, 0, 0, 5);
+		gbc_baseSave.insets = new Insets(0, 0, 0, 3);
 		gbc_baseSave.fill = GridBagConstraints.HORIZONTAL;
 		gbc_baseSave.gridx = 3;
 		gbc_baseSave.gridy = 0;
@@ -114,7 +114,7 @@ public class ResistanceLine extends JPanel {
 		
 		JLabel label_2 = new JLabel("+");
 		GridBagConstraints gbc_label_2 = new GridBagConstraints();
-		gbc_label_2.insets = new Insets(0, 0, 0, 5);
+		gbc_label_2.insets = new Insets(0, 0, 0, 3);
 		gbc_label_2.anchor = GridBagConstraints.EAST;
 		gbc_label_2.gridx = 4;
 		gbc_label_2.gridy = 0;
@@ -130,7 +130,7 @@ public class ResistanceLine extends JPanel {
 		abilityField.setColumns(3);
 		abilityField.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_ability = new GridBagConstraints();
-		gbc_ability.insets = new Insets(0, 0, 0, 5);
+		gbc_ability.insets = new Insets(0, 0, 0, 3);
 		gbc_ability.fill = GridBagConstraints.HORIZONTAL;
 		gbc_ability.gridx = 5;
 		gbc_ability.gridy = 0;
@@ -139,7 +139,7 @@ public class ResistanceLine extends JPanel {
 		JLabel label_3 = new JLabel("+");
 		GridBagConstraints gbc_label_3 = new GridBagConstraints();
 		gbc_label_3.anchor = GridBagConstraints.EAST;
-		gbc_label_3.insets = new Insets(0, 0, 0, 5);
+		gbc_label_3.insets = new Insets(0, 0, 0, 3);
 		gbc_label_3.gridx = 6;
 		gbc_label_3.gridy = 0;
 		add(label_3, gbc_label_3);
@@ -153,7 +153,7 @@ public class ResistanceLine extends JPanel {
 		magicField.setColumns(3);
 		magicField.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_magic = new GridBagConstraints();
-		gbc_magic.insets = new Insets(0, 0, 0, 5);
+		gbc_magic.insets = new Insets(0, 0, 0, 3);
 		gbc_magic.fill = GridBagConstraints.HORIZONTAL;
 		gbc_magic.gridx = 7;
 		gbc_magic.gridy = 0;
@@ -162,7 +162,7 @@ public class ResistanceLine extends JPanel {
 		JLabel label_4 = new JLabel("+");
 		GridBagConstraints gbc_label_4 = new GridBagConstraints();
 		gbc_label_4.anchor = GridBagConstraints.EAST;
-		gbc_label_4.insets = new Insets(0, 0, 0, 5);
+		gbc_label_4.insets = new Insets(0, 0, 0, 3);
 		gbc_label_4.gridx = 8;
 		gbc_label_4.gridy = 0;
 		add(label_4, gbc_label_4);
@@ -176,7 +176,7 @@ public class ResistanceLine extends JPanel {
 		miscField.setColumns(3);
 		miscField.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_misc = new GridBagConstraints();
-		gbc_misc.insets = new Insets(0, 0, 0, 5);
+		gbc_misc.insets = new Insets(0, 0, 0, 3);
 		gbc_misc.fill = GridBagConstraints.HORIZONTAL;
 		gbc_misc.gridx = 9;
 		gbc_misc.gridy = 0;
@@ -184,7 +184,7 @@ public class ResistanceLine extends JPanel {
 		
 		JLabel label_5 = new JLabel("+");
 		GridBagConstraints gbc_label_5 = new GridBagConstraints();
-		gbc_label_5.insets = new Insets(0, 0, 0, 5);
+		gbc_label_5.insets = new Insets(0, 0, 0, 3);
 		gbc_label_5.anchor = GridBagConstraints.EAST;
 		gbc_label_5.gridx = 10;
 		gbc_label_5.gridy = 0;
@@ -214,7 +214,7 @@ public class ResistanceLine extends JPanel {
 		tempField.setColumns(3);
 		tempField.setBorder(new LineBorder(Color.BLACK));
 		GridBagConstraints gbc_temp = new GridBagConstraints();
-		gbc_temp.insets = new Insets(3, 5, 0, 5);
+		gbc_temp.insets = new Insets(3, 3, 0, 3);
 		gbc_temp.gridx = 0;
 		gbc_temp.gridy = 0;
 		panel.add(tempField, gbc_temp);
@@ -236,8 +236,8 @@ public class ResistanceLine extends JPanel {
 	
 	public void setModel(DDCharacter model) {
 		final Ability.Scores abil = access.apply(model.getAbilities());
-		totalObserver.setObserved(model, abil);
-		baseObserver.setObserved(model, abil);
+		totalObserver.setObserved(model, model, abil);
+		baseObserver.setObserved(model, model, abil);
 		abilObserver.setObserved(abil);
 	}
 

部分文件因文件數量過多而無法顯示