v2.0.0: implement the Champion's dual wield mechanic

This commit is contained in:
Evan Debenham
2023-01-04 15:48:30 -05:00
parent 08c4c55d45
commit 69250f8bda
10 changed files with 261 additions and 17 deletions

View File

@@ -536,7 +536,7 @@ actors.hero.herosubclass.warden_short_desc=The _Warden_ can see through tall gra
actors.hero.herosubclass.warden_desc=The Warden has a strong connection to nature which grants her a variety of bonus effects relating to grass and plants. She is able to see through tall and furrowed grass as if it were empty space.\n\nThe Warden causes grass to sprout up around any seed she throws or plants, and gains special effects when trampling plants. These special effects replace the regular plant effects, meaning that no plant is harmful for her to step on.
actors.hero.herosubclass.champion=champion
actors.hero.herosubclass.champion_short_desc=The _Champion_ can wield a two weapons. She attacks with her primary weapon, can use abilities from either, and they share upgrades.
actors.hero.herosubclass.champion_desc=The Champion is a master of melee weapons who can equip a secondary weapon in addition to her primary one. She attacks with her primary weapon, but can swap her primary weapon instantly at any time. The secondary weapon's ability can be used at any time, and has its own ability charges.\n\nIf one of her two weapons is higher level than the other and the same or higher tier, then the weaker weapon will inherit the upgrade level of the stronger one!
actors.hero.herosubclass.champion_desc=The Champion is a master of melee weapons who can equip a secondary weapon in addition to her primary one. Her regular attacks use her primary weapon, but she can swap her primary weapon instantly. The secondary weapon's ability can be used at any time, and has its own ability charges.\n\nIf one of her two weapons is higher level than the other and the same or higher tier, then the weaker weapon will be boosted to the upgrade level of the stronger one!
actors.hero.herosubclass.adept=adept
actors.hero.herosubclass.adept_short_desc=The _Adept_ builds energy while fighting. This energy can be spent on a variety of unique abilities.
actors.hero.herosubclass.adept_desc=The Adept is a master of ... . As she defeats enemies, she gains energy which can be used on a variety of defensive and utlity-focused abilities. This energy does not fade over time, but has a cap based on the Adept's level.\n\nTODO

View File

@@ -1605,6 +1605,7 @@ items.weapon.melee.meleeweapon.ability_no_charge=You don't have enough energy to
items.weapon.melee.meleeweapon.ability_no_target=There is no target there.
items.weapon.melee.meleeweapon.ability_bad_position=That target can't be reached.
items.weapon.melee.meleeweapon.prompt=Select a Target
items.weapon.melee.meleeweapon.swap=Swap Weapons
items.weapon.melee.shortsword.name=shortsword
items.weapon.melee.shortsword.ability_name=cleave
@@ -1902,6 +1903,10 @@ items.kindofmisc.unequip_title=Unequip one item
items.kindofmisc.unequip_message=You must unequip one of these items first. Select an item to swap with.
items.kindofweapon.equip_cursed=Your grip involuntarily tightens around the weapon.
items.kindofweapon.which_equip_msg=Which weapon slot would you like to equip this item to?\n\nThe Champion directly attacks with her primary weapon, but can use the ability of either weapon. Both weapons have their own charge count.\n\nThe Champion can also swap her primary and secondary weapon instantly.
items.kindofweapon.which_equip_primary=Primary (%s)
items.kindofweapon.which_equip_secondary=Secondary (%s)
items.kindofweapon.empty=empty
items.kingscrown.name=Dwarf King's crown
items.kingscrown.ac_wear=WEAR

View File

@@ -75,6 +75,11 @@ public class Bones {
switch (Random.Int(7)) {
case 0:
item = hero.belongings.weapon;
//if the hero has two weapons (champion), pick the stronger one
if (hero.belongings.secondWep != null && hero.belongings.secondWep.trueLevel() > item.trueLevel()){
item = hero.belongings.secondWep;
break;
}
break;
case 1:
item = hero.belongings.armor;

View File

@@ -22,6 +22,7 @@
package com.shatteredpixel.shatteredpixeldungeon.actors.hero;
import com.shatteredpixel.shatteredpixeldungeon.Badges;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.GamesInProgress;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LostInventory;
import com.shatteredpixel.shatteredpixeldungeon.items.EquipableItem;
@@ -57,6 +58,10 @@ public class Belongings implements Iterable<Item> {
cap++;
}
}
if (Dungeon.hero != null && Dungeon.hero.belongings.secondWep != null){
//secondary weapons still occupy an inv. slot
cap--;
}
return cap;
}
}
@@ -79,6 +84,9 @@ public class Belongings implements Iterable<Item> {
//used when thrown weapons temporary become the current weapon
public KindOfWeapon thrownWeapon = null;
//used by the champion subclass
public KindOfWeapon secondWep = null;
//*** these accessor methods are so that worn items can be affected by various effects/debuffs
// we still want to access the raw equipped items in cases where effects should be ignored though,
// such as when equipping something, showing an interface, or dealing with items from a dead hero
@@ -131,6 +139,15 @@ public class Belongings implements Iterable<Item> {
}
}
public KindOfWeapon secondWep(){
boolean lostInvent = owner != null && owner.buff(LostInventory.class) != null;
if (!lostInvent || (secondWep != null && secondWep.keptThoughLostInvent)){
return secondWep;
} else {
return null;
}
}
// ***
private static final String WEAPON = "weapon";
@@ -139,6 +156,8 @@ public class Belongings implements Iterable<Item> {
private static final String MISC = "misc";
private static final String RING = "ring";
private static final String SECOND_WEP = "second_wep";
public void storeInBundle( Bundle bundle ) {
backpack.storeInBundle( bundle );
@@ -148,6 +167,7 @@ public class Belongings implements Iterable<Item> {
bundle.put( ARTIFACT, artifact );
bundle.put( MISC, misc );
bundle.put( RING, ring );
bundle.put( SECOND_WEP, secondWep );
}
public void restoreFromBundle( Bundle bundle ) {
@@ -169,6 +189,9 @@ public class Belongings implements Iterable<Item> {
ring = (Ring) bundle.get(RING);
if (ring() != null) ring().activate( owner );
secondWep = (KindOfWeapon) bundle.get(SECOND_WEP);
if (secondWep() != null) secondWep().activate(owner);
}
public static void preview( GamesInProgress.Info info, Bundle bundle ) {
@@ -305,6 +328,10 @@ public class Belongings implements Iterable<Item> {
ring().identify();
Badges.validateItemLevelAquired(ring());
}
if (secondWep() != null){
secondWep().identify();
Badges.validateItemLevelAquired(secondWep());
}
for (Item item : backpack) {
if (item instanceof EquipableItem || item instanceof Wand) {
item.cursedKnown = true;
@@ -314,7 +341,7 @@ public class Belongings implements Iterable<Item> {
}
public void uncurseEquipped() {
ScrollOfRemoveCurse.uncurse( owner, armor(), weapon(), artifact(), misc(), ring());
ScrollOfRemoveCurse.uncurse( owner, armor(), weapon(), artifact(), misc(), ring(), secondWep());
}
public Item randomUnequipped() {
@@ -346,7 +373,7 @@ public class Belongings implements Iterable<Item> {
private Iterator<Item> backpackIterator = backpack.iterator();
private Item[] equipped = {weapon, armor, artifact, misc, ring};
private Item[] equipped = {weapon, armor, artifact, misc, ring, secondWep};
private int backpackIndex = equipped.length;
@Override
@@ -392,6 +419,9 @@ public class Belongings implements Iterable<Item> {
case 4:
equipped[4] = ring = null;
break;
case 5:
equipped[5] = secondWep = null;
break;
default:
backpackIterator.remove();
}

View File

@@ -28,11 +28,15 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
import com.shatteredpixel.shatteredpixeldungeon.ui.ActionIndicator;
import com.shatteredpixel.shatteredpixeldungeon.utils.BArray;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.shatteredpixel.shatteredpixeldungeon.windows.WndOptions;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.PathFinder;
import com.watabou.utils.Random;
@@ -44,9 +48,45 @@ abstract public class KindOfWeapon extends EquipableItem {
protected String hitSound = Assets.Sounds.HIT;
protected float hitSoundPitch = 1f;
@Override
public void execute(Hero hero, String action) {
if (hero.subClass == HeroSubClass.CHAMPION && action.equals(AC_EQUIP)){
GameScene.show(new WndOptions(
new ItemSprite(this),
Messages.titleCase(name()),
Messages.get(KindOfWeapon.class, "which_equip_msg"),
Messages.get(KindOfWeapon.class, "which_equip_primary", Messages.titleCase(hero.belongings.weapon != null ? hero.belongings.weapon.name() : Messages.get(KindOfWeapon.class, "empty"))),
Messages.get(KindOfWeapon.class, "which_equip_secondary", Messages.titleCase(hero.belongings.secondWep != null ? hero.belongings.secondWep.name() : Messages.get(KindOfWeapon.class, "empty")))
){
@Override
protected void onSelect(int index) {
super.onSelect(index);
if (index == 0){
int slot = Dungeon.quickslot.getSlot( KindOfWeapon.this );
doEquip(hero);
if (slot != -1) {
Dungeon.quickslot.setSlot( slot, KindOfWeapon.this );
updateQuickslot();
}
}
if (index == 1){
int slot = Dungeon.quickslot.getSlot( KindOfWeapon.this );
equipSecondary(hero);
if (slot != -1) {
Dungeon.quickslot.setSlot( slot, KindOfWeapon.this );
updateQuickslot();
}
}
}
});
} else {
super.execute(hero, action);
}
}
@Override
public boolean isEquipped( Hero hero ) {
return hero.belongings.weapon() == this;
return hero.belongings.weapon() == this || hero.belongings.secondWep() == this;
}
@Override
@@ -62,7 +102,7 @@ abstract public class KindOfWeapon extends EquipableItem {
Badges.validateDuelistUnlock();
ActionIndicator.updateIcon();
updateQuickslot();
cursedKnown = true;
if (cursed) {
equipCursed( hero );
@@ -92,11 +132,58 @@ abstract public class KindOfWeapon extends EquipableItem {
}
}
private boolean equipSecondary( Hero hero ){
detachAll( hero.belongings.backpack );
if (hero.belongings.secondWep == null || hero.belongings.secondWep.doUnequip( hero, true )) {
hero.belongings.secondWep = this;
activate( hero );
Talent.onItemEquipped(hero, this);
Badges.validateDuelistUnlock();
ActionIndicator.updateIcon();
updateQuickslot();
cursedKnown = true;
if (cursed) {
equipCursed( hero );
GLog.n( Messages.get(KindOfWeapon.class, "equip_cursed") );
}
if (hero.hasTalent(Talent.SWIFT_EQUIP)) {
if (hero.buff(Talent.SwiftEquipCooldown.class) == null){
hero.spendAndNext(-hero.cooldown());
Buff.affect(hero, Talent.SwiftEquipCooldown.class, 49f)
.secondUse = hero.pointsInTalent(Talent.SWIFT_EQUIP) == 2;
} else if (hero.buff(Talent.SwiftEquipCooldown.class).hasSecondUse()) {
hero.spendAndNext(-hero.cooldown());
hero.buff(Talent.SwiftEquipCooldown.class).secondUse = false;
} else {
hero.spendAndNext(TIME_TO_EQUIP);
}
} else {
hero.spendAndNext(TIME_TO_EQUIP);
}
return true;
} else {
collect( hero.belongings.backpack );
return false;
}
}
@Override
public boolean doUnequip( Hero hero, boolean collect, boolean single ) {
boolean second = hero.belongings.secondWep == this;
if (super.doUnequip( hero, collect, single )) {
hero.belongings.weapon = null;
if (second){
hero.belongings.secondWep = null;
} else {
hero.belongings.weapon = null;
}
return true;
} else {

View File

@@ -21,6 +21,7 @@
package com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee;
import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ArtifactRecharge;
@@ -31,12 +32,19 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Recharging;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroClass;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent;
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.KindOfWeapon;
import com.shatteredpixel.shatteredpixeldungeon.items.weapon.Weapon;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.CellSelector;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.ui.ActionIndicator;
import com.shatteredpixel.shatteredpixeldungeon.ui.HeroIcon;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.watabou.noosa.Image;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.Bundle;
import com.watabou.utils.Random;
@@ -100,8 +108,12 @@ public class MeleeWeapon extends Weapon {
GLog.w(Messages.get(this, "ability_need_equip"));
usesTargeting = false;
}
} else if ((Buff.affect(hero, Charger.class).charges + Buff.affect(hero, Charger.class).partialCharge)
< abilityChargeUse(hero)) {
} else if (hero.belongings.weapon == this &&
(Buff.affect(hero, Charger.class).charges + Buff.affect(hero, Charger.class).partialCharge) < abilityChargeUse(hero)) {
GLog.w(Messages.get(this, "ability_no_charge"));
usesTargeting = false;
} else if (hero.belongings.secondWep == this &&
(Buff.affect(hero, Charger.class).secondCharges + Buff.affect(hero, Charger.class).secondPartialCharge) < abilityChargeUse(hero)) {
GLog.w(Messages.get(this, "ability_no_charge"));
usesTargeting = false;
} else {
@@ -149,10 +161,19 @@ public class MeleeWeapon extends Weapon {
protected void onAbilityUsed( Hero hero ){
Charger charger = Buff.affect(hero, Charger.class);
charger.partialCharge -= abilityChargeUse( hero );
while (charger.partialCharge < 0){
charger.charges--;
charger.partialCharge++;
if (Dungeon.hero.belongings.weapon == this) {
charger.partialCharge -= abilityChargeUse(hero);
while (charger.partialCharge < 0) {
charger.charges--;
charger.partialCharge++;
}
} else {
charger.secondPartialCharge -= abilityChargeUse(hero);
while (charger.secondPartialCharge < 0) {
charger.secondCharges--;
charger.secondPartialCharge++;
}
}
if (hero.heroClass == HeroClass.DUELIST
@@ -198,7 +219,23 @@ public class MeleeWeapon extends Weapon {
public int STRReq(int lvl){
return STRReq(tier, lvl);
}
@Override
public int buffedLvl() {
if (Dungeon.hero.subClass == HeroSubClass.CHAMPION && isEquipped(Dungeon.hero)){
KindOfWeapon other = null;
if (Dungeon.hero.belongings.weapon() != this) other = Dungeon.hero.belongings.weapon();
if (Dungeon.hero.belongings.secondWep() != this) other = Dungeon.hero.belongings.secondWep();
if (other instanceof MeleeWeapon
&& tier <= ((MeleeWeapon) other).tier
&& other.level() > super.buffedLvl()){
return other.level();
}
}
return super.buffedLvl();
}
@Override
public int damageRoll(Char owner) {
int damage = augment.damageFactor(super.damageRoll( owner ));
@@ -278,7 +315,11 @@ public class MeleeWeapon extends Weapon {
if (isEquipped(Dungeon.hero)
&& Dungeon.hero.buff(Charger.class) != null) {
Charger buff = Dungeon.hero.buff(Charger.class);
return buff.charges + "/" + buff.chargeCap();
if (Dungeon.hero.belongings.weapon == this) {
return buff.charges + "/" + buff.chargeCap();
} else {
return buff.secondCharges + "/" + buff.secondChargeCap();
}
} else {
return super.status();
}
@@ -302,12 +343,16 @@ public class MeleeWeapon extends Weapon {
return price;
}
public static class Charger extends Buff {
public static class Charger extends Buff implements ActionIndicator.Action {
private int charges = 3;
private float partialCharge;
//offhand charge as well?
//champion subclass
private int secondCharges = 0;
private float secondPartialCharge;
@Override
public boolean act() {
LockedFloor lock = target.buff(LockedFloor.class);
@@ -330,14 +375,52 @@ public class MeleeWeapon extends Weapon {
} else {
partialCharge = 0;
}
if (Dungeon.hero.subClass == HeroSubClass.CHAMPION
&& secondCharges < secondChargeCap()) {
if (lock == null || lock.regenOn()) {
secondPartialCharge += 1 / (100f - (chargeCap() - 2 * secondCharges)); // 100 to 80 turns per charge
}
if (secondPartialCharge >= 1) {
secondCharges++;
secondPartialCharge--;
updateQuickslot();
}
} else {
secondPartialCharge = 0;
}
if (ActionIndicator.action != this && Dungeon.hero.subClass == HeroSubClass.CHAMPION) {
ActionIndicator.setAction(this);
}
spend(TICK);
return true;
}
@Override
public void fx(boolean on) {
if (on && Dungeon.hero.subClass == HeroSubClass.CHAMPION) {
ActionIndicator.setAction(this);
}
}
@Override
public void detach() {
super.detach();
ActionIndicator.clearAction(this);
}
public int chargeCap(){
return Math.min(10, 3 + (Dungeon.hero.lvl-1)/3);
}
public int secondChargeCap(){
return chargeCap()/2;
}
public void gainCharge( float charge ){
if (charges < chargeCap()) {
partialCharge += charge;
@@ -366,6 +449,28 @@ public class MeleeWeapon extends Weapon {
charges = bundle.getInt(CHARGES);
partialCharge = bundle.getFloat(PARTIALCHARGE);
}
@Override
public String actionName() {
return Messages.get(MeleeWeapon.class, "swap");
}
@Override
public Image actionIcon() {
return new HeroIcon(HeroSubClass.CHAMPION);
}
@Override
public void doAction() {
KindOfWeapon temp = Dungeon.hero.belongings.weapon;
Dungeon.hero.belongings.weapon = Dungeon.hero.belongings.secondWep;
Dungeon.hero.belongings.secondWep = temp;
Dungeon.hero.sprite.operate(Dungeon.hero.pos);
Sample.INSTANCE.play(Assets.Sounds.UNLOCK);
Item.updateQuickslot();
}
}
}

View File

@@ -100,6 +100,9 @@ public class Dart extends MissileWeapon {
private void updateCrossbow(){
if (Dungeon.hero.belongings.weapon() instanceof Crossbow){
bow = (Crossbow) Dungeon.hero.belongings.weapon();
} else if (Dungeon.hero.belongings.secondWep() instanceof Crossbow) {
//player can instant swap anyway, so this is just QoL
bow = (Crossbow) Dungeon.hero.belongings.secondWep();
} else {
bow = null;
}

View File

@@ -310,7 +310,12 @@ public class InventoryPane extends Component {
equipped.get(3).item(stuff.misc == null ? new WndBag.Placeholder( ItemSpriteSheet.SOMETHING ) : stuff.misc);
equipped.get(4).item(stuff.ring == null ? new WndBag.Placeholder( ItemSpriteSheet.RING_HOLDER ) : stuff.ring);
ArrayList<Item> items = lastBag.items;
ArrayList<Item> items = (ArrayList<Item>) lastBag.items.clone();
if (lastBag == stuff.backpack && stuff.secondWep != null){
items.add(0, stuff.secondWep);
}
int j = 0;
for (int i = 0; i < 20; i++){
if (i == 0 && lastBag != stuff.backpack){

View File

@@ -84,7 +84,8 @@ public class InventorySlot extends ItemSlot {
item == Dungeon.hero.belongings.armor ||
item == Dungeon.hero.belongings.artifact ||
item == Dungeon.hero.belongings.misc ||
item == Dungeon.hero.belongings.ring;
item == Dungeon.hero.belongings.ring ||
item == Dungeon.hero.belongings.secondWep;
bg.texture( TextureCache.createSolid( equipped ? EQUIPPED : NORMAL ) );
bg.resetColor();

View File

@@ -254,6 +254,9 @@ public class WndBag extends WndTabbed {
if (container != Dungeon.hero.belongings.backpack){
placeItem(container);
count--; //don't count this one, as it's not actually inside of itself
} else if (stuff.secondWep != null) {
//second weapon always goes to the front of view on main bag
placeItem(stuff.secondWep);
}
// Items in the bag, except other containers (they have tags at the bottom)