Browse Source

Merge branch 'master' into spellbook

Conflicts:
	src/org/leumasjaffe/charsheet/model/DDClass.java
Sam Jaffe 8 năm trước cách đây
mục cha
commit
3655699054

+ 11 - 0
resources/Potato.json

@@ -50,6 +50,17 @@
     }
   },
   
+  "skills":[
+    {"name":"Concentration","ranks":3,"pointsSpent":3},
+    {"name":"Craft (blacksmithing)","ranks":4,"pointsSpent":4},
+    {"name":"Diplomacy","ranks":1,"pointsSpent":1},
+    {"name":"Heal","ranks":4,"pointsSpent":4},
+    {"name":"Knowledge (arcana)","ranks":6,"pointsSpent":6},
+    {"name":"Knowledge (religion)","ranks":5,"pointsSpent":5},
+    {"name":"Knowledge (nature)","ranks":6,"pointsSpent":6},
+    {"name":"Perform (sing)","ranks":3,"pointsSpent":3}
+  ],
+  
   "inventory":{
     "items":[
       {

+ 28 - 0
resources/classes/Bard.json

@@ -14,6 +14,34 @@
     ],
     []
   ],
+  "skillPoints":6,
+  "skills":[
+    "Appraise",
+    "Balance",
+    "Bluff",
+    "Climb",
+    "Concentration",
+    "Craft (*)",
+    "Decipher Script",
+    "Diplomacy",
+    "Disguise",
+    "Escape Artist",
+    "Gather Information",
+    "Hide",
+    "Jump",
+    "Knowledge (*)",
+    "Listen",
+    "Move Silently",
+    "Perform (*)",
+    "Profession (*)",
+    "Sense Motive",
+    "Sleight of Hand",
+    "Speak Language",
+    "Spellcraft",
+    "Swim",
+    "Tumble",
+    "Use Magic Device"
+  ],
   "spells":{
     "@type":"Spontaneous",
     "group":"ARCANE",

+ 13 - 0
resources/classes/Cleric.json

@@ -11,6 +11,19 @@
       }
     ]
   ],
+  "skillPoints":2,
+  "skills":[
+    "Concentration",
+    "Craft (*)",
+    "Diplomacy",
+    "Heal",
+    "Knowledge (arcana)",
+    "Knowledge (history)",
+    "Knowledge (religion)",
+    "Knowledge (the planes)",
+    "Profession (*)",
+    "Spellcraft"
+  ],
   "spells":{
     "@type":"Inspired",
     "group":"DIVINE",

+ 38 - 0
resources/skills/skills.json

@@ -0,0 +1,38 @@
+[
+  {"name":"Appraise","ability":"INT","requiresTraining":false},
+  {"name":"Balance","ability":"DEX","requiresTraining":false},
+  {"name":"Bluff","ability":"CHA","requiresTraining":false},
+  {"name":"Climb","ability":"STR","requiresTraining":false},
+  {"name":"Concentration","ability":"CON","requiresTraining":false},
+  {"name":"Decipher Script","ability":"INT","requiresTraining":true},
+  {"name":"Diplomacy","ability":"CHA","requiresTraining":false},
+  {"name":"Disable Device","ability":"INT","requiresTraining":true},
+  {"name":"Disguise","ability":"CHA","requiresTraining":false},
+  {"name":"Escape Artist","ability":"DEX","requiresTraining":false},
+  {"name":"Forgery","ability":"INT","requiresTraining":false},
+  {"name":"Gather Information","ability":"CHA","requiresTraining":false},
+  {"name":"Handle Animal","ability":"CHA","requiresTraining":true},
+  {"name":"Heal","ability":"WIS","requiresTraining":false},
+  {"name":"Hide","ability":"DEX","requiresTraining":false},
+  {"name":"Intimidate","ability":"CHA","requiresTraining":false},
+  {"name":"Jump","ability":"STR","requiresTraining":false},
+  {"name":"Listen","ability":"WIS","requiresTraining":false},
+  {"name":"Move Silently","ability":"DEX","requiresTraining":false},
+  {"name":"Open Lock","ability":"DEX","requiresTraining":true},
+  {"name":"Ride","ability":"DEX","requiresTraining":false},
+  {"name":"Search","ability":"INT","requiresTraining":false},
+  {"name":"Sense Motive","ability":"WIS","requiresTraining":false},
+  {"name":"Sleight of Hand","ability":"DEX","requiresTraining":true},
+  {"name":"Speak Language","requiresTraining":true},
+  {"name":"Spellcraft","ability":"INT","requiresTraining":true},
+  {"name":"Spot","ability":"WIS","requiresTraining":false},
+  {"name":"Survival","ability":"WIS","requiresTraining":false},
+  {"name":"Swim","ability":"STR","requiresTraining":false},
+  {"name":"Tumble","ability":"DEX","requiresTraining":true},
+  {"name":"Use Magic Device","ability":"CHA","requiresTraining":true},
+  {"name":"Use Rope","ability":"DEX","requiresTraining":false},
+  {"name":"Craft (*)","ability":"INT","requiresTraining":false},
+  {"name":"Knowledge (*)","ability":"INT","requiresTraining":true},
+  {"name":"Perform (*)","ability":"CHA","requiresTraining":true},
+  {"name":"Profession (*)","ability":"WIS","requiresTraining":true}
+]

+ 11 - 0
src/org/leumasjaffe/charsheet/model/DDCharacter.java

@@ -7,6 +7,7 @@ import java.util.Set;
 
 import org.leumasjaffe.charsheet.model.inventory.DDInventory;
 import org.leumasjaffe.charsheet.model.observable.ObjectValue;
+import org.leumasjaffe.charsheet.model.skill.DDSkills;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
@@ -46,6 +47,8 @@ public class DDCharacter {
 	
 	@NonNull Ability abilities = new Ability();
 	
+	@NonNull DDSkills skills = new DDSkills(Collections.emptyList());
+	
 	@NonNull DDInventory inventory = new DDInventory();
 
 	public String getClassAndLevelString() {
@@ -77,4 +80,12 @@ public class DDCharacter {
 	public int getWillSave() {
 		return classes.stream().mapToInt(c -> c.getWill()).sum();
 	}
+	
+	public boolean isClassSkill(final String skillName) {
+		return classes.stream().anyMatch( cc -> cc.isClassSkill(skillName) );
+	}
+
+	public int getLevel() {
+		return classes.stream().mapToInt(DDCharacterClass::getLevel).sum();
+	}
 }

+ 8 - 0
src/org/leumasjaffe/charsheet/model/DDCharacterClass.java

@@ -48,6 +48,10 @@ public class DDCharacterClass {
 	public String toString() {
 		return getName() + " " + getLevel();
 	}
+	
+	public int getSkillPoints() {
+		return name.base.getSkillPoints();
+	}
 
 	public int getBab() {
 		return name.base.getBab().getBonus(level);
@@ -64,4 +68,8 @@ public class DDCharacterClass {
 	public int getWill() {
 		return name.base.getWill().getBonus(level);
 	}
+	
+	public boolean isClassSkill(final String skill) {
+		return name.base.isClassSkill(skill);
+	}
 }

+ 11 - 0
src/org/leumasjaffe/charsheet/model/DDClass.java

@@ -4,6 +4,7 @@ import java.io.File;
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.DDSpellList;
@@ -25,6 +26,7 @@ import lombok.experimental.FieldDefaults;
 public class DDClass {
 	@NonNull String name;
 	
+	int skillPoints;
 	@NonNull AttackQuality bab;
 	@NonNull SaveQuality fort;
 	@NonNull SaveQuality ref;
@@ -32,6 +34,8 @@ public class DDClass {
 	
 	@Getter(AccessLevel.NONE) @NonNull List<List<DDFeature>> features;
 	
+	@NonNull Set<String> skills;
+	
 	@Getter(AccessLevel.PACKAGE) @NonNull Optional<DDSpellList> spells;
 	
 	@SneakyThrows
@@ -44,4 +48,11 @@ public class DDClass {
 	public Collection<DDSpell> getSpellList( int level ) {
 		return spells.get().getSpellList().get( level ).getSpellList();
 	}
+	
+	public boolean isClassSkill(final String skillName) {
+		if (skillName.contains("(")) {
+			return skills.contains(skillName) || skills.contains(skillName.replaceFirst("\\(.*\\)", "(*)"));
+		}
+		return skills.contains(skillName);
+	}
 }

+ 33 - 0
src/org/leumasjaffe/charsheet/model/skill/DDSkill.java

@@ -0,0 +1,33 @@
+package org.leumasjaffe.charsheet.model.skill;
+
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.experimental.Delegate;
+import lombok.Setter;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@Data
+@FieldDefaults(level=AccessLevel.PRIVATE)
+public class DDSkill {
+	final @Getter(AccessLevel.NONE) @Delegate DDSkillPrototype name;
+
+//	boolean isClassSkill = false;
+	@Setter(value=AccessLevel.PRIVATE) IntValue ranks = new IntValue(0); // CC skills are 2/1 point, you cannot have half-ranks
+	// This would be 2x ranks if cross-class, 1x if class.
+	// Unless you gain it as a class skill later, in which case it might be in-between
+	@Setter(value=AccessLevel.PRIVATE) int pointsSpent = 0;
+	
+	public DDSkill(DDSkillPrototype proto) {
+		this.name = proto;
+	}
+	
+	public void spendPoints(int ranks, boolean isCC) {
+		this.ranks.value(this.ranks.value() + ranks);
+		this.pointsSpent += (ranks * (isCC ? 2 : 1));
+	}
+}

+ 65 - 0
src/org/leumasjaffe/charsheet/model/skill/DDSkillPrototype.java

@@ -0,0 +1,65 @@
+package org.leumasjaffe.charsheet.model.skill;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@Data
+@FieldDefaults(level=AccessLevel.PRIVATE)
+public class DDSkillPrototype {
+	final String name;
+	final boolean requiresTraining;
+	String ability;
+	
+	private static final Map<String, DDSkillPrototype> prototypes;
+	
+	static {
+		Map<String, DDSkillPrototype> tmp = new HashMap<>();
+		final ObjectMapper mapper = new ObjectMapper();
+		mapper.registerModule(new Jdk8Module());
+		try {
+			final Collection<DDSkillPrototype> col = mapper.readValue(new File("resources/skills/skills.json"), 
+					mapper.getTypeFactory().constructCollectionType(Collection.class, DDSkillPrototype.class));
+			tmp = col.stream().collect(Collectors.toMap(t -> t.getName(), t -> t));
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		prototypes = Collections.unmodifiableMap(tmp);
+	}
+	
+	public DDSkillPrototype(String name) {
+		DDSkillPrototype base = getPrototype(name).get();
+		this.name = name;
+		this.requiresTraining = base.requiresTraining;
+		this.ability = base.ability;
+	}
+	
+	public static Optional<DDSkillPrototype> getPrototype(String name) {
+		if (name.contains("(")) {
+			DDSkillPrototype proto = prototypes.get(name.replaceFirst("\\(.*\\)", "(*)"));
+			if ( proto == null ) return Optional.empty();
+			return Optional.of(new DDSkillPrototype(name, proto.requiresTraining, proto.ability));
+		} else {
+			return Optional.ofNullable(prototypes.get(name));
+		}
+	}
+
+	public static Stream<DDSkillPrototype> untrained() {
+		return prototypes.values().stream().filter(p -> !p.requiresTraining && !p.getName().contains("(*)"));
+	}
+}

+ 33 - 0
src/org/leumasjaffe/charsheet/model/skill/DDSkills.java

@@ -0,0 +1,33 @@
+package org.leumasjaffe.charsheet.model.skill;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class DDSkills {
+	Map<String, DDSkill> skills = new TreeMap<>();
+	
+	@JsonCreator
+	public DDSkills(Collection<DDSkill> in) {
+		skills.putAll(DDSkillPrototype.untrained().collect(Collectors.toMap(t -> t.getName(), t -> new DDSkill(t))));
+		skills.putAll(in.stream().collect(Collectors.toMap(t -> t.getName(), t -> t)));
+	}
+	
+	@JsonValue
+	private Collection<DDSkill> getSerial() {
+		return skills.values().stream().filter(s -> s.getPointsSpent() > 0).collect(Collectors.toList());
+	}
+
+	public Collection<DDSkill> getSkills() {
+		return Collections.unmodifiableCollection(skills.values());
+	}
+}

+ 4 - 0
src/org/leumasjaffe/charsheet/util/StringHelper.java

@@ -9,6 +9,10 @@ public class StringHelper {
 		else { return o.toString(); }
 	}
 	
+	public String toString(float f) {
+		return Float.toString(f);
+	}
+	
 	public String toString(int i) {
 		return Integer.toString(i);
 	}

+ 10 - 3
src/org/leumasjaffe/charsheet/view/D20Sheet.java

@@ -21,6 +21,7 @@ import javax.swing.JMenuItem;
 import javax.swing.KeyStroke;
 
 import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.view.dev.DeveloperMenu;
 
 import java.awt.event.KeyEvent;
 import java.awt.event.WindowEvent;
@@ -43,8 +44,9 @@ public class D20Sheet extends JFrame {
 	SummaryTab summaryTab;
 	List<ClassTab> classTabs = new ArrayList<>();
 	JPanel abilitiesTab;
-	JPanel skillTab;
+	SkillTab skillTab;
 	EquipmentTab equipmentTab;
+	private DeveloperMenu developerMenu;
 	
 	public D20Sheet() {
 		// Set up local state variables
@@ -57,7 +59,7 @@ public class D20Sheet extends JFrame {
 		
 		summaryTab = new SummaryTab();
 		abilitiesTab = new JPanel();
-		skillTab = new JPanel();
+		skillTab = new SkillTab();
 		equipmentTab = new EquipmentTab();
 		
 		JMenuBar menuBar = new JMenuBar();
@@ -99,7 +101,10 @@ public class D20Sheet extends JFrame {
 		mntmExit.addActionListener( e -> { this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); } );
 		mntmExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.CTRL_MASK));
 		mnFile.add(mntmExit);
-
+		
+		developerMenu = new DeveloperMenu();
+		menuBar.add(developerMenu);
+		
 		// Set up post-GUI dependencies
 		setModel(model);
 	}
@@ -140,6 +145,8 @@ public class D20Sheet extends JFrame {
 		summaryTab.setModel(model);
 		model.getClasses().stream().forEach(cc -> classTabs.add(new ClassTab(cc)));
 		equipmentTab.setModel(model);
+		skillTab.setModel(model);
+		developerMenu.setModel(model);
 		
 		reorderTabs();
 	}

+ 142 - 0
src/org/leumasjaffe/charsheet/view/SkillTab.java

@@ -0,0 +1,142 @@
+package org.leumasjaffe.charsheet.view;
+
+import javax.swing.JPanel;
+import java.awt.GridBagLayout;
+import javax.swing.JScrollPane;
+import java.awt.GridBagConstraints;
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.skill.DDSkills;
+import org.leumasjaffe.charsheet.view.skills.SkillLine;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import java.awt.Dimension;
+import java.awt.Insets;
+import javax.swing.JLabel;
+import javax.swing.JTextField;
+import java.awt.Component;
+import javax.swing.Box;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class SkillTab extends JPanel {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	JPanel skillPanel;
+	JTextField txtTotalSkillPoints;
+	JTextField txtClassSkills;
+	JTextField txtCrossClass;
+
+	public SkillTab() {
+		setPreferredSize(new Dimension(600, 300));
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.insets = new Insets(0, 0, 5, 0);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0, 0, 0, 0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblTotalSkillPoints = new JLabel("Total Skill Points:");
+		GridBagConstraints gbc_lblTotalSkillPoints = new GridBagConstraints();
+		gbc_lblTotalSkillPoints.insets = new Insets(0, 0, 0, 5);
+		gbc_lblTotalSkillPoints.anchor = GridBagConstraints.EAST;
+		gbc_lblTotalSkillPoints.gridx = 0;
+		gbc_lblTotalSkillPoints.gridy = 0;
+		panel.add(lblTotalSkillPoints, gbc_lblTotalSkillPoints);
+		
+		txtTotalSkillPoints = new JTextField();
+		txtTotalSkillPoints.setEditable(false);
+		GridBagConstraints gbc_txtTotalSkillPoints = new GridBagConstraints();
+		gbc_txtTotalSkillPoints.insets = new Insets(0, 0, 0, 5);
+		gbc_txtTotalSkillPoints.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtTotalSkillPoints.gridx = 1;
+		gbc_txtTotalSkillPoints.gridy = 0;
+		panel.add(txtTotalSkillPoints, gbc_txtTotalSkillPoints);
+		txtTotalSkillPoints.setColumns(10);
+		
+		Component horizontalStrut = Box.createHorizontalStrut(20);
+		GridBagConstraints gbc_horizontalStrut = new GridBagConstraints();
+		gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5);
+		gbc_horizontalStrut.gridx = 2;
+		gbc_horizontalStrut.gridy = 0;
+		panel.add(horizontalStrut, gbc_horizontalStrut);
+		
+		JLabel lblMaxRanksClasscross = new JLabel("Max Ranks Class/Cross Class:");
+		GridBagConstraints gbc_lblMaxRanksClasscross = new GridBagConstraints();
+		gbc_lblMaxRanksClasscross.insets = new Insets(0, 0, 0, 5);
+		gbc_lblMaxRanksClasscross.anchor = GridBagConstraints.EAST;
+		gbc_lblMaxRanksClasscross.gridx = 3;
+		gbc_lblMaxRanksClasscross.gridy = 0;
+		panel.add(lblMaxRanksClasscross, gbc_lblMaxRanksClasscross);
+		
+		txtClassSkills = new JTextField();
+		txtClassSkills.setEditable(false);
+		GridBagConstraints gbc_txtClassSkills = new GridBagConstraints();
+		gbc_txtClassSkills.insets = new Insets(0, 0, 0, 5);
+		gbc_txtClassSkills.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtClassSkills.gridx = 4;
+		gbc_txtClassSkills.gridy = 0;
+		panel.add(txtClassSkills, gbc_txtClassSkills);
+		txtClassSkills.setColumns(10);
+		
+		JLabel label = new JLabel("/");
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.insets = new Insets(0, 0, 0, 5);
+		gbc_label.anchor = GridBagConstraints.EAST;
+		gbc_label.gridx = 5;
+		gbc_label.gridy = 0;
+		panel.add(label, gbc_label);
+		
+		txtCrossClass = new JTextField();
+		txtCrossClass.setEditable(false);
+		GridBagConstraints gbc_txtCrossClass = new GridBagConstraints();
+		gbc_txtCrossClass.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtCrossClass.gridx = 6;
+		gbc_txtCrossClass.gridy = 0;
+		panel.add(txtCrossClass, gbc_txtCrossClass);
+		txtCrossClass.setColumns(10);
+		
+		JScrollPane scrollPane = new JScrollPane();
+		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
+		gbc_scrollPane.fill = GridBagConstraints.BOTH;
+		gbc_scrollPane.gridx = 0;
+		gbc_scrollPane.gridy = 1;
+		add(scrollPane, gbc_scrollPane);
+		
+		skillPanel = new JPanel();
+		scrollPane.setViewportView(skillPanel);
+		skillPanel.setLayout(new VerticalLayout());
+	}
+	
+	public void setModel(final DDCharacter model) {
+		// TODO (sjaffe): Observables
+		skillPanel.removeAll();
+		int[] totalPoints = {0};
+		final DDSkills skills = model.getSkills();
+		skills.getSkills().stream().forEach( skill -> {
+			skillPanel.add(new SkillLine(model, skill));
+			totalPoints[0] += skill.getPointsSpent();
+		});
+		txtTotalSkillPoints.setText(Integer.toString(totalPoints[0]));
+		int classSkill = model.getLevel() + 3;
+		txtClassSkills.setText(Integer.toString(classSkill));
+		txtCrossClass.setText(Integer.toString(classSkill / 2));
+	}
+	
+}

+ 45 - 0
src/org/leumasjaffe/charsheet/view/dev/DeveloperMenu.java

@@ -0,0 +1,45 @@
+package org.leumasjaffe.charsheet.view.dev;
+
+import java.util.stream.Collectors;
+
+import javax.swing.JDialog;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.view.skills.SkillLevelUpDialogue;
+
+public class DeveloperMenu extends JMenu {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	DDCharacter[] model = { null };
+	
+	public DeveloperMenu() {
+		super("Developer");
+		JMenuItem mntmLevelUp = new JMenuItem("Level Up - Skill");
+		mntmLevelUp.addActionListener( e -> {
+			Object[] choices = model[0].getClasses().stream().map(DDCharacterClass::getName).collect(Collectors.toList()).toArray();
+			String clazz = (String) JOptionPane.showInputDialog(this.getParent(), "Which Class is Leveling Up?", "Level Up - Skill", 
+					JOptionPane.QUESTION_MESSAGE, null, choices, choices[0]);
+			if (clazz != null) {
+				final JDialog dialog = new JDialog();
+				dialog.setTitle("Level Up - Skill Allocation");
+				dialog.setModal(true);
+				dialog.setSize(510, 600);
+
+				dialog.setContentPane(new SkillLevelUpDialogue(model[0], model[0].getClasses().stream().filter(c -> c.getName().equals(clazz)).findFirst().get()));
+				dialog.setVisible(true);
+				dialog.pack();
+			}
+		});
+		add(mntmLevelUp);
+	}
+	
+	public void setModel(DDCharacter model) {
+		this.model[0] = model;
+	}
+}

+ 120 - 0
src/org/leumasjaffe/charsheet/view/skills/SkillLevelUpDialogue.java

@@ -0,0 +1,120 @@
+package org.leumasjaffe.charsheet.view.skills;
+
+import javax.swing.JPanel;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import java.awt.GridBagLayout;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.model.skill.DDSkills;
+import org.leumasjaffe.observer.ObservableListener;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import java.awt.Dimension;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class SkillLevelUpDialogue extends JPanel {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	
+	public SkillLevelUpDialogue(final DDCharacter chara, final DDCharacterClass cclass) {
+		final IntValue pointsAvaliable = new IntValue(Math.max(1, cclass.getSkillPoints() + Ability.modifier(chara.getAbilities().getBase().getInt().value())));
+		
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.insets = new Insets(0, 0, 5, 0);
+		gbc_panel.fill = GridBagConstraints.HORIZONTAL;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 0;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JLabel lblPointsRemaining = new JLabel("Points Remaining:");
+		GridBagConstraints gbc_lblPointsRemaining = new GridBagConstraints();
+		gbc_lblPointsRemaining.insets = new Insets(0, 0, 0, 5);
+		gbc_lblPointsRemaining.anchor = GridBagConstraints.EAST;
+		gbc_lblPointsRemaining.gridx = 0;
+		gbc_lblPointsRemaining.gridy = 0;
+		panel.add(lblPointsRemaining, gbc_lblPointsRemaining);
+		
+		JTextField pointsRemaining = new JTextField();
+		pointsRemaining.setEditable(false);
+		GridBagConstraints gbc_pointsRemaining = new GridBagConstraints();
+		gbc_pointsRemaining.insets = new Insets(0, 0, 0, 5);
+		gbc_pointsRemaining.fill = GridBagConstraints.HORIZONTAL;
+		gbc_pointsRemaining.gridx = 1;
+		gbc_pointsRemaining.gridy = 0;
+		panel.add(pointsRemaining, gbc_pointsRemaining);
+		pointsRemaining.setColumns(10);
+		
+		JScrollPane scrollPane = new JScrollPane();
+		GridBagConstraints gbc_scrollPane = new GridBagConstraints();
+		gbc_scrollPane.fill = GridBagConstraints.BOTH;
+		gbc_scrollPane.gridx = 0;
+		gbc_scrollPane.gridy = 1;
+		add(scrollPane, gbc_scrollPane);
+		
+		JPanel skillPanel = new JPanel();
+		skillPanel.setMinimumSize(new Dimension(300, 200));
+		scrollPane.setViewportView(skillPanel);
+		skillPanel.setLayout(new VerticalLayout());
+		
+		JButton btnSubmitSkillChange = new JButton("Submit Skill Change");
+		btnSubmitSkillChange.setEnabled(false);
+		GridBagConstraints gbc_btnSubmitSkillChange = new GridBagConstraints();
+		gbc_btnSubmitSkillChange.gridx = 2;
+		gbc_btnSubmitSkillChange.gridy = 0;
+		panel.add(btnSubmitSkillChange, gbc_btnSubmitSkillChange);
+		
+		final List<SkillLevelUpLine> lines = new ArrayList<>();
+		final DDSkills skills = chara.getSkills();
+		skills.getSkills().stream().forEach( skill -> {
+			SkillLevelUpLine line = new SkillLevelUpLine(chara, cclass, skill, pointsAvaliable);
+			skillPanel.add(line);
+			lines.add(line);
+		});
+		
+		ObservableListener<JTextField, IntValue> purchaseListener = new ObservableListener<>(pointsRemaining, (c, v) -> {
+			btnSubmitSkillChange.setEnabled(v.value() == 0);
+			c.setText(Integer.toString(v.value()));
+		});
+		purchaseListener.setObserved(pointsAvaliable);
+		
+		btnSubmitSkillChange.addActionListener(e -> {
+			((JDialog) this.getParent().getParent().getParent()).dispose();
+			lines.stream().forEach(l -> {
+				l.applyChange();
+			});
+		});
+	}
+	
+}

+ 234 - 0
src/org/leumasjaffe/charsheet/view/skills/SkillLevelUpLine.java

@@ -0,0 +1,234 @@
+package org.leumasjaffe.charsheet.view.skills;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.model.skill.DDSkill;
+import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+import java.awt.GridBagLayout;
+import javax.swing.JCheckBox;
+import java.awt.GridBagConstraints;
+import javax.swing.JLabel;
+import java.awt.Insets;
+import java.util.function.IntFunction;
+import java.awt.Dimension;
+import javax.swing.JTextField;
+import java.awt.Color;
+import javax.swing.border.MatteBorder;
+import javax.swing.SwingConstants;
+import java.awt.Component;
+import javax.swing.Box;
+import javax.swing.JButton;
+import java.awt.Font;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class SkillLevelUpLine extends JPanel {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	boolean isClassSkill;
+	DDSkill skill;
+	IntValue current;
+	IndirectObservableListener<JTextField, DDCharacter> totalListener;
+	
+	public SkillLevelUpLine(final DDCharacter chara, final DDCharacterClass cclass, final DDSkill skill, IntValue pointsAvaliable) {
+		isClassSkill = cclass.isClassSkill(skill.getName());
+		this.skill = skill;
+		current = new IntValue(0);
+		final int pointsPerRank = isClassSkill ? 1 : 2;
+		final int maxPoints = (chara.getLevel() + 3) / pointsPerRank - skill.getRanks().value();
+		
+		setBorder(new MatteBorder(0, 0, 1, 0, (Color) new Color(0, 0, 0)));
+		setPreferredSize(new Dimension(475, 22));
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JCheckBox checkBoxIsClassSkill = new JCheckBox("");
+		checkBoxIsClassSkill.setToolTipText("Class Skill?");
+		checkBoxIsClassSkill.setSelected(cclass.isClassSkill(skill.getName()));
+		checkBoxIsClassSkill.setEnabled(false);
+		GridBagConstraints gbc_checkBoxIsClassSkill = new GridBagConstraints();
+		gbc_checkBoxIsClassSkill.insets = new Insets(1, 0, 0, 5);
+		gbc_checkBoxIsClassSkill.gridx = 0;
+		gbc_checkBoxIsClassSkill.gridy = 0;
+		add(checkBoxIsClassSkill, gbc_checkBoxIsClassSkill);
+		
+		JLabel lblName = new JLabel(skill.getName());
+		lblName.setMaximumSize(new Dimension(150, 14));
+		lblName.setMinimumSize(new Dimension(150, 14));
+		lblName.setPreferredSize(new Dimension(150, 14));
+		GridBagConstraints gbc_lblName = new GridBagConstraints();
+		gbc_lblName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblName.insets = new Insets(1, 0, 0, 5);
+		gbc_lblName.gridx = 1;
+		gbc_lblName.gridy = 0;
+		add(lblName, gbc_lblName);
+		
+		JLabel lblAbil = new JLabel(skill.getAbility());
+		lblAbil.setMaximumSize(new Dimension(30, 14));
+		lblAbil.setMinimumSize(new Dimension(30, 14));
+		lblAbil.setPreferredSize(new Dimension(30, 14));
+		GridBagConstraints gbc_lblAbil = new GridBagConstraints();
+		gbc_lblAbil.insets = new Insets(1, 0, 0, 5);
+		gbc_lblAbil.anchor = GridBagConstraints.EAST;
+		gbc_lblAbil.gridx = 2;
+		gbc_lblAbil.gridy = 0;
+		add(lblAbil, gbc_lblAbil);
+		
+		JTextField total = new JTextField();
+		total.setToolTipText("Skill Modifier");
+		total.setHorizontalAlignment(SwingConstants.CENTER);
+		total.setEditable(false);
+		total.setMinimumSize(new Dimension(30, 20));
+		total.setMaximumSize(new Dimension(30, 20));
+		total.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_total = new GridBagConstraints();
+		gbc_total.insets = new Insets(1, 0, 0, 5);
+		gbc_total.fill = GridBagConstraints.HORIZONTAL;
+		gbc_total.gridx = 3;
+		gbc_total.gridy = 0;
+		add(total, gbc_total);
+		total.setColumns(10);
+		
+		JLabel label = new JLabel("=");
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.anchor = GridBagConstraints.EAST;
+		gbc_label.insets = new Insets(1, 0, 0, 5);
+		gbc_label.gridx = 4;
+		gbc_label.gridy = 0;
+		add(label, gbc_label);
+		
+		JTextField modifier = new JTextField();
+		modifier.setToolTipText("Ability Modifier");
+		modifier.setHorizontalAlignment(SwingConstants.CENTER);
+		modifier.setEditable(false);
+		modifier.setMinimumSize(new Dimension(30, 20));
+		modifier.setMaximumSize(new Dimension(30, 20));
+		modifier.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_modifier = new GridBagConstraints();
+		gbc_modifier.insets = new Insets(1, 0, 0, 5);
+		gbc_modifier.fill = GridBagConstraints.HORIZONTAL;
+		gbc_modifier.gridx = 5;
+		gbc_modifier.gridy = 0;
+		add(modifier, gbc_modifier);
+		modifier.setColumns(10);
+		
+		JLabel label_1 = new JLabel("+");
+		GridBagConstraints gbc_label_1 = new GridBagConstraints();
+		gbc_label_1.anchor = GridBagConstraints.EAST;
+		gbc_label_1.insets = new Insets(1, 0, 0, 5);
+		gbc_label_1.gridx = 6;
+		gbc_label_1.gridy = 0;
+		add(label_1, gbc_label_1);
+		
+		JTextField ranks = new JTextField(StringHelper.toString(skill.getRanks()));
+		ranks.setToolTipText("Ranks");
+		ranks.setHorizontalAlignment(SwingConstants.CENTER);
+		ranks.setEditable(false);
+		ranks.setMinimumSize(new Dimension(30, 20));
+		ranks.setMaximumSize(new Dimension(30, 20));
+		ranks.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_ranks = new GridBagConstraints();
+		gbc_ranks.insets = new Insets(1, 0, 0, 5);
+		gbc_ranks.fill = GridBagConstraints.HORIZONTAL;
+		gbc_ranks.gridx = 7;
+		gbc_ranks.gridy = 0;
+		add(ranks, gbc_ranks);
+		ranks.setColumns(10);
+		
+		Component horizontalStrut = Box.createHorizontalStrut(20);
+		GridBagConstraints gbc_horizontalStrut = new GridBagConstraints();
+		gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5);
+		gbc_horizontalStrut.gridx = 8;
+		gbc_horizontalStrut.gridy = 0;
+		add(horizontalStrut, gbc_horizontalStrut);
+		
+		JButton plus = new JButton("+");
+		plus.setMargin(new Insets(2, 2, 2, 2));
+		plus.setFont(new Font("Tahoma", Font.PLAIN, 8));
+		plus.setPreferredSize(new Dimension(30, 19));
+		plus.setMinimumSize(new Dimension(30, 19));
+		GridBagConstraints gbc_plus = new GridBagConstraints();
+		gbc_plus.insets = new Insets(0, 0, 1, 5);
+		gbc_plus.gridx = 9;
+		gbc_plus.gridy = 0;
+		add(plus, gbc_plus);
+		
+		JButton minus = new JButton("-");
+		minus.setMargin(new Insets(2, 2, 2, 2));
+		minus.setFont(new Font("Tahoma", Font.PLAIN, 8));
+		minus.setMinimumSize(new Dimension(30, 19));
+		minus.setPreferredSize(new Dimension(30, 19));
+		GridBagConstraints gbc_minus = new GridBagConstraints();
+		gbc_minus.insets = new Insets(0, 0, 1, 5);
+		gbc_minus.gridx = 10;
+		gbc_minus.gridy = 0;
+		add(minus, gbc_minus);
+		
+		JTextField points = new JTextField();
+		points.setMinimumSize(new Dimension(30, 20));
+		points.setText("0");
+		points.setEditable(false);
+		GridBagConstraints gbc_points = new GridBagConstraints();
+		gbc_points.gridx = 11;
+		gbc_points.gridy = 0;
+		add(points, gbc_points);
+		points.setColumns(10);
+		
+		IntFunction<Void> lambda = (value) -> {
+			pointsAvaliable.value(pointsAvaliable.value() - (value * pointsPerRank));
+			current.value(current.value() + value);
+			points.setText(Integer.toString(current.value()));
+			ObserverDispatch.notifySubscribers(pointsAvaliable, this);
+			ObserverDispatch.notifySubscribers(current, this);
+			return null;
+		};
+		
+		plus.addActionListener((e) -> {
+			if (pointsAvaliable.value() >= pointsPerRank && current.value() < maxPoints) { lambda.apply(1); }
+		});
+		minus.addActionListener((e) -> {
+			if (current.value() > 0) { lambda.apply(-1); }
+		});
+
+		if ( skill.getAbility().isEmpty() ) {
+			totalListener = new IndirectObservableListener<>(total,
+					(c, v) -> {
+						c.setText(StringHelper.toString(skill.getRanks().value() + current.value()));
+					});
+			totalListener.setObserved(chara, current);
+		} else {
+			totalListener = new IndirectObservableListener<>(total,
+					(c, v) -> {
+						final int skillRanks = skill.getRanks().value();
+						final int mod = Ability.modifier(Ability.fields.get(skill.getAbility())
+								.apply(chara.getAbilities().getBase()).value());
+						c.setText(StringHelper.toString(skillRanks + mod + current.value()));
+					});
+			
+			final IntValue abilScore = 	Ability.fields.get(skill.getAbility())
+					.apply(chara.getAbilities().getBase());
+			modifier.setText(StringHelper.toString(Ability.modifier(abilScore.value())));
+			totalListener.setObserved(chara, current);
+		}
+	}
+
+	void applyChange() {
+		skill.spendPoints(current.value(), !isClassSkill);
+		ObserverDispatch.notifySubscribers(skill.getRanks(), this);
+	}
+}

+ 191 - 0
src/org/leumasjaffe/charsheet/view/skills/SkillLine.java

@@ -0,0 +1,191 @@
+package org.leumasjaffe.charsheet.view.skills;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.Ability;
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.observable.IntValue;
+import org.leumasjaffe.charsheet.model.skill.DDSkill;
+import org.leumasjaffe.charsheet.util.StringHelper;
+import org.leumasjaffe.observer.IndirectObservableListener;
+import org.leumasjaffe.observer.ObservableListener;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+import java.awt.GridBagLayout;
+import javax.swing.JCheckBox;
+import java.awt.GridBagConstraints;
+import javax.swing.JLabel;
+import java.awt.Insets;
+import java.awt.Dimension;
+import javax.swing.JTextField;
+import java.awt.Color;
+import javax.swing.border.MatteBorder;
+import javax.swing.SwingConstants;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class SkillLine extends JPanel {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+	ObservableListener<JTextField, IntValue> modifierListener, skillListener;
+	IndirectObservableListener<JTextField, DDCharacter> totalListener;
+
+	public SkillLine(final DDCharacter chara, final DDSkill skill) {
+		setBorder(new MatteBorder(0, 0, 1, 0, (Color) new Color(0, 0, 0)));
+		setPreferredSize(new Dimension(550, 22));
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JCheckBox checkBoxIsClassSkill = new JCheckBox("");
+		checkBoxIsClassSkill.setToolTipText("Class Skill?");
+		checkBoxIsClassSkill.setSelected(chara.isClassSkill(skill.getName()));
+		checkBoxIsClassSkill.setEnabled(false);
+		GridBagConstraints gbc_checkBoxIsClassSkill = new GridBagConstraints();
+		gbc_checkBoxIsClassSkill.insets = new Insets(1, 0, 0, 0);
+		gbc_checkBoxIsClassSkill.gridx = 0;
+		gbc_checkBoxIsClassSkill.gridy = 0;
+		add(checkBoxIsClassSkill, gbc_checkBoxIsClassSkill);
+		
+		JLabel lblName = new JLabel(skill.getName());
+		lblName.setMaximumSize(new Dimension(150, 14));
+		lblName.setMinimumSize(new Dimension(150, 14));
+		lblName.setPreferredSize(new Dimension(150, 14));
+		GridBagConstraints gbc_lblName = new GridBagConstraints();
+		gbc_lblName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblName.insets = new Insets(1, 0, 0, 0);
+		gbc_lblName.gridx = 1;
+		gbc_lblName.gridy = 0;
+		add(lblName, gbc_lblName);
+		
+		JLabel lblAbil = new JLabel(skill.getAbility());
+		lblAbil.setMaximumSize(new Dimension(30, 14));
+		lblAbil.setMinimumSize(new Dimension(30, 14));
+		lblAbil.setPreferredSize(new Dimension(30, 14));
+		GridBagConstraints gbc_lblAbil = new GridBagConstraints();
+		gbc_lblAbil.insets = new Insets(1, 0, 0, 0);
+		gbc_lblAbil.anchor = GridBagConstraints.EAST;
+		gbc_lblAbil.gridx = 2;
+		gbc_lblAbil.gridy = 0;
+		add(lblAbil, gbc_lblAbil);
+		
+		JTextField total = new JTextField();
+		total.setToolTipText("Skill Modifier");
+		total.setHorizontalAlignment(SwingConstants.CENTER);
+		total.setEditable(false);
+		total.setMinimumSize(new Dimension(30, 20));
+		total.setMaximumSize(new Dimension(30, 20));
+		total.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_total = new GridBagConstraints();
+		gbc_total.insets = new Insets(1, 0, 0, 0);
+		gbc_total.fill = GridBagConstraints.HORIZONTAL;
+		gbc_total.gridx = 3;
+		gbc_total.gridy = 0;
+		add(total, gbc_total);
+		total.setColumns(10);
+		
+		JLabel label = new JLabel("=");
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.anchor = GridBagConstraints.EAST;
+		gbc_label.insets = new Insets(1, 0, 0, 1);
+		gbc_label.gridx = 4;
+		gbc_label.gridy = 0;
+		add(label, gbc_label);
+		
+		JTextField modifier = new JTextField();
+		modifier.setToolTipText("Ability Modifier");
+		modifier.setHorizontalAlignment(SwingConstants.CENTER);
+		modifier.setEditable(false);
+		modifier.setMinimumSize(new Dimension(30, 20));
+		modifier.setMaximumSize(new Dimension(30, 20));
+		modifier.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_modifier = new GridBagConstraints();
+		gbc_modifier.insets = new Insets(1, 0, 0, 0);
+		gbc_modifier.fill = GridBagConstraints.HORIZONTAL;
+		gbc_modifier.gridx = 5;
+		gbc_modifier.gridy = 0;
+		add(modifier, gbc_modifier);
+		modifier.setColumns(10);
+		
+		JLabel label_1 = new JLabel("+");
+		GridBagConstraints gbc_label_1 = new GridBagConstraints();
+		gbc_label_1.anchor = GridBagConstraints.EAST;
+		gbc_label_1.insets = new Insets(1, 0, 0, 1);
+		gbc_label_1.gridx = 6;
+		gbc_label_1.gridy = 0;
+		add(label_1, gbc_label_1);
+		
+		JTextField ranks = new JTextField(StringHelper.toString(skill.getRanks()));
+		ranks.setToolTipText("Ranks");
+		ranks.setHorizontalAlignment(SwingConstants.CENTER);
+		ranks.setEditable(false);
+		ranks.setMinimumSize(new Dimension(30, 20));
+		ranks.setMaximumSize(new Dimension(30, 20));
+		ranks.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_ranks = new GridBagConstraints();
+		gbc_ranks.insets = new Insets(1, 0, 0, 0);
+		gbc_ranks.fill = GridBagConstraints.HORIZONTAL;
+		gbc_ranks.gridx = 7;
+		gbc_ranks.gridy = 0;
+		add(ranks, gbc_ranks);
+		ranks.setColumns(10);
+		
+		JLabel label_2 = new JLabel("+");
+		GridBagConstraints gbc_label_2 = new GridBagConstraints();
+		gbc_label_2.insets = new Insets(1, 0, 0, 1);
+		gbc_label_2.anchor = GridBagConstraints.EAST;
+		gbc_label_2.gridx = 8;
+		gbc_label_2.gridy = 0;
+		add(label_2, gbc_label_2);
+		
+		JTextField misc = new JTextField();
+		misc.setToolTipText("Miscellaneous Modifier");
+		misc.setHorizontalAlignment(SwingConstants.CENTER);
+		misc.setMinimumSize(new Dimension(30, 20));
+		misc.setMaximumSize(new Dimension(30, 20));
+		misc.setPreferredSize(new Dimension(30, 20));
+		GridBagConstraints gbc_misc = new GridBagConstraints();
+		gbc_misc.insets = new Insets(1, 0, 0, 0);
+		gbc_misc.fill = GridBagConstraints.HORIZONTAL;
+		gbc_misc.gridx = 9;
+		gbc_misc.gridy = 0;
+		add(misc, gbc_misc);
+		misc.setColumns(10);
+		
+		
+		if ( skill.getAbility().isEmpty() ) {
+			totalListener = new IndirectObservableListener<>(total,
+					(c, v) -> {
+						c.setText(StringHelper.toString(skill.getRanks()));
+					});
+			modifierListener = null;
+			totalListener.setObserved(chara, skill.getRanks());
+		} else {
+			totalListener = new IndirectObservableListener<>(total,
+					(c, v) -> {
+						final float skillRanks = skill.getRanks().value();
+						final int mod = Ability.modifier(Ability.fields.get(skill.getAbility())
+								.apply(chara.getAbilities().getBase()).value());
+						c.setText(StringHelper.toString(skillRanks + mod));
+					});
+			modifierListener = new ObservableListener<>(modifier, 
+					( c, v ) -> c.setText(StringHelper.toString(Ability.modifier(v.value()))));
+			
+			final IntValue abilScore = 	Ability.fields.get(skill.getAbility())
+					.apply(chara.getAbilities().getBase());
+			totalListener.setObserved(chara, skill.getRanks(), abilScore);
+			modifierListener.setObserved(abilScore);
+		}
+		skillListener = new ObservableListener<>(ranks, (c, v) -> {
+			c.setText(StringHelper.toString(v));
+		});
+		skillListener.setObserved(skill.getRanks());
+	}
+	
+}