/* * 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.character.GURPSCharacter; import com.trollworks.gcs.common.DataFile; import com.trollworks.gcs.common.LoadState; import com.trollworks.gcs.template.Template; 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.utility.Localization; import com.trollworks.toolkit.utility.text.Numbers; import java.io.IOException; import java.text.MessageFormat; import java.util.HashMap; import java.util.HashSet; /** A GURPS Technique. */ public class Technique extends Skill { @Localize("Technique") @Localize(locale = "de", value = "Technik") @Localize(locale = "ru", value = "Техника") @Localize(locale = "es", value = "Técnica") private static String TECHNIQUE_DEFAULT_NAME; @Localize("{0}Requires a skill named {1}\n") @Localize(locale = "de", value = "{0}Benötigt eine Fertigkeit namens {1}") @Localize(locale = "ru", value = "{0}Требует умение {1}\n") @Localize(locale = "es", value = "{0}Requiere la habilidad {1}\n") private static String REQUIRES_SKILL; @Localize("{0}Requires at least 1 point in the skill named {1}\n") @Localize(locale = "de", value = "{0}Benötigt mindestens einen Punkt in der Fertigkeit namens {1}") @Localize(locale = "ru", value = "{0}Требуется хотя бы 1 очко в умении {1}\n") @Localize(locale = "es", value = "{0}Requiere al menos 1 punto en la habilidad {1}\n") private static String REQUIRES_POINTS; static { Localization.initialize(); } /** The XML tag used for items. */ public static final String TAG_TECHNIQUE = "technique"; //$NON-NLS-1$ private static final String ATTRIBUTE_LIMIT = "limit"; //$NON-NLS-1$ private SkillDefault mDefault; private boolean mLimited; private int mLimitModifier; /** * Calculates the technique level. * * @param character The character the technique will be attached to. * @param name The name of the technique. * @param specialization The specialization of the technique. * @param def The default the technique is based on. * @param difficulty The difficulty of the technique. * @param points The number of points spent in the technique. * @param limited Whether the technique has been limited or not. * @param limitModifier The maximum bonus the technique can grant. * @return The calculated technique level. */ public static SkillLevel calculateTechniqueLevel(GURPSCharacter character, String name, String specialization, SkillDefault def, SkillDifficulty difficulty, int points, boolean limited, int limitModifier) { int relativeLevel = 0; int level = Integer.MIN_VALUE; if (character != null) { level = getBaseLevel(character, def); if (level != Integer.MIN_VALUE) { int baseLevel = level; level += def.getModifier(); if (difficulty == SkillDifficulty.H) { points--; } if (points > 0) { relativeLevel = points; } if (level != Integer.MIN_VALUE) { level += relativeLevel + character.getIntegerBonusFor(ID_NAME + "/" + name.toLowerCase()) + character.getSkillComparedIntegerBonusFor(ID_NAME + "*", name, specialization); //$NON-NLS-1$ //$NON-NLS-2$ } if (limited) { int max = baseLevel + limitModifier; if (level > max) { relativeLevel -= level - max; level = max; } } } } return new SkillLevel(level, relativeLevel); } private static int getBaseLevel(GURPSCharacter character, SkillDefault def) { SkillDefaultType type = def.getType(); if (type == SkillDefaultType.Skill) { Skill skill = getBaseSkill(character, def); return skill != null ? skill.getLevel() : Integer.MIN_VALUE; } // Take the modifier back out, as we wanted the base, not the final value. return type.getSkillLevelFast(character, def, null) - def.getModifier(); } /** * Creates a string suitable for displaying the level. * * @param level The skill level. * @param relativeLevel The relative skill level. * @param modifier The modifer to the skill level. * @return The formatted string. */ public static String getTechniqueDisplayLevel(int level, int relativeLevel, int modifier) { if (level < 0) { return "-"; //$NON-NLS-1$ } return Numbers.format(level) + "/" + Numbers.formatWithForcedSign(relativeLevel + modifier); //$NON-NLS-1$ } /** * Creates a new technique. * * @param dataFile The data file to associate it with. */ public Technique(DataFile dataFile) { super(dataFile, false); mDefault = new SkillDefault(SkillDefaultType.Skill, DEFAULT_NAME, null, 0); updateLevel(false); } /** * Creates a clone of an existing technique and associates it with the specified data file. * * @param dataFile The data file to associate it with. * @param technique The technique to clone. * @param forSheet Whether this is for a character sheet or a list. */ public Technique(DataFile dataFile, Technique technique, boolean forSheet) { super(dataFile, technique, false, forSheet); mPoints = forSheet ? technique.mPoints : getDifficulty() == SkillDifficulty.A ? 1 : 2; mDefault = new SkillDefault(technique.mDefault); mLimited = technique.mLimited; mLimitModifier = technique.mLimitModifier; updateLevel(false); } /** * Loads a technique 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 Technique(DataFile dataFile, XMLReader reader, LoadState state) throws IOException { this(dataFile); load(reader, state); if (!(dataFile instanceof GURPSCharacter) && !(dataFile instanceof Template)) { mPoints = getDifficulty() == SkillDifficulty.A ? 1 : 2; } } @Override public boolean isEquivalentTo(Object obj) { if (obj == this) { return true; } if (obj instanceof Technique && super.isEquivalentTo(obj)) { Technique row = (Technique) obj; if (mLimited == row.mLimited && mLimitModifier == row.mLimitModifier) { return mDefault.equals(row.mDefault); } } return false; } @Override public String getLocalizedName() { return TECHNIQUE_DEFAULT_NAME; } @Override public String getXMLTagName() { return TAG_TECHNIQUE; } @Override public String getRowType() { return TECHNIQUE_DEFAULT_NAME; } @Override protected void prepareForLoad(LoadState state) { super.prepareForLoad(state); mDefault = new SkillDefault(SkillDefaultType.Skill, DEFAULT_NAME, null, 0); mLimited = false; mLimitModifier = 0; } @Override protected void loadAttributes(XMLReader reader, LoadState state) { String value = reader.getAttribute(ATTRIBUTE_LIMIT); if (value != null && value.length() > 0) { mLimited = true; try { mLimitModifier = Integer.parseInt(value); } catch (Exception exception) { mLimited = false; mLimitModifier = 0; } } super.loadAttributes(reader, state); } @Override protected void loadSubElement(XMLReader reader, LoadState state) throws IOException { if (SkillDefault.TAG_ROOT.equals(reader.getName())) { mDefault = new SkillDefault(reader); } else { super.loadSubElement(reader, state); } } @Override public void saveSelf(XMLWriter out, boolean forUndo) { super.saveSelf(out, forUndo); mDefault.save(out); } @Override protected void saveAttributes(XMLWriter out, boolean forUndo) { if (mLimited) { out.writeAttribute(ATTRIBUTE_LIMIT, mLimitModifier); } } /** * @param builder The {@link StringBuilder} to append this technique's satisfied/unsatisfied * description to. May be <code>null</code>. * @param prefix The prefix to add to each line appended to the builder. * @return <code>true</code> if this technique has its default satisfied. */ public boolean satisfied(StringBuilder builder, String prefix) { if (mDefault.getType().isSkillBased()) { Skill skill = getCharacter().getBestSkillNamed(mDefault.getName(), mDefault.getSpecialization(), false, new HashSet<String>()); boolean satisfied = skill != null && skill.getPoints() > 0; if (!satisfied && builder != null) { if (skill == null) { builder.append(MessageFormat.format(REQUIRES_SKILL, prefix, mDefault.getFullName())); } else { builder.append(MessageFormat.format(REQUIRES_POINTS, prefix, mDefault.getFullName())); } } return satisfied; } return true; } @Override protected SkillLevel calculateLevelSelf() { return calculateTechniqueLevel(getCharacter(), getName(), getSpecialization(), getDefault(), getDifficulty(), getPoints(), isLimited(), getLimitModifier()); } @Override public void updateLevel(boolean notify) { if (mDefault != null) { super.updateLevel(notify); } } /** * @param difficulty The difficulty to set. * @return Whether it was modified or not. */ public boolean setDifficulty(SkillDifficulty difficulty) { return setDifficulty(getAttribute(), difficulty); } @Override public String getSpecialization() { return mDefault.getFullName(); } @Override public boolean setSpecialization(String specialization) { return false; } @Override public String getTechLevel() { return null; } @Override public boolean setTechLevel(String techLevel) { return false; } /** @return The default to base the technique on. */ public SkillDefault getDefault() { return mDefault; } /** * @param def The new default to base the technique on. * @return Whether anything was changed. */ public boolean setDefault(SkillDefault def) { if (!mDefault.equals(def)) { mDefault = new SkillDefault(def); return true; } return false; } @Override public void setDifficultyFromText(String text) { text = text.trim(); if (SkillDifficulty.A.name().equalsIgnoreCase(text)) { setDifficulty(SkillDifficulty.A); } else if (SkillDifficulty.H.name().equalsIgnoreCase(text)) { setDifficulty(SkillDifficulty.H); } } @Override public String getDifficultyAsText(boolean localized) { SkillDifficulty difficulty = getDifficulty(); return localized ? difficulty.toString() : difficulty.name(); } /** @return Whether the maximum level is limited. */ public boolean isLimited() { return mLimited; } /** * Sets whether the maximum level is limited. * * @param limited The value to set. * @return Whether anything was changed. */ public boolean setLimited(boolean limited) { if (limited != mLimited) { mLimited = limited; return true; } return false; } /** @return The limit modifier. */ public int getLimitModifier() { return mLimitModifier; } /** * Sets the value of limit modifier. * * @param limitModifier The value to set. * @return Whether anything was changed. */ public boolean setLimitModifier(int limitModifier) { if (mLimitModifier != limitModifier) { mLimitModifier = limitModifier; return true; } return false; } @Override public RowEditor<? extends ListRow> createEditor() { return new TechniqueEditor(this); } @Override public void fillWithNameableKeys(HashSet<String> set) { super.fillWithNameableKeys(set); mDefault.fillWithNameableKeys(set); } @Override public void applyNameableKeys(HashMap<String, String> map) { super.applyNameableKeys(map); mDefault.applyNameableKeys(map); } @Override public String getModifierNotes() { StringBuilder buffer = new StringBuilder(super.getModifierNotes()); if (buffer.length() > 0) { buffer.append(' '); } buffer.append(DEFAULTED_FROM); buffer.append(mDefault); return buffer.toString(); } @Override public Skill getDefaultSkill() { return getBaseSkill(getCharacter(), mDefault); } @Override public int swapDefault() { // Do nothing: Default is fixed return 0; } @Override public boolean canSwapDefaults(Skill skill) { return false; } }