/* * 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.weapon; import com.trollworks.gcs.advantage.Advantage; import com.trollworks.gcs.character.GURPSCharacter; import com.trollworks.gcs.common.DataFile; import com.trollworks.gcs.equipment.Equipment; import com.trollworks.gcs.feature.LeveledAmount; import com.trollworks.gcs.feature.WeaponBonus; import com.trollworks.gcs.skill.Skill; import com.trollworks.gcs.skill.SkillDefault; import com.trollworks.gcs.skill.SkillDefaultType; import com.trollworks.gcs.spell.Spell; import com.trollworks.gcs.widgets.outline.ListRow; import com.trollworks.toolkit.io.xml.XMLNodeType; import com.trollworks.toolkit.io.xml.XMLReader; import com.trollworks.toolkit.io.xml.XMLWriter; import com.trollworks.toolkit.utility.Dice; import com.trollworks.toolkit.utility.text.Numbers; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; /** The stats for a weapon. */ public abstract class WeaponStats { private static final String TAG_DAMAGE = "damage"; //$NON-NLS-1$ private static final String TAG_STRENGTH = "strength"; //$NON-NLS-1$ private static final String TAG_USAGE = "usage"; //$NON-NLS-1$ /** The prefix used in front of all IDs for weapons. */ public static final String PREFIX = GURPSCharacter.CHARACTER_PREFIX + "weapon."; //$NON-NLS-1$ /** The field ID for damage changes. */ public static final String ID_DAMAGE = PREFIX + TAG_DAMAGE; /** The field ID for strength changes. */ public static final String ID_STRENGTH = PREFIX + TAG_STRENGTH; /** The field ID for usage changes. */ public static final String ID_USAGE = PREFIX + TAG_USAGE; /** An empty string. */ protected static final String EMPTY = ""; //$NON-NLS-1$ private ListRow mOwner; private String mDamage; private String mStrength; private String mUsage; private ArrayList<SkillDefault> mDefaults; /** * Creates a new weapon. * * @param owner The owning piece of equipment or advantage. */ protected WeaponStats(ListRow owner) { mOwner = owner; mDamage = EMPTY; mStrength = EMPTY; mUsage = EMPTY; mDefaults = new ArrayList<>(); initialize(); } /** * Creates a clone of the specified weapon. * * @param owner The owning piece of equipment or advantage. * @param other The weapon to clone. */ protected WeaponStats(ListRow owner, WeaponStats other) { mOwner = owner; mDamage = other.mDamage; mStrength = other.mStrength; mUsage = other.mUsage; mDefaults = new ArrayList<>(); for (SkillDefault skillDefault : other.mDefaults) { mDefaults.add(new SkillDefault(skillDefault)); } } /** * Creates a weapon. * * @param owner The owning piece of equipment or advantage. * @param reader The reader to load from. */ public WeaponStats(ListRow owner, XMLReader reader) throws IOException { this(owner); String marker = reader.getMarker(); do { if (reader.next() == XMLNodeType.START_TAG) { loadSelf(reader); } } while (reader.withinMarker(marker)); } /** * Creates a clone of this weapon. * * @param owner The owning piece of equipment or advantage. * @return The cloned weapon. */ public abstract WeaponStats clone(ListRow owner); /** Called so that sub-classes can initialize themselves. */ protected abstract void initialize(); /** @param reader The reader to load from. */ protected void loadSelf(XMLReader reader) throws IOException { String name = reader.getName(); if (TAG_DAMAGE.equals(name)) { mDamage = reader.readText(); } else if (TAG_STRENGTH.equals(name)) { mStrength = reader.readText(); } else if (TAG_USAGE.equals(name)) { mUsage = reader.readText(); } else if (SkillDefault.TAG_ROOT.equals(name)) { mDefaults.add(new SkillDefault(reader)); } else { reader.skipTag(name); } } /** @return The root XML tag to use when saving. */ protected abstract String getRootTag(); /** * Saves the weapon. * * @param out The XML writer to use. */ public void save(XMLWriter out) { out.startSimpleTagEOL(getRootTag()); out.simpleTagNotEmpty(TAG_DAMAGE, mDamage); out.simpleTagNotEmpty(TAG_STRENGTH, mStrength); out.simpleTagNotEmpty(TAG_USAGE, mUsage); saveSelf(out); for (SkillDefault skillDefault : mDefaults) { skillDefault.save(out); } out.endTagEOL(getRootTag(), true); } /** * Called so that sub-classes can save their own data. * * @param out The XML writer to use. */ protected abstract void saveSelf(XMLWriter out); /** @return The defaults for this weapon. */ public List<SkillDefault> getDefaults() { return Collections.unmodifiableList(mDefaults); } /** * @param defaults The new defaults for this weapon. * @return Whether there was a change or not. */ public boolean setDefaults(List<SkillDefault> defaults) { if (!mDefaults.equals(defaults)) { mDefaults = new ArrayList<>(defaults); return true; } return false; } /** @param id The ID to use for notification. */ protected void notifySingle(String id) { if (mOwner != null) { mOwner.notifySingle(id); } } /** @return A description of the weapon. */ public String getDescription() { if (mOwner instanceof Equipment) { return ((Equipment) mOwner).getDescription(); } if (mOwner instanceof Advantage) { return ((Advantage) mOwner).getName(); } if (mOwner instanceof Spell) { return ((Spell) mOwner).getName(); } if (mOwner instanceof Skill) { return ((Skill) mOwner).getName(); } return EMPTY; } @Override public String toString() { return getDescription(); } /** @return The notes for this weapon. */ public String getNotes() { return mOwner != null ? mOwner.getNotes() : EMPTY; } /** @return The damage. */ public String getDamage() { return mDamage; } /** @return The damage, fully resolved for the user's sw or thr, if possible. */ public String getResolvedDamage() { DataFile df = mOwner.getDataFile(); String damage = mDamage; if (df instanceof GURPSCharacter) { GURPSCharacter character = (GURPSCharacter) df; HashSet<WeaponBonus> bonuses = new HashSet<>(); for (SkillDefault one : getDefaults()) { if (one.getType().isSkillBased()) { bonuses.addAll(character.getWeaponComparedBonusesFor(Skill.ID_NAME + "*", one.getName(), one.getSpecialization())); //$NON-NLS-1$ bonuses.addAll(character.getWeaponComparedBonusesFor(Skill.ID_NAME + "/" + one.getName(), one.getName(), one.getSpecialization())); //$NON-NLS-1$ } } damage = resolveDamage(damage, bonuses); } return damage.trim(); } private String resolveDamage(String damage, HashSet<WeaponBonus> bonuses) { int maxST = getMinStrengthValue() * 3; GURPSCharacter character = (GURPSCharacter) mOwner.getDataFile(); int st = character.getStrength() + character.getStrikingStrengthBonus(); Dice dice; String savedDamage; if (maxST > 0 && maxST < st) { st = maxST; } dice = GURPSCharacter.getSwing(st); do { savedDamage = damage; damage = resolveDamage(damage, "sw", dice); //$NON-NLS-1$ } while (!savedDamage.equals(damage)); dice = GURPSCharacter.getThrust(st); do { savedDamage = damage; damage = resolveDamage(damage, "thr", dice); //$NON-NLS-1$ } while (!savedDamage.equals(damage)); return resolveDamageBonuses(damage, bonuses); } private String resolveDamage(String damage, String type, Dice dice) { int where = damage.indexOf(type); if (where != -1) { int last = where + type.length(); int max = damage.length(); StringBuffer buffer = new StringBuffer(); int tmp; if (where > 0) { buffer.append(damage.substring(0, where)); } tmp = skipSpaces(damage, last); if (tmp < max) { char ch = damage.charAt(tmp); if (ch == '+' || ch == '-') { int modifier = 0; tmp = skipSpaces(damage, tmp + 1); while (tmp < max) { char digit = damage.charAt(tmp); if (isDigit(digit)) { modifier *= 10; modifier += digit - '0'; tmp++; } else { break; } } if (ch == '-') { modifier = -modifier; } last = tmp; dice = dice.clone(); dice.add(modifier); } if (last < max - 1 && damage.charAt(last) == ':') { tmp = last + 1; ch = damage.charAt(tmp++); if (ch == '+' || ch == '-') { int perDie = 0; while (tmp < max) { char digit = damage.charAt(tmp); if (isDigit(digit)) { perDie *= 10; perDie += digit - '0'; tmp++; } else { break; } } last = tmp; if (perDie > 0) { if (ch == '-') { perDie = -perDie; } dice = dice.clone(); dice.add(perDie * dice.getDieCount()); } } } } buffer.append(dice.toString()); if (last < max) { buffer.append(damage.substring(last)); } return buffer.toString(); } return damage; } private static boolean isDigit(char ch) { return ch >= '0' && ch <= '9'; } private String resolveDamageBonuses(String damage, HashSet<WeaponBonus> bonuses) { int max = damage.length(); int start = 0; while (true) { int where = damage.indexOf('d', start); if (where < 1) { return damage; } char digit = damage.charAt(where - 1); if (isDigit(digit)) { while (where > 0 && isDigit(damage.charAt(where - 1))) { where--; } StringBuffer buffer = new StringBuffer(); if (where > 0) { buffer.append(damage.substring(0, where)); } int[] dicePos = Dice.extractDicePosition(damage.substring(where)); Dice dice = new Dice(damage.substring(where + dicePos[0], where + dicePos[1] + 1)); if (mOwner instanceof Advantage) { Advantage advantage = (Advantage) mOwner; if (advantage.isLeveled()) { dice.multiply(advantage.getLevels()); } } for (WeaponBonus bonus : bonuses) { LeveledAmount lvlAmt = bonus.getAmount(); int amt = lvlAmt.getIntegerAmount(); if (lvlAmt.isPerLevel()) { dice.add(amt * dice.getDieCount()); } else { dice.add(amt); } } buffer.append(dice.toString()); if (where + dicePos[1] + 1 < max) { buffer.append(damage.substring(where + dicePos[1] + 1)); } return buffer.toString(); } start = where + 1; } } /** * @param buffer The string to find the next non-space character within. * @param index The index to start looking. * @return The index of the next non-space character. */ @SuppressWarnings("static-method") protected int skipSpaces(String buffer, int index) { int max = buffer.length(); while (index < max && buffer.charAt(index) == ' ') { index++; } return index; } /** * Sets the value of damage. * * @param damage The value to set. */ public void setDamage(String damage) { damage = sanitize(damage); if (!mDamage.equals(damage)) { mDamage = damage; notifySingle(ID_DAMAGE); } } /** @return The skill level. */ public int getSkillLevel() { DataFile df = mOwner.getDataFile(); if (df instanceof GURPSCharacter) { return getSkillLevel((GURPSCharacter) df); } return 0; } private int getSkillLevel(GURPSCharacter character) { int best = Integer.MIN_VALUE; for (SkillDefault skillDefault : getDefaults()) { SkillDefaultType type = skillDefault.getType(); int level = type.getSkillLevelFast(character, skillDefault, new HashSet<String>()); if (level > best) { best = level; } } if (best != Integer.MIN_VALUE) { int minST = getMinStrengthValue() - (character.getStrength() + character.getStrikingStrengthBonus()); if (minST > 0) { best -= minST; if (best < 0) { best = 0; } } } else { best = 0; } return best; } /** @return The minimum ST to use this weapon, or -1 if there is none. */ public int getMinStrengthValue() { StringBuilder builder = new StringBuilder(); int count = mStrength.length(); boolean started = false; for (int i = 0; i < count; i++) { char ch = mStrength.charAt(i); if (Character.isDigit(ch)) { builder.append(ch); started = true; } else if (started) { break; } } return started ? Numbers.extractInteger(builder.toString(), -1, false) : -1; } /** @return The usage. */ public String getUsage() { return mUsage; } /** @param usage The value to set. */ public void setUsage(String usage) { usage = sanitize(usage); if (!mUsage.equals(usage)) { mUsage = usage; notifySingle(ID_USAGE); } } /** @return The strength. */ public String getStrength() { return mStrength; } /** * Sets the value of strength. * * @param strength The value to set. */ public void setStrength(String strength) { strength = sanitize(strength); if (!mStrength.equals(strength)) { mStrength = strength; notifySingle(ID_STRENGTH); } } /** @return The owner. */ public ListRow getOwner() { return mOwner; } /** * Sets the value of owner. * * @param owner The value to set. */ public void setOwner(ListRow owner) { mOwner = owner; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof WeaponStats) { WeaponStats ws = (WeaponStats) obj; return mDamage.equals(ws.mDamage) && mStrength.equals(ws.mStrength) && mUsage.equals(ws.mUsage) && mDefaults.equals(ws.mDefaults); } return false; } @Override public int hashCode() { return super.hashCode(); } /** * @param data The data to sanitize. * @return The original data, or "", if the data was <code>null</code>. */ @SuppressWarnings("static-method") protected String sanitize(String data) { if (data == null) { return EMPTY; } return data; } }