Parcourir la source

Merge branch 'hitdice'

* hitdice:
  Adding HitDice panel and update to level up. Adding Ability update to level up commit.
  Fix bug in spell selection
  Make BoolGate much cleaner in design
  Add hit dice to each class's JSON def
Sam Jaffe il y a 8 ans
Parent
commit
17e921323e

+ 1 - 0
resources/classes/Bard.json

@@ -1,5 +1,6 @@
 {
   "name":"Bard",
+  "hitDice":6,
   "bab":"AVERAGE",
   "fort":"POOR",
   "ref":"GOOD",

+ 1 - 0
resources/classes/Cleric.json

@@ -1,5 +1,6 @@
 {
   "name":"Cleric",
+  "hitDice":8,
   "bab":"AVERAGE",
   "fort":"GOOD",
   "ref":"POOR",

+ 1 - 0
resources/classes/Druid.json

@@ -1,5 +1,6 @@
 {
   "name":"Druid",
+  "hitDice":8,
   "bab":"AVERAGE",
   "fort":"GOOD",
   "ref":"POOR",

+ 1 - 0
resources/classes/Wizard.json

@@ -1,5 +1,6 @@
 {
   "name":"Wizard",
+  "hitDice":4,
   "bab":"POOR",
   "fort":"POOR",
   "ref":"POOR",

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

@@ -42,6 +42,7 @@ public class DDClass {
 	@NonNull String name;
 	
 	int skillPoints;
+	int hitDice;
 	@NonNull AttackQuality bab;
 	@NonNull SaveQuality fort;
 	@NonNull SaveQuality ref;

+ 34 - 2
src/main/lombok/org/leumasjaffe/charsheet/model/observable/BoolGate.java

@@ -1,8 +1,10 @@
 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;
@@ -14,18 +16,42 @@ public class BoolGate extends Observable.Instance {
 	boolean[] data;
 	int size;
 	
-	@AllArgsConstructor
+	@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(Object source, boolean bool) {
+		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) {
@@ -56,4 +82,10 @@ public class BoolGate extends Observable.Instance {
 	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;
+	}
 }

+ 12 - 4
src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_AbilityPanel.java

@@ -57,7 +57,7 @@ public class LU_AbilityPanel extends JPanel {
 		this.ability = clone(info.ddCharacter.getAbilities());
 		this.gate = gate;
 		this.canGainAbility = info.ddCharacter.getLevel() % 4 == 3;
-		this.gate.set(null, !canGainAbility);
+		this.gate.set(!canGainAbility);
 		ButtonGroup group = new ButtonGroup();
 
 		GridBagLayout gridBagLayout = new GridBagLayout();
@@ -68,10 +68,18 @@ public class LU_AbilityPanel extends JPanel {
 		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.insets = new Insets(0, 0, 5, 0);
+		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);
@@ -111,7 +119,7 @@ public class LU_AbilityPanel extends JPanel {
 		panel_2.add(btnClear, gbc_btnClear);
 		btnClear.setEnabled(canGainAbility);
 		btnClear.addActionListener(e -> {
-			gate.set(null, false);
+			gate.set(false);
 			group.clearSelection();
 			resetAbility();
 		});
@@ -156,7 +164,7 @@ public class LU_AbilityPanel extends JPanel {
 			resetAbility();
 			setCurrentSelection(y);
 			updateAbility(this.ability, true);
-			gate.set(null, true);
+			gate.set(true);
 		});
 		radioButton.setEnabled(canGainAbility);
 	}

+ 59 - 16
src/main/lombok/org/leumasjaffe/charsheet/view/level/LU_FeaturesPanel.java

@@ -19,42 +19,85 @@ import java.awt.Insets;
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 class LU_FeaturesPanel extends JPanel {
 	static int HD_INDEX = 0, ABIL_INDEX = 1, FEAT_INDEX = 2;
-	ObservableListener<LU_FeaturesPanel, BoolGate> listener;
-	
-	public LU_FeaturesPanel(LevelUpClassInfo info, BoolGate.Handle ready) {		
-		BoolGate gate = new BoolGate(3);
+	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, 0.0, Double.MIN_VALUE};
+		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 features = new JPanel(new VerticalLayout(2));
+		JPanel panel = new JPanel();
 		GridBagConstraints gbc_panel = new GridBagConstraints();
-		gbc_panel.insets = new Insets(0, 0, 0, 5);
 		gbc_panel.fill = GridBagConstraints.BOTH;
-		gbc_panel.gridx = 0;
+		gbc_panel.gridx = 1;
 		gbc_panel.gridy = 0;
-		add(features, gbc_panel);
+		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()));
 		});
 		
-		LU_AbilityPanel abilPanel = new LU_AbilityPanel(info, gate.handle(ABIL_INDEX));
+		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 = 1;
+		gbc_abilPanel.gridx = 0;
 		gbc_abilPanel.gridy = 0;
-		add(abilPanel, gbc_abilPanel);
+		panel.add(abilPanel, gbc_abilPanel);
 		
-		this.listener = new ObservableListener<>(this, (c, v) -> {
-			ready.set(null, v.all());
-		});
-		this.listener.setObserved(gate);
+		this.listener = gate.makeListener();
+	}
+	
+	public void commitAllChanges() {
+		this.hdPanel.commitChange(info.ddCharacter.getHealth());
+		this.abilPanel.commitChange(info.ddCharacter.getAbilities());
 	}
 	
 	@Override

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

+ 3 - 6
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpSpellPanel.java

@@ -67,7 +67,7 @@ class LevelUpSpellPanel extends JPanel {
 	SpellPicker pick;
 	SelectSpellsPanel.Info info;
 	@Getter List<SelectSpellsPanel> panels;
-	ObservableListener<LevelUpSpellPanel, BoolGate> allReady;
+	ObservableListener<Void, BoolGate.Meta> allReady;
 
 	public LevelUpSpellPanel(SpellPicker pick, SelectSpellsPanel.Info info, 
 			BoolGate.Handle readyCount) {
@@ -99,7 +99,7 @@ class LevelUpSpellPanel extends JPanel {
 		final IntValue val = getSharedAllowedSlots(info);
 		final Map<Integer, Integer> spells = getNewSpells(val);
 		final int sharedSlots = val.value();
-		BoolGate gate = new BoolGate(newHighestSpellLevel);
+		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;
@@ -110,10 +110,7 @@ class LevelUpSpellPanel extends JPanel {
 			panels.add(lvl);
 			panel.add(lvl);
 		}
-		allReady = new ObservableListener<>(this, (c, v) -> {
-			readyCount.set(this, v.all());
-		});
-		allReady.setObserved(gate);
+		allReady = gate.makeListener();
 	}
 	
 	private Map<Integer, Integer> getNewSpells(IntValue sharedSpellCountLimit) {

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

@@ -52,6 +52,8 @@ class UpdateClassWithLevelPanel extends JPanel {
 	BoolGate readyCount = new BoolGate(4);
 	ObservableListener<Consumer<Boolean>, BoolGate> listener;
 	@NonFinal ObservableListener<UpdateClassWithLevelPanel, BoolGate> learnAndPrepareListener = null;
+
+	LU_FeaturesPanel featurePanel;
 	
 	public UpdateClassWithLevelPanel(LevelUpClassInfo info, VoidVoidFunction back,
 			Consumer<Boolean> setReady) {
@@ -72,8 +74,8 @@ class UpdateClassWithLevelPanel extends JPanel {
 		gbc_tabbedPane.gridy = 0;
 		add(tabbedPane, gbc_tabbedPane);
 
-		JPanel features = new LU_FeaturesPanel(info, readyCount.handle(FEATURE_INDEX));
-		tabbedPane.addTab("Features", null, features, null);
+		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) {
@@ -120,10 +122,7 @@ class UpdateClassWithLevelPanel extends JPanel {
 		gbc_horizontalGlue.gridy = 0;
 		panel.add(horizontalGlue, gbc_horizontalGlue);
 
-		listener = new ObservableListener<>(setReady, (c, v) -> {
-			c.accept(v.all());
-		});
-		listener.setObserved(readyCount);
+		listener = readyCount.makeListener(setReady);
 	}
 
 	private void createPanelForPrepareSpells() {
@@ -212,6 +211,7 @@ class UpdateClassWithLevelPanel extends JPanel {
 		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());

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

@@ -20,6 +20,7 @@ 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;
@@ -30,7 +31,7 @@ import javax.swing.ScrollPaneConstants;
 public class PrepareSpellsDialog extends JPanel {	
 	int[] ready = {0};
 	int highestSpellLevel;
-	ObservableListener<PrepareSpellsDialog, BoolGate> allReady;
+	ObservableListener<Consumer<Boolean>, BoolGate> allReady;
 
 	public PrepareSpellsDialog(DDCharacter chara, DDCharacterClass dclass) {
 		highestSpellLevel = dclass.getHighestSpellLevel();
@@ -77,10 +78,7 @@ public class PrepareSpellsDialog extends JPanel {
 		
 		List<SelectPreparedSpellsPanel> panels = new ArrayList<>();
 		final BoolGate gate = new BoolGate(highestSpellLevel);
-		allReady = new ObservableListener<>(this, (c, v) -> {
-			btnPrepareTheseSpells.setEnabled(v.all());
-		});
-		allReady.setObserved(gate);
+		allReady = gate.makeListener(btnPrepareTheseSpells::setEnabled);
 		for (int i = 0; i < highestSpellLevel; ++i) {
 			SelectPreparedSpellsPanel lvl = new SelectPreparedSpellsPanel(chara, dclass, gate.handle(i), i);
 			panels.add(lvl);

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

@@ -232,7 +232,7 @@ public class SelectSpellsPanel extends JPanel {
 				for (Object o : modelPrepared.data) {
 					this.prepared.add(DDSpell.fromString((String) o)); // TODO
 				}
-				gate.set(this, true);
+				gate.set(true);
 			}
 		});
 	}
@@ -267,7 +267,7 @@ public class SelectSpellsPanel extends JPanel {
 			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];
+				modelPrepared.data[j] = modelKnown.data[rows[i]];
 				sharedValue.value(sharedValue.value() - 1);
 				break;
 			}