瀏覽代碼

Altering Secondary spellbooks.
- Only one allowed
- Obtains callback to primary book, so Specialist Wizard can do things.

Sam Jaffe 8 年之前
父節點
當前提交
5a728efc1e

+ 3 - 2
resources/Potato.json

@@ -50,8 +50,9 @@
     {
       "level": 2,
       "name": "Bard",
-      "spellBook": [{
+      "spellBook": {
         "@c": ".impl.Spontaneous",
+        "name":"Bard",
         "spellInfo": {
           "0": {
             "spellsPerDay": 3,
@@ -70,7 +71,7 @@
             "spellsKnown": []
           }
         }
-      }]
+      }
     }
   ],
   "race": "Half-Elemental (E)",

+ 0 - 2
resources/spells/default.json

@@ -99,7 +99,6 @@
     "name":"Entangle",
     "classToLevel":{
       "Druid":1,
-      "Cleric":1,
       "Domain::Plant":1,
       "Ranger":1
     },
@@ -124,7 +123,6 @@
     "name":"Barkskin",
     "classToLevel":{
       "Druid":2,
-      "Cleric":2,
       "Domain::Plant":2,
       "Ranger":2
     },

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

@@ -1,6 +1,7 @@
 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;
@@ -10,6 +11,8 @@ 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 lombok.AccessLevel;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -50,7 +53,26 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 //	@NonNull List<Integer> healthRolls;
 	@Delegate @Getter(AccessLevel.NONE) Reference name;
 	
-	List<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);
+			}
+		}
+		DDSpellbook main;
+		Optional<DDSpellbook> secondary = Optional.empty();
+	}
+	
+	Optional<DDSpellbookWrapper> spellBook;
 	
 	public String toString() {
 		return getName() + " " + getLevel();

+ 20 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpell.java

@@ -2,9 +2,9 @@ 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;
+import java.util.SortedMap;
 
 import org.leumasjaffe.charsheet.model.magic.dimension.Area;
 import org.leumasjaffe.charsheet.model.magic.dimension.Duration;
@@ -16,6 +16,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Getter;
 import lombok.NonNull;
@@ -64,7 +65,7 @@ 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 = new HashSet<>();
@@ -84,8 +85,23 @@ public class DDSpell {
 		return subSchool == null ? school.toString() : school.toString() + " (" + subSchool.toString() + ")";
 	}
 	
-	public int getClassLevel(final String clas) {
-		return classToLevel.getOrDefault(clas, -1);
+	@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);
 	}
 	
 	@JsonCreator public static DDSpell fromString(String str) { 

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

@@ -8,10 +8,18 @@ import org.leumasjaffe.observer.Observable;
 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 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 );
 	
@@ -33,4 +41,8 @@ public abstract class DDSpellbook extends Observable {
 	public abstract void castSpell( int level, final DDSpell spell );
 	
 	public abstract void prepareSpells(int level, List<DDSpell> spells);
+
+	public String getSingleName() {
+		return getName();
+	}
 }

+ 14 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Domain.java

@@ -8,9 +8,11 @@ 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.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
@@ -20,7 +22,7 @@ import lombok.EqualsAndHashCode;
 import lombok.SneakyThrows;
 
 @Data @EqualsAndHashCode(callSuper=true)
-public class Domain extends Prepared {
+public class Domain extends Prepared implements DDSpellbook.Secondary {
 	@Data @EqualsAndHashCode
 	private static class SpellBookImpl {
 		@JsonCreator @SneakyThrows
@@ -35,9 +37,20 @@ public class Domain extends Prepared {
 		List<DDSpell> spells;
 	}
 	
+	@JsonIgnore DDSpellbook mainSpellbook;
 	Set<SpellBookImpl> domains;
 	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();

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

@@ -35,6 +35,11 @@ public class Inspired extends Prepared {
 	@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;

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

@@ -10,7 +10,10 @@ import lombok.experimental.UtilityClass;
 public class ObserverHelper {
 	public void notifyObservableHierarchy(final DDCharacterClass dclass, final Object src) {
 		ObserverDispatch.notifySubscribers(dclass, src);
-		dclass.getSpellBook().forEach(sb -> ObserverDispatch.notifySubscribers(sb, src));
+		dclass.getSpellBook().ifPresent(wrap -> {
+			ObserverDispatch.notifySubscribers(wrap.getMain(), src);
+			wrap.getSecondary().ifPresent(sb -> ObserverDispatch.notifySubscribers(sb, src));
+		});
 	}
 	
 	public void notifyObservableHierarchy(final Ability.Scores abil, final Object src) {

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

@@ -172,14 +172,14 @@ public class ClassTab extends JPanel {
 		gbc_levelBenefits.gridy = 1;
 		add(levelBenefits, gbc_levelBenefits);
 		
-		if (!model.getSpellBook().isEmpty()) {
+		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

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

@@ -23,6 +23,7 @@ import javax.swing.UIManager;
 
 import org.leumasjaffe.charsheet.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
 import org.leumasjaffe.charsheet.observer.ObserverHelper;
 
 import java.awt.event.KeyEvent;
@@ -122,14 +123,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().forEach(sb -> {
-					if (sb.preparesSpells()) {
-						DialogBuilder.createPrepareSpellsDialog(this, model, dclass, sb);
-					} 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
@@ -146,6 +142,16 @@ public class D20Sheet extends JFrame {
 		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));

+ 17 - 12
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellLevelPanel.java

@@ -16,10 +16,10 @@ 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;
-import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 
 import lombok.AccessLevel;
@@ -35,10 +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 Map<DDSpellbook, Collection<DDSpell>> spells = new HashMap<>();
-//		dclass.getSpellBook().forEach(sb -> spells.put(sb, getSpells.apply(sb, 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};
@@ -69,23 +66,31 @@ 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();
-			for (int i = 0; i < dclass.getSpellBook().size(); ++i) {
-				final DDSpellbook sb = dclass.getSpellBook().get(i);
+			{
+				final DDSpellbook sb = wrap.getMain();
 				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())));
 			}
+			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().toArray(new Observable[0]));
+		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) {
-		// TODO figure out what to do about that get(0)
-		this(new SpellsKnownHeader(level, dclass.getSpellBook().get(0), ability),
+		this(new SpellsKnownHeader(level, dclass.getSpellBook().get().getMain(), ability),
 				chara, dclass, level, DDSpellbook::spellsKnownAtLevel);
 	}
 	

+ 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(), ability), chara, dclass, level, DDSpellbook::spellsPreparedAtLevel);
+		super(new SpellsPerDayHeader(level, dclass.getSpellBook().get(), ability), 
+				chara, dclass, level, DDSpellbook::spellsPreparedAtLevel);
 	}
 	
 	public boolean isCastableFromHere() { return true; }

+ 5 - 4
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;
 
@@ -21,11 +22,11 @@ class SpellMenu extends JPopupMenu {
 
 	public SpellMenu(DDCharacter chara, final DDCharacterClass dclass, final DDSpellbook book,
 			final DDSpell spell, boolean isPrepared) {
-		final int spellLevel = spell.getClassLevel(dclass.getName());
+		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);
@@ -35,11 +36,11 @@ class SpellMenu extends JPopupMenu {
 		if (isPrepared) {
 			JMenuItem mntmCast = new JMenuItem("Cast");
 			mntmCast.addActionListener(e -> {
-				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);
+				book.castSpell(spellLevel.level, spell);
 				ObserverDispatch.notifySubscribers(book, null);
 			});
 			add(mntmCast);

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

@@ -10,13 +10,12 @@ 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.model.DDCharacterClass.DDSpellbookWrapper;
 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;
@@ -65,18 +64,14 @@ public class SpellPanel extends JPanel {
 				new AppendSpellLevelOperation(chara, ability, SpellLevelPerDayPanel::new));
 		listenerKnown = new IndirectObservableListener<>(known, 
 				new AppendSpellLevelOperation(chara, ability, SpellLevelPanel::new));
-		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);
+		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());			
 		}
-		return rval;
 	}
 
 	@RequiredArgsConstructor
@@ -90,14 +85,16 @@ public class SpellPanel extends JPanel {
 		@Override
 		public void accept(final JPanel root, final DDCharacterClass dclass) {		
 			for (int i = previousHighestSpellLevel; i < dclass.getHighestSpellLevel(); ++i) {
-				if (hasSpellsKnownAtLevel(dclass, i)) break;
+				if (hasNoSpellsKnownAtLevel(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);
+		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);
 		}
 	}
 	

+ 12 - 10
src/main/lombok/org/leumasjaffe/charsheet/view/magic/SpellsPerDayHeader.java

@@ -3,16 +3,14 @@ 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;
 import javax.swing.JTextField;
 
 import org.leumasjaffe.charsheet.model.Ability;
-import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.charsheet.model.DDCharacterClass.DDSpellbookWrapper;
 import org.leumasjaffe.observer.IndirectObservableListener;
-import org.leumasjaffe.observer.Observable;
 import org.leumasjaffe.observer.ObserverDispatch;
 
 import java.awt.Dimension;
@@ -24,9 +22,9 @@ class SpellsPerDayHeader extends JPanel {
 	 */
 	private static final long serialVersionUID = 1L;
 
-	IndirectObservableListener<JTextField, List<DDSpellbook>> listener;
+	IndirectObservableListener<JTextField, DDSpellbookWrapper> listener;
 
-	public SpellsPerDayHeader(int level, List<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, 35, 0, 45, 0, 45, 0, 0};
@@ -109,15 +107,19 @@ class SpellsPerDayHeader extends JPanel {
 		textFieldOutOf.setEditable(false);
 		textFieldOutOf.setColumns(10);
 		
-		listener = new IndirectObservableListener<JTextField, List<DDSpellbook>>(textFieldRemaining, (c, v) -> {
+		listener = new IndirectObservableListener<JTextField, DDSpellbookWrapper>(textFieldRemaining, (c, v) -> {
 			c.setText(getSpellPerDayListing(level, v));
 		});
-		listener.setObserved(model, model.toArray(new Observable[0]));
+		if (model.getSecondary().isPresent()) {
+			listener.setObserved(model, model.getMain(), model.getSecondary().get());
+		} else {
+			listener.setObserved(model, model.getMain());
+		}
 	}
 	
-	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);
+	private String getSpellPerDayListing(int level, DDSpellbookWrapper wrap) {
+		return wrap.getMain().numSpellsPerDayAtLevel(level) +
+				wrap.getSecondary().map(sb -> "+" + sb.numSpellsPerDayAtLevel(level)).orElse("");
 	}
 
 	@Override