/* * Copyright 2008 (C) Thomas Parker <thpr@users.sourceforge.net> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package plugin.lsttokens; import java.math.BigDecimal; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import pcgen.base.formula.Formula; import pcgen.base.lang.StringUtil; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Constants; import pcgen.cdom.base.FormulaFactory; import pcgen.cdom.base.Ungranted; import pcgen.cdom.enumeration.FormulaKey; import pcgen.cdom.enumeration.IntegerKey; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.StringKey; import pcgen.cdom.enumeration.Type; import pcgen.cdom.inst.EquipmentHead; import pcgen.cdom.reference.CDOMDirectSingleRef; import pcgen.cdom.reference.CDOMSingleRef; import pcgen.cdom.util.CControl; import pcgen.cdom.util.ControlUtilities; import pcgen.core.Equipment; import pcgen.core.Globals; import pcgen.core.SizeAdjustment; import pcgen.core.SpecialProperty; import pcgen.core.WeaponProf; import pcgen.core.bonus.Bonus; import pcgen.core.bonus.BonusObj; import pcgen.core.prereq.Prerequisite; import pcgen.core.prereq.PrerequisiteUtilities; import pcgen.rules.context.Changes; import pcgen.rules.context.LoadContext; import pcgen.rules.persistence.token.AbstractTokenWithSeparator; import pcgen.rules.persistence.token.CDOMPrimaryToken; import pcgen.rules.persistence.token.ParseResult; import pcgen.rules.persistence.token.PostDeferredToken; import pcgen.util.Logging; /** * @author djones4 * */ public class NaturalattacksLst extends AbstractTokenWithSeparator<CDOMObject> implements CDOMPrimaryToken<CDOMObject>, PostDeferredToken<CDOMObject> { private static final Class<WeaponProf> WEAPONPROF_CLASS = WeaponProf.class; /** * @see pcgen.persistence.lst.LstToken#getTokenName() */ @Override public String getTokenName() { return "NATURALATTACKS"; //$NON-NLS-1$ } @Override protected char separator() { return '|'; } /** * NATURAL WEAPONS CODE <p>first natural weapon is primary, the rest are * secondary; NATURALATTACKS:primary weapon name,weapon type,num * attacks,damage|secondary1 weapon name,weapon type,num * attacks,damage|secondary2 format is exactly as it would be in an * equipment lst file Type is of the format Weapon.Natural.Melee.Bludgeoning * number of attacks is the number of attacks with that weapon at BAB (for * primary), or BAB - 5 (for secondary) */ @Override protected ParseResult parseTokenWithSeparator(LoadContext context, CDOMObject obj, String value) { if (obj instanceof Ungranted) { return new ParseResult.Fail("Cannot use " + getTokenName() + " on an Ungranted object type: " + obj.getClass().getSimpleName(), context); } // Currently, this isn't going to work with monk attacks // - their unarmed stuff won't be affected. /* * This does not immediately resolve the Size, because it is an order of * operations issue. This token must allow the SIZE token to appear * AFTER this token in the LST file. Thus a deferred resolution (using a * Resolver) is required. */ int count = 1; StringTokenizer attackTok = new StringTokenizer(value, Constants.PIPE); // This is wrong as we need to replace old natural weapons // with "better" ones while (attackTok.hasMoreTokens()) { String tokString = attackTok.nextToken(); ParseResult pr = checkForIllegalSeparator(',', tokString); if (!pr.passed()) { return pr; } Equipment anEquip = createNaturalWeapon(context, obj, tokString.intern()); if (anEquip == null) { return ParseResult.INTERNAL_ERROR; //return new ParseResult.Fail("Natural Weapon Creation Failed for : " // + tokString, context); } if (count == 1) { anEquip.setModifiedName("Natural/Primary"); } else { anEquip.setModifiedName("Natural/Secondary"); } anEquip.setOutputIndex(0); anEquip.setOutputSubindex(count); // these values need to be locked. anEquip.setQty(new Float(1)); anEquip.setNumberCarried(new Float(1)); context.getObjectContext().addToList(obj, ListKey.NATURAL_WEAPON, anEquip); count++; } return ParseResult.SUCCESS; } /** * Create the Natural weapon equipment item aTok = primary weapon * name,weapon type,num attacks,damage for Example: * Tentacle,Weapon.Natural.Melee.Slashing,*4,1d6 * * @param aTok * @param size * @return natural weapon */ private Equipment createNaturalWeapon(LoadContext context, CDOMObject obj, String wpn) { StringTokenizer commaTok = new StringTokenizer(wpn, Constants.COMMA); int numTokens = commaTok.countTokens(); if (numTokens < 4) { Logging.errorPrint("Invalid Build of " + "Natural Weapon in " + getTokenName() + ": " + wpn); return null; } String attackName = commaTok.nextToken(); if (attackName.equalsIgnoreCase(Constants.LST_NONE)) { Logging.errorPrint("Attempt to Build 'None' as a " + "Natural Weapon in " + getTokenName() + ": " + wpn); return null; } attackName = attackName.intern(); Equipment anEquip = new Equipment(); anEquip.setName(attackName); anEquip.put(ObjectKey.PARENT, obj); /* * This really can't be raw equipment... It really never needs to be * referred to, but this means that duplicates are never being detected * and resolved... this needs to have a KEY defined, to keep it * unique... hopefully this is good enough :) * * CONSIDER This really isn't that great, because it's String dependent, * and may not remove identical items... it certainly works, but is ugly */ // anEquip.setKeyName(obj.getClass().getSimpleName() + "," // + obj.getKeyName() + "," + wpn); /* * Perhaps the construction above should be through context just to * guarantee uniqueness of the key?? - that's too paranoid */ EquipmentHead equipHead = anEquip.getEquipmentHead(1); String profType = commaTok.nextToken(); if (hasIllegalSeparator('.', profType)) { return null; } StringTokenizer dotTok = new StringTokenizer(profType, Constants.DOT); while (dotTok.hasMoreTokens()) { Type type = Type.getConstant(dotTok.nextToken()); anEquip.addToListFor(ListKey.TYPE, type); } String numAttacks = commaTok.nextToken(); boolean attacksFixed = !numAttacks.isEmpty() && numAttacks.charAt(0) == '*'; if (attacksFixed) { numAttacks = numAttacks.substring(1); } anEquip.put(ObjectKey.ATTACKS_PROGRESS, !attacksFixed); try { int bonusAttacks = Integer.parseInt(numAttacks) - 1; final BonusObj aBonus = Bonus.newBonus(context, "WEAPON|ATTACKS|" + bonusAttacks); if (aBonus == null) { Logging.errorPrint(getTokenName() + " was given invalid number of attacks: " + bonusAttacks); return null; } anEquip.addToListFor(ListKey.BONUS, aBonus); } catch (NumberFormatException exc) { Logging.errorPrint("Non-numeric value for number of attacks in " + getTokenName() + ": '" + numAttacks + "'"); return null; } equipHead.put(StringKey.DAMAGE, commaTok.nextToken()); // sage_sam 02 Dec 2002 for Bug #586332 // allow hands to be required to equip natural weapons int handsrequired = 0; while (commaTok.hasMoreTokens()) { final String hString = commaTok.nextToken(); if (hString.startsWith("SPROP=")) { anEquip.addToListFor(ListKey.SPECIAL_PROPERTIES, SpecialProperty.createFromLst(hString.substring(6))); } else { try { handsrequired = Integer .parseInt(hString); } catch (NumberFormatException exc) { Logging.errorPrint("Non-numeric value for hands required: '" + hString + "'"); return null; } } } anEquip.put(IntegerKey.SLOTS, handsrequired); anEquip.put(ObjectKey.WEIGHT, BigDecimal.ZERO); WeaponProf cwp = context.getReferenceContext().silentlyGetConstructedCDOMObject( WEAPONPROF_CLASS, attackName); if (cwp == null) { cwp = context.getReferenceContext().constructNowIfNecessary(WEAPONPROF_CLASS, attackName); cwp.addToListFor(ListKey.TYPE, Type.NATURAL); } CDOMSingleRef<WeaponProf> wp = context.getReferenceContext().getCDOMReference( WEAPONPROF_CLASS, attackName); anEquip.put(ObjectKey.WEAPON_PROF, wp); anEquip.addToListFor(ListKey.IMPLIED_WEAPONPROF, wp); if (!ControlUtilities.hasControlToken(context, CControl.CRITRANGE)) { equipHead.put(IntegerKey.CRIT_RANGE, 1); } if (!ControlUtilities.hasControlToken(context, CControl.CRITMULT)) { equipHead.put(IntegerKey.CRIT_MULT, 2); } return anEquip; } @Override public String[] unparse(LoadContext context, CDOMObject obj) { Changes<Equipment> changes = context.getObjectContext().getListChanges( obj, ListKey.NATURAL_WEAPON); Collection<Equipment> eqadded = changes.getAdded(); if (eqadded == null || eqadded.isEmpty()) { return null; } StringBuilder sb = new StringBuilder(); boolean first = true; for (Equipment lstw : eqadded) { if (!first) { sb.append(Constants.PIPE); } Equipment eq = Equipment.class.cast(lstw); String name = eq.getDisplayName(); // TODO objcontext.getString(eq, StringKey.NAME); if (name == null) { context.addWriteMessage(getTokenName() + " expected Equipment to have a name"); return null; } sb.append(name).append(Constants.COMMA); List<Type> type = eq.getListFor(ListKey.TYPE); if (type == null || type.isEmpty()) { context.addWriteMessage(getTokenName() + " expected Equipment to have a type"); return null; } sb.append(StringUtil.join(type, Constants.DOT)); sb.append(Constants.COMMA); Boolean attProgress = eq.get(ObjectKey.ATTACKS_PROGRESS); if (attProgress == null) { context.addWriteMessage(getTokenName() + " expected Equipment to know ATTACKS_PROGRESS state"); return null; } else if (!attProgress.booleanValue()) { sb.append(Constants.CHAR_ASTERISK); } List<BonusObj> bonuses = eq.getListFor(ListKey.BONUS); if (bonuses == null || bonuses.isEmpty()) { sb.append("1"); } else { if (bonuses.size() != 1) { context.addWriteMessage(getTokenName() + " expected only one BONUS on Equipment: " + bonuses); return null; } // TODO Validate BONUS type? BonusObj extraAttacks = bonuses.iterator().next(); sb.append(Integer.parseInt(extraAttacks.getValue()) + 1); } sb.append(Constants.COMMA); EquipmentHead head = eq.getEquipmentHeadReference(1); if (head == null) { context.addWriteMessage(getTokenName() + " expected an EquipmentHead on Equipment"); return null; } String damage = head.get(StringKey.DAMAGE); if (damage == null) { context.addWriteMessage(getTokenName() + " expected a Damage on EquipmentHead"); return null; } sb.append(damage); Integer hands = eq.get(IntegerKey.SLOTS); if (hands != null && hands != 0) { sb.append(',').append(hands); } List<SpecialProperty> spropList = eq.getSafeListFor(ListKey.SPECIAL_PROPERTIES); for (SpecialProperty sprop : spropList) { sb.append(",SPROP=").append(sprop.toString()); } first = false; } return new String[] { sb.toString() }; } @Override public Class<CDOMObject> getTokenClass() { return CDOMObject.class; } @Override public boolean process(LoadContext context, CDOMObject obj) { List<Equipment> natWeapons = obj.getListFor(ListKey.NATURAL_WEAPON); if (natWeapons != null) { Formula sizeFormula = obj.getSafe(FormulaKey.SIZE); // If the size was just a default, check for a size prereq and use that instead. if (obj.get(FormulaKey.SIZE) == null && obj.hasPreReqTypeOf("SIZE")) { Integer requiredSize = getRequiredSize(obj); if (requiredSize != null) { sizeFormula = FormulaFactory.getFormulaFor(requiredSize); } } if (sizeFormula.isStatic()) { int isize = sizeFormula.resolveStatic().intValue(); SizeAdjustment size = context .getReferenceContext() .getSortedList(SizeAdjustment.class, IntegerKey.SIZEORDER).get(isize); for (Equipment e : natWeapons) { CDOMDirectSingleRef<SizeAdjustment> sizeRef = CDOMDirectSingleRef.getRef(size); e.put(ObjectKey.BASESIZE, sizeRef); e.put(ObjectKey.SIZE, sizeRef); } } else { Logging.errorPrint("SIZE in " + obj.getClass().getSimpleName() + " " + obj.getKeyName() + " must not be a variable " + "if it contains a NATURALATTACKS token"); } } return true; } /** * Retrieve the required size (i.e. PRESIZE) for the object defining the attack. Will * only return a value if there is a single size. * @param obj The defining object. * @return The size integer, or null if none (or multiple) specified. */ private Integer getRequiredSize(CDOMObject obj) { Set<Prerequisite> sizePrereqs = new HashSet<>(); for (Prerequisite prereq : obj.getPrerequisiteList()) { sizePrereqs.addAll(PrerequisiteUtilities.getPreReqsOfKind(prereq, "SIZE")); } Integer requiredSize = null; for (Prerequisite prereq : sizePrereqs) { SizeAdjustment sa = Globals .getContext() .getReferenceContext() .silentlyGetConstructedCDOMObject(SizeAdjustment.class, prereq.getOperand()); final int targetSize = sa.get(IntegerKey.SIZEORDER); if (requiredSize != null && requiredSize != targetSize) { return null; } requiredSize = targetSize; } return requiredSize; } @Override public Class<CDOMObject> getDeferredTokenClass() { return getTokenClass(); } @Override public int getPriority() { return 0; } }