Преглед на файлове

Merge branch 'master' into feats

* master: (64 commits)
  Make level dialog so that you do not need to learn all spells before you are able to prepare spells. Instead, "<none>"s will be added for every empty slot and the system will automatically update the list when you edit the Known group.
  Remove unneeded include
  Move responsibility of fetching of available spells, tuple data, and allowsDuplicates boolean into SpellPicker
  Simplifying SelectSpellsPanel functionality by moving the 'toPrepare' minimum size rule to the only place that actually needs it.
  Remove unneeded imports
  Fixing bug in PrepareSpellsDialog closing
  Move the IntValue wrapper outside of DDSpellbook, since it doesn't belong there.
  Relocate equipitemcontroller
  Fixing a bug in spell preparation
  Start work on making Domain spells pickable on level
  Allowing Cleric Domain to be fetched in a new class construction
  Fixing bug in PrepareSpellsDialog closing
  Move the IntValue wrapper outside of DDSpellbook, since it doesn't belong there.
  Relocate equipitemcontroller
  Fixing various issues's leftover code that was not correctly merged
  Fixing a bug in spell preparation
  Fixing all compiler errors from merge
  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
  ...

# Conflicts:
#	src/main/lombok/org/leumasjaffe/charsheet/model/features/DDProperty.java
Sam Jaffe преди 8 години
родител
ревизия
32ba2ad910
променени са 100 файла, в които са добавени 3348 реда и са изтрити 740 реда
  1. 15 0
      .gitmodules
  2. 1 1
      README.md
  3. 1 0
      include/event
  4. 1 0
      include/format
  5. 1 0
      include/function
  6. 1 0
      include/graphics
  7. 1 0
      include/observer
  8. 25 0
      include/pom.xml
  9. 191 149
      pom.xml
  10. 40 51
      resources/Potato.json
  11. 9 4
      resources/classes/Bard.json
  12. 5 3
      resources/classes/Cleric.json
  13. 71 0
      resources/classes/Druid.json
  14. 37 0
      resources/classes/Wizard.json
  15. 1 0
      resources/items/item.json
  16. 119 0
      resources/spells/default.json
  17. 15 0
      resources/spells/domain/plant.json
  18. 5 0
      src/main/lombok/org/leumasjaffe/charsheet/config/Constants.java
  19. 7 7
      src/main/lombok/org/leumasjaffe/charsheet/controller/EquipItemController.java
  20. 23 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/magic/ChooseSpellTuple.java
  21. 38 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/magic/LearnSpellPicker.java
  22. 25 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/magic/PrepareDomainSpellPicker.java
  23. 29 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/magic/PrepareNewlyLearnedSpellPicker.java
  24. 31 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/magic/PrepareSpellPicker.java
  25. 14 0
      src/main/lombok/org/leumasjaffe/charsheet/controller/magic/SpellPicker.java
  26. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/Ability.java
  27. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacter.java
  28. 55 13
      src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java
  29. 28 4
      src/main/lombok/org/leumasjaffe/charsheet/model/DDClass.java
  30. 2 0
      src/main/lombok/org/leumasjaffe/charsheet/model/features/DDProperty.java
  31. 2 0
      src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/Flat.java
  32. 2 0
      src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/PerSpellLevel.java
  33. 3 1
      src/main/lombok/org/leumasjaffe/charsheet/model/features/impl/Simple.java
  34. 16 10
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDArmor.java
  35. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEnchantableItem.java
  36. 4 3
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDEquipment.java
  37. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDInventory.java
  38. 41 39
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDItem.java
  39. 51 0
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDItemFactory.java
  40. 23 19
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDWeapon.java
  41. 5 2
      src/main/lombok/org/leumasjaffe/charsheet/model/inventory/Money.java
  42. 27 5
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpell.java
  43. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellFactory.java
  44. 6 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellList.java
  45. 23 3
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellbook.java
  46. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Area.java
  47. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Duration.java
  48. 98 0
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Domain.java
  49. 17 4
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Inspired.java
  50. 48 6
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Researched.java
  51. 20 4
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Spontaneous.java
  52. 91 0
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/BoolGate.java
  53. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/IntValue.java
  54. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/ObjectValue.java
  55. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/observable/StringValue.java
  56. 10 5
      src/main/lombok/org/leumasjaffe/charsheet/observer/ObserverHelper.java
  57. 3 2
      src/main/lombok/org/leumasjaffe/charsheet/observer/helper/IntValueHelper.java
  58. 3 2
      src/main/lombok/org/leumasjaffe/charsheet/observer/helper/StringValueHelper.java
  59. 7 6
      src/main/lombok/org/leumasjaffe/charsheet/view/ClassTab.java
  60. 95 39
      src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java
  61. 14 8
      src/main/lombok/org/leumasjaffe/charsheet/view/DeveloperMenu.java
  62. 25 4
      src/main/lombok/org/leumasjaffe/charsheet/view/DialogBuilder.java
  63. 77 0
      src/main/lombok/org/leumasjaffe/charsheet/view/config/ConfigEnumPanel.java
  64. 79 0
      src/main/lombok/org/leumasjaffe/charsheet/view/config/ConfigPanel.java
  65. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ArmorPanel.java
  66. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentInfoMenu.java
  67. 19 20
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentPanel.java
  68. 158 5
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/InventoryPanel.java
  69. 12 9
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemInfoMenu.java
  70. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ItemPanel.java
  71. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/LoadoutMenu.java
  72. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/ShieldPanel.java
  73. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/inventory/WeaponPanel.java
  74. 98 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/ChooseClassLevelUpDialog.java
  75. 44 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/ExperienceDialog.java
  76. 193 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_AbilityPanel.java
  77. 108 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_FeaturesPanel.java
  78. 244 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_HitDicePanel.java
  79. 19 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpClassInfo.java
  80. 99 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpDialog.java
  81. 120 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpSpellPanel.java
  82. 236 0
      src/main/lombok/org/leumasjaffe/charsheet/view/level/UpdateClassWithLevelPanel.java
  83. 25 21
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/PrepareSpellsDialog.java
  84. 107 65
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectPreparedSpellsPanel.java
  85. 2 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellInfoPanel.java
  86. 24 6
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPanel.java
  87. 2 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPerDayPanel.java
  88. 4 2
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLine.java
  89. 8 7
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellMenu.java
  90. 16 3
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellPanel.java
  91. 5 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsKnownHeader.java
  92. 23 9
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java
  93. 13 103
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpDialog.java
  94. 14 7
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpLine.java
  95. 115 0
      src/main/lombok/org/leumasjaffe/charsheet/view/skills/SkillLevelUpPanel.java
  96. 1 4
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityBox.java
  97. 1 4
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityLine.java
  98. 1 4
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AbilityPanel.java
  99. 37 54
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/ArmorLine.java
  100. 0 0
      src/main/lombok/org/leumasjaffe/charsheet/view/summary/AttackLine.java

+ 15 - 0
.gitmodules

@@ -0,0 +1,15 @@
+[submodule "include/event"]
+	path = include/event
+	url = http://209.6.89.209:3000/samjaffe/java-util-event.git
+[submodule "include/graphics"]
+	path = include/graphics
+	url = http://209.6.89.209:3000/samjaffe/java-util-graphics.git
+[submodule "include/observer"]
+	path = include/observer
+	url = http://209.6.89.209:3000/samjaffe/java-util-observer.git
+[submodule "include/format"]
+	path = include/format
+	url = http://209.6.89.209:3000/samjaffe/java-util-format.git
+[submodule "include/function"]
+	path = include/function
+	url = http://209.6.89.209:3000/samjaffe/java-util-function.git

+ 1 - 1
README.md

@@ -17,7 +17,7 @@ This guide assumes the following:
 #### Build
 1. Clone/Download this repository
 2. Open a command prompt in d20-charsheet
-3. Enter the command `mvn clean package`
+3. Enter the command `mvn -f include/pom.xml clean package`
 
 #### Run  
 * From the d20-charsheet directory, run the below command for your OS.  

+ 1 - 0
include/event

@@ -0,0 +1 @@
+Subproject commit 12f2d8b40fd3696f5bcae1b69666a05d5bc697ae

+ 1 - 0
include/format

@@ -0,0 +1 @@
+Subproject commit 5efe6472cf59d3036611c74b90ed1cf636b44433

+ 1 - 0
include/function

@@ -0,0 +1 @@
+Subproject commit 58275d1940a2f46f951f5623ebbfac5cff3255fb

+ 1 - 0
include/graphics

@@ -0,0 +1 @@
+Subproject commit 52e1bc2a6cae15d37641354661ed6cb4a8b16d79

+ 1 - 0
include/observer

@@ -0,0 +1 @@
+Subproject commit 69bd32cc5566eaed85883d800b814a48c94cf07e

+ 25 - 0
include/pom.xml

@@ -0,0 +1,25 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.leumasjaffe</groupId>
+  <artifactId>d20-charsheet-aggregator</artifactId>
+  <version>0.1</version>
+  <packaging>pom</packaging>
+
+  <!-- FIXME change it to the project's website -->
+  <url>http://maven.apache.org</url>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <modules>
+    <module>event</module>
+    <module>graphics</module>
+    <module>observer</module>
+    <module>format</module>
+    <module>function</module>
+    <module>..</module>
+  </modules>
+</project>

+ 191 - 149
pom.xml

@@ -1,151 +1,193 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-	<modelVersion>4.0.0</modelVersion>
-	<groupId>samjaffe</groupId>
-	<artifactId>d20-charsheet</artifactId>
-	<version>0.0.1-SNAPSHOT</version>
-	<properties>
-		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-	</properties>
-	<build>
-		<pluginManagement>
-			<plugins>
-				<plugin>
-					<groupId>org.eclipse.m2e</groupId>
-					<artifactId>lifecycle-mapping</artifactId>
-					<version>1.0.0</version>
-					<configuration>
-						<lifecycleMappingMetadata>
-							<pluginExecutions>
-								<pluginExecution>
-									<pluginExecutionFilter>
-										<groupId>org.projectlombok</groupId>
-										<artifactId>lombok-maven-plugin</artifactId>
-										<versionRange>[1,)</versionRange>
-										<goals>
-											<goal>delombok</goal>
-										</goals>
-									</pluginExecutionFilter>
-									<action>
-										<ignore />
-									</action>
-								</pluginExecution>
-							</pluginExecutions>
-						</lifecycleMappingMetadata>
-					</configuration>
-				</plugin>
-			</plugins>
-		</pluginManagement>
-		<sourceDirectory>target/generated-sources/delombok</sourceDirectory>
-		<plugins>
-			<plugin>
-				<artifactId>maven-compiler-plugin</artifactId>
-				<version>3.5.1</version>
-				<configuration>
-					<compilerVersion>1.8</compilerVersion>
-					<source>1.8</source>
-					<target>1.8</target>
-				</configuration>
-			</plugin>
-			<plugin>
-				<groupId>org.projectlombok</groupId>
-				<artifactId>lombok-maven-plugin</artifactId>
-				<version>1.16.18.0</version>
-				<executions>
-					<execution>
-						<id>delombok</id>
-						<phase>generate-sources</phase>
-						<goals>
-							<goal>delombok</goal>
-						</goals>
-					</execution>
-				</executions>
-				<configuration>
-					<addOutputDirectory>false</addOutputDirectory>
-				</configuration>
-			</plugin>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-jar-plugin</artifactId>
-				<version>2.4</version>
-				<configuration>
-					<archive>
-						<manifest>
-							<addClasspath>true</addClasspath>
-							<mainClass>org.leumasjaffe.charsheet.Test</mainClass>
-							<classpathPrefix>dependency-jars/</classpathPrefix>
-						</manifest>
-					</archive>
-				</configuration>
-			</plugin>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-dependency-plugin</artifactId>
-				<version>2.5.1</version>
-				<executions>
-					<execution>
-						<id>copy-dependencies</id>
-						<phase>package</phase>
-						<goals>
-							<goal>copy-dependencies</goal>
-						</goals>
-						<configuration>
-							<outputDirectory>
-								${project.build.directory}/dependency-jars/
-							</outputDirectory>
-						</configuration>
-					</execution>
-				</executions>
-			</plugin>
-		</plugins>
-	</build>
-	<dependencies>
-		<dependency>
-			<groupId>org.projectlombok</groupId>
-			<artifactId>lombok</artifactId>
-			<version>1.16.8</version>
-			<scope>provided</scope>
-		</dependency>
-		<dependency>
-			<groupId>com.fasterxml.jackson.core</groupId>
-			<artifactId>jackson-annotations</artifactId>
-			<version>2.7.3</version>
-		</dependency>
-		<dependency>
-			<groupId>com.fasterxml.jackson.core</groupId>
-			<artifactId>jackson-databind</artifactId>
-			<version>2.7.3</version>
-		</dependency>
-		<dependency>
-			<groupId>com.google.guava</groupId>
-			<artifactId>guava</artifactId>
-			<version>19.0</version>
-		</dependency>
-		<dependency>
-			<groupId>com.fasterxml.jackson.datatype</groupId>
-			<artifactId>jackson-datatype-jdk8</artifactId>
-			<version>2.7.3</version>
-		</dependency>
-		<dependency>
-			<groupId>org.swinglabs</groupId>
-			<artifactId>swingx</artifactId>
-			<version>1.6.1</version>
-		</dependency>
-		<dependency>
-			<groupId>junit</groupId>
-			<artifactId>junit</artifactId>
-			<version>4.12</version>
-			<scope>test</scope>
-		</dependency>
-		<dependency>
-			<groupId>org.apache.logging.log4j</groupId>
-			<artifactId>log4j-api</artifactId>
-			<version>2.8.2</version>
-		</dependency>
-		<dependency>
-			<groupId>org.apache.logging.log4j</groupId>
-			<artifactId>log4j-core</artifactId>
-			<version>2.8.2</version>
-		</dependency>
-	</dependencies>
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>d20-charsheet</artifactId>
+  <version>0.0.1-SNAPSHOT</version>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.projectlombok</groupId>
+                    <artifactId>lombok-maven-plugin</artifactId>
+                    <versionRange>[1,)</versionRange>
+                    <goals>
+                      <goal>delombok</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <sourceDirectory>target/generated-sources/delombok</sourceDirectory>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.5.1</version>
+        <configuration>
+          <compilerVersion>1.8</compilerVersion>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok-maven-plugin</artifactId>
+        <version>1.16.18.0</version>
+        <executions>
+          <execution>
+            <id>delombok</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>delombok</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <addOutputDirectory>false</addOutputDirectory>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+          <archive>
+            <manifest>
+              <addClasspath>true</addClasspath>
+              <mainClass>org.leumasjaffe.charsheet.Test</mainClass>
+              <classpathPrefix>dependency-jars/</classpathPrefix>
+            </manifest>
+          </archive>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+        </configuration>
+        <executions>
+          <execution>
+            <id>make-assembly</id> <!-- this is used for inheritance merges -->
+            <phase>package</phase> <!-- bind to the packaging phase -->
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.5.1</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>
+                ${project.build.directory}/dependency-jars/
+              </outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+      <version>1.16.8</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+      <version>2.7.3</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.7.3</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>19.0</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.datatype</groupId>
+      <artifactId>jackson-datatype-jdk8</artifactId>
+      <version>2.7.3</version>
+    </dependency>
+    <dependency>
+      <groupId>org.swinglabs</groupId>
+      <artifactId>swingx</artifactId>
+      <version>1.6.1</version>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+      <version>2.8.2</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+      <version>2.8.2</version>
+    </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>event</artifactId>
+      <version>0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>graphics</artifactId>
+      <version>0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>observer</artifactId>
+      <version>0.3</version>
+    </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>format</artifactId>
+      <version>0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>function</artifactId>
+      <version>0.1</version>
+    </dependency>
+  </dependencies>
+  <parent>
+    <groupId>org.leumasjaffe</groupId>
+    <artifactId>d20-charsheet-aggregator</artifactId>
+    <version>0.1</version>
+    <relativePath>include/pom.xml</relativePath>
+  </parent>
 </project>

+ 40 - 51
resources/Potato.json

@@ -5,45 +5,54 @@
     {
       "level": 3,
       "name": "Cleric",
-      "spellBook": {
-        "@c": ".impl.Inspired",
-        "classRef": "Cleric",
-        "spellInfo": {
-          "0": {
-            "spellsPerDay": 4,
-            "spellsPrepared": [
-              "Create Water"
-            ],
-            "spellsPreparedPreviously": [
-              "Create Water",
-              "Create Water",
-              "Create Water",
-              "Create Water"
-            ]
-          },
-          "1": {
-            "spellsPerDay": 2,
-            "spellsPrepared": [
-              "Cure Light Wounds"
-            ],
-            "spellsPreparedPreviously": [
-              "Cure Light Wounds",
-              "Cure Light Wounds"
-            ]
-          },
-          "2": {
-            "spellsPerDay": 1,
-            "spellsPrepared": [],
-            "spellsPreparedPreviously": []
+      "spellBook": [
+        {
+          "@c": ".impl.Inspired",
+          "classRef": "Cleric",
+          "spellInfo": {
+            "0": {
+              "spellsPerDay": 4,
+              "spellsPrepared": [
+                "Create Water"
+              ],
+              "spellsPreparedPreviously": [
+                "Create Water",
+                "Create Water",
+                "Create Water",
+                "Create Water"
+              ]
+            },
+            "1": {
+              "spellsPerDay": 2,
+              "spellsPrepared": [
+                "Cure Light Wounds"
+              ],
+              "spellsPreparedPreviously": [
+                "Cure Light Wounds",
+                "Cure Light Wounds"
+              ]
+            },
+            "2": {
+              "spellsPerDay": 1,
+              "spellsPrepared": [],
+              "spellsPreparedPreviously": []
+            }
           }
+        },
+        {
+          "@c": ".impl.Domain",
+          "domains": ["Plant"],
+          "spellsPrepared": ["Entangle", "Barkskin"],
+          "spellsPreparedPreviously": ["Entangle", "Barkskin"]
         }
-      }
+      ]
     },
     {
       "level": 2,
       "name": "Bard",
       "spellBook": {
         "@c": ".impl.Spontaneous",
+        "name":"Bard",
         "spellInfo": {
           "0": {
             "spellsPerDay": 3,
@@ -118,17 +127,7 @@
         "name": "Quarterstaff",
         "count": 1,
         "countEquipped": 1,
-        "value": {"pp": 0, "gp": 0, "sp": 0, "cp": 0},
-        "page": "PH116",
-        "slot": "TWO_HANDS",
-        "weight": 4,
         "weapon": {
-          "damage": "1d6",
-          "secondaryDamage": "1d6",
-          "criticalThreat": 20,
-          "criticalDamage": 2,
-          "range": "Melee",
-          "type": "Bludgeoning",
           "masterwork": true
         }
       },
@@ -136,17 +135,7 @@
         "name": "Full Plate Armor",
         "count": 1,
         "countEquipped": 1,
-        "value": {"pp": 0, "gp": 1500, "sp": 0, "cp": 0},
-        "page": "PH123",
-        "slot": "BODY",
-        "weight": 50,
         "armor": {
-          "acBonus": 8,
-          "type": "Heavy",
-          "maxDex": 1,
-          "checkPenalty": -6,
-          "spellFailure": 35,
-          "speed": 15,
           "masterwork": true,
           "bonus": "+1"
         }

+ 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\":{}},{\"@c\":\".impl.Domain\"}]",
     "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"]
+    ]
+  }
+}

+ 1 - 0
resources/items/item.json

@@ -0,0 +1 @@
+[]

Файловите разлики са ограничени, защото са твърде много
+ 119 - 0
resources/spells/default.json


+ 15 - 0
resources/spells/domain/plant.json

@@ -0,0 +1,15 @@
+{
+  "name":"Domain::Plant",
+  "powers":[],
+  "spells":[
+    "Entangle",
+    "Barkskin",
+    "Plant Growth",
+    "Command Plants",
+    "Wall of Thorns",
+    "Repel Wood",
+    "Animate Plants",
+    "Control Plants",
+    "Shambler"
+  ]
+}

+ 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
+	}
 }

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

@@ -1,4 +1,4 @@
-package org.leumasjaffe.charsheet.controller;
+package org.leumasjaffe.charsheet.controller.inventory;
 
 import static org.leumasjaffe.charsheet.model.inventory.EquipmentSlot.*;
 
@@ -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);
 		}
 	}
 
@@ -42,7 +42,7 @@ public class EquipItemController {
 
 		private boolean selectToReplaceAllOf(final EquipmentSlot base,
 				final EquipmentSlot slot1, final EquipmentSlot slot2) {
-			if (inv.get(slot1).getSlot() == base) {
+			if (inv.get(slot1).get().getSlot() == base) {
 				return selectToReplace(slot1);
 			} else if (JOptionPane.showConfirmDialog(null, createDialogTwoSlots(REPLACE_BOTH, slot1, slot2), 
 					QUERY_REPLACE, JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
@@ -78,19 +78,19 @@ public class EquipItemController {
 		}
 
 		private void doUnequip(final EquipmentSlot slot) {
-			inv.get(slot).adjustCounEquipped(-1);
+			inv.get(slot).get().adjustCounEquipped(-1);
 			inv.unequip(slot);
 		}		
 
 		private EquipItemDialog createDialogOneSlot(final EquipmentSlot slot) {
-			return new EquipItemDialog(REPLACE_ONE, slot, inv.get(slot));
+			return new EquipItemDialog(REPLACE_ONE, slot, inv.get(slot).get());
 		}
 		
 		private EquipItemDialog createDialogTwoSlots(final String message, 
 				final EquipmentSlot slot1, final EquipmentSlot slot2) {
 			return new EquipItemDialog(message,
-					new EquipItemDialog.Tuple(slot1, inv.get(slot1)),
-					new EquipItemDialog.Tuple(slot2, inv.get(slot2)));
+					new EquipItemDialog.Tuple(slot1, inv.get(slot1).get()),
+					new EquipItemDialog.Tuple(slot2, inv.get(slot2).get()));
 		}
 	}
 }

+ 23 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/magic/ChooseSpellTuple.java

@@ -0,0 +1,23 @@
+package org.leumasjaffe.charsheet.controller.magic;
+
+import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.charsheet.util.AbilityHelper;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
+public class ChooseSpellTuple {
+	DDCharacter chara;
+	DDCharacterClass dclass;
+	DDSpellbook spellBook;
+	
+	public Ability.Scores ability() {
+		return AbilityHelper.get(chara, dclass);
+	}
+}

+ 38 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/magic/LearnSpellPicker.java

@@ -0,0 +1,38 @@
+package org.leumasjaffe.charsheet.controller.magic;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.observer.Observable;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class LearnSpellPicker extends Observable.Instance implements SpellPicker {
+	@Getter ChooseSpellTuple info;
+	
+	@Override
+	public boolean allowsDuplicates() {
+		return false;
+	}
+	
+	@Override
+	public List<Integer> getSpellCounts(int level) {
+		if (level <= 0) return Collections.emptyList();
+		return info.dclass.getProto().getSpells().get().getKnown().get(level-1);
+	}
+	
+	@Override
+	public Collection<DDSpell> getAvailableSpells(int i) {
+		Collection<DDSpell> spells = new ArrayList<>(info.dclass.getProto().getSpellList(i));
+		spells.removeAll(info.spellBook.spellsKnownAtLevel(i));
+		return spells;
+	}
+}

+ 25 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/magic/PrepareDomainSpellPicker.java

@@ -0,0 +1,25 @@
+package org.leumasjaffe.charsheet.controller.magic;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class PrepareDomainSpellPicker extends PrepareSpellPicker {	
+	
+	public PrepareDomainSpellPicker(ChooseSpellTuple info) {
+		super(info);
+	}
+
+	@Override
+	public List<Integer> getSpellCounts(int level) {
+		// FIXME: This only works if there's only ever 1 bonus spell per circle
+		// FIXME: This doesn't handle the fact that domains don't have a bonus 0-th level
+		// FIXME: Secondary Spellbooks are based on feature activation...
+		// TODO: Select Cleric Domains
+		// TODO: Select Wizard Schools
+		return super.getSpellCounts(level).stream().map(i -> i != 0 ? 1 : 0).collect(Collectors.toList());
+	}
+}

+ 29 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/magic/PrepareNewlyLearnedSpellPicker.java

@@ -0,0 +1,29 @@
+package org.leumasjaffe.charsheet.controller.magic;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.IntFunction;
+
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class PrepareNewlyLearnedSpellPicker extends PrepareSpellPicker {	
+	IntFunction<Collection<DDSpell>> getNewSpells;
+	
+	public PrepareNewlyLearnedSpellPicker(ChooseSpellTuple info, IntFunction<Collection<DDSpell>> func) {
+		super(info);
+		this.getNewSpells = func;
+	}
+
+	@Override
+	public Collection<DDSpell> getAvailableSpells(int level) {
+		final Collection<DDSpell> start = new ArrayList<>(super.getAvailableSpells(level));
+		if (getNewSpells.apply(level) != null) {
+			start.addAll(getNewSpells.apply(level));
+		}
+		return start;
+	}
+}

+ 31 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/magic/PrepareSpellPicker.java

@@ -0,0 +1,31 @@
+package org.leumasjaffe.charsheet.controller.magic;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.observer.Observable;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class PrepareSpellPicker extends Observable.Instance implements SpellPicker {
+	@Getter ChooseSpellTuple info;
+	
+	@Override
+	public List<Integer> getSpellCounts(int level) {
+		// TODO: Bonus spells for high ability scores
+		if (level <= 0) return Collections.emptyList();
+		return info.dclass.getProto().getSpells().get().getPerDay().get(level-1);
+	}
+	
+	@Override
+	public Collection<DDSpell> getAvailableSpells(int level) {
+		return info.spellBook.spellsKnownAtLevel(level);
+	}
+}

+ 14 - 0
src/main/lombok/org/leumasjaffe/charsheet/controller/magic/SpellPicker.java

@@ -0,0 +1,14 @@
+package org.leumasjaffe.charsheet.controller.magic;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+import org.leumasjaffe.observer.Observable;
+
+public interface SpellPicker extends Observable {
+	ChooseSpellTuple getInfo();
+	default boolean allowsDuplicates() { return true; }
+	public List<Integer> getSpellCounts(int level);
+	public Collection<DDSpell> getAvailableSpells(int level);
+}

+ 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 = "";

+ 55 - 13
src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java

@@ -11,17 +11,22 @@ import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 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 com.fasterxml.jackson.annotation.JsonValue;
+
 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;
 		
@@ -41,7 +46,6 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 			return base.toString();
 		}
 
-		@SuppressWarnings("unused")
 		public Reference(final String name) {
 			this.base = DDClass.getFromResource(name);
 		}
@@ -51,29 +55,61 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 //	@NonNull List<Integer> healthRolls;
 	@Delegate @Getter(AccessLevel.NONE) Reference name;
 	
-	Optional<DDSpellbook> spellBook;
+	@Getter
+	public static class DDSpellbookWrapper {
+		@JsonCreator
+		public DDSpellbookWrapper(DDSpellbook main) {
+			this.main = main;
+		}
+		
+		@JsonCreator
+		public DDSpellbookWrapper(List<DDSpellbook> books) {
+			this.main = books.get(0);
+			if (books.size() > 1) {
+				this.secondary = Optional.of(books.get(1));
+				DDSpellbook.Secondary.class.cast(books.get(1)).setMainSpellbook(main);
+			}
+		}
+		
+		@JsonValue
+		private DDSpellbook[] actual() {
+			return Stream.concat(Stream.of(main), 
+					secondary.map(Stream::of).orElse(Stream.empty())).toArray(DDSpellbook[]::new);
+		}
+		
+		DDSpellbook main;
+		Optional<DDSpellbook> secondary = Optional.empty();
+	}
+	
+	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());
 	}
 	
@@ -81,18 +117,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;
@@ -9,12 +10,16 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
+import org.leumasjaffe.charsheet.model.DDCharacterClass.DDSpellbookWrapper;
 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.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<DDSpellbookWrapper> createNewSpellBook()  {
+		return spells.flatMap(sl -> sl.getSpellBookTypeName()).map(st -> createSpellbookImpl(st));
+	}
+
+	@SneakyThrows({JsonMappingException.class, JsonParseException.class, IOException.class})
+	private DDSpellbookWrapper createSpellbookImpl(String st) {
+		if (!st.startsWith("[") && !st.startsWith("{")) {
+			st = new StringFormatter("{{\"@c\":\"{}\"}}").format(st);
+		}
+		return mapper.readValue(st.getBytes(), DDSpellbookWrapper.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(final Object key);
 	void accumulate(final Tree<String, GroupedBonus> props, final Object... data);
 }

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

@@ -6,9 +6,11 @@ import org.leumasjaffe.charsheet.model.features.GroupedBonus;
 import org.leumasjaffe.collections.Tree;
 
 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

@@ -8,9 +8,11 @@ import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.collections.Tree;
 
 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 org.leumasjaffe.charsheet.model.features.GroupedBonus;
 import org.leumasjaffe.collections.Tree;
 
 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 void accumulate(Tree<String, GroupedBonus> props, Object... data) {}
 }

+ 16 - 10
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDArmor.java

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

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

@@ -6,7 +6,7 @@ import java.util.List;
 import lombok.Data;
 
 @Data
-class DDEnchantableItem {
+class DDEnchantedItem {
 	boolean isMasterwork = false;
 	Enhancement bonus = Enhancement.NONE;
 	List<Enchantment> enchantments = new ArrayList<>();

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

@@ -3,6 +3,7 @@ package org.leumasjaffe.charsheet.model.inventory;
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -17,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) {
@@ -98,8 +99,8 @@ public class DDEquipment extends Observable {
 		return Collections.unmodifiableSet(equipment.keySet());
 	}
 	
-	public DDItem get(EquipmentSlot slot) {
-		return equipment.get(slot);
+	public Optional<DDItem> get(EquipmentSlot slot) {
+		return Optional.ofNullable(equipment.get(slot));
 	}
 	
 	public boolean containsKey(EquipmentSlot slot) {

+ 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;

+ 41 - 39
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDItem.java

@@ -1,56 +1,57 @@
 package org.leumasjaffe.charsheet.model.inventory;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Optional;
 
 import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.charsheet.model.observable.StringValue;
 
-import com.fasterxml.jackson.annotation.JsonAnyGetter;
-import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.Delegate;
 import lombok.experimental.FieldDefaults;
 
-@AllArgsConstructor @NoArgsConstructor
-@Data
 @FieldDefaults(level=AccessLevel.PRIVATE)
 public class DDItem {
-	String name = "";
-	IntValue count = new IntValue(1);
-	IntValue countEquipped = new IntValue(0);
-	float weight = 0.f;
-	Money value = new Money(0, 0, 0, 0);
-	StringValue page = new StringValue();
-	EquipmentSlot slot = EquipmentSlot.NONE;
-	DDWeapon weapon = null;
-	DDArmor armor = null;
-	Map<String, Object> properties = new HashMap<>();
-	
-	public boolean isWeapon() { return weapon != null; }
-	public boolean isArmor() { return armor != null; }
-	
-	@SuppressWarnings("unchecked")
-	public <T> T getProperty(final String key) {
-		return (T) properties.get(key);
+	@Data @FieldDefaults(level=AccessLevel.PRIVATE)
+	public static class Prototype {
+		String name = "";
+		float weight = 0.f;
+		Money value = new Money(0, 0, 0, 0);
+		StringValue page = new StringValue();
+		EquipmentSlot slot = EquipmentSlot.NONE;
+		Optional<DDWeapon> weapon = Optional.empty();
+		Optional<DDArmor> armor = Optional.empty();
 	}
 	
-	@JsonAnySetter 
-	public void setProperty(final String key, final Object prop) {
-		if ( properties == null ) { properties = new HashMap<>(); }
-		properties.put(key, prop);
+	@Delegate Prototype name;
+	@Getter IntValue count = new IntValue(1);
+	@Getter IntValue countEquipped = new IntValue(0);
+
+	@JsonCreator
+	public DDItem(@JsonProperty("name") String name, 
+			@JsonProperty("count") int count, 
+			@JsonProperty("countEquipped") int countEquipped, 
+			@JsonProperty("weapon") Optional<DDEnchantedItem> weapon, 
+			@JsonProperty("armor") Optional<DDEnchantedItem> armor) {
+		this.name = DDItemFactory.loadItem(name);
+		this.count.value(count);
+		this.countEquipped.value(countEquipped);
+		weapon.ifPresent(e -> getWeapon().get().setEnchant(e));
+		armor.ifPresent(e -> getArmor().get().setEnchant(e));
 	}
-	
+		
 	public String getFullName() {
-		return (isWeapon() ? weapon.getNameModifier() : isArmor() ? armor.getNameModifier() : "") + getName(); 
+		return getWeapon().map(DDWeapon::getNameModifier).orElse(
+				getArmor().map(DDArmor::getNameModifier).orElse("")) + getName();
 	}
 	
 	public Money getActualValue() {
-		return isWeapon() ? value.sum(weapon.getActualValue()) : isArmor() ? value.sum(armor.getActualValue()) : value;
+		return getValue().sum(getWeapon().map(DDWeapon::getActualValue).orElse(
+				getArmor().map(DDArmor::getActualValue).orElse(Money.fromCopper(0))));
 	}
 	
 	public void adjustCount(int amt) {
@@ -61,13 +62,14 @@ public class DDItem {
 		this.countEquipped.value(this.countEquipped.value() + amt);
 	}
 	
-	@JsonAnyGetter 
-	private Map<String, Object> getProperties() { 
-		if ( properties == null ) { properties = new HashMap<>(); }
-		return Collections.unmodifiableMap(properties);
-	}
-	
 	public int getUnequippedCount() {
 		return count.value() - countEquipped.value();
 	}
+
+	@JsonProperty("name")
+	private String json_getName() { return getName(); }
+	@JsonProperty("armor")
+	private Optional<DDEnchantedItem> json_getArmor() { return getWeapon().map(DDWeapon::getEnchant); }
+	@JsonProperty("weapon")
+	private Optional<DDEnchantedItem> json_getWeapon() { return getArmor().map(DDArmor::getEnchant); }
 }

+ 51 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDItemFactory.java

@@ -0,0 +1,51 @@
+package org.leumasjaffe.charsheet.model.inventory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import lombok.AccessLevel;
+import lombok.SneakyThrows;
+import lombok.Synchronized;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+final class DDItemFactory {
+	Set<String> resourcesLoaded = new HashSet<>();
+	Map<String, DDItem.Prototype> itemStore = new HashMap<>();
+	
+	ObjectMapper mapper = new ObjectMapper();
+
+	
+	static {
+		mapper.registerModule(new Jdk8Module());
+		loadIfAbsent("resources/items/item.json");
+		loadIfAbsent("resources/items/armor.json");
+		loadIfAbsent("resources/items/weapon.json");
+	}
+	
+	public DDItem.Prototype loadItem(final String name) {
+		return itemStore.get(name);
+	}
+	
+	@Synchronized
+	@SneakyThrows
+	private void loadIfAbsent(final String rname) {
+		final List<DDItem.Prototype> temp = mapper.readValue(
+				new File(rname),
+				new TypeReference<ArrayList<DDItem.Prototype>>() {
+				});
+		resourcesLoaded.add(rname);
+		temp.forEach(s -> itemStore.put(s.getName(), s));
+	}
+}

+ 23 - 19
src/main/lombok/org/leumasjaffe/charsheet/model/inventory/DDWeapon.java

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

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

@@ -1,14 +1,17 @@
 package org.leumasjaffe.charsheet.model.inventory;
 
+import org.leumasjaffe.observer.Observable;
+
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.experimental.FieldDefaults;
 
 @AllArgsConstructor
-@Data
+@Data @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE)
-public class Money implements Comparable<Money> {
+public class Money extends Observable.Instance implements Comparable<Money> {
 	int pp, gp, sp, cp;
 	
 	public Money assign(final Money other) {

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

@@ -1,9 +1,10 @@
 package org.leumasjaffe.charsheet.model.magic;
 
 import java.util.EnumSet;
-import java.util.Map;
+import java.util.HashSet;
 import java.util.Optional;
 import java.util.Set;
+import java.util.SortedMap;
 
 import org.leumasjaffe.charsheet.model.magic.dimension.Area;
 import org.leumasjaffe.charsheet.model.magic.dimension.Duration;
@@ -13,8 +14,10 @@ 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;
 import lombok.Data;
 import lombok.Getter;
 import lombok.NonNull;
@@ -63,10 +66,10 @@ public class DDSpell {
 	}
 	
 	@NonNull String name;
-	@NonNull @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) Map<String, Integer> classToLevel;
+	@NonNull @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) SortedMap<String, Integer> classToLevel;
 	@NonNull School school;
 	SubSchool subSchool;
-	@NonNull Set<String> keywords;
+	@NonNull Set<String> keywords = new HashSet<>();
 	
 	@NonNull EnumSet<Component> components = EnumSet.noneOf(Component.class);
 	@NonNull DDActionType castingTime;
@@ -83,8 +86,27 @@ public class DDSpell {
 		return subSchool == null ? school.toString() : school.toString() + " (" + subSchool.toString() + ")";
 	}
 	
-	public int getClassLevel(final String clas) {
-		return classToLevel.get(clas);
+	@AllArgsConstructor
+	@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
+	public static class SpellClassInfo {
+		String subClass;
+		int level;
+		public String toString() {
+			return (subClass.isEmpty() ? "" : "[" + subClass + "] ") + level;
+		}
+	}
+	
+	public SpellClassInfo getClassLevel(final String clas) {
+		final SortedMap<String, Integer> sub = classToLevel.tailMap(clas);
+		if (sub.firstKey().equals(clas)) { return new SpellClassInfo("", sub.get(clas)); }
+		else if (sub.firstKey().startsWith(clas)) { 
+			return new SpellClassInfo(sub.firstKey().substring(clas.length() + 2), sub.get(sub.firstKey()));
+		}
+		return new SpellClassInfo("", -1);
+	}
+	
+	@JsonValue public String getName() {
+		return this.name;
 	}
 	
 	@JsonCreator public static DDSpell fromString(String str) { 

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

@@ -29,10 +29,10 @@ final class DDSpellFactory {
 	
 	static {
 		mapper.registerModule(new Jdk8Module());
-		loadIfAbsent("resources/spells/default.json");
 	}
 	
 	public DDSpell loadSpell(final String name) {
+		loadIfAbsent("resources/spells/default.json");
 		return spellStore.get(name);
 	}
 	

+ 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;

+ 23 - 3
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellbook.java

@@ -5,20 +5,32 @@ import java.util.List;
 
 import org.leumasjaffe.observer.Observable;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
 
+import lombok.Getter;
 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);
+	}
+	
+	@Getter @Setter String name;
 	@NonNull public abstract Collection<DDSpell> spellsKnownAtLevel( int level );
 	@NonNull public abstract List<DDSpell> spellsPreparedAtLevel( int level );
 	
 	@NonNull public List<DDSpell> getSpellsPreparedPreviouslyForLevel(int level) { return spellsPreparedAtLevel(level); }	
 
-
+	public boolean learnsSpells() { return false; }
 	public boolean preparesSpells() { return false; }
+	@JsonIgnore public int getSharedAllowedSlots() {
+		return -1;
+	}
 	
 	public int numSpellsKnownAtLevel( int level ) {
 		return spellsKnownAtLevel( level ).size();
@@ -32,5 +44,13 @@ public abstract class DDSpellbook extends Observable {
 	
 	public abstract void castSpell( int level, final DDSpell spell );
 	
-	public abstract void prepareSpells(int level, List<DDSpell> spells);
+	@JsonIgnore
+	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);
 }

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

@@ -64,7 +64,7 @@ public interface Area {
 			switch (emit) {
 			case NONE: return "sphere";
 			case EMANATION:	return "spherical " + emit;
-			default: return emit.toString();
+			default: return emit.toString().toLowerCase();
 			}
 		}
 	}

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

@@ -83,7 +83,7 @@ public interface Duration {
 		public String toString() {
 			final __Pair p = new __Pair(measure, length, per);
 			final StringBuilder str = new StringBuilder(StringHelper.format("{} {3}{0>1?s:} + {} {}{1>1?s:}/{?level:{} levels}",
-					p.value[0], p.value[1], p.measure, step));
+					p.value[0], p.value[1], p.measure, step == 1, step));
 			return str.toString();
 		}
 	}

+ 98 - 0
src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Domain.java

@@ -0,0 +1,98 @@
+package org.leumasjaffe.charsheet.model.magic.impl;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.SneakyThrows;
+
+@Data @EqualsAndHashCode(callSuper=true)
+public class Domain extends Prepared implements DDSpellbook.Secondary {
+	@Data @EqualsAndHashCode
+	private static class SpellBookImpl {
+		@JsonCreator @SneakyThrows
+		public static SpellBookImpl create(String name) {
+			ObjectMapper mapper = new ObjectMapper();
+			mapper.registerModule(new Jdk8Module());
+			return mapper.readValue(new File("resources/spells/domain/" + name.toLowerCase() + ".json"), 
+					new TypeReference<SpellBookImpl>() {});
+		}
+		@JsonValue private String getImplName() { return name.replaceAll(".*::", ""); }
+		String name;
+		List<Object> powers;
+		List<DDSpell> spells;
+	}
+	
+	@JsonIgnore DDSpellbook mainSpellbook;
+	Set<SpellBookImpl> domains = new LinkedHashSet<>();
+	List<DDSpell> spellsPrepared, spellsPreparedPreviously;
+	
+	@Override
+	public String getName() {
+		return mainSpellbook.getName() + " " + getSingleName();
+	}
+	
+	@Override
+	public String getSingleName() {
+		return "Domain";
+	}
+
+	@Override
+	public Collection<DDSpell> spellsKnownAtLevel(int level) {
+		if (level == 0) return Collections.emptyList();
+		return domains.stream().map(d -> d.spells.get(level-1)).collect(Collectors.toList());
+	}
+
+	@Override
+	public List<DDSpell> spellsPreparedAtLevel(int level) {
+		if (spellsPreparedPreviously.size() < level || level == 0) return Collections.emptyList();
+		final DDSpell spell = spellsPrepared.get(level-1);
+		return spell == null ? Collections.emptyList() : Collections.singletonList(spell);
+	}
+
+	@Override
+	public int numSpellsPerDayAtLevel(int level) {
+		return mainSpellbook.numSpellsKnownAtLevel(level) != 0 ? 1 : 0;
+	}
+
+	@Override
+	public void castSpell(int level, DDSpell spell) {
+		if (!Objects.equals(spellsPrepared.get(level-1), spell)) {
+			throw new IllegalArgumentException("Casting a domain spell that was not prepared");
+		}
+		spellsPrepared.set(level-1, null);
+	}
+
+	@Override
+	public void prepareSpells(int level, Collection<DDSpell> spells) {
+		if (level == 0) return;
+		if (spells.size() != 1) {
+			throw new IllegalArgumentException("Can only prepare one domain spell per domain class");
+		}
+		spellsPrepared.set(level-1, spells.iterator().next());
+		spellsPreparedPreviously.set(level-1, spells.iterator().next());
+	}
+
+	@Override
+	public List<DDSpell> getSpellsPreparedPreviouslyForLevel(int level) {
+		if (spellsPreparedPreviously.size() < level || level == 0) return Collections.emptyList();
+		return Collections.singletonList(spellsPreparedPreviously.get(level-1));
+	}
+}

+ 17 - 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,11 +34,20 @@ 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;
 	@NonNull ClassReference classRef;
 
+	@Override
+	public String getName() {
+		return classRef.ref.getName();
+	}
+	
 	@Override
 	public int numSpellsPerDayAtLevel( int level ) {
 		return get(level).spellsPerDay;
@@ -64,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
@@ -73,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();

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

@@ -1,30 +1,60 @@
 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 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 int getSharedAllowedSlots() {
+		return freeSpellsPerLevel;
+	}
+
+	@Override
+	public boolean learnsSpells() {
+		return true;
+	}
 
 	@Override
 	public int numSpellsPerDayAtLevel( int level ) {
@@ -33,7 +63,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 +88,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);

+ 20 - 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,17 @@ 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) {
+		if (spells == null) return;
 		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 = "";
 	

+ 10 - 5
src/main/lombok/org/leumasjaffe/charsheet/observer/ObserverHelper.java

@@ -9,13 +9,18 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public class ObserverHelper {
 	public void notifyObservableHierarchy(final DDCharacterClass dclass, final Object src) {
-		ObserverDispatch.notifySubscribers(dclass, src);
-		dclass.getSpellBook().ifPresent(sb -> ObserverDispatch.notifySubscribers(sb, src));
+		ObserverDispatch.notifySubscribers(dclass);
+		// FIXME
+		dclass.getSpellBook().ifPresent(wrap -> {
+			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 );

+ 7 - 6
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};
@@ -172,19 +173,19 @@ public class ClassTab extends JPanel {
 		gbc_levelBenefits.gridy = 1;
 		add(levelBenefits, gbc_levelBenefits);
 		
-		if (model.getSpellBook().isPresent()) {
+		model.getSpellBook().ifPresent(wrap -> {
 			SpellPanel spells = new SpellPanel(chara, model);
 			GridBagConstraints gbc_spells = new GridBagConstraints();
 			gbc_spells.fill = GridBagConstraints.BOTH;
 			gbc_spells.gridx = 0;
 			gbc_spells.gridy = 2;
 			add(spells, gbc_spells);
-		}
+		});
 	}
 	
 	@Override
 	public String getName() {
-		return title;
+		return model.getName();
 	}
 	
 	@Override

+ 95 - 39
src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java

@@ -1,8 +1,33 @@
 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.model.magic.DDSpellbook;
+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,31 +38,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.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();
@@ -48,7 +53,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"));
@@ -99,6 +106,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)); } );
@@ -109,27 +121,30 @@ 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
 			// Step 2: Regenerate spells prepared
 			// Step 2.1: If Spontaneous, skip (2)
 			for (DDCharacterClass dclass : model.getClasses()) {
-				dclass.getSpellBook().ifPresent(sb -> {
-					if (sb.preparesSpells()) {
-						DialogBuilder.createPrepareSpellsDialog(this, model, dclass);
-					} else {
-						for (int i = 0; i < dclass.getHighestSpellLevel(); ++i) {
-							sb.prepareSpells(i, null);
-						}
-					}
+				dclass.getSpellBook().ifPresent(wrap -> {
+					runPrepareSpells(dclass, wrap.getMain());
+					wrap.getSecondary().ifPresent(sb -> runPrepareSpells(dclass, sb));
 				});
 			}
 			// Step N: regenerate spellbooks
@@ -143,9 +158,20 @@ public class D20Sheet extends JFrame {
 		menuBar.add(developerMenu);
 		
 		// Set up post-GUI dependencies
+		listener = new ObservableListener<>(this, (c, v) -> c.reloadModel());
 		setModel(model);
 	}
 
+	private void runPrepareSpells(DDCharacterClass dclass, DDSpellbook sb) {
+		if (sb.preparesSpells()) {
+			DialogBuilder.createPrepareSpellsDialog(this, model, dclass, sb);
+		} else {
+			for (int i = 0; i < dclass.getHighestSpellLevel(); ++i) {
+				sb.prepareSpells(i, null);
+			}
+		}
+	}
+
 	public D20Sheet(final String initialFile) {
 		this();
 		loadModelResource(new File(initialFile));
@@ -159,7 +185,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 ) {
@@ -167,7 +193,7 @@ public class D20Sheet extends JFrame {
 			saveModelResource();
 		}
 	}
-
+	
 	@SneakyThrows
 	private void loadModelResource(File selectedFile) {
 		currentlyOpenFile = selectedFile;
@@ -180,16 +206,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) {

+ 25 - 4
src/main/lombok/org/leumasjaffe/charsheet/view/DialogBuilder.java

@@ -8,14 +8,19 @@ 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);
@@ -26,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) {
-		createDialogue(parent, "Prepare Spells - " + dclass.getName(), new PrepareSpellsDialog(chara, dclass));
+	public void createPrepareSpellsDialog(final JFrame parent, DDCharacter chara, DDCharacterClass dclass, DDSpellbook 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/ArmorPanel.java

@@ -26,7 +26,7 @@ public class ArmorPanel extends JPanel {
 	private static final long serialVersionUID = 1L;
 
 	public ArmorPanel(DDItem item) {
-		final DDArmor armor = item.getArmor();
+		final DDArmor armor = item.getArmor().get();
 		
 		setPreferredSize(new Dimension(280, 70));
 		GridBagLayout gridBagLayout = new GridBagLayout();
@@ -131,7 +131,7 @@ public class ArmorPanel extends JPanel {
 		panel.add(armorTypeField, gbc_armorTypeField);
 		armorTypeField.setColumns(10);
 		
-		JTextField armorBonusField = new JTextField(StringHelper.toSignedString(item.getArmor().getActualAcBonus()));
+		JTextField armorBonusField = new JTextField(StringHelper.toSignedString(armor.getActualAcBonus()));
 		armorBonusField.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_armorBonusField = new GridBagConstraints();
 		gbc_armorBonusField.insets = new Insets(0, 0, 0, 0);

+ 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);

+ 19 - 20
src/main/lombok/org/leumasjaffe/charsheet/view/inventory/EquipmentPanel.java

@@ -86,56 +86,55 @@ public class EquipmentPanel extends JPanel {
 	private void updateModel(final DDEquipment equip) {
 		final Set<EquipmentSlot> manual = EnumSet.noneOf(EquipmentSlot.class);
 		equipment.removeAll();	
-		final DDItem armor = equip.get(BODY);
-		if (armor != null && armor.isArmor()) {
+		final Optional<DDItem> armor = equip.get(BODY);
+		armor.flatMap(DDItem::getArmor).ifPresent(a -> {
 			manual.add(BODY);
-			createWithRightClickMenu(ArmorPanel::new, equip, BODY, BODY);
-		}
+			createWithRightClickMenu(ArmorPanel::new, equip, armor.get(), BODY);
+		});
 		
-		final DDItem main = equip.get(MAIN_HAND);
-		final DDItem off = equip.get(OFF_HAND);
+		final Optional<DDItem> main = equip.get(MAIN_HAND);
+		final Optional<DDItem> off = equip.get(OFF_HAND);
 	
 		final Optional<Function<DDItem, JPanel>> makeMain = getEquipmentRightClickPanelFactory(main);
 		final Optional<Function<DDItem, JPanel>> makeOff = getEquipmentRightClickPanelFactory(off);
 		
-		if (main == off && !couldDualWieldThis(main)) {
+		if (main.equals(off) && !couldDualWieldThis(main)) {
 			makeMain.ifPresent(f -> {
 				manual.add(MAIN_HAND);
 				manual.add(OFF_HAND);
-				createWithRightClickMenu(f, equip, TWO_HANDS, MAIN_HAND);
+				createWithRightClickMenu(f, equip, main.get(), TWO_HANDS);
 			});
 		} else {
 			makeMain.ifPresent(f -> {
 				manual.add(MAIN_HAND);
-				createWithRightClickMenu(f, equip, MAIN_HAND, MAIN_HAND);
+				createWithRightClickMenu(f, equip, main.get(), MAIN_HAND);
 			});
 			makeOff.ifPresent(f -> {
 				manual.add(OFF_HAND);
-				createWithRightClickMenu(f, equip, OFF_HAND, OFF_HAND);
+				createWithRightClickMenu(f, equip, off.get(), OFF_HAND);
 			});
 		}
 		
-		equip.keySet().stream().filter( slot -> equip.get(slot) != null && !manual.contains(slot) )
+		equip.keySet().stream().filter( slot -> !manual.contains(slot) )
 		.forEach( slot -> {
-			createWithRightClickMenu(null, equip, slot, slot);
+			createWithRightClickMenu(null, equip, equip.get(slot).get(), slot);
 		});
 		repaint();
 	}
 
-	private Optional<Function<DDItem, JPanel>> getEquipmentRightClickPanelFactory(final DDItem item) {
-		if (item == null) { return Optional.empty(); }
-		else if (item.isWeapon()) { return Optional.of(WeaponPanel::new); }
-		else if (item.isArmor()) { return Optional.of(ShieldPanel::new); }
+	private Optional<Function<DDItem, JPanel>> getEquipmentRightClickPanelFactory(final Optional<DDItem> item) {
+		if (!item.isPresent()) { return Optional.empty(); }
+		else if (item.get().getWeapon().isPresent()) { return Optional.of(WeaponPanel::new); }
+		else if (item.get().getArmor().isPresent()) { return Optional.of(ShieldPanel::new); }
 		else { return Optional.empty(); }
 	}
 
-	private boolean couldDualWieldThis(final DDItem item) {
-		return item != null && item.getSlot() == EquipmentSlot.ONE_HAND && item.getCount().value() > 1;
+	private boolean couldDualWieldThis(final Optional<DDItem> item) {
+		return item.map(i -> i.getSlot() == EquipmentSlot.ONE_HAND && i.getCount().value() > 1).orElse(false);
 	}
 	
 	private void createWithRightClickMenu(final Function<DDItem, JPanel> make,
-			final DDEquipment equip, final EquipmentSlot slot, EquipmentSlot get) {
-		final DDItem item = equip.get(get);
+			final DDEquipment equip, final DDItem item, final EquipmentSlot slot) {
 		final JPanel panel = make.apply(item);
 		equipment.add(panel);
 		panel.addMouseListener(new PopClickListener(

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

@@ -10,6 +10,7 @@ import javax.swing.border.MatteBorder;
 import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.inventory.DDInventory;
+import org.leumasjaffe.charsheet.model.inventory.Money;
 import org.leumasjaffe.observer.ObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 
@@ -20,6 +21,10 @@ import java.awt.Color;
 import java.awt.Font;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Dimension;
+
+import javax.swing.JTextField;
 
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class InventoryPanel extends JPanel {
@@ -29,21 +34,160 @@ public class InventoryPanel extends JPanel {
 	private static final long serialVersionUID = 1L;
 	JComponent inventory;
 	ObservableListener<InventoryPanel, DDInventory> inventoryObserver;
+	ObservableListener<JTextField, Money> moneyObserver;
+	private JTextField txtPp;
+	private JTextField txtGp;
+	private JTextField txtSp;
+	private JTextField txtCp;
 	
 	public InventoryPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0};
-		gridBagLayout.rowHeights = new int[]{0};
-		gridBagLayout.columnWeights = new double[]{Double.MIN_VALUE};
-		gridBagLayout.rowWeights = new double[]{Double.MIN_VALUE};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		
+		JPanel panel = new JPanel();
+		panel.setPreferredSize(new Dimension(280, 30));
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.insets = new Insets(0, 0, 5, 0);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{100, 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[]{1.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblMoney = new JLabel("Money");
+		lblMoney.setOpaque(true);
+		lblMoney.setHorizontalAlignment(SwingConstants.CENTER);
+		lblMoney.setForeground(Color.WHITE);
+		lblMoney.setFont(new Font("Tahoma", Font.BOLD, 20));
+		lblMoney.setBorder(new MatteBorder(0, 0, 1, 0, (Color) Color.WHITE));
+		lblMoney.setBackground(Color.BLACK);
+		GridBagConstraints gbc_lblMoney = new GridBagConstraints();
+		gbc_lblMoney.fill = GridBagConstraints.BOTH;
+		gbc_lblMoney.insets = new Insets(0, 0, 0, 1);
+		gbc_lblMoney.gridx = 0;
+		gbc_lblMoney.gridy = 0;
+		panel.add(lblMoney, gbc_lblMoney);
+		
+		JPanel panel_1 = new JPanel();
+		GridBagConstraints gbc_panel_1 = new GridBagConstraints();
+		gbc_panel_1.fill = GridBagConstraints.BOTH;
+		gbc_panel_1.insets = new Insets(0, 0, 1, 0);
+		gbc_panel_1.gridx = 1;
+		gbc_panel_1.gridy = 0;
+		panel.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};
+		gbl_panel_1.columnWeights = new double[]{1.0, 1.0, 1.0, 1.0, Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{Double.MIN_VALUE};
+		panel_1.setLayout(gbl_panel_1);
+		
+		JLabel lblPp = new JLabel("pp");
+		GridBagConstraints gbc_lblPp = new GridBagConstraints();
+		gbc_lblPp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblPp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblPp.gridx = 0;
+		gbc_lblPp.gridy = 0;
+		panel_1.add(lblPp, gbc_lblPp);
+		lblPp.setOpaque(true);
+		lblPp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblPp.setForeground(Color.WHITE);
+		lblPp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblPp.setBackground(Color.BLACK);
+		
+		JLabel lblGp = new JLabel("gp");
+		GridBagConstraints gbc_lblGp = new GridBagConstraints();
+		gbc_lblGp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblGp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblGp.gridx = 1;
+		gbc_lblGp.gridy = 0;
+		panel_1.add(lblGp, gbc_lblGp);
+		lblGp.setOpaque(true);
+		lblGp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblGp.setForeground(Color.WHITE);
+		lblGp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblGp.setBackground(Color.BLACK);
+		
+		JLabel lblSp = new JLabel("sp");
+		GridBagConstraints gbc_lblSp = new GridBagConstraints();
+		gbc_lblSp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblSp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblSp.gridx = 2;
+		gbc_lblSp.gridy = 0;
+		panel_1.add(lblSp, gbc_lblSp);
+		lblSp.setOpaque(true);
+		lblSp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblSp.setForeground(Color.WHITE);
+		lblSp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblSp.setBackground(Color.BLACK);
+		
+		JLabel lblCp = new JLabel("cp");
+		GridBagConstraints gbc_lblCp = new GridBagConstraints();
+		gbc_lblCp.insets = new Insets(0, 0, 0, 1);
+		gbc_lblCp.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblCp.gridx = 3;
+		gbc_lblCp.gridy = 0;
+		panel_1.add(lblCp, gbc_lblCp);
+		lblCp.setOpaque(true);
+		lblCp.setHorizontalAlignment(SwingConstants.CENTER);
+		lblCp.setForeground(Color.WHITE);
+		lblCp.setFont(new Font("Tahoma", Font.BOLD, 8));
+		lblCp.setBackground(Color.BLACK);
+		
+		txtPp = new JTextField();
+		GridBagConstraints gbc_txtPP = new GridBagConstraints();
+		gbc_txtPP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtPP.insets = new Insets(0, 0, 0, 0);
+		gbc_txtPP.gridx = 0;
+		gbc_txtPP.gridy = 1;
+		panel_1.add(txtPp, gbc_txtPP);
+		txtPp.setEditable(false);
+		txtPp.setColumns(5);
+		
+		txtGp = new JTextField();
+		GridBagConstraints gbc_txtGP = new GridBagConstraints();
+		gbc_txtGP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtGP.insets = new Insets(0, 0, 0, 0);
+		gbc_txtGP.gridx = 1;
+		gbc_txtGP.gridy = 1;
+		panel_1.add(txtGp, gbc_txtGP);
+		txtGp.setEditable(false);
+		txtGp.setColumns(5);
+		
+		txtSp = new JTextField();
+		GridBagConstraints gbc_txtSP = new GridBagConstraints();
+		gbc_txtSP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtSP.insets = new Insets(0, 0, 0, 0);
+		gbc_txtSP.gridx = 2;
+		gbc_txtSP.gridy = 1;
+		panel_1.add(txtSp, gbc_txtSP);
+		txtSp.setEditable(false);
+		txtSp.setColumns(5);
+		
+		txtCp = new JTextField();
+		GridBagConstraints gbc_txtCP = new GridBagConstraints();
+		gbc_txtCP.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtCP.gridx = 3;
+		gbc_txtCP.gridy = 1;
+		panel_1.add(txtCp, gbc_txtCP);
+		txtCp.setEditable(false);
+		txtCp.setColumns(5);
+		
 		JScrollPane scrollPane = new JScrollPane();
 		scrollPane.setOpaque(false);
 		GridBagConstraints gbc_scrollPane_1 = new GridBagConstraints();
 		gbc_scrollPane_1.fill = GridBagConstraints.BOTH;
 		gbc_scrollPane_1.gridx = 0;
-		gbc_scrollPane_1.gridy = 0;
+		gbc_scrollPane_1.gridy = 1;
 		add(scrollPane, gbc_scrollPane_1);
 		
 		JLabel lblInventory = new JLabel("Inventory");
@@ -60,8 +204,16 @@ public class InventoryPanel extends JPanel {
 		scrollPane.setViewportView(inventory);
 		inventory.setLayout(new VerticalLayout(5));
 		
-		inventoryObserver = new ObservableListener<>(this, 
+		inventoryObserver = new ObservableListener<>(this,
 				(self, inv) -> self.updateModel(inv) );
+		
+		moneyObserver = new ObservableListener<>(txtPp, 
+				($, money) -> {
+					txtPp.setText("" + money.getPp());
+					txtGp.setText("" + money.getGp());
+					txtSp.setText("" + money.getSp());
+					txtCp.setText("" + money.getCp());
+				});
 	}
 
 	private void updateModel(DDInventory inv) {
@@ -71,6 +223,7 @@ public class InventoryPanel extends JPanel {
 
 	public void setModel(DDCharacter model) {
 		inventoryObserver.setObserved(model.getInventory());
+		moneyObserver.setObserved(model.getInventory().getWealth());
 	}
 	
 	@Override

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

@@ -2,7 +2,7 @@ package org.leumasjaffe.charsheet.view.inventory;
 
 import javax.swing.JPopupMenu;
 
-import org.leumasjaffe.charsheet.controller.EquipItemController;
+import org.leumasjaffe.charsheet.controller.inventory.EquipItemController;
 import org.leumasjaffe.charsheet.model.inventory.DDInventory;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
 import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
@@ -28,10 +28,10 @@ class ItemInfoMenu extends JPopupMenu {
 	private static final long serialVersionUID = 1L;
 	
 	IndirectObservableListener<JMenuItem, IntValue> sellListener, equipListener = null;
-	ObservableListener<JMenuItem, DDInventory> buyListener;
+	ObservableListener<JMenuItem, Money> buyListener;
 
 	public ItemInfoMenu(final DDInventory inv, final DDItem item) {
-		final int rawValue = item.getValue().asCopper();
+		final int rawValue = item.getActualValue().asCopper();
 		final BuySellAction doTxn = new BuySellAction(inv, item);
 		final BuySellDialogHelper dlg = new BuySellDialogHelper(item.getFullName());
 
@@ -46,22 +46,24 @@ class ItemInfoMenu extends JPopupMenu {
 
 		JMenuItem mntmBuy = new JMenuItem("Purchase");
 		mntmBuy.addActionListener(e -> {
-			doTxn.applyTransaction(dlg.getNumUnits("Purchase", rawValue, inv.getWealth().asCopper() / rawValue), rawValue);
+			doTxn.applyTransaction(dlg.getNumUnits("Purchase", rawValue, rawValue == 0 ? 100 : 
+				inv.getWealth().asCopper() / rawValue), rawValue);
 		});
 		buyListener = new ObservableListener<>(mntmBuy, (c, v) -> {
-			c.setEnabled(inv.getWealth().asCopper() >= item.getValue().asCopper());
+			c.setEnabled(v.asCopper() >= item.getActualValue().asCopper());
 		});
-		buyListener.setObserved(inv);
+		buyListener.setObserved(inv.getWealth());
 		add(mntmBuy);
 
 		JMenuItem mntmSell = new JMenuItem("Sell");	
 		mntmSell.addActionListener(e -> {
-			doTxn.applyTransaction(-dlg.getNumUnits("Sell", rawValue / 2, item.getCount().value()), rawValue / 2);
+			doTxn.applyTransaction(-dlg.getNumUnits("Sell", rawValue / 2, 
+					item.getCount().value() - item.getCountEquipped().value()), rawValue / 2);
 		});
 		sellListener = new IndirectObservableListener<>(mntmSell, (c, v) -> {
 			c.setEnabled(item.getUnequippedCount() > 0);
 		});
-		sellListener.setObserved(item.getCount(), item.getCountEquipped());
+		sellListener.setObserved(item.getCount(), item.getCount(), item.getCountEquipped());
 		add(mntmSell);
 
 		if (item.getSlot() != EquipmentSlot.NONE) {
@@ -108,7 +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(item.getCount());
+			ObserverDispatch.notifySubscribers(wealth);
 		}
 	}
 }

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

@@ -152,7 +152,7 @@ public class ItemPanel extends JPanel {
 				comp.repaint();
 			}
 		});
-		countListener.setObserved(item.getCount(), item.getCountEquipped());
+		countListener.setObserved(item.getCount(), item.getCount(), item.getCountEquipped());
 		addMouseListener(new PopClickListener(new ItemInfoMenu(inv, item)));
 	}
 

+ 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);

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

@@ -26,7 +26,7 @@ public class ShieldPanel extends JPanel {
 	private static final long serialVersionUID = 1L;
 
 	public ShieldPanel(DDItem item) {
-		final DDArmor armor = item.getArmor();
+		final DDArmor armor = item.getArmor().get();
 		
 		setPreferredSize(new Dimension(280, 70));
 		GridBagLayout gridBagLayout = new GridBagLayout();

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

@@ -26,7 +26,7 @@ public class WeaponPanel extends JPanel {
 	private static final long serialVersionUID = 1L;
 
 	public WeaponPanel(final DDItem item) {
-		final DDWeapon weapon = item.getWeapon();
+		final DDWeapon weapon = item.getWeapon().get();
 		
 		setPreferredSize(new Dimension(280, 70));
 		GridBagLayout gridBagLayout = new GridBagLayout();
@@ -227,7 +227,7 @@ public class WeaponPanel extends JPanel {
 		panel_1.add(rangeField, gbc_rangeField);
 		rangeField.setColumns(10);
 		
-		JTextField typeField = new JTextField(item.getWeapon().getType().toString());
+		JTextField typeField = new JTextField(weapon.getType().toString());
 		typeField.setHorizontalAlignment(SwingConstants.CENTER);
 		typeField.setColumns(10);
 		GridBagConstraints gbc_typeField = new GridBagConstraints();

+ 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();
+	}	
+}

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

@@ -0,0 +1,120 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JPanel;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.controller.magic.ChooseSpellTuple;
+import org.leumasjaffe.charsheet.controller.magic.SpellPicker;
+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.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.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import javax.swing.JScrollPane;
+import java.awt.GridBagConstraints;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
+class LevelUpSpellPanel extends JPanel {
+	int[] ready = {0};
+	@NonFinal int spellLevelsGrown = 0;
+	int oldHighestSpellLevel, newHighestSpellLevel, toLevel;
+	SpellPicker pick;
+	ChooseSpellTuple info;
+	@Getter List<SelectSpellsPanel> panels;
+	ObservableListener<Void, BoolGate.Meta> allReady;
+
+	public LevelUpSpellPanel(SpellPicker pick, ChooseSpellTuple 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 = new IntValue(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(pick, gate.handle(i), i,
+					createArray(Math.max(spells.get(i), sharedSlots)), val);
+			panels.add(lvl);
+			panel.add(lvl);
+		}
+		allReady = gate.makeListener();
+	}
+	
+	private Collection<DDSpell> createArray(int max) {
+		return IntStream.range(0, max).<DDSpell>mapToObj(i -> null).collect(Collectors.toList());
+	}
+
+	private Map<Integer, Integer> getNewSpells(IntValue sharedSpellCountLimit) {
+		final Map<Integer, Integer> map = new TreeMap<>();
+		final List<Integer> spellsAtPreviousLevel = pick.getSpellCounts(toLevel-1);
+		final List<Integer> spellsAtCurrentLevel = pick.getSpellCounts(toLevel);
+		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 int getSharedAllowedSlots(ChooseSpellTuple info) {
+		return info.spellBook.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);
+	}
+}

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

@@ -0,0 +1,236 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import java.util.ArrayList;
+import java.util.Collections;
+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.controller.magic.ChooseSpellTuple;
+import org.leumasjaffe.charsheet.controller.magic.LearnSpellPicker;
+import org.leumasjaffe.charsheet.controller.magic.PrepareDomainSpellPicker;
+import org.leumasjaffe.charsheet.controller.magic.PrepareNewlyLearnedSpellPicker;
+import org.leumasjaffe.charsheet.controller.magic.PrepareSpellPicker;
+import org.leumasjaffe.charsheet.controller.magic.SpellPicker;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.DDCharacterClass.DDSpellbookWrapper;
+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.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,
+			PREPARE_BONUS_SPELL_INDEX = 4;
+
+	LevelUpClassInfo levelUpInfo;
+	JTabbedPane tabbedPane;
+	
+	SkillLevelUpPanel skills;
+	@NonFinal Optional<LevelUpSpellPanel> learnSpells = Optional.empty();
+	@NonFinal Optional<LevelUpSpellPanel> prepSpells = Optional.empty();
+	@NonFinal Optional<LevelUpSpellPanel> prepBonusSpells = Optional.empty();
+	
+	BoolGate readyCount = new BoolGate(5);
+	ObservableListener<Consumer<Boolean>, BoolGate> listener;
+	@NonFinal ObservableListener<SpellPicker, SpellPicker> learnAndPrepareListener = null;
+
+	LU_FeaturesPanel featurePanel;
+
+	
+	public UpdateClassWithLevelPanel(LevelUpClassInfo info, VoidVoidFunction back,
+			Consumer<Boolean> setReady) {
+		readyCount.set(LEARN_SPELL_INDEX, true);
+		readyCount.set(PREPARE_SPELL_INDEX, true);
+		readyCount.set(PREPARE_BONUS_SPELL_INDEX, true);
+		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(wrap -> initSpellTabs(info, wrap));
+
+		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);
+	}
+	
+	void initSpellTabs(LevelUpClassInfo info, DDSpellbookWrapper wrap) {
+		DDSpellbook sb = wrap.getMain();
+		readyCount.set(LEARN_SPELL_INDEX, !sb.learnsSpells());
+		readyCount.set(PREPARE_SPELL_INDEX, !sb.preparesSpells());
+		if (sb.learnsSpells()) {
+			createPanelsForLearnSpell(new ChooseSpellTuple(info.ddCharacter, info.ddClass, sb));
+		} else if (sb.preparesSpells()) {
+			createPanelForPrepareSpells(new ChooseSpellTuple(info.ddCharacter, info.ddClass, sb));
+		}
+		wrap.getSecondary().ifPresent(sec -> {
+			readyCount.set(PREPARE_BONUS_SPELL_INDEX, false);
+			createBonusSpellbookPreparePanel(new ChooseSpellTuple(info.ddCharacter, info.ddClass, sec));
+		});
+	}
+
+	private void createPanelForPrepareSpells(ChooseSpellTuple info) {
+		prepSpells = Optional.of(new LevelUpSpellPanel(new PrepareSpellPicker(info), info, readyCount.handle(PREPARE_SPELL_INDEX)));
+		tabbedPane.addTab("Prepare Spells", null, prepSpells.get(), null);
+	}
+
+	private void createPanelsForLearnSpell(ChooseSpellTuple info) {
+		SpellPicker pick = new LearnSpellPicker(info);
+		learnSpells = Optional.of(new LevelUpSpellPanel(pick, info, readyCount.handle(LEARN_SPELL_INDEX)));
+		tabbedPane.addTab("Learn Spells", null, learnSpells.get(), null);
+		if (info.spellBook.preparesSpells()) {
+			createPrepareLearnedSpellPanel(learnSpells.get(), info, pick);
+		}
+	}
+	
+	private void createBonusSpellbookPreparePanel(ChooseSpellTuple info) {
+		prepBonusSpells = Optional.of(new LevelUpSpellPanel(new PrepareDomainSpellPicker(info),
+				new ChooseSpellTuple(levelUpInfo.ddCharacter, levelUpInfo.ddClass, info.spellBook),
+				readyCount.handle(PREPARE_BONUS_SPELL_INDEX)));
+		tabbedPane.addTab("Bonus Spells", null, prepBonusSpells.get(), null);
+	}
+
+	private void createPrepareLearnedSpellPanel(LevelUpSpellPanel spells, ChooseSpellTuple info, SpellPicker learn) {
+		SpellPicker pick = new PrepareNewlyLearnedSpellPicker(info, level -> 
+			Optional.ofNullable(spells.getPanels().get(level))
+			.map(SelectSpellsPanel::getPrepared)
+			.orElse(Collections.emptyList())
+		);
+		
+		prepSpells = Optional.of(new LevelUpSpellPanel(pick,
+				new ChooseSpellTuple(levelUpInfo.ddCharacter, levelUpInfo.ddClass, info.spellBook),
+				readyCount.handle(PREPARE_SPELL_INDEX)));
+		tabbedPane.addTab("Prepare Spells", null, prepSpells.get(), null);
+		
+		learnAndPrepareListener = ObservableListener.cascade(learn, pick);
+	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+		ObserverDispatch.unsubscribeAll(learnAndPrepareListener);
+	}
+
+	
+	private void commitSpellbook(DDSpellbookWrapper wrap) {
+		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<>(wrap.getMain().spellsKnownAtLevel(i));
+				known.addAll(selections.get(i).getPrepared());
+				wrap.getMain().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> prep = new ArrayList<>(wrap.getMain().spellsPreparedAtLevel(i));
+				prep.addAll(selections.get(i).getPrepared());
+				wrap.getMain().prepareSpells(i, prep);
+			}
+		});
+		prepBonusSpells.ifPresent(pan -> {
+			final List<SelectSpellsPanel> selections = pan.getPanels();
+			for (int i = 0; i < selections.size(); ++i) {
+				if (selections.get(i) == null) continue;
+				List<DDSpell> prep = new ArrayList<>(wrap.getSecondary().get().spellsPreparedAtLevel(i));
+				prep.addAll(selections.get(i).getPrepared());
+				wrap.getSecondary().get().prepareSpells(i, prep);
+			}
+		});
+	}
+	
+	public void commitAllChanges() {
+		final String className = levelUpInfo.ddClass.getName();
+		skills.commitAllChanges();
+		levelUpInfo.ddClass.getSpellBook().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);
+	}
+}

+ 25 - 21
src/main/lombok/org/leumasjaffe/charsheet/view/magic/PrepareSpellsDialog.java

@@ -3,11 +3,16 @@ package org.leumasjaffe.charsheet.view.magic;
 import javax.swing.JPanel;
 
 import org.jdesktop.swingx.VerticalLayout;
-import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.controller.magic.ChooseSpellTuple;
+import org.leumasjaffe.charsheet.controller.magic.PrepareSpellPicker;
+import org.leumasjaffe.charsheet.controller.magic.SpellPicker;
 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.util.AbilityHelper;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -19,23 +24,20 @@ 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) {
+	public PrepareSpellsDialog(DDCharacter chara, DDCharacterClass dclass, DDSpellbook spellBook) {
 		highestSpellLevel = dclass.getHighestSpellLevel();
 		ready[0] = highestSpellLevel;
 		
@@ -78,27 +80,29 @@ public class PrepareSpellsDialog extends JPanel {
 		JPanel panel = new JPanel(new VerticalLayout(5));
 		scrollPane.setViewportView(panel);
 		
-		final Ability.Scores score = AbilityHelper.get(chara, dclass);
-
-		final Prepared spellBook = (Prepared) dclass.getSpellBook().get();
-		List<SelectPreparedSpellsPanel> panels = new ArrayList<>();
+		List<SelectSpellsPanel> panels = new ArrayList<>();
+		final BoolGate gate = new BoolGate(highestSpellLevel);
+		allReady = gate.makeListener(btnPrepareTheseSpells::setEnabled);
+		ChooseSpellTuple tup = new ChooseSpellTuple(chara, dclass, spellBook);
+		SpellPicker pick = new PrepareSpellPicker(tup);
 		for (int i = 0; i < highestSpellLevel; ++i) {
-			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, i, dclass, score);
+			SelectSpellsPanel lvl = new SelectSpellsPanel(pick, gate.handle(i), i,
+					((Prepared) spellBook).getSpellsPreparedPreviouslyForLevel(i), null);
 			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);
 		}
 		
 		btnPrepareTheseSpells.addActionListener(e -> {
-			((JDialog) this.getParent().getParent().getParent()).dispose();
 			for (int i = 0; i < highestSpellLevel; ++i) {
 				spellBook.prepareSpells(i, panels.get(i).getPrepared());
 			}
+			((JDialog) this.getParent().getParent().getParent()).dispose();
 		});
 	}
 
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(allReady);
+	}
 }

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

@@ -2,6 +2,7 @@ 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;
@@ -9,13 +10,14 @@ import javax.swing.JPopupMenu;
 import javax.swing.JTable;
 import javax.swing.ListSelectionModel;
 
-import org.leumasjaffe.charsheet.model.Ability;
-import org.leumasjaffe.charsheet.model.DDCharacter;
-import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.controller.magic.SpellPicker;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
-import org.leumasjaffe.charsheet.model.magic.impl.Prepared;
+import org.leumasjaffe.charsheet.model.observable.BoolGate;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.event.SelectTableRowPopupMenuListener;
 import org.leumasjaffe.format.StringHelper;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
@@ -35,9 +37,12 @@ import javax.swing.JMenuItem;
 import javax.swing.JOptionPane;
 import javax.swing.JScrollPane;
 
+@SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-class SelectPreparedSpellsPanel extends JPanel {
+public class SelectSpellsPanel extends JPanel {
 	
+	static String NONE = "<none>";
+
 	@AllArgsConstructor
 	private static class SelectSpellModel extends AbstractTableModel {
 		/**
@@ -45,11 +50,11 @@ class SelectPreparedSpellsPanel extends JPanel {
 		 */
 		private static final long serialVersionUID = 1L;
 		
-		final Object[] data;
+		Object[] data;
 
 		@Override
 		public int getRowCount() {
-			return data.length;
+			return data == null ? 0 : data.length;
 		}
 
 		@Override
@@ -70,24 +75,29 @@ class SelectPreparedSpellsPanel extends JPanel {
 		}
 	}
 
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = 1L;
-	
 	public static final String READY = "Is Filled Out";
 	
-	@Getter List<DDSpell> prepared;
-	
+	@Getter Collection<DDSpell> prepared;
+	List<DDSpell> known = new ArrayList<>();
+	SpellPicker pick;
+	IntValue sharedValue;
 	SelectSpellModel modelPrepared, modelKnown;
 
-	public SelectPreparedSpellsPanel(DDCharacter chara, int level, DDCharacterClass dclass, Ability.Scores score) {
-		putClientProperty(READY, true);
-		final Prepared spellBook = (Prepared) dclass.getSpellBook().get();
-		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());
+	BoolGate.Handle gate;
+	JTable tablePrepared, tableKnown;
+	
+	ObservableListener<JTable, SpellPicker> listener;
+	
+	public SelectSpellsPanel(SpellPicker pick, BoolGate.Handle gate, int level, 
+			Collection<DDSpell> prepared, IntValue sharedValue) {
+		this.pick = pick;
+		this.gate = gate;
+		this.sharedValue = sharedValue == null ? new IntValue(-1) : sharedValue;
+		this.prepared = new ArrayList<>(prepared);
+		this.modelPrepared = new SelectSpellModel(createModel(prepared));
+		this.modelKnown = new SelectSpellModel(null);
+		gate.set(countNone() == 0);
+		this.sharedValue.value(this.sharedValue.value() - this.modelPrepared.data.length + countNone());
 		
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 40, 0, 0};
@@ -96,7 +106,7 @@ class SelectPreparedSpellsPanel extends JPanel {
 		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		
-		JPanel panel = new ChooseSpellsPerDayHeader(level, spellBook, score);
+		JPanel panel = new ChooseSpellsPerDayHeader(level, pick.getInfo().spellBook, pick.getInfo().ability());
 		GridBagConstraints gbc_panel = new GridBagConstraints();
 		gbc_panel.gridwidth = 3;
 		gbc_panel.insets = new Insets(0, 0, 5, 5);
@@ -115,7 +125,7 @@ class SelectPreparedSpellsPanel extends JPanel {
 		gbc_scrollPane_1.gridy = 1;
 		add(scrollPane_1, gbc_scrollPane_1);
 		
-		JTable tablePrepared = new JTable(modelPrepared);
+		tablePrepared = new JTable(modelPrepared);
 		tablePrepared.setTableHeader(null);
 		scrollPane_1.setViewportView(tablePrepared);
 		tablePrepared.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
@@ -144,7 +154,7 @@ class SelectPreparedSpellsPanel extends JPanel {
 		gbc_scrollPane.gridy = 1;
 		add(scrollPane, gbc_scrollPane);
 		
-		JTable tableKnown = new JTable(modelKnown);
+		tableKnown = new JTable(modelKnown);
 		tableKnown.setTableHeader(null);
 		scrollPane.setViewportView(tableKnown);
 		tableKnown.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
@@ -154,8 +164,8 @@ class SelectPreparedSpellsPanel extends JPanel {
 		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));
+			JFrame frame = new JFrame(spell.getName() +  " (" + pick.getInfo().dclass.getName() + " " + level + ")");
+			frame.add(new SpellInfoPanel(pick.getInfo().chara, pick.getInfo().dclass, spell));
 			frame.pack();
 			frame.setVisible(true);
 		});
@@ -169,17 +179,7 @@ class SelectPreparedSpellsPanel extends JPanel {
 		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);
-			}
-		});
+		button.addActionListener(e -> removeSpell());
 		
 		JButton button_1 = new JButton("<<");
 		button_1.setMargin(new Insets(2, 8, 2, 8));
@@ -189,39 +189,75 @@ class SelectPreparedSpellsPanel extends JPanel {
 		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);
-			}
+		button_1.addActionListener(e -> insertSpell());
+		
+		listener = new ObservableListener<>(tableKnown, (c, v) -> {
+			known.clear();
+			known.addAll(v.getAvailableSpells(level));
+			this.modelKnown.data = createModel(known);
 		});
+		listener.setObserved(pick);
+	}
+	
+	private void removeSpell() {
+		final int row = tablePrepared.getSelectedRow();
+		if (row != -1 && !modelPrepared.data[row].equals(NONE)) {
+			this.sharedValue.value(this.sharedValue.value() + 1);
+			modelPrepared.setValueAt(NONE, row, 0);
+		}
+		tablePrepared.getSelectionModel().clearSelection();
+		tablePrepared.repaint();
+		this.gate.set(false);
+	}
+
+	private void insertSpell() {
+		final int[] rows = tableKnown.getSelectedRows();
+		final int[] orows = tablePrepared.getSelectedRows();
+		if (this.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]];
+				this.sharedValue.value(this.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();
+		
+		this.prepared.clear();
+		for (Object o : modelPrepared.data) {
+			this.prepared.add(DDSpell.fromString((String) o)); // TODO
+		}
+		gate.set(countNone() == 0);
+		ObserverDispatch.notifySubscribers(pick);
+	}
+
+	private boolean wouldHaveIllegalDuplicate(int row) {
+		return !pick.allowsDuplicates() && Arrays.asList(modelPrepared.data).contains(modelKnown.data[row]);
+	}
+
+	private String[] createModel(Collection<DDSpell> prepared) {
+		return prepared.stream().map(s -> s == null ? NONE : s.getName()).toArray(String[]::new);
 	}
 
 	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[i];
+				if (!modelPrepared.data[j].equals(NONE)) continue;
+				modelPrepared.data[j] = modelKnown.data[rows[i]];
+				sharedValue.value(sharedValue.value() - 1);
+				break;
 			}
 		}
 	}
@@ -229,8 +265,14 @@ class SelectPreparedSpellsPanel extends JPanel {
 	private int countNone() {
 		int cnt = 0;
 		for (Object o : modelPrepared.data) {
-			if (o.equals("<none>")) ++cnt;
+			if (o.equals(NONE)) ++cnt;
 		}
 		return cnt;
 	}
+	
+	@Override
+	public void removeNotify() {
+		super.removeNotify();
+		ObserverDispatch.unsubscribeAll(listener);
+	}
 }

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

@@ -219,7 +219,8 @@ class SpellInfoPanel extends JPanel {
 		gbc_lblTarget.gridy = 4;
 		panel.add(lblTarget, gbc_lblTarget);
 		
-		JTextField target = new JTextField(asString(spell.getTarget().getResolved(classLevel.value())));
+		JTextField target = new JTextField(spell.getTarget() == null ? "" :
+			asString(spell.getTarget().getResolved(classLevel.value())));
 		target.setEditable(false);
 		target.setColumns(10);
 		GridBagConstraints gbc_target = new GridBagConstraints();

+ 24 - 6
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPanel.java

@@ -1,6 +1,9 @@
 package org.leumasjaffe.charsheet.view.magic;
 
 import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.SwingConstants;
+
 import java.awt.GridBagLayout;
 import java.awt.Component;
 import javax.swing.Box;
@@ -13,6 +16,7 @@ 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.DDCharacterClass.DDSpellbookWrapper;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.observer.IndirectObservableListener;
@@ -31,9 +35,7 @@ class SpellLevelPanel extends JPanel {
 	IndirectObservableListener<JPanel, DDCharacterClass> listener;
 
 	protected SpellLevelPanel(JPanel header, DDCharacter chara, DDCharacterClass dclass, int level, 
-			BiFunction<DDSpellbook, Integer, Collection<DDSpell>> getSpells) {		
-		final Collection<DDSpell> spells = getSpells.apply(dclass.getSpellBook().get(), level);
-
+			BiFunction<DDSpellbook, Integer, Collection<DDSpell>> getSpells) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
@@ -64,16 +66,32 @@ class SpellLevelPanel extends JPanel {
 		gbc_horizontalStrut.gridy = 2;
 		add(horizontalStrut, gbc_horizontalStrut);
 		
+		final DDSpellbookWrapper wrap = dclass.getSpellBook().get();
 		listener = new IndirectObservableListener<>(panel, (c, v) -> { 
 			c.removeAll();
-			spells.forEach(spell -> c.add(new SpellLine(chara, v, spell, isCastableFromHere())));
+			{
+				final DDSpellbook sb = wrap.getMain();
+				final Collection<DDSpell> spells = getSpells.apply(sb, level);
+				spells.forEach(spell -> c.add(new SpellLine(chara, v, sb, spell, isCastableFromHere())));
+			}
+			wrap.getSecondary().ifPresent(sb -> {
+				final Collection<DDSpell> spells = getSpells.apply(sb, level);
+				if (!spells.isEmpty()) c.add(new JSeparator(SwingConstants.HORIZONTAL));
+				spells.forEach(spell -> c.add(new SpellLine(chara, v, sb, spell, isCastableFromHere())));
+			});
 			c.repaint();
+			c.validate();
 		});
-		listener.setObserved(dclass, dclass.getSpellBook().get());
+		if (wrap.getSecondary().isPresent()) {
+			listener.setObserved(dclass, wrap.getMain(), wrap.getSecondary().get());			
+		} else {
+			listener.setObserved(dclass, wrap.getMain());			
+		}
 	}
 	
 	public SpellLevelPanel(DDCharacter chara, DDCharacterClass dclass, int level, Ability.Scores ability) {
-		this(new SpellsKnownHeader(level, dclass.getSpellBook().get(), ability), chara, dclass, level, DDSpellbook::spellsKnownAtLevel);
+		this(new SpellsKnownHeader(level, dclass.getSpellBook().get().getMain(), ability),
+				chara, dclass, level, DDSpellbook::spellsKnownAtLevel);
 	}
 	
 	public boolean isCastableFromHere() { return false; }

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

@@ -17,7 +17,8 @@ class SpellLevelPerDayPanel extends SpellLevelPanel {
 	private static final long serialVersionUID = 1L;
 	
 	public SpellLevelPerDayPanel(DDCharacter chara, DDCharacterClass dclass, int level, Ability.Scores ability) {
-		super(new SpellsPerDayHeader(level, dclass.getSpellBook().get(), ability), chara, dclass, level, DDSpellbook::spellsPreparedAtLevel);
+		super(new SpellsPerDayHeader(level, dclass.getSpellBook().get(), ability), 
+				chara, dclass, level, DDSpellbook::spellsPreparedAtLevel);
 	}
 	
 	public boolean isCastableFromHere() { return true; }

+ 4 - 2
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLine.java

@@ -5,6 +5,7 @@ import javax.swing.JPanel;
 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.event.PopClickListener;
 
 import java.awt.GridBagLayout;
@@ -21,7 +22,8 @@ class SpellLine extends JPanel {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	public SpellLine(DDCharacter chara, final DDCharacterClass dclass, final DDSpell spell, boolean isPrepared) {
+	public SpellLine(DDCharacter chara, final DDCharacterClass dclass, final DDSpellbook book,
+			final DDSpell spell, boolean isPrepared) {
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
@@ -102,7 +104,7 @@ class SpellLine extends JPanel {
 		gbc_lblAction_1.gridy = 0;
 		add(lblRange, gbc_lblAction_1);
 		
-		addMouseListener(new PopClickListener(new SpellMenu(chara, dclass, spell, isPrepared)));
+		addMouseListener(new PopClickListener(new SpellMenu(chara, dclass, book, spell, isPrepared)));
 	}
 
 }

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

@@ -5,6 +5,7 @@ import javax.swing.JPopupMenu;
 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.DDSpell.SpellClassInfo;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.observer.ObserverDispatch;
 
@@ -19,12 +20,13 @@ class SpellMenu extends JPopupMenu {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	public SpellMenu(DDCharacter chara, final DDCharacterClass dclass, final DDSpell spell, boolean isPrepared) {
-		final int spellLevel = spell.getClassLevel(dclass.getName());
+	public SpellMenu(DDCharacter chara, final DDCharacterClass dclass, final DDSpellbook book,
+			final DDSpell spell, boolean isPrepared) {
+		final SpellClassInfo spellLevel = spell.getClassLevel(book.getSingleName());
 		
 		JMenuItem mntmInfo = new JMenuItem("Info");
 		mntmInfo.addActionListener( e -> {
-			JFrame frame = new JFrame(spell.getName() +  " (" + dclass.getName() + " " + spellLevel + ")");
+			JFrame frame = new JFrame(spell.getName() +  " (" + book.getName() + " " + spellLevel + ")");
 			frame.add(new SpellInfoPanel(chara, dclass, spell));
 			frame.pack();
 			frame.setVisible(true);
@@ -34,13 +36,12 @@ class SpellMenu extends JPopupMenu {
 		if (isPrepared) {
 			JMenuItem mntmCast = new JMenuItem("Cast");
 			mntmCast.addActionListener(e -> {
-				final DDSpellbook book = dclass.getSpellBook().get();
-				if (book.numSpellsPerDayRemainingAtLevel(spellLevel) == 0) {
+				if (book.numSpellsPerDayRemainingAtLevel(spellLevel.level) == 0) {
 					JOptionPane.showMessageDialog(this, "Cannot cast any more spells", "Error", JOptionPane.ERROR_MESSAGE);
 					return;
 				}
-				book.castSpell(spellLevel, spell);
-				ObserverDispatch.notifySubscribers(book, null);
+				book.castSpell(spellLevel.level, spell);
+				ObserverDispatch.notifySubscribers(book);
 			});
 			add(mntmCast);
 		}

+ 16 - 3
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellPanel.java

@@ -12,6 +12,7 @@ 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.DDCharacterClass.DDSpellbookWrapper;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
 import org.leumasjaffe.function.QuadFunction;
 import org.leumasjaffe.observer.IndirectObservableListener;
@@ -61,10 +62,16 @@ public class SpellPanel extends JPanel {
 				
 		listenerPerDay = new IndirectObservableListener<>(prepared, 
 				new AppendSpellLevelOperation(chara, ability, SpellLevelPerDayPanel::new));
-		listenerPerDay.setObserved(dclass, ability, dclass.getLevel(), dclass.getSpellBook().get());
 		listenerKnown = new IndirectObservableListener<>(known, 
 				new AppendSpellLevelOperation(chara, ability, SpellLevelPanel::new));
-		listenerKnown.setObserved(dclass, ability, dclass.getLevel(), dclass.getSpellBook().get());
+		final DDSpellbookWrapper wrap = dclass.getSpellBook().get();
+		if (wrap.getSecondary().isPresent()) {
+			listenerPerDay.setObserved(dclass, ability, dclass.getLevel(), wrap.getMain(), wrap.getSecondary().get());
+			listenerKnown.setObserved(dclass, ability, dclass.getLevel(), wrap.getMain(), wrap.getSecondary().get());			
+		} else {
+			listenerPerDay.setObserved(dclass, ability, dclass.getLevel(), wrap.getMain());
+			listenerKnown.setObserved(dclass, ability, dclass.getLevel(), wrap.getMain());			
+		}
 	}
 
 	@RequiredArgsConstructor
@@ -78,11 +85,17 @@ public class SpellPanel extends JPanel {
 		@Override
 		public void accept(final JPanel root, final DDCharacterClass dclass) {		
 			for (int i = previousHighestSpellLevel; i < dclass.getHighestSpellLevel(); ++i) {
-				if (dclass.getSpellBook().get().numSpellsKnownAtLevel(i) == 0) break;
+				if (hasNoSpellsKnownAtLevel(dclass, i)) break;
 				root.add(function.apply(chara, dclass, i, ability));
 			}
 			previousHighestSpellLevel = dclass.getHighestSpellLevel();
 		}
+
+		private boolean hasNoSpellsKnownAtLevel(final DDCharacterClass dclass, final int i) {
+			final DDSpellbookWrapper wrap = dclass.getSpellBook().get();
+			return wrap.getMain().numSpellsKnownAtLevel(i) == 0 &&
+					wrap.getSecondary().map(sb -> sb.numSpellsKnownAtLevel(i) == 0).orElse(true);
+		}
 	}
 	
 	@Override

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

@@ -13,6 +13,7 @@ import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.format.StringHelper;
 
 import java.awt.Dimension;
+import javax.swing.SwingConstants;
 
 class SpellsKnownHeader extends JPanel {
 	/**
@@ -23,7 +24,7 @@ class SpellsKnownHeader extends JPanel {
 	public SpellsKnownHeader(int level, DDSpellbook model, Ability.Scores ability) {
 		setPreferredSize(new Dimension(350, 20));
 		GridBagLayout gridBagLayout = new GridBagLayout();
-		gridBagLayout.columnWidths = new int[]{0, 30, 0, 30, 0, 30, 0, 0};
+		gridBagLayout.columnWidths = new int[]{0, 30, 0, 35, 0, 35, 0, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
 		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
@@ -38,6 +39,7 @@ class SpellsKnownHeader extends JPanel {
 		add(lblSpellLevel, gbc_lblSpellLevel);
 		
 		JTextField textFieldLevel = new JTextField(Integer.toString(level));
+		textFieldLevel.setHorizontalAlignment(SwingConstants.CENTER);
 		textFieldLevel.setPreferredSize(new Dimension(30, 20));
 		textFieldLevel.setMaximumSize(new Dimension(30, 20));
 		textFieldLevel.setEditable(false);
@@ -58,6 +60,7 @@ class SpellsKnownHeader extends JPanel {
 		add(lblSaveDc, gbc_lblSaveDc);
 		
 		JTextField textFieldSpellSave = new JTextField(Integer.toString(10 + level + ability.modifier()));
+		textFieldSpellSave.setHorizontalAlignment(SwingConstants.CENTER);
 		textFieldSpellSave.setPreferredSize(new Dimension(30, 20));
 		textFieldSpellSave.setMaximumSize(new Dimension(30, 20));
 		textFieldSpellSave.setEditable(false);
@@ -77,6 +80,7 @@ class SpellsKnownHeader extends JPanel {
 		add(lblSpellsPerDay, gbc_lblSpellsPerDay);
 		
 		JTextField textFieldRemaining = new JTextField(StringHelper.toString(model.numSpellsKnownAtLevel(level), -1, "--"));
+		textFieldRemaining.setHorizontalAlignment(SwingConstants.CENTER);
 		textFieldRemaining.setPreferredSize(new Dimension(30, 20));
 		textFieldRemaining.setMaximumSize(new Dimension(30, 20));
 		GridBagConstraints gbc_textFieldRemaining = new GridBagConstraints();

+ 23 - 9
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java

@@ -9,11 +9,12 @@ import javax.swing.JPanel;
 import javax.swing.JTextField;
 
 import org.leumasjaffe.charsheet.model.Ability;
-import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
-import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.charsheet.model.DDCharacterClass.DDSpellbookWrapper;
+import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.Dimension;
+import javax.swing.SwingConstants;
 
 class SpellsPerDayHeader extends JPanel {
 	/**
@@ -21,12 +22,12 @@ class SpellsPerDayHeader extends JPanel {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	ObservableListener<JTextField, DDSpellbook> listener;
+	IndirectObservableListener<JTextField, DDSpellbookWrapper> listener;
 
-	public SpellsPerDayHeader(int level, DDSpellbook model, Ability.Scores ability) {
+	public SpellsPerDayHeader(int level, DDSpellbookWrapper model, Ability.Scores ability) {
 		setPreferredSize(new Dimension(350, 20));
 		GridBagLayout gridBagLayout = new GridBagLayout();
-		gridBagLayout.columnWidths = new int[]{0, 30, 0, 30, 0, 30, 0, 30, 0, 0};
+		gridBagLayout.columnWidths = new int[]{0, 30, 0, 35, 0, 45, 0, 45, 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, 1.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
@@ -41,6 +42,7 @@ class SpellsPerDayHeader extends JPanel {
 		add(lblSpellLevel, gbc_lblSpellLevel);
 		
 		JTextField textFieldLevel = new JTextField(Integer.toString(level));
+		textFieldLevel.setHorizontalAlignment(SwingConstants.CENTER);
 		textFieldLevel.setEditable(false);
 		GridBagConstraints gbc_textFieldLevel = new GridBagConstraints();
 		gbc_textFieldLevel.fill = GridBagConstraints.HORIZONTAL;
@@ -59,6 +61,7 @@ class SpellsPerDayHeader extends JPanel {
 		add(lblSaveDc, gbc_lblSaveDc);
 		
 		JTextField textFieldSpellSave = new JTextField(Integer.toString(10 + level + ability.modifier()));
+		textFieldSpellSave.setHorizontalAlignment(SwingConstants.CENTER);
 		textFieldSpellSave.setEditable(false);
 		textFieldSpellSave.setColumns(10);
 		GridBagConstraints gbc_textFieldSpellSave = new GridBagConstraints();
@@ -76,6 +79,7 @@ class SpellsPerDayHeader extends JPanel {
 		add(lblSpellsPerDay, gbc_lblSpellsPerDay);
 		
 		JTextField textFieldRemaining = new JTextField();
+		textFieldRemaining.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_textFieldRemaining = new GridBagConstraints();
 		gbc_textFieldRemaining.fill = GridBagConstraints.HORIZONTAL;
 		gbc_textFieldRemaining.insets = new Insets(0, 0, 0, 5);
@@ -92,7 +96,8 @@ class SpellsPerDayHeader extends JPanel {
 		gbc_label.gridy = 0;
 		add(label, gbc_label);
 		
-		JTextField textFieldOutOf = new JTextField(Integer.toString(model.numSpellsPerDayAtLevel(level)));
+		JTextField textFieldOutOf = new JTextField(getSpellPerDayListing(level, model));
+		textFieldOutOf.setHorizontalAlignment(SwingConstants.CENTER);
 		GridBagConstraints gbc_textFieldOutOf = new GridBagConstraints();
 		gbc_textFieldOutOf.insets = new Insets(0, 0, 0, 5);
 		gbc_textFieldOutOf.fill = GridBagConstraints.HORIZONTAL;
@@ -102,12 +107,21 @@ class SpellsPerDayHeader extends JPanel {
 		textFieldOutOf.setEditable(false);
 		textFieldOutOf.setColumns(10);
 		
-		listener = new ObservableListener<JTextField, DDSpellbook>(textFieldRemaining, (c, v) -> {
-			c.setText(Integer.toString(v.numSpellsPerDayRemainingAtLevel(level)));
+		listener = new IndirectObservableListener<JTextField, DDSpellbookWrapper>(textFieldRemaining, (c, v) -> {
+			c.setText(getSpellPerDayListing(level, v));
 		});
-		listener.setObserved(model);
+		if (model.getSecondary().isPresent()) {
+			listener.setObserved(model, model.getMain(), model.getSecondary().get());
+		} else {
+			listener.setObserved(model, model.getMain());
+		}
 	}
 	
+	private String getSpellPerDayListing(int level, DDSpellbookWrapper wrap) {
+		return wrap.getMain().numSpellsPerDayAtLevel(level) +
+				wrap.getSecondary().map(sb -> "+" + sb.numSpellsPerDayAtLevel(level)).orElse("");
+	}
+
 	@Override
 	public void removeNotify() {
 		super.removeNotify();

+ 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,

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

@@ -12,6 +12,7 @@ import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.inventory.DDInventory;
 import org.leumasjaffe.charsheet.model.inventory.DDItem;
 import org.leumasjaffe.charsheet.model.inventory.EquipmentSlot;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.format.StringHelper;
 import org.leumasjaffe.observer.IndirectObservableListener;
 import org.leumasjaffe.observer.ObservableListener;
@@ -94,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;
@@ -102,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);
@@ -110,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;
@@ -126,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;
@@ -134,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;
@@ -150,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;
@@ -158,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;
@@ -174,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;
@@ -182,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;
@@ -198,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;
@@ -206,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;
@@ -222,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;
@@ -230,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;
@@ -246,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;
@@ -254,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;
@@ -358,57 +359,39 @@ public class ArmorLine extends JPanel {
 		
 		armorTotalObserver = new IndirectObservableListener<>(total, (c, v) -> {
 			final DDInventory inv = v.getInventory();
-			int iarmor = 0;
-			int ishield = 0;
-			int dex = v.getAbilities().getDex().modifier();
-			int isize = v.getSize().value().modifier;
-			int inatural = 0;
-			int ideflect = 0;
-			int imisc = 0;
-			{
-				final DDItem body = inv.getEquipment().get(EquipmentSlot.BODY);
-				if ( body != null && body.isArmor() ) {
-					iarmor = body.getArmor().getActualAcBonus();
-					dex = Math.min(dex, body.getArmor().getMaxDex());
-				}
-			}
-			{
-				final DDItem offHand = inv.getEquipment().get(EquipmentSlot.OFF_HAND);
-				if ( offHand != null && offHand.isArmor() ) {
-					ishield = offHand.getArmor().getActualAcBonus();
-				}
-			}
-			c.setText(StringHelper.toString(10 + iarmor + ishield + 
-					dex + isize + inatural + ideflect + imisc));
+			IntValue iarmor = new IntValue(0), ishield = new IntValue(0);
+			IntValue dex = new IntValue(v.getAbilities().getDex().modifier());
+			int isize = v.getSize().value().modifier, inatural = 0, ideflect = 0, imisc = 0;
+			
+			inv.getEquipment().get(EquipmentSlot.BODY).flatMap(DDItem::getArmor).ifPresent( a -> {
+				iarmor.value(a.getActualAcBonus());
+				dex.value(Math.min(dex.value(), a.getMaxDex()));
+			});
+			inv.getEquipment().get(EquipmentSlot.OFF_HAND).flatMap(DDItem::getArmor).ifPresent(
+					a -> ishield.value(a.getActualAcBonus()));
+			c.setText(StringHelper.toString(10 + iarmor.value() + ishield.value() + 
+					dex.value() + isize + inatural + ideflect + imisc));
 		});
 		
 		armorArmorObserver = new ObservableListener<>(armor, (c, v) -> {
-			int iarmor = 0;
-			final DDItem body = v.getEquipment().get(EquipmentSlot.BODY);
-			if ( body != null && body.isArmor() ) {
-				iarmor = body.getArmor().getActualAcBonus();
-			}
+			IntValue iarmor = new IntValue(0);
+			v.getEquipment().get(EquipmentSlot.BODY).flatMap(DDItem::getArmor).ifPresent(
+					a -> iarmor.value(a.getActualAcBonus()));
 			c.setText(StringHelper.toString(iarmor));
 		});
 		
 		armorShieldObserver = new ObservableListener<>(shield, (c, v) -> {
-			int iarmor = 0;
-			final DDItem offHand = v.getEquipment().get(EquipmentSlot.OFF_HAND);
-			if ( offHand != null && offHand.isArmor() ) {
-				iarmor = offHand.getArmor().getActualAcBonus();
-			}
+			IntValue iarmor = new IntValue(0);
+			v.getEquipment().get(EquipmentSlot.OFF_HAND).flatMap(DDItem::getArmor).ifPresent(
+					a -> iarmor.value(a.getActualAcBonus()));
 			c.setText(StringHelper.toString(iarmor));
 		});
 		
 		armorDexObserver = new IndirectObservableListener<>(dexterity, (c, v) -> {
 			final DDInventory inv = v.getInventory();
-			int dex = v.getAbilities().getDex().modifier();
-			{
-				final DDItem body = inv.getEquipment().get(EquipmentSlot.BODY);
-				if ( body != null && body.isArmor() ) {
-					dex = Math.min(dex, body.getArmor().getMaxDex());
-				}
-			}
+			IntValue dex = new IntValue(v.getAbilities().getDex().modifier());
+			inv.getEquipment().get(EquipmentSlot.BODY).flatMap(DDItem::getArmor).ifPresent(
+					a -> dex.value(Math.min(dex.value(), a.getMaxDex())));
 			c.setText(StringHelper.toString(dex));
 		});
 	}

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


Някои файлове не бяха показани, защото твърде много файлове са промени