Ver Fonte

Add initial support for cleric Domains.
This allows a class to have multiple spell books (though in practice, it shouldn't ever be more than 2) to support concepts like specialist schools.

This needs more fleshing out as a concept to allow Wizard Specialization.

Sam Jaffe há 8 anos atrás
pai
commit
c39eff00ec
23 ficheiros alterados com 284 adições e 72 exclusões
  1. 41 33
      resources/Potato.json
  2. 48 0
      resources/spells/default.json
  3. 15 0
      resources/spells/domain/plant.json
  4. 1 2
      src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java
  5. 3 2
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpell.java
  6. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellFactory.java
  7. 10 0
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Area.java
  8. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/dimension/Duration.java
  9. 82 0
      src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Domain.java
  10. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/observer/ObserverHelper.java
  11. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/ClassTab.java
  12. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/D20Sheet.java
  13. 3 2
      src/main/lombok/org/leumasjaffe/charsheet/view/DialogBuilder.java
  14. 3 3
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/PrepareSpellsDialog.java
  15. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SelectPreparedSpellsPanel.java
  16. 2 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellInfoPanel.java
  17. 17 4
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPanel.java
  18. 1 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPerDayPanel.java
  19. 4 2
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLine.java
  20. 2 2
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellMenu.java
  21. 19 3
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellPanel.java
  22. 5 1
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsKnownHeader.java
  23. 20 8
      src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java

+ 41 - 33
resources/Potato.json

@@ -5,44 +5,52 @@
     {
       "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": {
+      "spellBook": [{
         "@c": ".impl.Spontaneous",
         "spellInfo": {
           "0": {
@@ -62,7 +70,7 @@
             "spellsKnown": []
           }
         }
-      }
+      }]
     }
   ],
   "race": "Half-Elemental (E)",

Diff do ficheiro suprimidas por serem muito extensas
+ 48 - 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"
+  ]
+}

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

@@ -1,7 +1,6 @@
 package org.leumasjaffe.charsheet.model;
 
 import java.util.List;
-import java.util.Optional;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
@@ -51,7 +50,7 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 //	@NonNull List<Integer> healthRolls;
 	@Delegate @Getter(AccessLevel.NONE) Reference name;
 	
-	Optional<DDSpellbook> spellBook;
+	List<DDSpellbook> spellBook;
 	
 	public String toString() {
 		return getName() + " " + getLevel();

+ 3 - 2
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpell.java

@@ -1,6 +1,7 @@
 package org.leumasjaffe.charsheet.model.magic;
 
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -66,7 +67,7 @@ public class DDSpell {
 	@NonNull @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) Map<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;
@@ -84,7 +85,7 @@ public class DDSpell {
 	}
 	
 	public int getClassLevel(final String clas) {
-		return classToLevel.get(clas);
+		return classToLevel.getOrDefault(clas, -1);
 	}
 	
 	@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);
 	}
 	

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

@@ -14,6 +14,7 @@ import lombok.experimental.FieldDefaults;
 
 @JsonTypeInfo(use=Id.NAME)
 @JsonSubTypes({
+	@Type(value=Area.Custom.class, name="Custom"),
 	@Type(value=Area.Line.class, name="Line"),
 	@Type(value=Area.Cone.class, name="Cone"),
 	@Type(value=Area.Sphere.class, name="Sphere"),
@@ -23,6 +24,15 @@ public interface Area {
 	public static enum Emission { BURST, EMANATION, SPREAD, NONE }
 	public static enum Shape { CONE, CYLINDER, LINE, SPHERE }
 
+	@RequiredArgsConstructor
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	public static class Custom implements Area {
+		String value;
+		public String toString() {
+			return value;
+		}
+	}
+	
 	@RequiredArgsConstructor
 	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	public static class Line implements Area {

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

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

@@ -0,0 +1,82 @@
+package org.leumasjaffe.charsheet.model.magic.impl;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+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 {
+	@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>() {});
+		}
+		String name;
+		List<Object> powers;
+		List<DDSpell> spells;
+	}
+	
+	Set<SpellBookImpl> domains;
+	List<DDSpell> spellsPrepared, spellsPreparedPreviously;
+	
+	@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 1; // TODO
+	}
+
+	@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, List<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.get(0));
+		spellsPreparedPreviously.set(level-1, spells.get(0));
+	}
+
+	@Override
+	public List<DDSpell> getSpellsPreparedPreviouslyForLevel(int level) {
+		if (spellsPreparedPreviously.size() < level || level == 0) return Collections.emptyList();
+		return Collections.singletonList(spellsPreparedPreviously.get(level-1));
+	}
+}

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

@@ -10,7 +10,7 @@ import lombok.experimental.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));
+		dclass.getSpellBook().forEach(sb -> ObserverDispatch.notifySubscribers(sb, src));
 	}
 	
 	public void notifyObservableHierarchy(final Ability.Scores abil, final Object src) {

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

@@ -172,7 +172,7 @@ public class ClassTab extends JPanel {
 		gbc_levelBenefits.gridy = 1;
 		add(levelBenefits, gbc_levelBenefits);
 		
-		if (model.getSpellBook().isPresent()) {
+		if (!model.getSpellBook().isEmpty()) {
 			SpellPanel spells = new SpellPanel(chara, model);
 			GridBagConstraints gbc_spells = new GridBagConstraints();
 			gbc_spells.fill = GridBagConstraints.BOTH;

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

@@ -122,9 +122,9 @@ public class D20Sheet extends JFrame {
 			// Step 2: Regenerate spells prepared
 			// Step 2.1: If Spontaneous, skip (2)
 			for (DDCharacterClass dclass : model.getClasses()) {
-				dclass.getSpellBook().ifPresent(sb -> {
+				dclass.getSpellBook().forEach(sb -> {
 					if (sb.preparesSpells()) {
-						DialogBuilder.createPrepareSpellsDialog(this, model, dclass);
+						DialogBuilder.createPrepareSpellsDialog(this, model, dclass, sb);
 					} else {
 						for (int i = 0; i < dclass.getHighestSpellLevel(); ++i) {
 							sb.prepareSpells(i, null);

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

@@ -8,6 +8,7 @@ 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.magic.PrepareSpellsDialog;
 import org.leumasjaffe.charsheet.view.skills.SkillLevelUpDialog;
 
@@ -29,7 +30,7 @@ public class DialogBuilder {
 		createDialogue(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) {
+		createDialogue(parent, "Prepare Spells - " + dclass.getName(), new PrepareSpellsDialog(chara, dclass, spellBook));
 	}
 }

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

@@ -6,6 +6,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.magic.DDSpellbook;
 import org.leumasjaffe.charsheet.model.magic.impl.Prepared;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
 
@@ -35,7 +36,7 @@ public class PrepareSpellsDialog extends JPanel {
 	int[] ready = {0};
 	int highestSpellLevel;
 
-	public PrepareSpellsDialog(DDCharacter chara, DDCharacterClass dclass) {
+	public PrepareSpellsDialog(DDCharacter chara, DDCharacterClass dclass, DDSpellbook spellBook) {
 		highestSpellLevel = dclass.getHighestSpellLevel();
 		ready[0] = highestSpellLevel;
 		
@@ -80,10 +81,9 @@ public class PrepareSpellsDialog extends JPanel {
 		
 		final Ability.Scores score = AbilityHelper.get(chara, dclass);
 
-		final Prepared spellBook = (Prepared) dclass.getSpellBook().get();
 		List<SelectPreparedSpellsPanel> panels = new ArrayList<>();
 		for (int i = 0; i < highestSpellLevel; ++i) {
-			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, i, dclass, score);
+			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, i, dclass, (Prepared) spellBook, score);
 			panels.add(lvl);
 			lvl.addPropertyChangeListener(SelectPreparedSpellsPanel.READY, e -> {
 				if ((Boolean) e.getNewValue()) ++ready[0];

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

@@ -81,9 +81,9 @@ class SelectPreparedSpellsPanel extends JPanel {
 	
 	SelectSpellModel modelPrepared, modelKnown;
 
-	public SelectPreparedSpellsPanel(DDCharacter chara, int level, DDCharacterClass dclass, Ability.Scores score) {
+	public SelectPreparedSpellsPanel(DDCharacter chara, int level, DDCharacterClass dclass, 
+			Prepared spellBook, 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());

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

@@ -220,7 +220,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();

+ 17 - 4
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;
@@ -16,6 +19,7 @@ import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
@@ -32,7 +36,8 @@ class SpellLevelPanel extends JPanel {
 
 	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);
+//		final Map<DDSpellbook, Collection<DDSpell>> spells = new HashMap<>();
+//		dclass.getSpellBook().forEach(sb -> spells.put(sb, getSpells.apply(sb, level)));
 
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
@@ -66,14 +71,22 @@ class SpellLevelPanel extends JPanel {
 		
 		listener = new IndirectObservableListener<>(panel, (c, v) -> { 
 			c.removeAll();
-			spells.forEach(spell -> c.add(new SpellLine(chara, v, spell, isCastableFromHere())));
+			for (int i = 0; i < dclass.getSpellBook().size(); ++i) {
+				final DDSpellbook sb = dclass.getSpellBook().get(i);
+				final Collection<DDSpell> spells = getSpells.apply(sb, level);
+				if (spells.isEmpty()) continue;
+				if (i != 0) c.add(new JSeparator(SwingConstants.HORIZONTAL));
+				spells.forEach(spell -> c.add(new SpellLine(chara, v, sb, spell, isCastableFromHere())));
+			}
 			c.repaint();
 		});
-		listener.setObserved(dclass, dclass.getSpellBook().get());
+		listener.setObserved(dclass, dclass.getSpellBook().toArray(new Observable[0]));
 	}
 	
 	public SpellLevelPanel(DDCharacter chara, DDCharacterClass dclass, int level, Ability.Scores ability) {
-		this(new SpellsKnownHeader(level, dclass.getSpellBook().get(), ability), chara, dclass, level, DDSpellbook::spellsKnownAtLevel);
+		// TODO figure out what to do about that get(0)
+		this(new SpellsKnownHeader(level, dclass.getSpellBook().get(0), ability),
+				chara, dclass, level, DDSpellbook::spellsKnownAtLevel);
 	}
 	
 	public boolean isCastableFromHere() { return false; }

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

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

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

@@ -19,7 +19,8 @@ class SpellMenu extends JPopupMenu {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	public SpellMenu(DDCharacter chara, final DDCharacterClass dclass, final DDSpell spell, boolean isPrepared) {
+	public SpellMenu(DDCharacter chara, final DDCharacterClass dclass, final DDSpellbook book,
+			final DDSpell spell, boolean isPrepared) {
 		final int spellLevel = spell.getClassLevel(dclass.getName());
 		
 		JMenuItem mntmInfo = new JMenuItem("Info");
@@ -34,7 +35,6 @@ 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) {
 					JOptionPane.showMessageDialog(this, "Cannot cast any more spells", "Error", JOptionPane.ERROR_MESSAGE);
 					return;

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

@@ -10,11 +10,13 @@ import javax.swing.JTabbedPane;
 
 import org.jdesktop.swingx.VerticalLayout;
 import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.Ability.Scores;
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.util.AbilityHelper;
 import org.leumasjaffe.function.QuadFunction;
 import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
@@ -61,10 +63,20 @@ 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());
+		listenerPerDay.setObserved(dclass, getObserveList(ability, dclass));
+		listenerKnown.setObserved(dclass, getObserveList(ability, dclass));			
+	}
+
+	private Observable[] getObserveList(Scores ability, DDCharacterClass dclass) {
+		Observable[] rval = new Observable[2+dclass.getSpellBook().size()];
+		rval[0] = ability;
+		rval[1] = dclass.getLevel();
+		for (int i = 0; i < dclass.getSpellBook().size(); ++i) {
+			rval[i+2] = dclass.getSpellBook().get(i);
+		}
+		return rval;
 	}
 
 	@RequiredArgsConstructor
@@ -78,11 +90,15 @@ 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 (hasSpellsKnownAtLevel(dclass, i)) break;
 				root.add(function.apply(chara, dclass, i, ability));
 			}
 			previousHighestSpellLevel = dclass.getHighestSpellLevel();
 		}
+
+		private boolean hasSpellsKnownAtLevel(final DDCharacterClass dclass, final int i) {
+			return dclass.getSpellBook().stream().allMatch(sb -> sb.numSpellsKnownAtLevel(i) == 0);
+		}
 	}
 	
 	@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();

+ 20 - 8
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java

@@ -3,6 +3,7 @@ package org.leumasjaffe.charsheet.view.magic;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
+import java.util.List;
 
 import javax.swing.JLabel;
 import javax.swing.JPanel;
@@ -10,10 +11,12 @@ import javax.swing.JTextField;
 
 import org.leumasjaffe.charsheet.model.Ability;
 import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
-import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.Dimension;
+import javax.swing.SwingConstants;
 
 class SpellsPerDayHeader extends JPanel {
 	/**
@@ -21,12 +24,12 @@ class SpellsPerDayHeader extends JPanel {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	ObservableListener<JTextField, DDSpellbook> listener;
+	IndirectObservableListener<JTextField, List<DDSpellbook>> listener;
 
-	public SpellsPerDayHeader(int level, DDSpellbook model, Ability.Scores ability) {
+	public SpellsPerDayHeader(int level, List<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, 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 +44,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 +63,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 +81,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 +98,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 +109,17 @@ 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, List<DDSpellbook>>(textFieldRemaining, (c, v) -> {
+			c.setText(getSpellPerDayListing(level, v));
 		});
-		listener.setObserved(model);
+		listener.setObserved(model, model.toArray(new Observable[0]));
 	}
 	
+	private String getSpellPerDayListing(int level, List<DDSpellbook> model) {
+		return model.stream().mapToInt(sb -> sb.numSpellsPerDayAtLevel(level))
+				.mapToObj(Integer::toString).reduce("", (l, r) -> l.isEmpty() ? r : l + "+" + r);
+	}
+
 	@Override
 	public void removeNotify() {
 		super.removeNotify();