Jelajahi Sumber

Making spell level up allow the selection of new known spells

Sam Jaffe 8 tahun lalu
induk
melakukan
51abfbc5a6

+ 6 - 3
resources/classes/Bard.json

@@ -48,14 +48,17 @@
     "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"]
     ]
   }
 }

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

@@ -93,13 +93,18 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 	}
 	
 	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;
+		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());

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

@@ -17,7 +17,7 @@ public abstract class DDSpellbook extends Observable {
 	
 	@NonNull public List<DDSpell> getSpellsPreparedPreviouslyForLevel(int level) { return spellsPreparedAtLevel(level); }	
 
-
+	public boolean learnsSpells() { return false; }
 	public boolean preparesSpells() { return false; }
 	
 	public int numSpellsKnownAtLevel( int level ) {
@@ -32,5 +32,5 @@ 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 abstract void prepareSpells(int level, Collection<DDSpell> collection);
 }

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

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

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

@@ -26,6 +26,11 @@ public class Researched extends Prepared {
 	
 	@NonNull Map<Integer, Researched.Level> spellInfo;
 
+	@Override
+	public boolean learnsSpells() {
+		return true;
+	}
+
 	@Override
 	public int numSpellsPerDayAtLevel( int level ) {
 		return get(level).spellsPerDay;
@@ -60,7 +65,7 @@ public class Researched extends Prepared {
 	}
 
 	@Override
-	public void prepareSpells(int level, List<DDSpell> spells) {
+	public void prepareSpells(int level, Collection<DDSpell> spells) {
 		final Level lInfo = get(level);
 		if (!lInfo.spellsKnown.containsAll(spells)) {
 			throw new IllegalArgumentException("Attempted to prepare spells that you don't know");

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

@@ -26,6 +26,11 @@ public class Spontaneous extends DDSpellbook {
 	
 	@NonNull Map<Integer, Spontaneous.Level> spellInfo;
 	
+	@Override
+	public boolean learnsSpells() {
+		return true;
+	}
+	
 	@Override
 	public int numSpellsPerDayAtLevel(int level) {
 		return get(level).spellsPerDay;
@@ -63,7 +68,7 @@ public class Spontaneous extends DDSpellbook {
 	}
 
 	@Override
-	public void prepareSpells(int level, List<DDSpell> spells) {
+	public void prepareSpells(int level, Collection<DDSpell> spells) {
 		final Level lInfo = get(level);
 		lInfo.spellsPerDayRemaining = lInfo.spellsPerDay;
 	}

+ 74 - 7
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpSpellPanel.java

@@ -2,22 +2,89 @@ package org.leumasjaffe.charsheet.view.level;
 
 import javax.swing.JPanel;
 
-import org.leumasjaffe.charsheet.model.DDClass;
-import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.view.level.UpdateClassWithLevelPanel.BoolArray;
+import org.leumasjaffe.charsheet.view.magic.SelectSpellsPanel;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
 import java.awt.GridBagLayout;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import javax.swing.JScrollPane;
+import java.awt.GridBagConstraints;
 
 @SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
 class LevelUpSpellPanel extends JPanel {
 
-	public LevelUpSpellPanel(DDSpellbook sb, DDClass base, int toLevel, BoolArray readyCount) {
+	int[] ready = {0};
+	@NonFinal int spellLevelsGrown = 0;
+	int oldHighestSpellLevel, newHighestSpellLevel;
+
+	public LevelUpSpellPanel(SelectSpellsPanel.Info info, int toLevel, BoolArray readyCount) {
+		newHighestSpellLevel = info.dclass.getHighestSpellLevel(toLevel);
+		oldHighestSpellLevel = info.dclass.getHighestSpellLevel();
+		
 		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.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);
+		
+		final List<SelectSpellsPanel> panels = new ArrayList<>();
+		final List<List<Integer>> spellList = info.dclass.getProto().getSpells().get().getKnown();
+		final List<Integer> spellsAtPreviousLevel = toLevel == 1 ? Collections.emptyList() :
+			spellList.get(toLevel-2);
+		final List<Integer> spellsAtCurrentLevel = spellList.get(toLevel-1);
+		for (int i = 0; i < newHighestSpellLevel; ++i) {
+			++spellLevelsGrown;
+			final int newSpells = diff(spellsAtCurrentLevel, spellsAtPreviousLevel, i,
+					isNewSpellCircle(i));
+			SelectSpellsPanel lvl = new SelectSpellsPanel(info, i,
+					new LinkedHashSet<>(), newSpells, getAvailableSpells(info, i), false);
+			panels.add(lvl);
+			lvl.addPropertyChangeListener(SelectSpellsPanel.READY, e -> {
+				if ((Boolean) e.getNewValue()) ++ready[0];
+				else --ready[0];
+				readyCount.data[1] = (ready[0] == spellLevelsGrown);
+				ObserverDispatch.notifySubscribers(readyCount, this);
+			});
+			panel.add(lvl);
+		}
+	}
+
+	private boolean isNewSpellCircle(int i) {
+		return i == (newHighestSpellLevel - 1) && oldHighestSpellLevel != newHighestSpellLevel;
+	}
+
+	private int diff(List<Integer> current, List<Integer> previous, int level, boolean newSpellLevel) {
+		return current.get(level) - (newSpellLevel ? 0 : previous.get(level));
+	}
 
+	private Collection<DDSpell> getAvailableSpells(SelectSpellsPanel.Info info, int i) {
+		Collection<DDSpell> spells = new ArrayList<>(info.dclass.getBase().getSpellList(i));
+		spells.removeAll(info.dclass.getSpellBook().get().spellsKnownAtLevel(i));
+		return spells;
 	}
 
 }

+ 4 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/level/UpdateClassWithLevelPanel.java

@@ -6,6 +6,7 @@ import java.util.stream.IntStream;
 import javax.swing.JPanel;
 
 import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.view.magic.SelectSpellsPanel;
 import org.leumasjaffe.charsheet.view.skills.SkillLevelUpPanel;
 import org.leumasjaffe.function.VoidVoidFunction;
 import org.leumasjaffe.observer.Observable;
@@ -66,7 +67,9 @@ class UpdateClassWithLevelPanel extends JPanel {
 		tabbedPane.addTab("Skills", null, skills, null);
 		
 		info.ddClass.getSpellBook().ifPresent(sb -> {
-			JPanel spells = new LevelUpSpellPanel(sb, info.ddClass.getBase(), info.toLevel, readyCount);
+			if (!sb.learnsSpells()) return;
+			JPanel spells = new LevelUpSpellPanel(new SelectSpellsPanel.Info(info.ddCharacter, info.ddClass),
+					info.toLevel, readyCount);
 			tabbedPane.addTab("Spells", null, spells, null);
 		});
 		

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

@@ -3,11 +3,9 @@ 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.model.DDCharacter;
 import org.leumasjaffe.charsheet.model.DDCharacterClass;
 import org.leumasjaffe.charsheet.model.magic.impl.Prepared;
-import org.leumasjaffe.charsheet.util.AbilityHelper;
 
 import lombok.AccessLevel;
 import lombok.experimental.FieldDefaults;
@@ -78,12 +76,9 @@ 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<>();
 		for (int i = 0; i < highestSpellLevel; ++i) {
-			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, i, dclass, score);
+			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, i, dclass);
 			panels.add(lvl);
 			lvl.addPropertyChangeListener(SelectPreparedSpellsPanel.READY, e -> {
 				if ((Boolean) e.getNewValue()) ++ready[0];
@@ -93,6 +88,7 @@ public class PrepareSpellsDialog extends JPanel {
 			panel.add(lvl);
 		}
 		
+		final Prepared spellBook = (Prepared) dclass.getSpellBook().get();
 		btnPrepareTheseSpells.addActionListener(e -> {
 			((JDialog) this.getParent().getParent().getParent()).dispose();
 			for (int i = 0; i < highestSpellLevel; ++i) {

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

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

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

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