/* * Pixel Dungeon * Copyright (C) 2012-2015 Oleg Dolya * * Shattered Pixel Dungeon * Copyright (C) 2014-2016 Evan Debenham * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ package com.shatteredpixel.shatteredpixeldungeon.actors.hero; import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Badges; import com.shatteredpixel.shatteredpixeldungeon.Bones; import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.GamesInProgress; import com.shatteredpixel.shatteredpixeldungeon.Statistics; import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; import com.shatteredpixel.shatteredpixeldungeon.actors.Char; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Barkskin; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Berserk; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Bless; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Combo; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Drowsy; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Fury; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Hunger; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invisibility; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Regeneration; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.SnipersMark; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Vertigo; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.NPC; import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; import com.shatteredpixel.shatteredpixeldungeon.effects.CheckedCell; import com.shatteredpixel.shatteredpixeldungeon.effects.Flare; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; import com.shatteredpixel.shatteredpixeldungeon.items.Amulet; import com.shatteredpixel.shatteredpixeldungeon.items.Ankh; import com.shatteredpixel.shatteredpixeldungeon.items.Dewdrop; import com.shatteredpixel.shatteredpixeldungeon.items.Heap; import com.shatteredpixel.shatteredpixeldungeon.items.Heap.Type; import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.KindOfWeapon; import com.shatteredpixel.shatteredpixeldungeon.items.armor.glyphs.Viscosity; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.CapeOfThorns; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.DriedRose; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.EtherealChains; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.TalismanOfForesight; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.TimekeepersHourglass; import com.shatteredpixel.shatteredpixeldungeon.items.keys.GoldenKey; import com.shatteredpixel.shatteredpixeldungeon.items.keys.IronKey; import com.shatteredpixel.shatteredpixeldungeon.items.keys.Key; import com.shatteredpixel.shatteredpixeldungeon.items.keys.SkeletonKey; import com.shatteredpixel.shatteredpixeldungeon.items.potions.Potion; import com.shatteredpixel.shatteredpixeldungeon.items.potions.PotionOfMight; import com.shatteredpixel.shatteredpixeldungeon.items.potions.PotionOfStrength; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfElements; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfEvasion; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfForce; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfFuror; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfHaste; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfMight; import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfTenacity; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.Scroll; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfMagicMapping; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfMagicalInfusion; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfUpgrade; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.missiles.MissileWeapon; import com.shatteredpixel.shatteredpixeldungeon.levels.Level; import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; import com.shatteredpixel.shatteredpixeldungeon.levels.features.AlchemyPot; import com.shatteredpixel.shatteredpixeldungeon.levels.features.Chasm; import com.shatteredpixel.shatteredpixeldungeon.levels.features.Sign; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.plants.Earthroot; import com.shatteredpixel.shatteredpixeldungeon.plants.Sungrass; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.InterlevelScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.SurfaceScene; import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.HeroSprite; import com.shatteredpixel.shatteredpixeldungeon.ui.AttackIndicator; import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator; import com.shatteredpixel.shatteredpixeldungeon.ui.QuickSlotButton; import com.shatteredpixel.shatteredpixeldungeon.utils.BArray; import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; import com.shatteredpixel.shatteredpixeldungeon.windows.WndMessage; import com.shatteredpixel.shatteredpixeldungeon.windows.WndResurrect; import com.shatteredpixel.shatteredpixeldungeon.windows.WndTradeItem; import com.watabou.noosa.Camera; import com.watabou.noosa.Game; import com.watabou.noosa.audio.Sample; import com.watabou.utils.Bundle; import com.watabou.utils.PathFinder; import com.watabou.utils.Random; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; public class Hero extends Char { { actPriority = 0; //acts at priority 0, baseline for the rest of behaviour. } public static final int MAX_LEVEL = 30; public static final int STARTING_STR = 10; private static final float TIME_TO_REST = 1f; private static final float TIME_TO_SEARCH = 2f; public HeroClass heroClass = HeroClass.ROGUE; public HeroSubClass subClass = HeroSubClass.NONE; private int attackSkill = 10; private int defenseSkill = 5; public boolean ready = false; private boolean damageInterrupt = true; public HeroAction curAction = null; public HeroAction lastAction = null; private Char enemy; private Item theKey; public boolean resting = false; public MissileWeapon rangedWeapon = null; public Belongings belongings; public int STR; public boolean weakened = false; public float awareness; public int lvl = 1; public int exp = 0; private ArrayList visibleEnemies; public Hero() { super(); name = Messages.get(this, "name"); HP = HT = 20; STR = STARTING_STR; awareness = 0.1f; belongings = new Belongings( this ); visibleEnemies = new ArrayList(); } public int STR() { int STR = this.STR; for (Buff buff : buffs(RingOfMight.Might.class)) { STR += ((RingOfMight.Might)buff).level; } return weakened ? STR - 2 : STR; } private static final String ATTACK = "attackSkill"; private static final String DEFENSE = "defenseSkill"; private static final String STRENGTH = "STR"; private static final String LEVEL = "lvl"; private static final String EXPERIENCE = "exp"; @Override public void storeInBundle( Bundle bundle ) { super.storeInBundle( bundle ); heroClass.storeInBundle( bundle ); subClass.storeInBundle( bundle ); bundle.put( ATTACK, attackSkill ); bundle.put( DEFENSE, defenseSkill ); bundle.put( STRENGTH, STR ); bundle.put( LEVEL, lvl ); bundle.put( EXPERIENCE, exp ); belongings.storeInBundle( bundle ); } @Override public void restoreFromBundle( Bundle bundle ) { super.restoreFromBundle( bundle ); heroClass = HeroClass.restoreInBundle( bundle ); subClass = HeroSubClass.restoreInBundle( bundle ); attackSkill = bundle.getInt( ATTACK ); defenseSkill = bundle.getInt( DEFENSE ); STR = bundle.getInt( STRENGTH ); updateAwareness(); lvl = bundle.getInt( LEVEL ); exp = bundle.getInt( EXPERIENCE ); belongings.restoreFromBundle( bundle ); } public static void preview( GamesInProgress.Info info, Bundle bundle ) { info.level = bundle.getInt( LEVEL ); } public String className() { return subClass == null || subClass == HeroSubClass.NONE ? heroClass.title() : subClass.title(); } public String givenName(){ return name.equals(Messages.get(this, "name")) ? className() : name; } public void live() { Buff.affect( this, Regeneration.class ); Buff.affect( this, Hunger.class ); } public int tier() { return belongings.armor == null ? 0 : belongings.armor.tier; } public boolean shoot( Char enemy, MissileWeapon wep ) { rangedWeapon = wep; boolean result = attack( enemy ); Invisibility.dispel(); rangedWeapon = null; return result; } @Override public int attackSkill( Char target ) { float accuracy = 1; if (rangedWeapon != null && Level.distance( pos, target.pos ) == 1) { accuracy *= 0.5f; } KindOfWeapon wep = rangedWeapon != null ? rangedWeapon : belongings.weapon; if (wep != null) { return (int)(attackSkill * accuracy * wep.accuracyFactor( this )); } else { return (int)(attackSkill * accuracy); } } @Override public int defenseSkill( Char enemy ) { int bonus = 0; for (Buff buff : buffs( RingOfEvasion.Evasion.class )) { bonus += ((RingOfEvasion.Evasion)buff).effectiveLevel; } float evasion = (float)Math.pow( 1.15, bonus ); if (paralysed > 0) { evasion /= 2; } int aEnc = belongings.armor != null ? belongings.armor.STRReq() - STR() : 9 - STR(); if (aEnc > 0) { return (int)(defenseSkill * evasion / Math.pow( 1.5, aEnc )); } else { if (heroClass == HeroClass.ROGUE) { return (int)((defenseSkill - aEnc) * evasion); } else { return (int)(defenseSkill * evasion); } } } @Override public int dr() { int dr = 0; Barkskin bark = buff(Barkskin.class); if (belongings.armor != null) dr += Math.max( belongings.armor.DR(), 0); if (belongings.weapon != null) dr += Math.max( belongings.weapon.defenceFactor( this ), 0 ); if (bark != null) dr += bark.level(); return dr; } @Override public int damageRoll() { KindOfWeapon wep = rangedWeapon != null ? rangedWeapon : belongings.weapon; int dmg; int bonus = 0; for (Buff buff : buffs( RingOfForce.Force.class )) { bonus += ((RingOfForce.Force)buff).level; } if (wep != null) { dmg = wep.damageRoll( this ) + bonus; } else { if (bonus != 0){ dmg = Random.NormalIntRange( RingOfForce.min(bonus, STR()), RingOfForce.max(bonus, STR()) ); } else { dmg = Random.NormalIntRange(1, Math.max(STR()-8, 1)); } } if (dmg < 0) dmg = 0; if (subClass == HeroSubClass.BERSERKER){ dmg = Buff.affect(this, Berserk.class).damageFactor(dmg); } return buff( Fury.class ) != null ? (int)(dmg * 1.5f) : dmg; } @Override public float speed() { float speed = super.speed(); int hasteLevel = 0; for (Buff buff : buffs( RingOfHaste.Haste.class )) { hasteLevel += ((RingOfHaste.Haste)buff).level; } if (hasteLevel != 0) speed *= Math.pow(1.2, hasteLevel); int aEnc = belongings.armor != null ? belongings.armor.STRReq() - STR() : 0; if (aEnc > 0) { return (float)(speed * Math.pow( 1.3, -aEnc )); } else { return ((HeroSprite)sprite).sprint( subClass == HeroSubClass.FREERUNNER && !isStarving() ) ? invisible > 0 ? 2f * speed : 1.5f * speed : speed; } } public boolean canAttack(Char enemy){ if (enemy == null || pos == enemy.pos) return false; //can always attack adjacent enemies if (Level.adjacent(pos, enemy.pos)) return true; KindOfWeapon wep = Dungeon.hero.belongings.weapon; if (wep != null && Level.distance( pos, enemy.pos ) <= wep.reachFactor(this)){ PathFinder.buildDistanceMap(enemy.pos, BArray.not(Level.solid, null), wep.reachFactor(this)); return PathFinder.distance[pos] <= wep.reachFactor(this); } else { return false; } } public float attackDelay() { KindOfWeapon wep = rangedWeapon != null ? rangedWeapon : belongings.weapon; if (wep != null) { return wep.speedFactor( this ); } else { //Normally putting furor speed on unarmed attacks would be unnecessary //But there's going to be that one guy who gets a furor+force ring combo //This is for that one guy, you shall get your fists of fury! int bonus = 0; for (Buff buff : buffs(RingOfFuror.Furor.class)) { bonus += ((RingOfFuror.Furor)buff).level; } return (float)(0.25 + (1 - 0.25)*Math.pow(0.8, bonus)); } } @Override public void spend( float time ) { TimekeepersHourglass.timeFreeze buff = buff(TimekeepersHourglass.timeFreeze.class); if (!(buff != null && buff.processTime(time))) super.spend( time ); } public void spendAndNext( float time ) { busy(); spend( time ); next(); } @Override public boolean act() { super.act(); if (paralysed > 0) { curAction = null; spendAndNext( TICK ); return false; } checkVisibleMobs(); if (curAction == null) { if (resting) { spend( TIME_TO_REST ); next(); return false; } ready(); return false; } else { resting = false; ready = false; if (curAction instanceof HeroAction.Move) { return actMove( (HeroAction.Move)curAction ); } else if (curAction instanceof HeroAction.Interact) { return actInteract( (HeroAction.Interact)curAction ); } else if (curAction instanceof HeroAction.Buy) { return actBuy( (HeroAction.Buy)curAction ); }else if (curAction instanceof HeroAction.PickUp) { return actPickUp( (HeroAction.PickUp)curAction ); } else if (curAction instanceof HeroAction.OpenChest) { return actOpenChest( (HeroAction.OpenChest)curAction ); } else if (curAction instanceof HeroAction.Unlock) { return actUnlock((HeroAction.Unlock) curAction); } else if (curAction instanceof HeroAction.Descend) { return actDescend( (HeroAction.Descend)curAction ); } else if (curAction instanceof HeroAction.Ascend) { return actAscend( (HeroAction.Ascend)curAction ); } else if (curAction instanceof HeroAction.Attack) { return actAttack( (HeroAction.Attack)curAction ); } else if (curAction instanceof HeroAction.Cook) { return actCook( (HeroAction.Cook)curAction ); } } return false; } public void busy() { ready = false; } private void ready() { sprite.idle(); curAction = null; damageInterrupt = true; ready = true; AttackIndicator.updateState(); GameScene.ready(); } public void interrupt() { if (isAlive() && curAction != null && curAction instanceof HeroAction.Move && curAction.dst != pos) { lastAction = curAction; } curAction = null; } public void resume() { curAction = lastAction; lastAction = null; damageInterrupt = false; act(); } private boolean actMove( HeroAction.Move action ) { if (getCloser( action.dst )) { return true; } else { if (Dungeon.level.map[pos] == Terrain.SIGN) { Sign.read(pos); } ready(); return false; } } private boolean actInteract( HeroAction.Interact action ) { NPC npc = action.npc; if (Level.adjacent( pos, npc.pos )) { ready(); sprite.turnTo( pos, npc.pos ); npc.interact(); return false; } else { if (Level.fieldOfView[npc.pos] && getCloser( npc.pos )) { return true; } else { ready(); return false; } } } private boolean actBuy( HeroAction.Buy action ) { int dst = action.dst; if (pos == dst || Level.adjacent( pos, dst )) { ready(); Heap heap = Dungeon.level.heaps.get( dst ); if (heap != null && heap.type == Type.FOR_SALE && heap.size() == 1) { GameScene.show( new WndTradeItem( heap, true ) ); } return false; } else if (getCloser( dst )) { return true; } else { ready(); return false; } } private boolean actCook( HeroAction.Cook action ) { int dst = action.dst; if (Dungeon.visible[dst]) { ready(); AlchemyPot.operate( this, dst ); return false; } else if (getCloser( dst )) { return true; } else { ready(); return false; } } private boolean actPickUp( HeroAction.PickUp action ) { int dst = action.dst; if (pos == dst) { Heap heap = Dungeon.level.heaps.get( pos ); if (heap != null) { Item item = heap.peek(); if (item.doPickUp( this )) { heap.pickUp(); if (item instanceof Dewdrop || item instanceof TimekeepersHourglass.sandBag || item instanceof DriedRose.Petal) { //Do Nothing } else { boolean important = ((item instanceof ScrollOfUpgrade || item instanceof ScrollOfMagicalInfusion) && ((Scroll)item).isKnown()) || ((item instanceof PotionOfStrength || item instanceof PotionOfMight) && ((Potion)item).isKnown()); if (important) { GLog.p( Messages.get(this, "you_now_have", item.name()) ); } else { GLog.i( Messages.get(this, "you_now_have", item.name()) ); } } if (!heap.isEmpty()) { GLog.i( Messages.get(this, "something_else") ); } curAction = null; } else { heap.sprite.drop(); ready(); } } else { ready(); } return false; } else if (getCloser( dst )) { return true; } else { ready(); return false; } } private boolean actOpenChest( HeroAction.OpenChest action ) { int dst = action.dst; if (Level.adjacent( pos, dst ) || pos == dst) { Heap heap = Dungeon.level.heaps.get( dst ); if (heap != null && (heap.type != Type.HEAP && heap.type != Type.FOR_SALE)) { theKey = null; if (heap.type == Type.LOCKED_CHEST || heap.type == Type.CRYSTAL_CHEST) { theKey = belongings.getKey( GoldenKey.class, Dungeon.depth ); if (theKey == null) { GLog.w( Messages.get(this, "locked_chest") ); ready(); return false; } } switch (heap.type) { case TOMB: Sample.INSTANCE.play( Assets.SND_TOMB ); Camera.main.shake( 1, 0.5f ); break; case SKELETON: case REMAINS: break; default: Sample.INSTANCE.play( Assets.SND_UNLOCK ); } spend( Key.TIME_TO_UNLOCK ); sprite.operate( dst ); } else { ready(); } return false; } else if (getCloser( dst )) { return true; } else { ready(); return false; } } private boolean actUnlock( HeroAction.Unlock action ) { int doorCell = action.dst; if (Level.adjacent( pos, doorCell )) { theKey = null; int door = Dungeon.level.map[doorCell]; if (door == Terrain.LOCKED_DOOR) { theKey = belongings.getKey( IronKey.class, Dungeon.depth ); } else if (door == Terrain.LOCKED_EXIT) { theKey = belongings.getKey( SkeletonKey.class, Dungeon.depth ); } if (theKey != null) { spend( Key.TIME_TO_UNLOCK ); sprite.operate( doorCell ); Sample.INSTANCE.play( Assets.SND_UNLOCK ); } else { GLog.w( Messages.get(this, "locked_door") ); ready(); } return false; } else if (getCloser( doorCell )) { return true; } else { ready(); return false; } } private boolean actDescend( HeroAction.Descend action ) { int stairs = action.dst; if (pos == stairs && pos == Dungeon.level.exit) { curAction = null; Buff buff = buff(TimekeepersHourglass.timeFreeze.class); if (buff != null) buff.detach(); for (Mob mob : Dungeon.level.mobs.toArray( new Mob[0] )) if (mob instanceof DriedRose.GhostHero) mob.destroy(); InterlevelScene.mode = InterlevelScene.Mode.DESCEND; Game.switchScene( InterlevelScene.class ); return false; } else if (getCloser( stairs )) { return true; } else { ready(); return false; } } private boolean actAscend( HeroAction.Ascend action ) { int stairs = action.dst; if (pos == stairs && pos == Dungeon.level.entrance) { if (Dungeon.depth == 1) { if (belongings.getItem( Amulet.class ) == null) { GameScene.show( new WndMessage( Messages.get(this, "leave") ) ); ready(); } else { Dungeon.win( Amulet.class ); Dungeon.deleteGame( Dungeon.hero.heroClass, true ); Game.switchScene( SurfaceScene.class ); } } else { curAction = null; Hunger hunger = buff( Hunger.class ); if (hunger != null && !hunger.isStarving()) { hunger.reduceHunger( -Hunger.STARVING / 10 ); } Buff buff = buff(TimekeepersHourglass.timeFreeze.class); if (buff != null) buff.detach(); for (Mob mob : Dungeon.level.mobs.toArray( new Mob[0] )) if (mob instanceof DriedRose.GhostHero) mob.destroy(); InterlevelScene.mode = InterlevelScene.Mode.ASCEND; Game.switchScene( InterlevelScene.class ); } return false; } else if (getCloser( stairs )) { return true; } else { ready(); return false; } } private boolean actAttack( HeroAction.Attack action ) { enemy = action.target; if (enemy.isAlive() && canAttack( enemy ) && !isCharmedBy( enemy )) { spend( attackDelay() ); sprite.attack( enemy.pos ); return false; } else { if (Level.fieldOfView[enemy.pos] && getCloser( enemy.pos )) { return true; } else { ready(); return false; } } } public void rest( boolean fullRest ) { spendAndNext( TIME_TO_REST ); if (!fullRest) { sprite.showStatus( CharSprite.DEFAULT, Messages.get(this, "wait") ); } resting = fullRest; } @Override public int attackProc( Char enemy, int damage ) { KindOfWeapon wep = rangedWeapon != null ? rangedWeapon : belongings.weapon; if (wep != null) wep.proc( this, enemy, damage ); switch (subClass) { case SNIPER: if (rangedWeapon != null) { Buff.prolong( this, SnipersMark.class, attackDelay() * 1.1f ).object = enemy.id(); } break; default: } return damage; } @Override public int defenseProc( Char enemy, int damage ) { Earthroot.Armor armor = buff( Earthroot.Armor.class ); if (armor != null) { damage = armor.absorb( damage ); } Sungrass.Health health = buff( Sungrass.Health.class ); if (health != null) { health.absorb( damage ); } if (belongings.armor != null) { damage = belongings.armor.proc( enemy, this, damage ); } return damage; } @Override public void damage( int dmg, Object src ) { if (buff(TimekeepersHourglass.timeStasis.class) != null) return; if (!(src instanceof Hunger || src instanceof Viscosity.DeferedDamage) && damageInterrupt) { interrupt(); resting = false; } if (this.buff(Drowsy.class) != null){ Buff.detach(this, Drowsy.class); GLog.w( Messages.get(this, "pain_resist") ); } CapeOfThorns.Thorns thorns = buff( CapeOfThorns.Thorns.class ); if (thorns != null) { dmg = thorns.proc(dmg, (src instanceof Char ? (Char)src : null), this); } int tenacity = 0; for (Buff buff : buffs(RingOfTenacity.Tenacity.class)) { tenacity += ((RingOfTenacity.Tenacity)buff).level; } if (tenacity != 0) //(HT - HP)/HT = heroes current % missing health. dmg = (int)Math.ceil((float)dmg * Math.pow(0.9, tenacity*((float)(HT - HP)/HT))); super.damage( dmg, src ); } private void checkVisibleMobs() { ArrayList visible = new ArrayList<>(); boolean newMob = false; Mob target = null; for (Mob m : Dungeon.level.mobs) { if (Level.fieldOfView[ m.pos ] && m.hostile) { visible.add(m); if (!visibleEnemies.contains( m )) { newMob = true; } if (QuickSlotButton.autoAim(m) != -1){ if (target == null){ target = m; } else if (distance(target) > distance(m)) { target = m; } } } } if (target != null && (QuickSlotButton.lastTarget == null || !QuickSlotButton.lastTarget.isAlive() || !Dungeon.visible[QuickSlotButton.lastTarget.pos])){ QuickSlotButton.target(target); } if (newMob) { interrupt(); resting = false; } visibleEnemies = visible; } public int visibleEnemies() { return visibleEnemies.size(); } public Mob visibleEnemy( int index ) { return visibleEnemies.get(index % visibleEnemies.size()); } private boolean getCloser( final int target ) { if (rooted) { Camera.main.shake( 1, 1f ); return false; } int step = -1; if (Level.adjacent( pos, target )) { if (Actor.findChar( target ) == null) { if (Level.pit[target] && !flying && !Level.solid[target]) { if (!Chasm.jumpConfirmed){ Chasm.heroJump(this); interrupt(); } else { Chasm.heroFall(target); } return false; } if (Level.passable[target] || Level.avoid[target]) { step = target; } } } else { int len = Level.LENGTH; boolean[] p = Level.passable; boolean[] v = Dungeon.level.visited; boolean[] m = Dungeon.level.mapped; boolean[] passable = new boolean[len]; for (int i=0; i < len; i++) { passable[i] = p[i] && (v[i] || m[i]); } step = Dungeon.findPath( this, pos, target, passable, Level.fieldOfView ); } if (step != -1) { sprite.move(pos, step); move(step); spend( 1 / speed() ); return true; } else { return false; } } public boolean handle( int cell ) { if (cell == -1) { return false; } Char ch; Heap heap; if (Dungeon.level.map[cell] == Terrain.ALCHEMY && cell != pos) { curAction = new HeroAction.Cook( cell ); } else if (Level.fieldOfView[cell] && (ch = Actor.findChar( cell )) instanceof Mob) { if (ch instanceof NPC) { curAction = new HeroAction.Interact( (NPC)ch ); } else { curAction = new HeroAction.Attack( ch ); } } else if ((heap = Dungeon.level.heaps.get( cell )) != null //moving to an item doesn't auto-pickup when enemies are near... && (visibleEnemies.size() == 0 || cell == pos || //...but only for standard heaps, chests and similar open as normal. (heap.type != Type.HEAP && heap.type != Type.FOR_SALE))) { switch (heap.type) { case HEAP: curAction = new HeroAction.PickUp( cell ); break; case FOR_SALE: curAction = heap.size() == 1 && heap.peek().price() > 0 ? new HeroAction.Buy( cell ) : new HeroAction.PickUp( cell ); break; default: curAction = new HeroAction.OpenChest( cell ); } } else if (Dungeon.level.map[cell] == Terrain.LOCKED_DOOR || Dungeon.level.map[cell] == Terrain.LOCKED_EXIT) { curAction = new HeroAction.Unlock( cell ); } else if (cell == Dungeon.level.exit && Dungeon.depth < 26) { curAction = new HeroAction.Descend( cell ); } else if (cell == Dungeon.level.entrance) { curAction = new HeroAction.Ascend( cell ); } else { curAction = new HeroAction.Move( cell ); lastAction = null; } return act(); } public void earnExp( int exp ) { this.exp += exp; float percent = exp/(float)maxExp(); EtherealChains.chainsRecharge chains = buff(EtherealChains.chainsRecharge.class); if (chains != null) chains.gainExp(percent); if (subClass == HeroSubClass.BERSERKER) Buff.affect(this, Berserk.class).recover(percent); boolean levelUp = false; while (this.exp >= maxExp()) { this.exp -= maxExp(); if (lvl < MAX_LEVEL) { lvl++; levelUp = true; HT += 5; HP += 5; attackSkill++; defenseSkill++; } else { Buff.prolong(this, Bless.class, 30f); this.exp = 0; GLog.p( Messages.get(this, "level_cap")); Sample.INSTANCE.play( Assets.SND_LEVELUP ); } if (lvl < 10) { updateAwareness(); } } if (levelUp) { GLog.p( Messages.get(this, "new_level"), lvl ); sprite.showStatus( CharSprite.POSITIVE, Messages.get(Hero.class, "level_up") ); Sample.INSTANCE.play( Assets.SND_LEVELUP ); Badges.validateLevelReached(); } } public int maxExp() { return 5 + lvl * 5; } void updateAwareness() { awareness = (float)(1 - Math.pow( (heroClass == HeroClass.ROGUE ? 0.85 : 0.90), (1 + Math.min( lvl, 9 )) * 0.5 )); } public boolean isStarving() { return buff(Hunger.class) != null && ((Hunger)buff( Hunger.class )).isStarving(); } @Override public void add( Buff buff ) { if (buff(TimekeepersHourglass.timeStasis.class) != null) return; super.add( buff ); if (sprite != null) { String msg = buff.heroMessage(); if (msg != null){ GLog.w(msg); } if (buff instanceof RingOfMight.Might) { if (((RingOfMight.Might) buff).level > 0) { HT += ((RingOfMight.Might) buff).level * 5; } } else if (buff instanceof Paralysis || buff instanceof Vertigo) { interrupt(); } } BuffIndicator.refreshHero(); } @Override public void remove( Buff buff ) { super.remove( buff ); if (buff instanceof RingOfMight.Might){ if (((RingOfMight.Might)buff).level > 0){ HT -= ((RingOfMight.Might) buff).level * 5; HP = Math.min(HT, HP); } } BuffIndicator.refreshHero(); } @Override public int stealth() { int stealth = super.stealth(); for (Buff buff : buffs( RingOfEvasion.Evasion.class )) { stealth += ((RingOfEvasion.Evasion)buff).effectiveLevel; } return stealth; } @Override public void die( Object cause ) { curAction = null; Ankh ankh = null; //look for ankhs in player inventory, prioritize ones which are blessed. for (Item item : belongings){ if (item instanceof Ankh) { if (ankh == null || ((Ankh) item).isBlessed()) { ankh = (Ankh) item; } } } if (ankh != null && ankh.isBlessed()) { this.HP = HT/4; //ensures that you'll get to act first in almost any case, to prevent reviving and then instantly dieing again. Buff.detach(this, Paralysis.class); spend(-cooldown()); new Flare(8, 32).color(0xFFFF66, true).show(sprite, 2f); CellEmitter.get(this.pos).start(Speck.factory(Speck.LIGHT), 0.2f, 3); ankh.detach(belongings.backpack); Sample.INSTANCE.play( Assets.SND_TELEPORT ); GLog.w( Messages.get(this, "revive") ); Statistics.ankhsUsed++; return; } Actor.fixTime(); super.die( cause ); if (ankh == null) { reallyDie( cause ); } else { Dungeon.deleteGame( Dungeon.hero.heroClass, false ); GameScene.show( new WndResurrect( ankh, cause ) ); } } public static void reallyDie( Object cause ) { int length = Level.LENGTH; int[] map = Dungeon.level.map; boolean[] visited = Dungeon.level.visited; boolean[] discoverable = Level.discoverable; for (int i=0; i < length; i++) { int terr = map[i]; if (discoverable[i]) { visited[i] = true; if ((Terrain.flags[terr] & Terrain.SECRET) != 0) { Dungeon.level.discover( i ); } } } Bones.leave(); Dungeon.observe(); Dungeon.hero.belongings.identify(); int pos = Dungeon.hero.pos; ArrayList passable = new ArrayList(); for (Integer ofs : Level.NEIGHBOURS8) { int cell = pos + ofs; if ((Level.passable[cell] || Level.avoid[cell]) && Dungeon.level.heaps.get( cell ) == null) { passable.add( cell ); } } Collections.shuffle( passable ); ArrayList items = new ArrayList( Dungeon.hero.belongings.backpack.items ); for (Integer cell : passable) { if (items.isEmpty()) { break; } Item item = Random.element( items ); Dungeon.level.drop( item, cell ).sprite.drop( pos ); items.remove( item ); } GameScene.gameOver(); if (cause instanceof Hero.Doom) { ((Hero.Doom)cause).onDeath(); } Dungeon.deleteGame( Dungeon.hero.heroClass, true ); } @Override public boolean isAlive() { if (subClass == HeroSubClass.BERSERKER){ Berserk berserk = buff(Berserk.class); if (berserk != null && berserk.berserking()){ return true; } } return super.isAlive(); } @Override public void move( int step ) { super.move( step ); if (!flying) { if (Level.water[pos]) { Sample.INSTANCE.play( Assets.SND_WATER, 1, 1, Random.Float( 0.8f, 1.25f ) ); } else { Sample.INSTANCE.play( Assets.SND_STEP ); } Dungeon.level.press(pos, this); } } @Override public void onMotionComplete() { Dungeon.observe(); search( false ); super.onMotionComplete(); } @Override public void onAttackComplete() { AttackIndicator.target(enemy); boolean hit = attack( enemy ); if (subClass == HeroSubClass.GLADIATOR){ if (hit) { Buff.affect( this, Combo.class ).hit(); } else { Combo combo = buff(Combo.class); if (combo != null) combo.miss(); } } curAction = null; Invisibility.dispel(); super.onAttackComplete(); } @Override public void onOperateComplete() { if (curAction instanceof HeroAction.Unlock) { if (theKey != null) { theKey.detach( belongings.backpack ); theKey = null; } int doorCell = ((HeroAction.Unlock)curAction).dst; int door = Dungeon.level.map[doorCell]; Level.set( doorCell, door == Terrain.LOCKED_DOOR ? Terrain.DOOR : Terrain.UNLOCKED_EXIT ); GameScene.updateMap( doorCell ); } else if (curAction instanceof HeroAction.OpenChest) { if (theKey != null) { theKey.detach( belongings.backpack ); theKey = null; } Heap heap = Dungeon.level.heaps.get( ((HeroAction.OpenChest)curAction).dst ); if (heap.type == Type.SKELETON || heap.type == Type.REMAINS) { Sample.INSTANCE.play( Assets.SND_BONES ); } heap.open( this ); } curAction = null; super.onOperateComplete(); } public boolean search( boolean intentional ) { boolean smthFound = false; int positive = 0; int negative = 0; int distance = 1 + positive + negative; float level = intentional ? (2 * awareness - awareness * awareness) : awareness; if (distance <= 0) { level /= 2 - distance; distance = 1; } int cx = pos % Level.WIDTH; int cy = pos / Level.WIDTH; int ax = cx - distance; if (ax < 0) { ax = 0; } int bx = cx + distance; if (bx >= Level.WIDTH) { bx = Level.WIDTH - 1; } int ay = cy - distance; if (ay < 0) { ay = 0; } int by = cy + distance; if (by >= Level.HEIGHT) { by = Level.HEIGHT - 1; } TalismanOfForesight.Foresight foresight = buff( TalismanOfForesight.Foresight.class ); //cursed talisman of foresight makes unintentionally finding things impossible. if (foresight != null && foresight.isCursed()){ level = -1; } for (int y = ay; y <= by; y++) { for (int x = ax, p = ax + y * Level.WIDTH; x <= bx; x++, p++) { if (Dungeon.visible[p]) { if (intentional) { sprite.parent.addToBack( new CheckedCell( p ) ); } if (Level.secret[p] && (intentional || Random.Float() < level)) { int oldValue = Dungeon.level.map[p]; GameScene.discoverTile( p, oldValue ); Dungeon.level.discover( p ); ScrollOfMagicMapping.discover( p ); smthFound = true; if (foresight != null && !foresight.isCursed()) foresight.charge(); } } } } if (intentional) { sprite.showStatus( CharSprite.DEFAULT, Messages.get(this, "search") ); sprite.operate( pos ); if (foresight != null && foresight.isCursed()){ GLog.n(Messages.get(this, "search_distracted")); spendAndNext(TIME_TO_SEARCH * 3); } else { spendAndNext(TIME_TO_SEARCH); } } if (smthFound) { GLog.w( Messages.get(this, "noticed_smth") ); Sample.INSTANCE.play( Assets.SND_SECRET ); interrupt(); } return smthFound; } public void resurrect( int resetLevel ) { HP = HT; Dungeon.gold = 0; exp = 0; belongings.resurrect( resetLevel ); live(); } @Override public HashSet> resistances() { RingOfElements.Resistance r = buff( RingOfElements.Resistance.class ); return r == null ? super.resistances() : r.resistances(); } @Override public HashSet> immunities() { HashSet> immunities = new HashSet>(); for (Buff buff : buffs()){ for (Class immunity : buff.immunities) immunities.add(immunity); } return immunities; } @Override public void next() { super.next(); } public static interface Doom { public void onDeath(); } }