v3.2.0: reworked thrown weapons to work in 'sets':

- sets are upgraded/enchanted/etc. as a unit, default size of 3
- upgrading completely repairs a set
- durability boost per upgrade down to 1.5x from 3x
- Internal buff added to prevent upgrade repair duplication exploits
- lots of effects that apply to melee weapons now apply to thrown: identification, enchants, curses, etc.
- liquid metal recipe adjusted (liquid metal itself still needs changes)
- Huntress ID talent changed now that thrown weapons are IDable
- darts effectively unchanged, they all belong to the same set, still not upgradeable
- no balance changes to accomodate this yet, tbd.
This commit is contained in:
Evan Debenham
2025-07-09 17:17:13 -04:00
parent 09c307032c
commit 954340216d
24 changed files with 402 additions and 152 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

@@ -1091,7 +1091,7 @@ actors.hero.talent.perfect_copy.desc=_+1:_ The shadow clone gains _10%_ of the h
actors.hero.talent.natures_bounty.title=nature's bounty actors.hero.talent.natures_bounty.title=nature's bounty
actors.hero.talent.natures_bounty.desc=_+1:_ The Huntress can find _4 berries_ hidden in tall grass as she explores the next few floors of the dungeon.\n\n_+2:_ The Huntress can find _6 berries_ hidden in tall grass as she explores the next few floors of the dungeon.\n\nBerries are eaten quickly, restore a small amount of satiety, and might contain a usable seed. actors.hero.talent.natures_bounty.desc=_+1:_ The Huntress can find _4 berries_ hidden in tall grass as she explores the next few floors of the dungeon.\n\n_+2:_ The Huntress can find _6 berries_ hidden in tall grass as she explores the next few floors of the dungeon.\n\nBerries are eaten quickly, restore a small amount of satiety, and might contain a usable seed.
actors.hero.talent.survivalists_intuition.title=survivalist's intuition actors.hero.talent.survivalists_intuition.title=survivalist's intuition
actors.hero.talent.survivalists_intuition.desc=_+1:_ The Huntress identifies all equipment _1.75x faster_.\n\n_+2:_ The Huntress identifies all equipment _2.5x faster_. actors.hero.talent.survivalists_intuition.desc=_+1:_ The Huntress identifies thrown weapons _3x faster_.\n\n_+2:_ The Huntress identifies thrown weapons _when she attacks with them_.
actors.hero.talent.followup_strike.title=followup strike actors.hero.talent.followup_strike.title=followup strike
actors.hero.talent.followup_strike.desc=_+1:_ When the Huntress hits an enemy with her bow or a thrown weapon, her next melee attack against that enemy deals _2 bonus damage_.\n\n_+2:_ When the Huntress hits an enemy with her bow or a thrown weapon, her next melee attack against that enemy deals _3 bonus damage_. actors.hero.talent.followup_strike.desc=_+1:_ When the Huntress hits an enemy with her bow or a thrown weapon, her next melee attack against that enemy deals _2 bonus damage_.\n\n_+2:_ When the Huntress hits an enemy with her bow or a thrown weapon, her next melee attack against that enemy deals _3 bonus damage_.
actors.hero.talent.natures_aid.title=nature's aid actors.hero.talent.natures_aid.title=nature's aid
@@ -1163,7 +1163,7 @@ items.scrolls.scrollofupgrade.name=scroll of upgrade
items.scrolls.scrollofupgrade.inv_title=Upgrade an item items.scrolls.scrollofupgrade.inv_title=Upgrade an item
items.scrolls.scrollofupgrade.weaken_curse=The scroll of upgrade weakens the curse on your item. items.scrolls.scrollofupgrade.weaken_curse=The scroll of upgrade weakens the curse on your item.
items.scrolls.scrollofupgrade.remove_curse=The scroll of upgrade cleanses the curse on your item! items.scrolls.scrollofupgrade.remove_curse=The scroll of upgrade cleanses the curse on your item!
items.scrolls.scrollofupgrade.desc=This scroll will upgrade a single item. Wands will increase in power and number of charges, weapons and armor will deal and block more damage, and the effects of rings will intensify.\n\nIt can even weaken or sometimes totally dispel curses, though it is not as potent as a scroll of remove curse. Unfortunately, the upgrading magic can also erase enchantments or glyphs on higher level gear. items.scrolls.scrollofupgrade.desc=This scroll will upgrade a single item or set of thrown weapons. Wands will increase in power and number of charges, weapons and armor will deal and block more damage, thrown weapon sets will be fully repaired and more durable, and the effects of rings will intensify.\n\nIt can even weaken or sometimes totally dispel curses, though it is not as potent as a scroll of remove curse. Unfortunately, the upgrading magic can also erase enchantments or glyphs on higher level gear.
@@ -2105,7 +2105,7 @@ items.weapon.missiles.darts.dart.tip_two=tip 2 darts with 1 seed
items.weapon.missiles.darts.dart.tip_one=tip 1 dart with 1 seed items.weapon.missiles.darts.dart.tip_one=tip 1 dart with 1 seed
items.weapon.missiles.darts.dart.tip_cancel=cancel items.weapon.missiles.darts.dart.tip_cancel=cancel
items.weapon.missiles.darts.dart.desc=These simple shafts of spike-tipped wood are weighted to fly true and sting their prey with a flick of the wrist. They can be tipped with seeds to gain bonus effects when thrown. items.weapon.missiles.darts.dart.desc=These simple shafts of spike-tipped wood are weighted to fly true and sting their prey with a flick of the wrist. They can be tipped with seeds to gain bonus effects when thrown.
items.weapon.missiles.darts.dart.unlimited_uses=However, due to their simple construction darts will effectively last forever. items.weapon.missiles.darts.dart.unlimited_uses=Due to their simple construction darts will effectively last forever.
items.weapon.missiles.darts.displacingdart.name=displacing dart items.weapon.missiles.darts.displacingdart.name=displacing dart
items.weapon.missiles.darts.displacingdart.desc=These darts are tipped with a fadeleaf-based compound which will teleport their target a short distance away. items.weapon.missiles.darts.displacingdart.desc=These darts are tipped with a fadeleaf-based compound which will teleport their target a short distance away.
@@ -2138,7 +2138,6 @@ items.weapon.missiles.darts.tippeddart.clean_desc=This action will remove the se
items.weapon.missiles.darts.tippeddart.clean_all=clean all items.weapon.missiles.darts.tippeddart.clean_all=clean all
items.weapon.missiles.darts.tippeddart.clean_one=clean one items.weapon.missiles.darts.tippeddart.clean_one=clean one
items.weapon.missiles.darts.tippeddart.cancel=cancel items.weapon.missiles.darts.tippeddart.cancel=cancel
items.weapon.missiles.darts.tippeddart.durability=Tipped darts will lose their tips and become regular darts when used.
items.weapon.missiles.darts.tippeddart.uses_left=This stack of darts has _%d/%d_ uses left before one tip wears off. items.weapon.missiles.darts.tippeddart.uses_left=This stack of darts has _%d/%d_ uses left before one tip wears off.
items.weapon.missiles.darts.tippeddart.unlimited_uses=_But these are of such high quality that they will effectively last forever._ items.weapon.missiles.darts.tippeddart.unlimited_uses=_But these are of such high quality that they will effectively last forever._
items.weapon.missiles.darts.tippeddart.about_to_break=Your dart's tip is about to expire. items.weapon.missiles.darts.tippeddart.about_to_break=Your dart's tip is about to expire.
@@ -2169,13 +2168,18 @@ items.weapon.missiles.javelin.desc=These larger throwing spears are weighted to
items.weapon.missiles.kunai.name=kunai items.weapon.missiles.kunai.name=kunai
items.weapon.missiles.kunai.desc=These small knives are very powerful in the hands of a skilled user. They are most effective against unaware enemies. items.weapon.missiles.kunai.desc=These small knives are very powerful in the hands of a skilled user. They are most effective against unaware enemies.
items.weapon.missiles.missileweapon.stats=This _tier-%1$d_ thrown weapon deals _%2$d-%3$d damage_ and requires _%4$d strength_ to use properly. items.weapon.missiles.missileweapon.stats_known=This set of _tier-%1$d_ thrown weapons deals _%2$d-%3$d damage_ and requires _%4$d strength_ to use properly.
items.weapon.missiles.missileweapon.distance=This weapon is designed to be used at a distance, it is more accurate against distant enemies, but also much less accurate at melee range. items.weapon.missiles.missileweapon.stats_unknown=Typically this set of _tier-%1$d_ thrown weapons deals _%2$d-%3$d damage_ and requires _%4$d strength_ to use properly.
items.weapon.missiles.missileweapon.probably_too_heavy=Probably this weapon is too heavy for you.
items.weapon.missiles.missileweapon.distance=Thrown weapons are more accurate against distant enemies, but less accurate at melee range.
items.weapon.missiles.missileweapon.durability=Thrown weapons will wear out and break as they are used. items.weapon.missiles.missileweapon.durability=Thrown weapons will wear out and break as they are used.
items.weapon.missiles.missileweapon.uses_left=This stack of weapons has _%d/%d_ uses left before one breaks. items.weapon.missiles.missileweapon.uses_left=This set of thrown weapons has _%d/%d_ uses left before one breaks.
items.weapon.missiles.missileweapon.unlimited_uses=_But these are of such high quality that they will effectively last forever._ items.weapon.missiles.missileweapon.unlimited_uses=This set is of such high quality that it will effectively last forever.
items.weapon.missiles.missileweapon.unknown_uses=Typically this set of thrown weapons will have _%d_ uses before one breaks.
items.weapon.missiles.missileweapon.curse_discover=This thrown weapon is cursed!
items.weapon.missiles.missileweapon.about_to_break=Your thrown weapon is about to break. items.weapon.missiles.missileweapon.about_to_break=Your thrown weapon is about to break.
items.weapon.missiles.missileweapon.has_broken=One of your thrown weapons has broken. items.weapon.missiles.missileweapon.has_broken=One of your thrown weapons has broken.
items.weapon.missiles.missileweapon.dust=The thrown weapon crumbles to dust as you touch it.
items.weapon.missiles.missileweapon$placeholder.name=thrown weapon items.weapon.missiles.missileweapon$placeholder.name=thrown weapon
items.weapon.missiles.shuriken.name=shuriken items.weapon.missiles.shuriken.name=shuriken
@@ -2195,11 +2199,11 @@ items.weapon.missiles.throwingspear.name=throwing spear
items.weapon.missiles.throwingspear.desc=These lightweight spears have thin frames which are clearly designed to be thrown, and not thrusted. items.weapon.missiles.throwingspear.desc=These lightweight spears have thin frames which are clearly designed to be thrown, and not thrusted.
items.weapon.missiles.throwingspike.name=throwing spike items.weapon.missiles.throwingspike.name=throwing spike
items.weapon.missiles.throwingspike.desc=These pointed shafts of metal are meant to be thrown into distant enemies. While they aren't very strong, their simple all-metal construction makes them reasonably durable. items.weapon.missiles.throwingspike.desc=These durable pointed shafts of metal are meant to be thrown into distant enemies.
items.weapon.missiles.throwingspike.discover_hint=One of the heroes starts with this item. items.weapon.missiles.throwingspike.discover_hint=One of the heroes starts with this item.
items.weapon.missiles.throwingstone.name=throwing stone items.weapon.missiles.throwingstone.name=throwing stone
items.weapon.missiles.throwingstone.desc=These stones are sanded down to make them able to be thrown with more power than a regular stone. Despite the craftsmanship they still aren't a very reliable weapon, but at least they won't stick to enemies. items.weapon.missiles.throwingstone.desc=These stones are sanded down to make them able to be thrown with more power than a regular stone.
items.weapon.missiles.throwingstone.discover_hint=One of the heroes starts with this item. items.weapon.missiles.throwingstone.discover_hint=One of the heroes starts with this item.
items.weapon.missiles.tomahawk.name=tomahawk items.weapon.missiles.tomahawk.name=tomahawk
@@ -77,7 +77,7 @@ journal.document.alchemy_guide.exotic_scrolls.body=Exotic scrolls can be made wi
journal.document.alchemy_guide.bombs.title=Enhanced Bombs journal.document.alchemy_guide.bombs.title=Enhanced Bombs
journal.document.alchemy_guide.bombs.body=A standard black powder bomb can be mixed with a specific item to create an enhanced bomb. journal.document.alchemy_guide.bombs.body=A standard black powder bomb can be mixed with a specific item to create an enhanced bomb.
journal.document.alchemy_guide.weapons.title=Enhancing Weapons journal.document.alchemy_guide.weapons.title=Enhancing Weapons
journal.document.alchemy_guide.weapons.body=Some of the lighter or more magical weapons can be used in alchemy!\n\nEach thrown weapon produces enough liquid metal to fully repair another weapon of the same level and tier.\n\nOne wand will produce enough arcane resin to upgrade two wands of the same level, but no higher than +3. journal.document.alchemy_guide.weapons.body=Some of the lighter or more magical weapons can be used in alchemy!\n\nEach thrown weapon set produces enough liquid metal to fully repair another set of the same level and tier. Note that alchemizing a set will destroy the whole set, even if it isn't all present!\n\nOne wand will produce enough arcane resin to upgrade two wands of the same level, but no higher than +3.
journal.document.alchemy_guide.brews_elixirs.title=Brews and Elixirs journal.document.alchemy_guide.brews_elixirs.title=Brews and Elixirs
journal.document.alchemy_guide.brews_elixirs.body=Brews and elixirs are advanced potions with a variety of effects, usually with a single use. journal.document.alchemy_guide.brews_elixirs.body=Brews and elixirs are advanced potions with a variety of effects, usually with a single use.
journal.document.alchemy_guide.spells.title=Spells journal.document.alchemy_guide.spells.title=Spells
@@ -375,6 +375,7 @@ windows.wndupgrade.damage=Damage
windows.wndupgrade.blocking=Blocking windows.wndupgrade.blocking=Blocking
windows.wndupgrade.weight=Weight windows.wndupgrade.weight=Weight
windows.wndupgrade.durability=Durability windows.wndupgrade.durability=Durability
windows.wndupgrade.quantity=Quantity
windows.wndupgrade.zap_damage=Zap Damage windows.wndupgrade.zap_damage=Zap Damage
windows.wndupgrade.corrosion_damage=Corrosion Damage windows.wndupgrade.corrosion_damage=Corrosion Damage
windows.wndupgrade.ward_damage=Ward Damage windows.wndupgrade.ward_damage=Ward Damage
@@ -1067,7 +1067,8 @@ public class Hero extends Char {
|| item instanceof TimekeepersHourglass.sandBag || item instanceof TimekeepersHourglass.sandBag
|| item instanceof DriedRose.Petal || item instanceof DriedRose.Petal
|| item instanceof Key || item instanceof Key
|| item instanceof Guidebook) { || item instanceof Guidebook
|| item.quantity() == 0) {
//Do Nothing //Do Nothing
} else if (item instanceof DarkGold) { } else if (item instanceof DarkGold) {
DarkGold existing = belongings.getItem(DarkGold.class); DarkGold existing = belongings.getItem(DarkGold.class);
@@ -690,6 +690,10 @@ public enum Talent {
if (item instanceof Wand){ if (item instanceof Wand){
factor *= 1f + 2.0f*hero.pointsInTalent(SCHOLARS_INTUITION); factor *= 1f + 2.0f*hero.pointsInTalent(SCHOLARS_INTUITION);
} }
// 3x/instant speed with Huntress talent (see MissileWeapon.proc)
if (item instanceof MissileWeapon){
factor *= 1f + 2.0f*hero.pointsInTalent(SURVIVALISTS_INTUITION);
}
// 2x/instant for Rogue (see onItemEqupped), also id's type on equip/on pickup // 2x/instant for Rogue (see onItemEqupped), also id's type on equip/on pickup
if (item instanceof Ring){ if (item instanceof Ring){
factor *= 1f + hero.pointsInTalent(THIEFS_INTUITION); factor *= 1f + hero.pointsInTalent(THIEFS_INTUITION);
@@ -23,6 +23,7 @@ package com.shatteredpixel.shatteredpixeldungeon.items;
import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
import com.shatteredpixel.shatteredpixeldungeon.effects.Splash; import com.shatteredpixel.shatteredpixeldungeon.effects.Splash;
@@ -128,6 +129,10 @@ public class LiquidMetal extends Item {
return item instanceof MissileWeapon && !(item instanceof Dart); return item instanceof MissileWeapon && !(item instanceof Dart);
} }
//TODO this needs to fix broken thrown weapons too
// should also only apply to IDed thrown weapons?
// TODO maybe thrown weps and wands can just be known uncursed in order to make recipe?
@Override @Override
public void onSelect( Item item ) { public void onSelect( Item item ) {
if (item != null && item instanceof MissileWeapon) { if (item != null && item instanceof MissileWeapon) {
@@ -171,31 +176,24 @@ public class LiquidMetal extends Item {
@Override @Override
public boolean testIngredients(ArrayList<Item> ingredients) { public boolean testIngredients(ArrayList<Item> ingredients) {
for (Item i : ingredients){ return ingredients.size() == 1
if (!(i instanceof MissileWeapon)){ && ingredients.get(0) instanceof MissileWeapon
return false; && ingredients.get(0).isIdentified()
} && !ingredients.get(0).cursed;
}
return !ingredients.isEmpty();
} }
@Override @Override
public int cost(ArrayList<Item> ingredients) { public int cost(ArrayList<Item> ingredients) {
int cost = 1; return 3;
for (Item i : ingredients){
cost += i.quantity();
}
return cost;
} }
@Override @Override
public Item brew(ArrayList<Item> ingredients) { public Item brew(ArrayList<Item> ingredients) {
Item result = sampleOutput(ingredients); Item result = sampleOutput(ingredients);
for (Item i : ingredients){ MissileWeapon w = (MissileWeapon) ingredients.get(0);
i.quantity(0); w.quantity(0);
} Buff.affect(Dungeon.hero, MissileWeapon.UpgradedSetTracker.class).levelThresholds.put(w.setID, Integer.MAX_VALUE);
return result; return result;
} }
@@ -168,7 +168,7 @@ public abstract class Recipe {
//******* //*******
private static Recipe[] variableRecipes = new Recipe[]{ private static Recipe[] variableRecipes = new Recipe[]{
new LiquidMetal.Recipe() //none for now
}; };
private static Recipe[] oneIngredientRecipes = new Recipe[]{ private static Recipe[] oneIngredientRecipes = new Recipe[]{
@@ -176,6 +176,7 @@ public abstract class Recipe {
new ExoticPotion.PotionToExotic(), new ExoticPotion.PotionToExotic(),
new ExoticScroll.ScrollToExotic(), new ExoticScroll.ScrollToExotic(),
new ArcaneResin.Recipe(), new ArcaneResin.Recipe(),
new LiquidMetal.Recipe(),
new BlizzardBrew.Recipe(), new BlizzardBrew.Recipe(),
new InfernalBrew.Recipe(), new InfernalBrew.Recipe(),
new AquaBrew.Recipe(), new AquaBrew.Recipe(),
@@ -254,9 +255,10 @@ public abstract class Recipe {
} }
public static boolean usableInRecipe(Item item){ public static boolean usableInRecipe(Item item){
//only upgradeable thrown weapons and wands allowed among equipment items
if (item instanceof EquipableItem){ if (item instanceof EquipableItem){
//only thrown weapons and wands allowed among equipment items return item.isIdentified() && !item.cursed &&
return item.isIdentified() && !item.cursed && item instanceof MissileWeapon; item instanceof MissileWeapon && item.isUpgradable();
} else if (item instanceof Wand) { } else if (item instanceof Wand) {
return item.isIdentified() && !item.cursed; return item.isIdentified() && !item.cursed;
} else { } else {
@@ -26,8 +26,8 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent;
import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.armor.Armor; import com.shatteredpixel.shatteredpixeldungeon.items.armor.Armor;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MeleeWeapon;
import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog; import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
@@ -76,7 +76,7 @@ public class PotionOfMastery extends ExoticPotion {
@Override @Override
public boolean itemSelectable(Item item) { public boolean itemSelectable(Item item) {
return return
(item instanceof MeleeWeapon && !((MeleeWeapon) item).masteryPotionBonus) (item instanceof Weapon && !(item instanceof SpiritBow) && !((Weapon) item).masteryPotionBonus)
|| (item instanceof Armor && !((Armor) item).masteryPotionBonus); || (item instanceof Armor && !((Armor) item).masteryPotionBonus);
} }
@@ -24,6 +24,7 @@ package com.shatteredpixel.shatteredpixeldungeon.items.scrolls;
import com.shatteredpixel.shatteredpixeldungeon.Challenges; import com.shatteredpixel.shatteredpixeldungeon.Challenges;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.Statistics; import com.shatteredpixel.shatteredpixeldungeon.Statistics;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
import com.shatteredpixel.shatteredpixeldungeon.effects.Transmuting; import com.shatteredpixel.shatteredpixeldungeon.effects.Transmuting;
import com.shatteredpixel.shatteredpixeldungeon.items.EquipableItem; import com.shatteredpixel.shatteredpixeldungeon.items.EquipableItem;
@@ -124,7 +125,11 @@ public class ScrollOfTransmutation extends InventoryScroll {
} }
Dungeon.hero.spend(-Dungeon.hero.cooldown()); //cancel equip/unequip time Dungeon.hero.spend(-Dungeon.hero.cooldown()); //cancel equip/unequip time
} else { } else {
item.detach(Dungeon.hero.belongings.backpack); if (item instanceof MissileWeapon){
item.detachAll(Dungeon.hero.belongings.backpack);
} else {
item.detach(Dungeon.hero.belongings.backpack);
}
if (!result.collect()) { if (!result.collect()) {
Dungeon.level.drop(result, curUser.pos).sprite.drop(); Dungeon.level.drop(result, curUser.pos).sprite.drop();
} else if (result.stackable && Dungeon.hero.belongings.getSimilar(result) != null){ } else if (result.stackable && Dungeon.hero.belongings.getSimilar(result) != null){
@@ -237,7 +242,7 @@ public class ScrollOfTransmutation extends InventoryScroll {
} while (Challenges.isItemBlocked(n) || n.getClass() == w.getClass()); } while (Challenges.isItemBlocked(n) || n.getClass() == w.getClass());
n.level(0); n.level(0);
n.quantity(1); n.quantity(w.quantity());
int level = w.trueLevel(); int level = w.trueLevel();
if (level > 0) { if (level > 0) {
n.upgrade( level ); n.upgrade( level );
@@ -254,6 +259,11 @@ public class ScrollOfTransmutation extends InventoryScroll {
n.augment = w.augment; n.augment = w.augment;
n.enchantHardened = w.enchantHardened; n.enchantHardened = w.enchantHardened;
//technically a new set, ensure old one is destroyed (except for darts)
if (w instanceof MissileWeapon && w.isUpgradable()){
Buff.affect(Dungeon.hero, MissileWeapon.UpgradedSetTracker.class).levelThresholds.put(((MissileWeapon) w).setID, Integer.MAX_VALUE);
}
return n; return n;
} }
@@ -29,9 +29,7 @@ import com.shatteredpixel.shatteredpixeldungeon.items.armor.Armor;
import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag; import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag;
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.InventoryScroll; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.InventoryScroll;
import com.shatteredpixel.shatteredpixeldungeon.items.stones.StoneOfEnchantment; import com.shatteredpixel.shatteredpixeldungeon.items.stones.StoneOfEnchantment;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MeleeWeapon;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
@@ -68,7 +66,7 @@ public class ScrollOfEnchantment extends ExoticScroll {
} }
public static boolean enchantable( Item item ){ public static boolean enchantable( Item item ){
return (item instanceof MeleeWeapon || item instanceof SpiritBow || item instanceof Armor); return (item instanceof Weapon || item instanceof Armor);
} }
private void confirmCancelation() { private void confirmCancelation() {
@@ -32,11 +32,8 @@ import com.shatteredpixel.shatteredpixeldungeon.items.quest.MetalShard;
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfMight; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfMight;
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfRemoveCurse; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfRemoveCurse;
import com.shatteredpixel.shatteredpixeldungeon.items.wands.Wand; import com.shatteredpixel.shatteredpixeldungeon.items.wands.Wand;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MagesStaff; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MagesStaff;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MeleeWeapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.MissileWeapon;
import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog; import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet;
import com.watabou.noosa.audio.Sample; import com.watabou.noosa.audio.Sample;
@@ -53,7 +50,7 @@ public class CurseInfusion extends InventorySpell {
@Override @Override
protected boolean usableOnItem(Item item) { protected boolean usableOnItem(Item item) {
return ((item instanceof EquipableItem && !(item instanceof MissileWeapon)) || item instanceof Wand); return ((item instanceof EquipableItem && item.isUpgradable()) || item instanceof Wand);
} }
@Override @Override
@@ -63,7 +60,7 @@ public class CurseInfusion extends InventorySpell {
Sample.INSTANCE.play(Assets.Sounds.CURSED); Sample.INSTANCE.play(Assets.Sounds.CURSED);
item.cursed = true; item.cursed = true;
if (item instanceof MeleeWeapon || item instanceof SpiritBow) { if (item instanceof Weapon) {
Weapon w = (Weapon) item; Weapon w = (Weapon) item;
if (w.enchantment != null) { if (w.enchantment != null) {
//if we are freshly applying curse infusion, don't replace an existing curse //if we are freshly applying curse infusion, don't replace an existing curse
@@ -295,6 +295,13 @@ public class SpiritBow extends Weapon {
image = ItemSpriteSheet.SPIRIT_ARROW; image = ItemSpriteSheet.SPIRIT_ARROW;
hitSound = Assets.Sounds.HIT_ARROW; hitSound = Assets.Sounds.HIT_ARROW;
setID = 0;
}
@Override
public int defaultQuantity() {
return 1;
} }
@Override @Override
@@ -67,6 +67,7 @@ import com.shatteredpixel.shatteredpixeldungeon.items.weapon.enchantments.Vampir
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MeleeWeapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MeleeWeapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.RunicBlade; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.RunicBlade;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.Scimitar; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.Scimitar;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.MissileWeapon;
import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog; import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
@@ -109,9 +110,11 @@ abstract public class Weapon extends KindOfWeapon {
public Augment augment = Augment.NONE; public Augment augment = Augment.NONE;
private static final int USES_TO_ID = 20; protected int usesToID(){
private float usesLeftToID = USES_TO_ID; return 20;
private float availableUsesToID = USES_TO_ID/2f; }
protected float usesLeftToID = usesToID();
protected float availableUsesToID = usesToID()/2f;
public Enchantment enchantment; public Enchantment enchantment;
public boolean enchantHardened = false; public boolean enchantHardened = false;
@@ -191,9 +194,10 @@ abstract public class Weapon extends KindOfWeapon {
public void onHeroGainExp( float levelPercent, Hero hero ){ public void onHeroGainExp( float levelPercent, Hero hero ){
levelPercent *= Talent.itemIDSpeedFactor(hero, this); levelPercent *= Talent.itemIDSpeedFactor(hero, this);
if (!levelKnown && isEquipped(hero) && availableUsesToID <= USES_TO_ID/2f) { if (!levelKnown && (isEquipped(hero) || this instanceof MissileWeapon)
&& availableUsesToID <= usesToID()/2f) {
//gains enough uses to ID over 0.5 levels //gains enough uses to ID over 0.5 levels
availableUsesToID = Math.min(USES_TO_ID/2f, availableUsesToID + levelPercent * USES_TO_ID); availableUsesToID = Math.min(usesToID()/2f, availableUsesToID + levelPercent * usesToID());
} }
} }
@@ -233,8 +237,8 @@ abstract public class Weapon extends KindOfWeapon {
@Override @Override
public void reset() { public void reset() {
super.reset(); super.reset();
usesLeftToID = USES_TO_ID; usesLeftToID = usesToID();
availableUsesToID = USES_TO_ID/2f; availableUsesToID = usesToID()/2f;
} }
@Override @Override
@@ -28,13 +28,13 @@ import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SmokeParticle;
import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.bombs.Bomb; import com.shatteredpixel.shatteredpixeldungeon.items.bombs.Bomb;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.MissileWeapon;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
import com.watabou.utils.PathFinder; import com.watabou.utils.PathFinder;
import com.watabou.utils.Random;
public class Explosive extends Weapon.Enchantment { public class Explosive extends Weapon.Enchantment {
@@ -47,7 +47,7 @@ public class Explosive extends Weapon.Enchantment {
public int proc( Weapon weapon, Char attacker, Char defender, int damage ) { public int proc( Weapon weapon, Char attacker, Char defender, int damage ) {
//average value of 5, or 20 hits to an explosion //average value of 5, or 20 hits to an explosion
int durToReduce = Math.round(Random.IntRange(0, 10) * procChanceMultiplier(attacker)); int durToReduce = Math.round(24 * procChanceMultiplier(attacker));
int currentDurability = durability; int currentDurability = durability;
durability -= durToReduce; durability -= durToReduce;
@@ -82,9 +82,20 @@ public class Explosive extends Weapon.Enchantment {
Item.updateQuickslot(); Item.updateQuickslot();
} }
if (weapon instanceof MissileWeapon
&& ((MissileWeapon)weapon).parent != null && ((MissileWeapon) weapon).parent.enchantment instanceof Explosive){
((Explosive) ((MissileWeapon) weapon).parent.enchantment).durability = durability;
}
return damage; return damage;
} }
public void merge(Explosive other){
if (other.durability < durability){
durability = other.durability;
}
}
@Override @Override
public boolean curse() { public boolean curse() {
return true; return true;
@@ -21,6 +21,7 @@
package com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles; package com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles;
import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char; import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
@@ -36,23 +37,29 @@ import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag; import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag;
import com.shatteredpixel.shatteredpixeldungeon.items.bags.MagicalHolster; import com.shatteredpixel.shatteredpixeldungeon.items.bags.MagicalHolster;
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfSharpshooting; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfSharpshooting;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.ParchmentScrap;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.ShardOfOblivion;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.curses.Explosive;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.enchantments.Projecting; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.enchantments.Projecting;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.darts.Dart; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.darts.Dart;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
import com.watabou.utils.Random; import com.watabou.utils.Random;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
abstract public class MissileWeapon extends Weapon { abstract public class MissileWeapon extends Weapon {
{ {
stackable = true; stackable = true;
levelKnown = true; quantity = defaultQuantity();
bones = true; bones = true;
@@ -60,6 +67,9 @@ abstract public class MissileWeapon extends Weapon {
usesTargeting = true; usesTargeting = true;
} }
//TODO maybe make this like actor IDs, instead of random? collisions unlikely, but it's messy
public long setID = new SecureRandom().nextLong();
//whether or not this instance of the item exists purely to trigger its effect. i.e. no dropping //whether or not this instance of the item exists purely to trigger its effect. i.e. no dropping
public boolean spawnedForEffect = false; public boolean spawnedForEffect = false;
@@ -72,10 +82,14 @@ abstract public class MissileWeapon extends Weapon {
public boolean holster; public boolean holster;
//used to reduce durability from the source weapon stack, rather than the one being thrown. //used to reduce durability from the source weapon stack, rather than the one being thrown.
protected MissileWeapon parent; public MissileWeapon parent;
public int tier; public int tier;
protected int usesToID(){
return 10; //half of a melee weapon
}
@Override @Override
public int min() { public int min() {
if (Dungeon.hero != null){ if (Dungeon.hero != null){
@@ -107,7 +121,11 @@ abstract public class MissileWeapon extends Weapon {
} }
public int STRReq(int lvl){ public int STRReq(int lvl){
return STRReq(tier, lvl) - 1; //1 less str than normal for their tier int req = STRReq(tier, lvl) - 1; //1 less str than normal for their tier
if (masteryPotionBonus){
req -= 2;
}
return req;
} }
//use the parent item if this has been thrown from a parent //use the parent item if this has been thrown from a parent
@@ -119,40 +137,23 @@ abstract public class MissileWeapon extends Weapon {
} }
} }
public Item upgrade( boolean enchant ) {
if (!bundleRestoring) {
durability = MAX_DURABILITY;
quantity = fullSetQuantity = defaultQuantity();
Buff.affect(Dungeon.hero, UpgradedSetTracker.class).levelThresholds.put(setID, level()+1);
}
return super.upgrade( enchant );
}
@Override @Override
//FIXME some logic here assumes the items are in the player's inventory. Might need to adjust
public Item upgrade() { public Item upgrade() {
if (!bundleRestoring) { if (!bundleRestoring) {
durability = MAX_DURABILITY; durability = MAX_DURABILITY;
if (quantity > 1) { quantity = fullSetQuantity = defaultQuantity();
MissileWeapon upgraded = (MissileWeapon) split(1); Buff.affect(Dungeon.hero, UpgradedSetTracker.class).levelThresholds.put(setID, level()+1);
upgraded.parent = null;
upgraded = (MissileWeapon) upgraded.upgrade();
//try to put the upgraded into inventory, if it didn't already merge
if (upgraded.quantity() == 1 && !upgraded.collect()) {
Dungeon.level.drop(upgraded, Dungeon.hero.pos);
}
updateQuickslot();
return upgraded;
} else {
super.upgrade();
Item similar = Dungeon.hero.belongings.getSimilar(this);
if (similar != null){
detach(Dungeon.hero.belongings.backpack);
Item result = similar.merge(this);
updateQuickslot();
return result;
}
updateQuickslot();
return this;
}
} else {
return super.upgrade();
} }
return super.upgrade();
} }
@Override @Override
@@ -169,7 +170,7 @@ abstract public class MissileWeapon extends Weapon {
} }
public boolean isSimilar( Item item ) { public boolean isSimilar( Item item ) {
return level() == item.level() && getClass() == item.getClass(); return trueLevel() == item.trueLevel() && getClass() == item.getClass() && setID == (((MissileWeapon) item).setID);
} }
@Override @Override
@@ -269,23 +270,82 @@ abstract public class MissileWeapon extends Weapon {
} }
} }
return super.proc(attacker, defender, damage); if ((cursed || hasCurseEnchant()) && !cursedKnown){
GLog.n(Messages.get(this, "curse_discover"));
}
cursedKnown = true;
if (parent != null) parent.cursedKnown = true;
//instant ID with the right talent
if (attacker == Dungeon.hero && Dungeon.hero.pointsInTalent(Talent.SURVIVALISTS_INTUITION) == 2){
usesLeftToID = Math.min(usesLeftToID, 0);
}
int result = super.proc(attacker, defender, damage);
//handle ID progress over parent/child
if (parent != null && parent.usesLeftToID > usesLeftToID){
float diff = parent.usesLeftToID - usesLeftToID;
parent.usesLeftToID -= diff;
parent.availableUsesToID -= diff;
if (usesLeftToID <= 0) {
if (ShardOfOblivion.passiveIDDisabled()){
parent.setIDReady();
} else {
parent.identify();
}
}
}
return result;
} }
@Override @Override
public Item random() { public Item virtual() {
if (!stackable) return this; Item item = super.virtual();
//2: 66.67% (2/3) ((MissileWeapon)item).setID = setID;
//3: 26.67% (4/15)
//4: 6.67% (1/15) return item;
quantity = 2; }
if (Random.Int(3) == 0) {
quantity++; public int defaultQuantity(){
return 3;
}
//this is tracked to show warnings when upgrading and some of the set isn't present
public int fullSetQuantity = defaultQuantity();
@Override
public Item random() {
//+0: 75% (3/4)
//+1: 20% (4/20)
//+2: 5% (1/20)
int n = 0;
if (Random.Int(4) == 0) {
n++;
if (Random.Int(5) == 0) { if (Random.Int(5) == 0) {
quantity++; n++;
} }
} }
level(n);
//we use a separate RNG here so that variance due to things like parchment scrap
//does not affect levelgen
Random.pushGenerator(Random.Long());
//30% chance to be cursed
//10% chance to be enchanted
float effectRoll = Random.Float();
if (effectRoll < 0.3f * ParchmentScrap.curseChanceMultiplier()) {
enchant(Enchantment.randomCurse());
cursed = true;
} else if (effectRoll >= 1f - (0.1f * ParchmentScrap.enchantChanceMultiplier())){
enchant();
}
Random.popGenerator();
return this; return this;
} }
@@ -328,13 +388,15 @@ abstract public class MissileWeapon extends Weapon {
durability = Math.min(durability, MAX_DURABILITY); durability = Math.min(durability, MAX_DURABILITY);
} }
public float durabilityPerUse(){ public final float durabilityPerUse(){
//classes that override durabilityPerUse can turn rounding off, to do their own rounding after more logic return durabilityPerUse(level());
return durabilityPerUse(true);
} }
protected final float durabilityPerUse( boolean rounded){ //classes that add steps onto durabilityPerUse can turn rounding off, to do their own rounding after more logic
float usages = baseUses * (float)(Math.pow(3, level())); protected boolean useRoundingInDurabilityCalc = true;
public float durabilityPerUse( int level ){
float usages = baseUses * (float)(Math.pow(1.5f, level));
//+50%/75% durability //+50%/75% durability
if (Dungeon.hero != null && Dungeon.hero.hasTalent(Talent.DURABLE_PROJECTILES)){ if (Dungeon.hero != null && Dungeon.hero.hasTalent(Talent.DURABLE_PROJECTILES)){
@@ -349,7 +411,7 @@ abstract public class MissileWeapon extends Weapon {
//at 100 uses, items just last forever. //at 100 uses, items just last forever.
if (usages >= 100f) return 0; if (usages >= 100f) return 0;
if (rounded){ if (useRoundingInDurabilityCalc){
usages = Math.round(usages); usages = Math.round(usages);
//add a tiny amount to account for rounding error for calculations like 1/3 //add a tiny amount to account for rounding error for calculations like 1/3
return (MAX_DURABILITY/usages) + 0.001f; return (MAX_DURABILITY/usages) + 0.001f;
@@ -366,6 +428,7 @@ abstract public class MissileWeapon extends Weapon {
if (parent.durability <= parent.durabilityPerUse()){ if (parent.durability <= parent.durabilityPerUse()){
durability = 0; durability = 0;
parent.durability = MAX_DURABILITY; parent.durability = MAX_DURABILITY;
parent.fullSetQuantity--;
if (parent.durabilityPerUse() < 100f) { if (parent.durabilityPerUse() < 100f) {
GLog.n(Messages.get(this, "has_broken")); GLog.n(Messages.get(this, "has_broken"));
} }
@@ -419,6 +482,34 @@ abstract public class MissileWeapon extends Weapon {
quantity -= 1; quantity -= 1;
durability += MAX_DURABILITY; durability += MAX_DURABILITY;
} }
masteryPotionBonus = masteryPotionBonus || ((MissileWeapon) other).masteryPotionBonus;
levelKnown = levelKnown || other.levelKnown;
cursedKnown = cursedKnown || other.cursedKnown;
enchantHardened = enchantHardened || ((MissileWeapon) other).enchantHardened;
//if other has a curse/enchant status that's a higher priority, copy it. in the following order:
//curse infused
if (!curseInfusionBonus && ((MissileWeapon) other).curseInfusionBonus && ((MissileWeapon) other).hasCurseEnchant()){
enchantment = ((MissileWeapon) other).enchantment;
curseInfusionBonus = true;
cursed = cursed || other.cursed;
//enchanted
} else if (!curseInfusionBonus && !hasGoodEnchant() && ((MissileWeapon) other).hasGoodEnchant()){
enchantment = ((MissileWeapon) other).enchantment;
cursed = other.cursed;
//nothing
} else if (!curseInfusionBonus && hasCurseEnchant() && !((MissileWeapon) other).hasCurseEnchant()){
enchantment = ((MissileWeapon) other).enchantment;
cursed = other.cursed;
}
//cursed (no copy as other cannot have a higher priority status)
//special case for explosive, as it tracks a variable
if (((MissileWeapon) other).enchantment instanceof Explosive
&& enchantment instanceof Explosive){
((Explosive) enchantment).merge((Explosive) ((MissileWeapon) other).enchantment);
}
} }
return this; return this;
} }
@@ -443,12 +534,20 @@ abstract public class MissileWeapon extends Weapon {
@Override @Override
public boolean doPickUp(Hero hero, int pos) { public boolean doPickUp(Hero hero, int pos) {
parent = null; parent = null;
return super.doPickUp(hero, pos); if (!UpgradedSetTracker.pickupValid(hero, this)){
Sample.INSTANCE.play( Assets.Sounds.ITEM );
hero.spendAndNext( TIME_TO_PICK_UP );
GLog.w(Messages.get(this, "dust"));
quantity(0);
return true;
} else {
return super.doPickUp(hero, pos);
}
} }
@Override @Override
public boolean isIdentified() { public boolean isIdentified() {
return true; return levelKnown && cursedKnown;
} }
@Override @Override
@@ -456,28 +555,31 @@ abstract public class MissileWeapon extends Weapon {
String info = super.info(); String info = super.info();
info += "\n\n" + Messages.get( MissileWeapon.class, "stats", if (levelKnown) {
tier, info += "\n\n" + Messages.get(MissileWeapon.class, "stats_known", tier, augment.damageFactor(min()), augment.damageFactor(max()), STRReq());
Math.round(augment.damageFactor(min())), if (Dungeon.hero != null) {
Math.round(augment.damageFactor(max())), if (STRReq() > Dungeon.hero.STR()) {
STRReq()); info += " " + Messages.get(Weapon.class, "too_heavy");
} else if (Dungeon.hero.STR() > STRReq()) {
if (Dungeon.hero != null) { info += " " + Messages.get(Weapon.class, "excess_str", Dungeon.hero.STR() - STRReq());
if (STRReq() > Dungeon.hero.STR()) { }
info += " " + Messages.get(Weapon.class, "too_heavy"); }
} else if (Dungeon.hero.STR() > STRReq()) { } else {
info += " " + Messages.get(Weapon.class, "excess_str", Dungeon.hero.STR() - STRReq()); info += "\n\n" + Messages.get(MissileWeapon.class, "stats_unknown", tier, min(0), max(0), STRReq(0));
if (Dungeon.hero != null && STRReq(0) > Dungeon.hero.STR()) {
info += " " + Messages.get(MissileWeapon.class, "probably_too_heavy");
} }
} }
if (enchantment != null && (cursedKnown || !enchantment.curse())){ if (enchantment != null && (cursedKnown || !enchantment.curse())){
info += "\n\n" + Messages.get(Weapon.class, "enchanted", enchantment.name()); info += "\n\n" + Messages.get(Weapon.class, "enchanted", enchantment.name());
if (enchantHardened) info += " " + Messages.get(Weapon.class, "enchant_hardened");
info += " " + Messages.get(enchantment, "desc"); info += " " + Messages.get(enchantment, "desc");
} else if (enchantHardened){
info += "\n\n" + Messages.get(Weapon.class, "hardened_no_enchant");
} }
if (cursed && isEquipped( Dungeon.hero )) { if (cursedKnown && cursed) {
info += "\n\n" + Messages.get(Weapon.class, "cursed_worn");
} else if (cursedKnown && cursed) {
info += "\n\n" + Messages.get(Weapon.class, "cursed"); info += "\n\n" + Messages.get(Weapon.class, "cursed");
} else if (!isIdentified() && cursedKnown){ } else if (!isIdentified() && cursedKnown){
info += "\n\n" + Messages.get(Weapon.class, "not_cursed"); info += "\n\n" + Messages.get(Weapon.class, "not_cursed");
@@ -485,17 +587,18 @@ abstract public class MissileWeapon extends Weapon {
info += "\n\n" + Messages.get(MissileWeapon.class, "distance"); info += "\n\n" + Messages.get(MissileWeapon.class, "distance");
info += "\n\n" + Messages.get(this, "durability"); if (levelKnown) {
if (durabilityPerUse() > 0) {
if (durabilityPerUse() > 0){ info += "\n\n" + Messages.get(this, "uses_left",
info += " " + Messages.get(this, "uses_left", (int) Math.ceil(durability / durabilityPerUse()),
(int)Math.ceil(durability/durabilityPerUse()), (int) Math.ceil(MAX_DURABILITY / durabilityPerUse()));
(int)Math.ceil(MAX_DURABILITY/durabilityPerUse())); } else {
} else { info += "\n\n" + Messages.get(this, "unlimited_uses");
info += " " + Messages.get(this, "unlimited_uses"); }
} else {
info += "\n\n" + Messages.get(this, "unknown_uses", (int) Math.ceil(MAX_DURABILITY / durabilityPerUse(0)));
} }
return info; return info;
} }
@@ -504,14 +607,19 @@ abstract public class MissileWeapon extends Weapon {
return 6 * tier * quantity * (level() + 1); return 6 * tier * quantity * (level() + 1);
} }
private static final String SET_ID = "set_id";
private static final String SPAWNED = "spawned"; private static final String SPAWNED = "spawned";
private static final String DURABILITY = "durability"; private static final String DURABILITY = "durability";
private static final String FULL_QUANTITY = "full_quantity";
@Override @Override
public void storeInBundle(Bundle bundle) { public void storeInBundle(Bundle bundle) {
super.storeInBundle(bundle); super.storeInBundle(bundle);
bundle.put(SET_ID, setID);
bundle.put(SPAWNED, spawnedForEffect); bundle.put(SPAWNED, spawnedForEffect);
bundle.put(DURABILITY, durability); bundle.put(DURABILITY, durability);
bundle.put(FULL_QUANTITY, fullSetQuantity);
} }
private static boolean bundleRestoring = false; private static boolean bundleRestoring = false;
@@ -521,8 +629,29 @@ abstract public class MissileWeapon extends Weapon {
bundleRestoring = true; bundleRestoring = true;
super.restoreFromBundle(bundle); super.restoreFromBundle(bundle);
bundleRestoring = false; bundleRestoring = false;
if (bundle.contains(SET_ID)){
setID = bundle.getLong(SET_ID);
//pre v3.2.0 logic
} else {
//if we have a higher than 0 level, assume that this was a solitary thrown wep upgrade
//turn it into a set of full quantity
if (level() > 0){
//set ID will be a random long
quantity = defaultQuantity();
fullSetQuantity = quantity;
//otherwise treat all currently spawned thrown weapons of the same class as if they are part of the same set
//darts already do this though and need no conversion
} else if (!(this instanceof Dart)){
levelKnown = cursedKnown = true;
setID = getClass().getSimpleName().hashCode();
}
}
spawnedForEffect = bundle.getBoolean(SPAWNED); spawnedForEffect = bundle.getBoolean(SPAWNED);
durability = bundle.getFloat(DURABILITY); durability = bundle.getFloat(DURABILITY);
fullSetQuantity = bundle.getInt(FULL_QUANTITY);
} }
public static class PlaceHolder extends MissileWeapon { public static class PlaceHolder extends MissileWeapon {
@@ -533,7 +662,13 @@ abstract public class MissileWeapon extends Weapon {
@Override @Override
public boolean isSimilar(Item item) { public boolean isSimilar(Item item) {
return item instanceof MissileWeapon; //yes, even though it uses a dart outline
return item instanceof MissileWeapon && !(item instanceof Dart);
}
@Override
public String status() {
return null;
} }
@Override @Override
@@ -541,4 +676,50 @@ abstract public class MissileWeapon extends Weapon {
return ""; return "";
} }
} }
//also used by liquid metal crafting to track when a set is consumed
public static class UpgradedSetTracker extends Buff {
public HashMap<Long, Integer> levelThresholds = new HashMap<>();
public static boolean pickupValid(Hero h, MissileWeapon w){
if (h.buff(UpgradedSetTracker.class) != null){
HashMap<Long, Integer> levelThresholds = h.buff(UpgradedSetTracker.class).levelThresholds;
if (levelThresholds.containsKey(w.setID)){
return w.level() >= levelThresholds.get(w.setID);
}
return true;
}
return true;
}
public static final String SET_IDD = "set_ids";
public static final String SET_LEVELS = "set_levels";
@Override
public void storeInBundle(Bundle bundle) {
super.storeInBundle(bundle);
long[] IDs = new long[levelThresholds.size()];
int[] levels = new int[levelThresholds.size()];
int i = 0;
for (Long ID : levelThresholds.keySet()){
IDs[i] = ID;
levels[i] = levelThresholds.get(ID);
i++;
}
bundle.put(SET_IDD, IDs);
bundle.put(SET_LEVELS, levels);
}
@Override
public void restoreFromBundle(Bundle bundle) {
super.restoreFromBundle(bundle);
long[] IDs = bundle.getLongArray(SET_IDD);
int[] levels = bundle.getIntArray(SET_LEVELS);
levelThresholds.clear();
for (int i = 0; i <IDs.length; i++){
levelThresholds.put(IDs[i], levels[i]);
}
}
}
} }
@@ -48,6 +48,8 @@ import java.util.ArrayList;
public class Dart extends MissileWeapon { public class Dart extends MissileWeapon {
{ {
levelKnown = true;
image = ItemSpriteSheet.DART; image = ItemSpriteSheet.DART;
hitSound = Assets.Sounds.HIT_ARROW; hitSound = Assets.Sounds.HIT_ARROW;
hitSoundPitch = 1.3f; hitSoundPitch = 1.3f;
@@ -56,6 +58,9 @@ public class Dart extends MissileWeapon {
//infinite, even with penalties //infinite, even with penalties
baseUses = 1000; baseUses = 1000;
//all darts share a set ID
setID = 0L;
} }
protected static final String AC_TIP = "TIP"; protected static final String AC_TIP = "TIP";
@@ -243,6 +248,16 @@ public class Dart extends MissileWeapon {
return false; return false;
} }
@Override
public boolean isIdentified() {
return true;
}
@Override
public int defaultQuantity() {
return 2;
}
@Override @Override
public int value() { public int value() {
return super.value()/2; //half normal value return super.value()/2; //half normal value
@@ -51,7 +51,7 @@ public class RotDart extends TippedDart {
} }
@Override @Override
public float durabilityPerUse() { public float durabilityPerUse(int level) {
return MAX_DURABILITY/5f; //always 5 uses return MAX_DURABILITY/5f; //always 5 uses
} }
} }
@@ -163,9 +163,13 @@ public abstract class TippedDart extends Dart {
private static int targetPos = -1; private static int targetPos = -1;
{
useRoundingInDurabilityCalc = false;
}
@Override @Override
public float durabilityPerUse() { public float durabilityPerUse(int level) {
float use = super.durabilityPerUse(false); float use = super.durabilityPerUse(level);
if (Dungeon.hero != null) { if (Dungeon.hero != null) {
use /= (1 + Dungeon.hero.pointsInTalent(Talent.DURABLE_TIPS)); use /= (1 + Dungeon.hero.pointsInTalent(Talent.DURABLE_TIPS));
@@ -38,6 +38,7 @@ import com.shatteredpixel.shatteredpixeldungeon.items.Recipe;
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.AlchemistsToolkit; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.AlchemistsToolkit;
import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag; import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.TrinketCatalyst; import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.TrinketCatalyst;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.MissileWeapon;
import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog; import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog;
import com.shatteredpixel.shatteredpixeldungeon.journal.Document; import com.shatteredpixel.shatteredpixeldungeon.journal.Document;
import com.shatteredpixel.shatteredpixeldungeon.journal.Journal; import com.shatteredpixel.shatteredpixeldungeon.journal.Journal;
@@ -299,7 +300,7 @@ public class AlchemyScene extends PixelScene {
if (item != null && inputs[0] != null) { if (item != null && inputs[0] != null) {
for (int i = 0; i < inputs.length; i++) { for (int i = 0; i < inputs.length; i++) {
if (inputs[i].item() == null) { if (inputs[i].item() == null) {
if (item instanceof LiquidMetal){ if (item instanceof LiquidMetal || item instanceof MissileWeapon){
inputs[i].item(item.detachAll(Dungeon.hero.belongings.backpack)); inputs[i].item(item.detachAll(Dungeon.hero.belongings.backpack));
} else { } else {
inputs[i].item(item.detach(Dungeon.hero.belongings.backpack)); inputs[i].item(item.detach(Dungeon.hero.belongings.backpack));
@@ -570,7 +571,7 @@ public class AlchemyScene extends PixelScene {
if (item != null && inputs[0] != null) { if (item != null && inputs[0] != null) {
for (int i = 0; i < inputs.length; i++) { for (int i = 0; i < inputs.length; i++) {
if (inputs[i].item() == null) { if (inputs[i].item() == null) {
if (item instanceof LiquidMetal){ if (item instanceof LiquidMetal || item instanceof MissileWeapon){
inputs[i].item(item.detachAll(Dungeon.hero.belongings.backpack)); inputs[i].item(item.detachAll(Dungeon.hero.belongings.backpack));
} else { } else {
inputs[i].item(item.detach(Dungeon.hero.belongings.backpack)); inputs[i].item(item.detach(Dungeon.hero.belongings.backpack));
@@ -792,7 +793,7 @@ public class AlchemyScene extends PixelScene {
ArrayList<Item> found = inventory.getAllSimilar(finding); ArrayList<Item> found = inventory.getAllSimilar(finding);
while (!found.isEmpty() && needed > 0){ while (!found.isEmpty() && needed > 0){
Item detached; Item detached;
if (finding instanceof LiquidMetal) { if (finding instanceof LiquidMetal || finding instanceof MissileWeapon) {
detached = found.get(0).detachAll(inventory.backpack); detached = found.get(0).detachAll(inventory.backpack);
} else { } else {
detached = found.get(0).detach(inventory.backpack); detached = found.get(0).detach(inventory.backpack);
@@ -345,14 +345,6 @@ public class QuickRecipe extends Component {
result.add(new QuickRecipe( new LiquidMetal.Recipe(), result.add(new QuickRecipe( new LiquidMetal.Recipe(),
new ArrayList<Item>(Arrays.asList(new MissileWeapon.PlaceHolder())), new ArrayList<Item>(Arrays.asList(new MissileWeapon.PlaceHolder())),
new LiquidMetal())); new LiquidMetal()));
result.add(new QuickRecipe( new LiquidMetal.Recipe(),
new ArrayList<Item>(Arrays.asList(new MissileWeapon.PlaceHolder().quantity(2))),
new LiquidMetal()));
result.add(new QuickRecipe( new LiquidMetal.Recipe(),
new ArrayList<Item>(Arrays.asList(new MissileWeapon.PlaceHolder().quantity(3))),
new LiquidMetal()));
result.add(null);
result.add(null);
result.add(new QuickRecipe( new ArcaneResin.Recipe(), result.add(new QuickRecipe( new ArcaneResin.Recipe(),
new ArrayList<Item>(Arrays.asList(new Wand.PlaceHolder())), new ArrayList<Item>(Arrays.asList(new Wand.PlaceHolder())),
new ArcaneResin())); new ArcaneResin()));
@@ -24,6 +24,7 @@ package com.shatteredpixel.shatteredpixeldungeon.windows;
import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Badges; import com.shatteredpixel.shatteredpixeldungeon.Badges;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Belongings; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Belongings;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.Blacksmith; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.Blacksmith;
@@ -35,7 +36,7 @@ import com.shatteredpixel.shatteredpixeldungeon.items.armor.Armor;
import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag; import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag;
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfUpgrade; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfUpgrade;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.MeleeWeapon; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.MissileWeapon;
import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog; import com.shatteredpixel.shatteredpixeldungeon.journal.Catalog;
import com.shatteredpixel.shatteredpixeldungeon.journal.Notes; import com.shatteredpixel.shatteredpixeldungeon.journal.Notes;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
@@ -272,13 +273,16 @@ public class WndBlacksmith extends Window {
if (second.isEquipped( Dungeon.hero )) { if (second.isEquipped( Dungeon.hero )) {
((EquipableItem)second).doUnequip( Dungeon.hero, false ); ((EquipableItem)second).doUnequip( Dungeon.hero, false );
} }
second.detach( Dungeon.hero.belongings.backpack ); second.detachAll( Dungeon.hero.belongings.backpack );
if (second instanceof Armor){ if (second instanceof Armor){
BrokenSeal seal = ((Armor) second).checkSeal(); BrokenSeal seal = ((Armor) second).checkSeal();
if (seal != null){ if (seal != null){
Dungeon.level.drop( seal, Dungeon.hero.pos ); Dungeon.level.drop( seal, Dungeon.hero.pos );
} }
} else if (second instanceof MissileWeapon){
Buff.affect(Dungeon.hero, MissileWeapon.UpgradedSetTracker.class)
.levelThresholds.put(((MissileWeapon) second).setID, Integer.MAX_VALUE);
} }
//preserves enchant/glyphs if present //preserves enchant/glyphs if present
@@ -346,8 +350,8 @@ public class WndBlacksmith extends Window {
} else if (item1.getClass() != item2.getClass()) { } else if (item1.getClass() != item2.getClass()) {
btnReforge.enable(false); btnReforge.enable(false);
//and not the literal same item (unless quantity is >1) //and not the literal same item
} else if (item1 == item2 && item1.quantity() == 1) { } else if (item1 == item2) {
btnReforge.enable(false); btnReforge.enable(false);
} else { } else {
@@ -375,7 +379,7 @@ public class WndBlacksmith extends Window {
public boolean itemSelectable(Item item) { public boolean itemSelectable(Item item) {
return item.isUpgradable() return item.isUpgradable()
&& item.isIdentified() && !item.cursed && item.isIdentified() && !item.cursed
&& ((item instanceof MeleeWeapon && !((Weapon) item).enchantHardened) && ((item instanceof Weapon && !((Weapon) item).enchantHardened)
|| (item instanceof Armor && !((Armor) item).glyphHardened)); || (item instanceof Armor && !((Armor) item).glyphHardened));
} }
@@ -245,12 +245,24 @@ public class WndUpgrade extends Window {
//durability //durability
if (toUpgrade instanceof MissileWeapon){ if (toUpgrade instanceof MissileWeapon){
//missile weapons are always IDed currently, so we always use true level //missile weapons are always IDed currently, so we always use true level
int uses1 = (int)Math.ceil(100f/((MissileWeapon) toUpgrade).durabilityPerUse()); int uses1, uses2;
int uses2 = (int)Math.ceil(300f/((MissileWeapon) toUpgrade).durabilityPerUse()); if (toUpgrade.levelKnown) {
uses1 = (int) Math.ceil(100f / ((MissileWeapon) toUpgrade).durabilityPerUse(toUpgrade.level()));
uses2 = (int) Math.ceil(100f / ((MissileWeapon) toUpgrade).durabilityPerUse(toUpgrade.level()+1));
} else {
uses1 = (int) Math.ceil(100f / ((MissileWeapon) toUpgrade).durabilityPerUse(0));
uses2 = (int) Math.ceil(100f / ((MissileWeapon) toUpgrade).durabilityPerUse(1));
}
bottom = fillFields(Messages.get(this, "durability"), bottom = fillFields(Messages.get(this, "durability"),
uses1 >= 100 ? "" : Integer.toString(uses1), uses1 >= 100 ? "" : Integer.toString(uses1),
uses2 >= 100 ? "" : Integer.toString(uses2), uses2 >= 100 ? "" : Integer.toString(uses2),
bottom); bottom);
bottom = fillFields(Messages.get(this, "quantity"),
Integer.toString(toUpgrade.quantity()),
Integer.toString(((MissileWeapon) toUpgrade).defaultQuantity()),
bottom);
} }
//we use a separate reference for wand properties so that mage's staff can include them //we use a separate reference for wand properties so that mage's staff can include them
@@ -393,6 +405,10 @@ public class WndUpgrade extends Window {
bottom = addMessage(Messages.get(this, "resin"), CharSprite.WARNING, bottom); bottom = addMessage(Messages.get(this, "resin"), CharSprite.WARNING, bottom);
} }
if (toUpgrade instanceof MissileWeapon && toUpgrade.quantity() < ((MissileWeapon) toUpgrade).fullSetQuantity){
bottom = addMessage("Weapons from this set that aren't in your inventory will crumble to dust.", CharSprite.WARNING, bottom);
}
// *** Buttons for confirming/cancelling *** // *** Buttons for confirming/cancelling ***
btnUpgrade = new RedButton(Messages.get(this, "upgrade")){ btnUpgrade = new RedButton(Messages.get(this, "upgrade")){