/* * Pixel Dungeon * Copyright (C) 2012-2015 Oleg Dolya * * Shattered Pixel Dungeon * Copyright (C) 2014-2015 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; import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.ResultDescriptions; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.*; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Bestiary; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Yog; import com.shatteredpixel.shatteredpixeldungeon.levels.Level; import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; import com.shatteredpixel.shatteredpixeldungeon.levels.features.Door; import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; import com.shatteredpixel.shatteredpixeldungeon.utils.Utils; import com.watabou.noosa.audio.Sample; import com.watabou.noosa.Camera; import com.watabou.utils.Bundlable; import com.watabou.utils.Bundle; import com.watabou.utils.GameMath; import com.watabou.utils.Random; import java.util.HashSet; public abstract class Char extends Actor { protected static final String TXT_HIT = "%s hit %s"; protected static final String TXT_KILL = "%s killed you..."; protected static final String TXT_DEFEAT = "%s defeated %s"; private static final String TXT_YOU_MISSED = "%s %s your attack"; private static final String TXT_SMB_MISSED = "%s %s %s's attack"; private static final String TXT_OUT_OF_PARALYSIS = "The pain snapped %s out of paralysis"; public int pos = 0; public CharSprite sprite; public String name = "mob"; public int HT; public int HP; protected float baseSpeed = 1; public int paralysed = 0; public boolean rooted = false; public boolean flying = false; public int invisible = 0; public int viewDistance = 8; private HashSet buffs = new HashSet(); @Override protected boolean act() { Dungeon.level.updateFieldOfView( this ); return false; } private static final String POS = "pos"; private static final String TAG_HP = "HP"; private static final String TAG_HT = "HT"; private static final String BUFFS = "buffs"; @Override public void storeInBundle( Bundle bundle ) { super.storeInBundle( bundle ); bundle.put( POS, pos ); bundle.put( TAG_HP, HP ); bundle.put( TAG_HT, HT ); bundle.put( BUFFS, buffs ); } @Override public void restoreFromBundle( Bundle bundle ) { super.restoreFromBundle( bundle ); pos = bundle.getInt( POS ); HP = bundle.getInt( TAG_HP ); HT = bundle.getInt( TAG_HT ); for (Bundlable b : bundle.getCollection( BUFFS )) { if (b != null) { ((Buff)b).attachTo( this ); } } } public boolean attack( Char enemy ) { boolean visibleFight = Dungeon.visible[pos] || Dungeon.visible[enemy.pos]; if (hit( this, enemy, false )) { if (visibleFight) { GLog.i( TXT_HIT, name, enemy.name ); } // FIXME int dr = this instanceof Hero && ((Hero)this).rangedWeapon != null && ((Hero)this).subClass == HeroSubClass.SNIPER ? 0 : Random.IntRange( 0, enemy.dr() ); int dmg = damageRoll(); int effectiveDamage = Math.max( dmg - dr, 0 ); effectiveDamage = attackProc( enemy, effectiveDamage ); effectiveDamage = enemy.defenseProc( this, effectiveDamage ); if (visibleFight) { Sample.INSTANCE.play( Assets.SND_HIT, 1, 1, Random.Float( 0.8f, 1.25f ) ); } // If the enemy is already dead, interrupt the attack. // This matters as defence procs can sometimes inflict self-damage, such as armor glyphs. if (!enemy.isAlive()){ return true; } //TODO: consider revisiting this and shaking in more cases. float shake = 0f; if (enemy == Dungeon.hero) shake = effectiveDamage / (enemy.HT / 4); if (shake > 1f) Camera.main.shake( GameMath.gate( 1, shake, 5), 0.3f ); enemy.damage( effectiveDamage, this ); if (buff(FireImbue.class) != null) buff(FireImbue.class).proc(enemy); if (buff(EarthImbue.class) != null) buff(EarthImbue.class).proc(enemy); enemy.sprite.bloodBurstA( sprite.center(), effectiveDamage ); enemy.sprite.flash(); if (!enemy.isAlive() && visibleFight) { if (enemy == Dungeon.hero) { if ( this instanceof Yog ) { Dungeon.fail( Utils.format( ResultDescriptions.NAMED, name) ); } else if (properties().contains(Property.MINIBOSS) || properties().contains(Property.BOSS)) { Dungeon.fail( Utils.format( ResultDescriptions.UNIQUE, name) ); } else { Dungeon.fail( Utils.format( ResultDescriptions.MOB, Utils.indefinite( name )) ); } GLog.n( TXT_KILL, name ); } else { GLog.i( TXT_DEFEAT, name, enemy.name ); } } return true; } else { if (visibleFight) { String defense = enemy.defenseVerb(); enemy.sprite.showStatus( CharSprite.NEUTRAL, defense ); if (this == Dungeon.hero) { GLog.i( TXT_YOU_MISSED, enemy.name, defense ); } else { GLog.i( TXT_SMB_MISSED, enemy.name, defense, name ); } Sample.INSTANCE.play(Assets.SND_MISS); } return false; } } public static boolean hit( Char attacker, Char defender, boolean magic ) { float acuRoll = Random.Float( attacker.attackSkill( defender ) ); float defRoll = Random.Float( defender.defenseSkill( attacker ) ); if (attacker.buff(Bless.class) != null) acuRoll *= 1.20f; if (defender.buff(Bless.class) != null) defRoll *= 1.20f; return (magic ? acuRoll * 2 : acuRoll) >= defRoll; } public int attackSkill( Char target ) { return 0; } public int defenseSkill( Char enemy ) { return 0; } public String defenseVerb() { return "dodged"; } public int dr() { return 0; } public int damageRoll() { return 1; } public int attackProc( Char enemy, int damage ) { return damage; } public int defenseProc( Char enemy, int damage ) { return damage; } public float speed() { return buff( Cripple.class ) == null ? baseSpeed : baseSpeed * 0.5f; } public void damage( int dmg, Object src ) { if (HP <= 0 || dmg < 0) { return; } if (this.buff(Frost.class) != null){ Buff.detach( this, Frost.class ); } if (this.buff(MagicalSleep.class) != null){ Buff.detach(this, MagicalSleep.class); } Class srcClass = src.getClass(); if (immunities().contains( srcClass )) { dmg = 0; } else if (resistances().contains( srcClass )) { dmg = Random.IntRange( 0, dmg ); } if (buff( Paralysis.class ) != null) { if (Random.Int( dmg ) >= Random.Int( HP )) { Buff.detach( this, Paralysis.class ); if (Dungeon.visible[pos]) { GLog.i( TXT_OUT_OF_PARALYSIS, name ); } } } HP -= dmg; if (dmg > 0 || src instanceof Char) { sprite.showStatus( HP > HT / 2 ? CharSprite.WARNING : CharSprite.NEGATIVE, Integer.toString( dmg ) ); } if (HP <= 0) { die( src ); } } public void destroy() { HP = 0; Actor.remove( this ); } public void die( Object src ) { destroy(); sprite.die(); } public boolean isAlive() { return HP > 0; } @Override protected void spend( float time ) { float timeScale = 1f; if (buff( Slow.class ) != null) { timeScale *= 0.5f; //slowed and chilled do not stack } else if (buff( Chill.class ) != null) { timeScale *= buff( Chill.class ).speedFactor(); } if (buff( Speed.class ) != null) { timeScale *= 2.0f; } super.spend( time / timeScale ); } public HashSet buffs() { return buffs; } @SuppressWarnings("unchecked") public HashSet buffs( Class c ) { HashSet filtered = new HashSet(); for (Buff b : buffs) { if (c.isInstance( b )) { filtered.add( (T)b ); } } return filtered; } @SuppressWarnings("unchecked") public T buff( Class c ) { for (Buff b : buffs) { if (c.isInstance( b )) { return (T)b; } } return null; } public boolean isCharmedBy( Char ch ) { int chID = ch.id(); for (Buff b : buffs) { if (b instanceof Charm && ((Charm)b).object == chID) { return true; } } return false; } public void add( Buff buff ) { buffs.add( buff ); Actor.add( buff ); if (sprite != null) switch(buff.type){ case POSITIVE: sprite.showStatus(CharSprite.POSITIVE, buff.toString()); break; case NEGATIVE: sprite.showStatus(CharSprite.NEGATIVE, buff.toString());break; case NEUTRAL: sprite.showStatus(CharSprite.NEUTRAL, buff.toString()); break; case SILENT: default: break; //show nothing } } public void remove( Buff buff ) { buffs.remove( buff ); Actor.remove( buff ); } public void remove( Class buffClass ) { for (Buff buff : buffs( buffClass )) { remove( buff ); } } @Override protected void onRemove() { for (Buff buff : buffs.toArray( new Buff[0] )) { buff.detach(); } } public void updateSpriteState() { for (Buff buff:buffs) { buff.fx( true ); } } public int stealth() { return 0; } public void move( int step ) { if (Level.adjacent( step, pos ) && buff( Vertigo.class ) != null) { sprite.interruptMotion(); int newPos = pos + Level.NEIGHBOURS8[Random.Int( 8 )]; if (!(Level.passable[newPos] || Level.avoid[newPos]) || Actor.findChar( newPos ) != null) return; else { sprite.move(pos, newPos); step = newPos; } } if (Dungeon.level.map[pos] == Terrain.OPEN_DOOR) { Door.leave( pos ); } pos = step; if (flying && Dungeon.level.map[pos] == Terrain.DOOR) { Door.enter( pos ); } if (this != Dungeon.hero) { sprite.visible = Dungeon.visible[pos]; } } public int distance( Char other ) { return Level.distance( pos, other.pos ); } public void onMotionComplete() { next(); } public void onAttackComplete() { next(); } public void onOperateComplete() { next(); } private static final HashSet> EMPTY = new HashSet>(); public HashSet> resistances() { return EMPTY; } public HashSet> immunities() { return EMPTY; } protected HashSet properties = new HashSet<>(); public HashSet properties() { return properties; } public enum Property{ BOSS, MINIBOSS, UNDEAD, EVIL, IMMOVABLE } }