/*
* 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.equipment;
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.LoadState;
import com.trollworks.gcs.feature.ContainedWeightReduction;
import com.trollworks.gcs.feature.Feature;
import com.trollworks.gcs.preferences.SheetPreferences;
import com.trollworks.gcs.skill.SkillDefault;
import com.trollworks.gcs.weapon.MeleeWeaponStats;
import com.trollworks.gcs.weapon.OldWeapon;
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.Enums;
import com.trollworks.toolkit.utility.units.WeightUnits;
import com.trollworks.toolkit.utility.units.WeightValue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/** A piece of equipment. */
public class Equipment extends ListRow implements HasSourceReference {
@Localize("Equipment")
@Localize(locale = "de", value = "Ausrüstung")
@Localize(locale = "ru", value = "Снаряжение")
@Localize(locale = "es", value = "Equipo")
private static String DEFAULT_NAME;
static {
Localization.initialize();
}
private static final int CURRENT_VERSION = 4;
private static final String NEWLINE = "\n"; //$NON-NLS-1$
private static final String SPACE = " "; //$NON-NLS-1$
private static final String DEFAULT_LEGALITY_CLASS = "4"; //$NON-NLS-1$
private static final String EMPTY = ""; //$NON-NLS-1$
/** The extension for Equipment lists. */
public static final String OLD_EQUIPMENT_EXTENSION = "eqp"; //$NON-NLS-1$
/** The XML tag used for items. */
public static final String TAG_EQUIPMENT = "equipment"; //$NON-NLS-1$
/** The XML tag used for containers. */
public static final String TAG_EQUIPMENT_CONTAINER = "equipment_container"; //$NON-NLS-1$
private static final String ATTRIBUTE_STATE = "state"; //$NON-NLS-1$
private static final String ATTRIBUTE_EQUIPPED = "equipped"; //$NON-NLS-1$
private static final String TAG_QUANTITY = "quantity"; //$NON-NLS-1$
private static final String TAG_DESCRIPTION = "description"; //$NON-NLS-1$
private static final String TAG_TECH_LEVEL = "tech_level"; //$NON-NLS-1$
private static final String TAG_LEGALITY_CLASS = "legality_class"; //$NON-NLS-1$
private static final String TAG_VALUE = "value"; //$NON-NLS-1$
private static final String TAG_WEIGHT = "weight"; //$NON-NLS-1$
private static final String TAG_REFERENCE = "reference"; //$NON-NLS-1$
/** The prefix used in front of all IDs for the equipment. */
public static final String PREFIX = GURPSCharacter.CHARACTER_PREFIX + "equipment."; //$NON-NLS-1$
/** The field ID for equipped/carried/not carried changes. */
public static final String ID_STATE = PREFIX + "State"; //$NON-NLS-1$
/** The field ID for quantity changes. */
public static final String ID_QUANTITY = PREFIX + "Quantity"; //$NON-NLS-1$
/** The field ID for description changes. */
public static final String ID_DESCRIPTION = PREFIX + "Description"; //$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 legality changes. */
public static final String ID_LEGALITY_CLASS = PREFIX + "LegalityClass"; //$NON-NLS-1$
/** The field ID for value changes. */
public static final String ID_VALUE = PREFIX + "Value"; //$NON-NLS-1$
/** The field ID for weight changes. */
public static final String ID_WEIGHT = PREFIX + "Weight"; //$NON-NLS-1$
/** The field ID for extended value changes */
public static final String ID_EXTENDED_VALUE = PREFIX + "ExtendedValue"; //$NON-NLS-1$
/** The field ID for extended weight changes */
public static final String ID_EXTENDED_WEIGHT = PREFIX + "ExtendedWeight"; //$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 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 equipment becomes or stops being a weapon. */
public static final String ID_WEAPON_STATUS_CHANGED = PREFIX + "WeaponStatus"; //$NON-NLS-1$
private EquipmentState mState;
private int mQuantity;
private String mDescription;
private String mTechLevel;
private String mLegalityClass;
private double mValue;
private WeightValue mWeight;
private double mExtendedValue;
private WeightValue mExtendedWeight;
private String mReference;
private ArrayList<WeaponStats> mWeapons;
/**
* Creates a new equipment.
*
* @param dataFile The data file to associate it with.
* @param isContainer Whether or not this row allows children.
*/
public Equipment(DataFile dataFile, boolean isContainer) {
super(dataFile, isContainer);
mState = EquipmentState.EQUIPPED;
mQuantity = 1;
mDescription = DEFAULT_NAME;
mTechLevel = EMPTY;
mLegalityClass = DEFAULT_LEGALITY_CLASS;
mReference = EMPTY;
mWeight = new WeightValue(0, SheetPreferences.getWeightUnits());
mExtendedWeight = new WeightValue(mWeight);
mWeapons = new ArrayList<>();
}
/**
* Creates a clone of an existing equipment and associates it with the specified data file.
*
* @param dataFile The data file to associate it with.
* @param equipment The equipment to clone.
* @param deep Whether or not to clone the children, grandchildren, etc.
*/
public Equipment(DataFile dataFile, Equipment equipment, boolean deep) {
super(dataFile, equipment);
boolean forSheet = dataFile instanceof GURPSCharacter;
mState = forSheet ? equipment.mState : EquipmentState.EQUIPPED;
mQuantity = forSheet ? equipment.mQuantity : 1;
mDescription = equipment.mDescription;
mTechLevel = equipment.mTechLevel;
mLegalityClass = equipment.mLegalityClass;
mValue = equipment.mValue;
mWeight = new WeightValue(equipment.mWeight);
mExtendedValue = mQuantity * mValue;
mExtendedWeight = new WeightValue(mWeight);
mExtendedWeight.setValue(mExtendedWeight.getValue() * mQuantity);
mReference = equipment.mReference;
mWeapons = new ArrayList<>(equipment.mWeapons.size());
for (WeaponStats weapon : equipment.mWeapons) {
if (weapon instanceof MeleeWeaponStats) {
mWeapons.add(new MeleeWeaponStats(this, (MeleeWeaponStats) weapon));
} else if (weapon instanceof RangedWeaponStats) {
mWeapons.add(new RangedWeaponStats(this, (RangedWeaponStats) weapon));
}
}
if (deep) {
int count = equipment.getChildCount();
for (int i = 0; i < count; i++) {
addChild(new Equipment(dataFile, (Equipment) equipment.getChild(i), true));
}
}
}
/**
* Loads an equipment 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 Equipment(DataFile dataFile, XMLReader reader, LoadState state) throws IOException {
this(dataFile, TAG_EQUIPMENT_CONTAINER.equals(reader.getName()));
load(reader, state);
}
@Override
public boolean isEquivalentTo(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Equipment && super.isEquivalentTo(obj)) {
Equipment row = (Equipment) obj;
if (mQuantity == row.mQuantity && mValue == row.mValue && mWeight.equals(row.mWeight) && mState == row.mState && mDescription.equals(row.mDescription) && mTechLevel.equals(row.mTechLevel) && mLegalityClass.equals(row.mLegalityClass) && 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_EQUIPMENT_CONTAINER : TAG_EQUIPMENT;
}
@Override
public int getXMLTagVersion() {
return CURRENT_VERSION;
}
@Override
public String getRowType() {
return DEFAULT_NAME;
}
@Override
protected void prepareForLoad(LoadState state) {
super.prepareForLoad(state);
mState = EquipmentState.EQUIPPED;
mQuantity = 1;
mDescription = DEFAULT_NAME;
mTechLevel = EMPTY;
mLegalityClass = DEFAULT_LEGALITY_CLASS;
mReference = EMPTY;
mValue = 0.0;
mWeight.setValue(0.0);
mWeapons = new ArrayList<>();
}
@Override
protected void loadAttributes(XMLReader reader, LoadState state) {
super.loadAttributes(reader, state);
if (mDataFile instanceof GURPSCharacter) {
if (state.mDataItemVersion == 0) {
if (state.mDefaultCarried) {
setState(reader.isAttributeSet(ATTRIBUTE_EQUIPPED) ? EquipmentState.EQUIPPED : EquipmentState.NOT_CARRIED);
} else {
setState(EquipmentState.NOT_CARRIED);
}
} else {
setState(Enums.extract(reader.getAttribute(ATTRIBUTE_STATE), EquipmentState.values(), EquipmentState.NOT_CARRIED));
}
}
}
@Override
protected void loadSubElement(XMLReader reader, LoadState state) throws IOException {
String name = reader.getName();
if (TAG_DESCRIPTION.equals(name)) {
mDescription = reader.readText().replace(NEWLINE, SPACE);
} else if (TAG_TECH_LEVEL.equals(name)) {
mTechLevel = reader.readText().replace(NEWLINE, SPACE);
} else if (TAG_LEGALITY_CLASS.equals(name)) {
mLegalityClass = reader.readText().replace(NEWLINE, SPACE);
} else if (TAG_VALUE.equals(name)) {
mValue = reader.readDouble(0.0);
} else if (TAG_WEIGHT.equals(name)) {
mWeight = WeightValue.extract(reader.readText(), false);
} else if (TAG_REFERENCE.equals(name)) {
mReference = reader.readText().replace(NEWLINE, SPACE);
} else if (!state.mForUndo && (TAG_EQUIPMENT.equals(name) || TAG_EQUIPMENT_CONTAINER.equals(name))) {
addChild(new Equipment(mDataFile, reader, state));
} 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 if (OldWeapon.TAG_ROOT.equals(name)) {
state.mOldWeapons.put(this, new OldWeapon(reader));
} else if (!canHaveChildren()) {
if (TAG_QUANTITY.equals(name)) {
mQuantity = reader.readInteger(1);
} else {
super.loadSubElement(reader, state);
}
} else {
super.loadSubElement(reader, state);
}
}
@Override
protected void finishedLoading(LoadState state) {
OldWeapon oldWeapon = state.mOldWeapons.remove(this);
if (oldWeapon != null) {
mWeapons.addAll(oldWeapon.getWeapons(this));
}
// We no longer have defaults... that was solely for the weapons
setDefaults(new ArrayList<SkillDefault>());
updateExtendedValue(false);
updateExtendedWeight(false);
super.finishedLoading(state);
}
@Override
protected void saveAttributes(XMLWriter out, boolean forUndo) {
if (mDataFile instanceof GURPSCharacter) {
out.writeAttribute(ATTRIBUTE_STATE, Enums.toId(mState));
}
}
@Override
protected void saveSelf(XMLWriter out, boolean forUndo) {
if (!canHaveChildren()) {
out.simpleTag(TAG_QUANTITY, mQuantity);
}
out.simpleTagNotEmpty(TAG_DESCRIPTION, mDescription);
out.simpleTagNotEmpty(TAG_TECH_LEVEL, mTechLevel);
out.simpleTagNotEmpty(TAG_LEGALITY_CLASS, mLegalityClass);
out.simpleTag(TAG_VALUE, mValue);
if (mWeight.getNormalizedValue() != 0) {
out.simpleTag(TAG_WEIGHT, mWeight.toString(false));
}
out.simpleTagNotEmpty(TAG_REFERENCE, mReference);
for (WeaponStats weapon : mWeapons) {
weapon.save(out);
}
}
@Override
public void update() {
updateExtendedValue(true);
updateExtendedWeight(true);
}
/** @return The quantity. */
public int getQuantity() {
return mQuantity;
}
/**
* @param quantity The quantity to set.
* @return Whether it was modified.
*/
public boolean setQuantity(int quantity) {
if (quantity != mQuantity) {
mQuantity = quantity;
startNotify();
notify(ID_QUANTITY, this);
updateContainingWeights(true);
updateContainingValues(true);
endNotify();
return true;
}
return false;
}
/** @return The description. */
public String getDescription() {
return mDescription;
}
/**
* @param description The description to set.
* @return Whether it was modified.
*/
public boolean setDescription(String description) {
if (!mDescription.equals(description)) {
mDescription = description;
notifySingle(ID_DESCRIPTION);
return true;
}
return false;
}
/** @return The tech level. */
public String getTechLevel() {
return mTechLevel;
}
/**
* @param techLevel The tech level to set.
* @return Whether it was modified.
*/
public boolean setTechLevel(String techLevel) {
if (!mTechLevel.equals(techLevel)) {
mTechLevel = techLevel;
notifySingle(ID_TECH_LEVEL);
return true;
}
return false;
}
/** @return The legality class. */
public String getLegalityClass() {
return mLegalityClass;
}
/**
* @param legalityClass The legality class to set.
* @return Whether it was modified.
*/
public boolean setLegalityClass(String legalityClass) {
if (!mLegalityClass.equals(legalityClass)) {
mLegalityClass = legalityClass;
notifySingle(ID_LEGALITY_CLASS);
return true;
}
return false;
}
/** @return The value. */
public double getValue() {
return mValue;
}
/**
* @param value The value to set.
* @return Whether it was modified.
*/
public boolean setValue(double value) {
if (value != mValue) {
mValue = value;
startNotify();
notify(ID_VALUE, this);
updateContainingValues(true);
endNotify();
return true;
}
return false;
}
/** @return The extended value. */
public double getExtendedValue() {
return mExtendedValue;
}
/** @return The weight. */
public WeightValue getWeight() {
return mWeight;
}
/**
* @param weight The weight to set.
* @return Whether it was modified.
*/
public boolean setWeight(WeightValue weight) {
if (!mWeight.equals(weight)) {
mWeight = new WeightValue(weight);
startNotify();
notify(ID_WEIGHT, this);
updateContainingWeights(true);
endNotify();
return true;
}
return false;
}
private boolean updateExtendedWeight(boolean okToNotify) {
WeightValue saved = mExtendedWeight;
int count = getChildCount();
WeightUnits units = mWeight.getUnits();
mExtendedWeight = new WeightValue(mWeight.getValue() * mQuantity, units);
WeightValue contained = new WeightValue(0, units);
for (int i = 0; i < count; i++) {
Equipment one = (Equipment) getChild(i);
if (one.isCarried()) {
WeightValue weight = one.mExtendedWeight;
if (SheetPreferences.areGurpsMetricRulesUsed()) {
if (units.isMetric()) {
weight = GURPSCharacter.convertToGurpsMetric(weight);
} else {
weight = GURPSCharacter.convertFromGurpsMetric(weight);
}
}
contained.add(weight);
}
}
int percentage = 0;
WeightValue reduction = new WeightValue(0, units);
for (Feature feature : getFeatures()) {
if (feature instanceof ContainedWeightReduction) {
ContainedWeightReduction cwr = (ContainedWeightReduction) feature;
if (cwr.isPercentage()) {
percentage += cwr.getPercentageReduction();
} else {
reduction.add(cwr.getAbsoluteReduction());
}
}
}
if (percentage > 0) {
if (percentage >= 100) {
contained = new WeightValue(0, units);
} else {
contained.subtract(new WeightValue(contained.getNormalizedValue() * percentage / 100, units));
}
}
contained.subtract(reduction);
if (contained.getNormalizedValue() > 0) {
mExtendedWeight.add(contained);
}
if (!saved.equals(mExtendedWeight)) {
if (okToNotify) {
notify(ID_EXTENDED_WEIGHT, this);
}
return true;
}
return false;
}
private void updateContainingWeights(boolean okToNotify) {
Row parent = this;
while (parent != null && parent instanceof Equipment) {
Equipment parentRow = (Equipment) parent;
if (parentRow.updateExtendedWeight(okToNotify)) {
parent = parentRow.getParent();
} else {
break;
}
}
}
private boolean updateExtendedValue(boolean okToNotify) {
double savedValue = mExtendedValue;
int count = getChildCount();
mExtendedValue = mQuantity * mValue;
for (int i = 0; i < count; i++) {
Equipment child = (Equipment) getChild(i);
mExtendedValue += child.mExtendedValue;
}
if (savedValue != mExtendedValue) {
if (okToNotify) {
notify(ID_EXTENDED_VALUE, this);
}
return true;
}
return false;
}
private void updateContainingValues(boolean okToNotify) {
Row parent = this;
while (parent != null && parent instanceof Equipment) {
Equipment parentRow = (Equipment) parent;
if (parentRow.updateExtendedValue(okToNotify)) {
parent = parentRow.getParent();
} else {
break;
}
}
}
/** @return The extended weight. */
public WeightValue getExtendedWeight() {
return mExtendedWeight;
}
/** @return Whether this item is carried. */
public boolean isCarried() {
return mState == EquipmentState.CARRIED || mState == EquipmentState.EQUIPPED;
}
/** @return Whether this item is equipped. */
public boolean isEquipped() {
return mState == EquipmentState.EQUIPPED;
}
/** @return The current {@link EquipmentState}. */
public EquipmentState getState() {
return mState;
}
/**
* @param state The new {@link EquipmentState}.
* @return Whether it was changed.
*/
public boolean setState(EquipmentState state) {
if (mState != state) {
mState = state;
startNotify();
notify(ID_STATE, this);
if (canHaveChildren()) {
for (Row child : getChildren()) {
((Equipment) child).setState(state);
}
}
Row parent = getParent();
if (parent != null) {
((Equipment) parent).updateContainingWeights(true);
}
endNotify();
return true;
}
return false;
}
@Override
public String getReference() {
return mReference;
}
@Override
public String getReferenceHighlight() {
return getDescription();
}
@Override
public boolean setReference(String reference) {
if (!mReference.equals(reference)) {
mReference = reference;
notifySingle(ID_REFERENCE);
return true;
}
return false;
}
@Override
public boolean contains(String text, boolean lowerCaseOnly) {
if (getDescription().toLowerCase().indexOf(text) != -1) {
return true;
}
return super.contains(text, lowerCaseOnly);
}
@Override
public Object getData(Column column) {
return EquipmentColumn.values()[column.getID()].getData(this);
}
@Override
public String getDataAsText(Column column) {
return EquipmentColumn.values()[column.getID()].getDataAsText(this);
}
@Override
public String toString() {
return getDescription();
}
/** @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;
}
@Override
public StdImage getIcon(boolean large) {
return GCSImages.getEquipmentIcons().getImage(large ? 64 : 16);
}
@Override
public RowEditor<? extends ListRow> createEditor() {
return new EquipmentEditor(this);
}
@Override
public void fillWithNameableKeys(HashSet<String> set) {
super.fillWithNameableKeys(set);
extractNameables(set, mDescription);
for (WeaponStats weapon : mWeapons) {
for (SkillDefault one : weapon.getDefaults()) {
one.fillWithNameableKeys(set);
}
}
}
@Override
public void applyNameableKeys(HashMap<String, String> map) {
super.applyNameableKeys(map);
mDescription = nameNameables(map, mDescription);
for (WeaponStats weapon : mWeapons) {
for (SkillDefault one : weapon.getDefaults()) {
one.applyNameableKeys(map);
}
}
}
@Override
protected String getCategoryID() {
return ID_CATEGORY;
}
}