/*
* Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, version 2.0. If a copy of the MPL was not distributed with
* this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, version 2.0.
*/
package com.trollworks.gcs.skill;
import com.trollworks.gcs.app.GCSImages;
import com.trollworks.gcs.character.GURPSCharacter;
import com.trollworks.gcs.common.DataFile;
import com.trollworks.gcs.common.HasSourceReference;
import com.trollworks.gcs.common.ListFile;
import com.trollworks.gcs.common.LoadState;
import com.trollworks.gcs.library.LibraryFile;
import com.trollworks.gcs.weapon.MeleeWeaponStats;
import com.trollworks.gcs.weapon.RangedWeaponStats;
import com.trollworks.gcs.weapon.WeaponStats;
import com.trollworks.gcs.widgets.outline.ListRow;
import com.trollworks.gcs.widgets.outline.RowEditor;
import com.trollworks.toolkit.annotation.Localize;
import com.trollworks.toolkit.io.xml.XMLReader;
import com.trollworks.toolkit.io.xml.XMLWriter;
import com.trollworks.toolkit.ui.image.StdImage;
import com.trollworks.toolkit.ui.widget.outline.Column;
import com.trollworks.toolkit.ui.widget.outline.Row;
import com.trollworks.toolkit.utility.Localization;
import com.trollworks.toolkit.utility.text.Numbers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** A GURPS Skill. */
public class Skill extends ListRow implements HasSourceReference {
@Localize("Skill")
@Localize(locale = "de", value = "Fertigkeit")
@Localize(locale = "ru", value = "Умение")
@Localize(locale = "es", value = "Habilidad")
static String DEFAULT_NAME;
@Localize("Default: ")
@Localize(locale = "de", value = "Grundwert: ")
@Localize(locale = "ru", value = "По умолчанию: ")
@Localize(locale = "es", value = "Valore por defecto: ")
static String DEFAULTED_FROM;
static {
Localization.initialize();
}
private static final int CURRENT_VERSION = 2;
/** The extension for Skill lists. */
public static final String OLD_SKILL_EXTENSION = "skl"; //$NON-NLS-1$
/** The XML tag used for items. */
public static final String TAG_SKILL = "skill"; //$NON-NLS-1$
/** The XML tag used for containers. */
public static final String TAG_SKILL_CONTAINER = "skill_container"; //$NON-NLS-1$
private static final String TAG_NAME = "name"; //$NON-NLS-1$
private static final String TAG_SPECIALIZATION = "specialization"; //$NON-NLS-1$
private static final String TAG_TECH_LEVEL = "tech_level"; //$NON-NLS-1$
private static final String TAG_DIFFICULTY = "difficulty"; //$NON-NLS-1$
private static final String TAG_POINTS = "points"; //$NON-NLS-1$
private static final String TAG_REFERENCE = "reference"; //$NON-NLS-1$
private static final String TAG_ENCUMBRANCE_PENALTY = "encumbrance_penalty_multiplier"; //$NON-NLS-1$
/** The prefix used in front of all IDs for the skills. */
public static final String PREFIX = GURPSCharacter.CHARACTER_PREFIX + "skill."; //$NON-NLS-1$
/** The field ID for name changes. */
public static final String ID_NAME = PREFIX + "Name"; //$NON-NLS-1$
/** The field ID for specialization changes. */
public static final String ID_SPECIALIZATION = PREFIX + "Specialization"; //$NON-NLS-1$
/** The field ID for tech level changes. */
public static final String ID_TECH_LEVEL = PREFIX + "TechLevel"; //$NON-NLS-1$
/** The field ID for level changes. */
public static final String ID_LEVEL = PREFIX + "Level"; //$NON-NLS-1$
/** The field ID for relative level changes. */
public static final String ID_RELATIVE_LEVEL = PREFIX + "RelativeLevel"; //$NON-NLS-1$
/** The field ID for difficulty changes. */
public static final String ID_DIFFICULTY = PREFIX + "Difficulty"; //$NON-NLS-1$
/** The field ID for point changes. */
public static final String ID_POINTS = PREFIX + "Points"; //$NON-NLS-1$
/** The field ID for page reference changes. */
public static final String ID_REFERENCE = PREFIX + "Reference"; //$NON-NLS-1$
/** The field ID for enumbrance penalty multiplier changes. */
public static final String ID_ENCUMBRANCE_PENALTY = PREFIX + "EncMultplier"; //$NON-NLS-1$
/** The field ID for when the categories change. */
public static final String ID_CATEGORY = PREFIX + "Category"; //$NON-NLS-1$
/** The field ID for when the row hierarchy changes. */
public static final String ID_LIST_CHANGED = PREFIX + "ListChanged"; //$NON-NLS-1$
/** The field ID for when the skill becomes or stops being a weapon. */
public static final String ID_WEAPON_STATUS_CHANGED = PREFIX + "WeaponStatus"; //$NON-NLS-1$
private static final String NEWLINE = "\n"; //$NON-NLS-1$
private static final String SPACE = " "; //$NON-NLS-1$
private static final String EMPTY = ""; //$NON-NLS-1$
private static final String ASTERISK = "*"; //$NON-NLS-1$
private static final String SLASH = "/"; //$NON-NLS-1$
private String mName;
private String mSpecialization;
private String mTechLevel;
private int mLevel;
private int mRelativeLevel;
private SkillAttribute mAttribute;
private SkillDifficulty mDifficulty;
/** The points spent. */
protected int mPoints;
private String mReference;
private int mEncumbrancePenaltyMultiplier;
private ArrayList<WeaponStats> mWeapons;
private SkillDefault mDefaultedFrom;
/**
* Creates a string suitable for displaying the level.
*
* @param level The skill level.
* @param relativeLevel The relative skill level.
* @param attribute The attribute the skill is based on.
* @param isContainer Whether this skill is a container or not.
* @return The formatted string.
*/
public static String getSkillDisplayLevel(int level, int relativeLevel, SkillAttribute attribute, boolean isContainer) {
if (isContainer) {
return EMPTY;
}
if (level < 0) {
return "-"; //$NON-NLS-1$
}
return Numbers.format(level) + SLASH + attribute + Numbers.formatWithForcedSign(relativeLevel);
}
/**
* Creates a new skill.
*
* @param dataFile The data file to associate it with.
* @param isContainer Whether or not this row allows children.
*/
public Skill(DataFile dataFile, boolean isContainer) {
super(dataFile, isContainer);
mName = getLocalizedName();
mSpecialization = EMPTY;
mTechLevel = null;
mAttribute = SkillAttribute.DX;
mDifficulty = SkillDifficulty.A;
mPoints = 1;
mReference = EMPTY;
mWeapons = new ArrayList<>();
updateLevel(false);
}
/**
* Creates a clone of an existing skill and associates it with the specified data file.
*
* @param dataFile The data file to associate it with.
* @param skill The skill to clone.
* @param deep Whether or not to clone the children, grandchildren, etc.
* @param forSheet Whether this is for a character sheet or a list.
*/
public Skill(DataFile dataFile, Skill skill, boolean deep, boolean forSheet) {
super(dataFile, skill);
mName = skill.mName;
mSpecialization = skill.mSpecialization;
mTechLevel = skill.mTechLevel;
mAttribute = skill.mAttribute;
mDifficulty = skill.mDifficulty;
mPoints = forSheet ? skill.mPoints : 1;
mReference = skill.mReference;
mEncumbrancePenaltyMultiplier = skill.mEncumbrancePenaltyMultiplier;
if (forSheet && dataFile instanceof GURPSCharacter) {
if (mTechLevel != null) {
mTechLevel = ((GURPSCharacter) dataFile).getDescription().getTechLevel();
}
} else {
if (mTechLevel != null && mTechLevel.trim().length() > 0) {
mTechLevel = EMPTY;
}
}
mWeapons = new ArrayList<>(skill.mWeapons.size());
for (WeaponStats weapon : skill.mWeapons) {
if (weapon instanceof MeleeWeaponStats) {
mWeapons.add(new MeleeWeaponStats(this, (MeleeWeaponStats) weapon));
} else if (weapon instanceof RangedWeaponStats) {
mWeapons.add(new RangedWeaponStats(this, (RangedWeaponStats) weapon));
}
}
updateLevel(false);
if (deep) {
int count = skill.getChildCount();
for (int i = 0; i < count; i++) {
Row row = skill.getChild(i);
if (row instanceof Technique) {
addChild(new Technique(dataFile, (Technique) row, forSheet));
} else {
addChild(new Skill(dataFile, (Skill) row, true, forSheet));
}
}
}
}
/**
* Loads a skill and associates it with the specified data file.
*
* @param dataFile The data file to associate it with.
* @param reader The XML reader to load from.
* @param state The {@link LoadState} to use.
*/
public Skill(DataFile dataFile, XMLReader reader, LoadState state) throws IOException {
this(dataFile, TAG_SKILL_CONTAINER.equals(reader.getName()));
load(reader, state);
}
@Override
public boolean isEquivalentTo(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Skill && getClass() == obj.getClass() && super.isEquivalentTo(obj)) {
Skill row = (Skill) obj;
if (mLevel == row.mLevel) {
if (mPoints == row.mPoints) {
if (mEncumbrancePenaltyMultiplier == row.mEncumbrancePenaltyMultiplier) {
if (mRelativeLevel == row.mRelativeLevel) {
if (mAttribute == row.mAttribute) {
if (mDifficulty == row.mDifficulty) {
if (mName.equals(row.mName)) {
if (mTechLevel == null ? row.mTechLevel == null : mTechLevel.equals(row.mTechLevel)) {
if (mSpecialization.equals(row.mSpecialization)) {
if (mReference.equals(row.mReference)) {
return mWeapons.equals(row.mWeapons);
}
}
}
}
}
}
}
}
}
}
}
return false;
}
@Override
public String getLocalizedName() {
return DEFAULT_NAME;
}
@Override
public String getListChangedID() {
return ID_LIST_CHANGED;
}
@Override
public String getXMLTagName() {
return canHaveChildren() ? TAG_SKILL_CONTAINER : TAG_SKILL;
}
@Override
public int getXMLTagVersion() {
return CURRENT_VERSION;
}
@Override
public String getRowType() {
return DEFAULT_NAME;
}
@Override
protected void prepareForLoad(LoadState state) {
super.prepareForLoad(state);
mName = getLocalizedName();
mSpecialization = EMPTY;
mTechLevel = null;
mAttribute = SkillAttribute.DX;
mDifficulty = SkillDifficulty.A;
mPoints = 1;
mReference = EMPTY;
mEncumbrancePenaltyMultiplier = 0;
mWeapons = new ArrayList<>();
}
@Override
protected void loadSubElement(XMLReader reader, LoadState state) throws IOException {
String name = reader.getName();
if (TAG_NAME.equals(name)) {
mName = reader.readText().replace(NEWLINE, SPACE);
} else if (TAG_SPECIALIZATION.equals(name)) {
mSpecialization = reader.readText().replace(NEWLINE, SPACE);
} else if (TAG_TECH_LEVEL.equals(name)) {
mTechLevel = reader.readText().replace(NEWLINE, SPACE);
if (mTechLevel != null) {
DataFile dataFile = getDataFile();
if (dataFile instanceof ListFile || dataFile instanceof LibraryFile) {
mTechLevel = EMPTY;
}
}
} else if (TAG_REFERENCE.equals(name)) {
mReference = reader.readText().replace(NEWLINE, SPACE);
} else if (!state.mForUndo && (TAG_SKILL.equals(name) || TAG_SKILL_CONTAINER.equals(name))) {
addChild(new Skill(mDataFile, reader, state));
} else if (!state.mForUndo && Technique.TAG_TECHNIQUE.equals(name)) {
addChild(new Technique(mDataFile, reader, state));
} else if (!canHaveChildren()) {
if (TAG_DIFFICULTY.equals(name)) {
setDifficultyFromText(reader.readText().replace(NEWLINE, SPACE));
} else if (TAG_POINTS.equals(name)) {
mPoints = reader.readInteger(1);
} else if (TAG_ENCUMBRANCE_PENALTY.equals(name)) {
mEncumbrancePenaltyMultiplier = Math.min(Math.max(reader.readInteger(0), 0), 9);
} else if (MeleeWeaponStats.TAG_ROOT.equals(name)) {
mWeapons.add(new MeleeWeaponStats(this, reader));
} else if (RangedWeaponStats.TAG_ROOT.equals(name)) {
mWeapons.add(new RangedWeaponStats(this, reader));
} else {
super.loadSubElement(reader, state);
}
} else {
super.loadSubElement(reader, state);
}
}
@Override
protected void finishedLoading(LoadState state) {
updateLevel(false);
super.finishedLoading(state);
}
@Override
public void saveSelf(XMLWriter out, boolean forUndo) {
out.simpleTag(TAG_NAME, mName);
if (!canHaveChildren()) {
out.simpleTagNotEmpty(TAG_SPECIALIZATION, mSpecialization);
if (mTechLevel != null) {
if (getCharacter() != null) {
out.simpleTagNotEmpty(TAG_TECH_LEVEL, mTechLevel);
} else {
out.startTag(TAG_TECH_LEVEL);
out.finishEmptyTagEOL();
}
}
if (mEncumbrancePenaltyMultiplier != 0) {
out.simpleTag(TAG_ENCUMBRANCE_PENALTY, mEncumbrancePenaltyMultiplier);
}
out.simpleTag(TAG_DIFFICULTY, getDifficultyAsText(false));
out.simpleTag(TAG_POINTS, mPoints);
for (WeaponStats weapon : mWeapons) {
weapon.save(out);
}
}
out.simpleTagNotEmpty(TAG_REFERENCE, mReference);
}
/** @return The weapon list. */
public List<WeaponStats> getWeapons() {
return Collections.unmodifiableList(mWeapons);
}
/**
* @param weapons The weapons to set.
* @return Whether it was modified.
*/
public boolean setWeapons(List<WeaponStats> weapons) {
if (!mWeapons.equals(weapons)) {
mWeapons = new ArrayList<>(weapons);
for (WeaponStats weapon : mWeapons) {
weapon.setOwner(this);
}
notifySingle(ID_WEAPON_STATUS_CHANGED);
return true;
}
return false;
}
/** @return The level. */
public int getLevel() {
return mLevel;
}
/** @return The relative level. */
public int getRelativeLevel() {
return mRelativeLevel;
}
/** @return The name. */
public String getName() {
return mName;
}
/**
* @param name The name to set.
* @return Whether it was changed.
*/
public boolean setName(String name) {
if (!mName.equals(name)) {
mName = name;
notifySingle(ID_NAME);
return true;
}
return false;
}
/** @return The specialization. */
public String getSpecialization() {
return mSpecialization;
}
/**
* @param specialization The specialization to set.
* @return Whether it was changed.
*/
public boolean setSpecialization(String specialization) {
if (!mSpecialization.equals(specialization)) {
mSpecialization = specialization;
notifySingle(ID_SPECIALIZATION);
return true;
}
return false;
}
/** @return The tech level. */
public String getTechLevel() {
return mTechLevel;
}
/**
* @param techLevel The tech level to set.
* @return Whether it was changed.
*/
public boolean setTechLevel(String techLevel) {
if (mTechLevel == null ? techLevel != null : !mTechLevel.equals(techLevel)) {
mTechLevel = techLevel;
notifySingle(ID_TECH_LEVEL);
return true;
}
return false;
}
/** @return The points. */
public int getPoints() {
if (canHaveChildren()) {
int sum = 0;
for (Row row : getChildren()) {
if (row instanceof Skill) {
sum += ((Skill) row).getPoints();
}
}
return sum;
}
return mPoints;
}
/**
* @param points The points to set.
* @return Whether it was changed.
*/
public boolean setPoints(int points) {
if (mPoints != points) {
mPoints = points;
startNotify();
notify(ID_POINTS, this);
updateLevel(true);
endNotify();
return true;
}
return false;
}
/**
* Call to force an update of the level and relative level for this skill or technique.
*
* @param notify Whether or not a notification should be issued on a change.
*/
public void updateLevel(boolean notify) {
int savedLevel = mLevel;
int savedRelativeLevel = mRelativeLevel;
SkillLevel level = calculateLevelSelf();
mLevel = level.mLevel;
mRelativeLevel = level.mRelativeLevel;
if (notify) {
startNotify();
if (savedLevel != mLevel) {
notify(ID_LEVEL, this);
}
if (savedRelativeLevel != mRelativeLevel) {
notify(ID_RELATIVE_LEVEL, this);
}
endNotify();
}
}
/** @return The calculated skill level. */
protected SkillLevel calculateLevelSelf() {
mDefaultedFrom = getBestDefaultWithPoints();
return calculateLevel(getCharacter(), getName(), getSpecialization(), getDefaults(), getAttribute(), getDifficulty(), getPoints(), new HashSet<String>(), getEncumbrancePenaltyMultiplier());
}
/**
* @param excludes Skills to exclude, other than this one.
* @return The calculated level.
*/
public int getLevel(HashSet<String> excludes) {
return calculateLevel(getCharacter(), getName(), getSpecialization(), getDefaults(), getAttribute(), getDifficulty(), getPoints(), excludes, getEncumbrancePenaltyMultiplier()).mLevel;
}
/** @return The attribute. */
public SkillAttribute getAttribute() {
return mAttribute;
}
/** @return The difficulty. */
public SkillDifficulty getDifficulty() {
return mDifficulty;
}
/**
* @param attribute The attribute to set.
* @param difficulty The difficulty to set.
* @return Whether it was changed.
*/
public boolean setDifficulty(SkillAttribute attribute, SkillDifficulty difficulty) {
if (mAttribute != attribute || mDifficulty != difficulty) {
mAttribute = attribute;
mDifficulty = difficulty;
startNotify();
notify(ID_DIFFICULTY, this);
updateLevel(true);
endNotify();
return true;
}
return false;
}
/** @return The encumbrance penalty multiplier. */
public int getEncumbrancePenaltyMultiplier() {
return mEncumbrancePenaltyMultiplier;
}
/**
* @param multiplier The multiplier to set.
* @return Whether it was changed.
*/
public boolean setEncumbrancePenaltyMultiplier(int multiplier) {
multiplier = Math.min(Math.max(multiplier, 0), 9);
if (mEncumbrancePenaltyMultiplier != multiplier) {
mEncumbrancePenaltyMultiplier = multiplier;
notifySingle(ID_ENCUMBRANCE_PENALTY);
return true;
}
return false;
}
@Override
public String getReference() {
return mReference;
}
@Override
public boolean setReference(String reference) {
if (!mReference.equals(reference)) {
mReference = reference;
notifySingle(ID_REFERENCE);
return true;
}
return false;
}
@Override
public String getReferenceHighlight() {
return getName();
}
@Override
public boolean contains(String text, boolean lowerCaseOnly) {
if (getName().toLowerCase().indexOf(text) != -1) {
return true;
}
if (getSpecialization().toLowerCase().indexOf(text) != -1) {
return true;
}
return super.contains(text, lowerCaseOnly);
}
@Override
public Object getData(Column column) {
return SkillColumn.values()[column.getID()].getData(this);
}
@Override
public String getDataAsText(Column column) {
return SkillColumn.values()[column.getID()].getDataAsText(this);
}
/** @param text The combined attribute/difficulty to set. */
public void setDifficultyFromText(String text) {
SkillAttribute[] attribute = SkillAttribute.values();
SkillDifficulty[] difficulty = SkillDifficulty.values();
String input = text.trim();
for (SkillAttribute element : attribute) {
// We have to go backwards through the list to avoid the
// regex grabbing the "H" in "VH".
for (int j = difficulty.length - 1; j >= 0; j--) {
if (input.matches("(?i).*" + element.name() + ".*/.*" + difficulty[j].name() + ".*")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
setDifficulty(element, difficulty[j]);
return;
}
}
}
}
/** @return The formatted attribute/difficulty. */
public String getDifficultyAsText() {
return getDifficultyAsText(true);
}
/**
* @param localized Whether to use localized versions of attribute and difficulty.
* @return The formatted attribute/difficulty.
*/
public String getDifficultyAsText(boolean localized) {
if (canHaveChildren()) {
return EMPTY;
}
StringBuilder buffer = new StringBuilder();
buffer.append(localized ? mAttribute.toString() : mAttribute.name());
buffer.append(SLASH);
buffer.append(localized ? mDifficulty.toString() : mDifficulty.name());
return buffer.toString();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(getName());
if (!canHaveChildren()) {
String techLevel = getTechLevel();
String specialization = getSpecialization();
if (techLevel != null) {
builder.append("/TL"); //$NON-NLS-1$
if (techLevel.length() > 0) {
builder.append(techLevel);
}
}
if (specialization.length() > 0) {
builder.append(" ("); //$NON-NLS-1$
builder.append(specialization);
builder.append(')');
}
}
return builder.toString();
}
@Override
public String getModifierNotes() {
StringBuilder buffer = new StringBuilder(super.getModifierNotes());
Skill skill = getDefaultSkill();
if (skill != null && mDefaultedFrom != null) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(DEFAULTED_FROM);
buffer.append(skill);
buffer.append(mDefaultedFrom.getModifierAsString());
}
return buffer.toString();
}
@Override
public StdImage getIcon(boolean large) {
return GCSImages.getSkillsIcons().getImage(large ? 64 : 16);
}
@Override
public RowEditor<? extends ListRow> createEditor() {
return new SkillEditor(this);
}
/**
* Calculates the skill level.
*
* @param character The character the skill will be attached to.
* @param name The name of the skill.
* @param specialization The specialization of the skill.
* @param defaults The defaults the skill has.
* @param attribute The attribute the skill is based on.
* @param difficulty The difficulty of the skill.
* @param points The number of points spent in the skill.
* @param excludes The set of skills to exclude from any default calculations.
* @param encPenaltyMult The encumbrance penalty multiplier.
* @return The calculated skill level.
*/
public SkillLevel calculateLevel(GURPSCharacter character, String name, String specialization, List<SkillDefault> defaults, SkillAttribute attribute, SkillDifficulty difficulty, int points, HashSet<String> excludes, int encPenaltyMult) {
int relativeLevel = difficulty.getBaseRelativeLevel();
int level = attribute.getBaseSkillLevel(character);
if (level != Integer.MIN_VALUE) {
if (difficulty != SkillDifficulty.W) {
if (mDefaultedFrom != null && mDefaultedFrom.getPoints() > 0) {
points += mDefaultedFrom.getPoints();
}
} else {
points /= 3;
}
if (points > 0) {
relativeLevel = calculateRelativeLevel(points, relativeLevel);
} else if (mDefaultedFrom != null && mDefaultedFrom.getPoints() < 0) {
relativeLevel = mDefaultedFrom.getAdjLevel() - level;
} else {
level = Integer.MIN_VALUE;
relativeLevel = 0;
}
if (level != Integer.MIN_VALUE) {
level += relativeLevel;
if (mDefaultedFrom != null) {
if (level < mDefaultedFrom.getAdjLevel()) {
level = mDefaultedFrom.getAdjLevel();
}
}
int bonus = character.getSkillComparedIntegerBonusFor(ID_NAME + ASTERISK, name, specialization);
level += bonus;
relativeLevel += bonus;
bonus = character.getIntegerBonusFor(ID_NAME + SLASH + name.toLowerCase());
level += bonus;
relativeLevel += bonus;
level += character.getEncumbranceLevel().getEncumbrancePenalty() * encPenaltyMult;
}
}
return new SkillLevel(level, relativeLevel);
}
/**
* Tries to switch defaults with its current default keeping skill level, by adding and freeing
* points as necessary. Freed points are kept in former default skill, added points are taken
* from unspent points.
*
* @return extra points spent to keep minimum levels.
*/
public int swapDefault() {
int extraPointsSpent = 0;
Skill baseSkill = getDefaultSkill();
if (baseSkill != null) {
// Find alternative default
mDefaultedFrom = getBestDefaultWithPoints(mDefaultedFrom);
startNotify();
baseSkill.updateLevel(true);
updateLevel(true);
notify(ID_NAME, this);
baseSkill.notify(ID_NAME, baseSkill);
endNotify();
}
return extraPointsSpent;
}
/**
* Returns {@code true} if default can be swapped with {@code skill}.
*
* @param skill Skill to check.
* @return {@code true} if default can be swapped with {@code skill}.
*/
public boolean canSwapDefaults(Skill skill) {
boolean result = false;
if (mDefaultedFrom != null && getPoints() > 0) {
if (skill != null && skill.hasDefaultTo(this)) {
result = true;
}
}
return result;
}
private boolean hasDefaultTo(Skill skill) {
boolean result = false;
for (SkillDefault skillDefault : getDefaults()) {
boolean skillBased = skillDefault.getType().isSkillBased();
boolean nameMatches = skillDefault.getName().equals(skill.getName());
boolean specializationMatches = skillDefault.getSpecialization() == null || skillDefault.getSpecialization().isEmpty() || skillDefault.getSpecialization().equals(skill.getSpecialization());
if (skillBased && nameMatches && specializationMatches) {
result = true;
break;
}
}
return result;
}
private static int calculateRelativeLevel(int points, int relativeLevel) {
if (points == 1) {
// relativeLevel is preset to this point value
} else if (points < 4) {
relativeLevel++;
} else {
relativeLevel += 1 + points / 4;
}
return relativeLevel;
}
private SkillDefault getBestDefaultWithPoints() {
return getBestDefaultWithPoints(null);
}
private SkillDefault getBestDefaultWithPoints(SkillDefault excludedDefault) {
SkillDefault best = getBestDefault(excludedDefault);
if (best != null) {
GURPSCharacter character = getCharacter();
int baseLine = getAttribute().getBaseSkillLevel(character) + getDifficulty().getBaseRelativeLevel();
int level = best.getLevel();
if (best.getType().isSkillBased()) {
String name = best.getName();
level -= character.getSkillComparedIntegerBonusFor(ID_NAME + ASTERISK, name, best.getSpecialization());
level -= character.getIntegerBonusFor(ID_NAME + SLASH + name.toLowerCase());
}
best.setAdjLevel(level);
if (level == baseLine) {
best.setPoints(1);
} else if (level == baseLine + 1) {
best.setPoints(2);
} else if (level > baseLine + 1) {
best.setPoints(4 * (level - (baseLine + 1)));
} else {
best.setPoints(-best.getLevel());
}
}
return best;
}
private SkillDefault getBestDefault(SkillDefault excludedDefault) {
GURPSCharacter character = getCharacter();
if (character != null) {
Collection<SkillDefault> defaults = getDefaults();
if (!defaults.isEmpty()) {
int best = Integer.MIN_VALUE;
SkillDefault bestSkill = null;
String exclude = toString();
HashSet<String> excludes = new HashSet<>();
excludes.add(exclude);
for (SkillDefault skillDefault : defaults) {
// For skill-based defaults, prune out any that already use a default that we
// are involved with
if (!skillDefault.equals(excludedDefault) && !isInDefaultChain(this, skillDefault, new HashSet<>())) {
int level = skillDefault.getType().getSkillLevel(character, skillDefault, excludes);
if (level > best) {
best = level;
bestSkill = new SkillDefault(skillDefault);
bestSkill.setLevel(level);
}
}
}
excludes.remove(exclude);
return bestSkill;
}
}
return null;
}
private boolean isInDefaultChain(Skill skill, SkillDefault skillDefault, Set<Skill> lookedAt) {
GURPSCharacter character = getCharacter();
if (character != null && skillDefault != null && skillDefault.getType().isSkillBased()) {
boolean hadOne = false;
for (Skill one : character.getSkillNamed(skillDefault.getName(), skillDefault.getSpecialization(), true, null)) {
if (one == skill) {
return true;
}
if (lookedAt.add(one)) {
if (isInDefaultChain(skill, one.mDefaultedFrom, lookedAt)) {
return true;
}
}
hadOne = true;
}
return !hadOne;
}
return false;
}
@Override
public void fillWithNameableKeys(HashSet<String> set) {
super.fillWithNameableKeys(set);
extractNameables(set, mName);
extractNameables(set, mSpecialization);
for (WeaponStats weapon : mWeapons) {
for (SkillDefault one : weapon.getDefaults()) {
one.fillWithNameableKeys(set);
}
}
}
@Override
public void applyNameableKeys(HashMap<String, String> map) {
super.applyNameableKeys(map);
mName = nameNameables(map, mName);
mSpecialization = nameNameables(map, mSpecialization);
for (WeaponStats weapon : mWeapons) {
for (SkillDefault one : weapon.getDefaults()) {
one.applyNameableKeys(map);
}
}
}
@Override
protected String getCategoryID() {
return ID_CATEGORY;
}
/**
* Returns the skill defaulted to.
*
* @param character Character
* @param skillDefault Skill default
* @return Returns the skill defaulted to.
*/
protected static Skill getBaseSkill(GURPSCharacter character, SkillDefault skillDefault) {
if (character != null && skillDefault != null && skillDefault.getType().isSkillBased()) {
return character.getBestSkillNamed(skillDefault.getName(), skillDefault.getSpecialization(), true, new HashSet<String>());
}
return null;
}
/**
* Skill the skill currently Defaults to.
*
* @return Skill the skill currently Defaults to.
*/
public Skill getDefaultSkill() {
return getBaseSkill(getCharacter(), mDefaultedFrom);
}
}