466 lines
11 KiB
Java
466 lines
11 KiB
Java
/*
|
|
* 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 <http://www.gnu.org/licenses/>
|
|
*/
|
|
package com.shatteredpixel.shatteredpixeldungeon.actors;
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.Assets;
|
|
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Bless;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Charm;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Chill;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Cripple;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.EarthImbue;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.FireImbue;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Frost;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Hunger;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.MagicalSleep;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Slow;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Speed;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Vertigo;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.Level;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain;
|
|
import com.shatteredpixel.shatteredpixeldungeon.levels.features.Door;
|
|
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
|
|
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
|
|
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
|
|
import com.watabou.noosa.Camera;
|
|
import com.watabou.noosa.audio.Sample;
|
|
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 {
|
|
|
|
public int pos = 0;
|
|
|
|
public CharSprite sprite;
|
|
|
|
public String name = "mob";
|
|
|
|
public int HT;
|
|
public int HP;
|
|
public int SHLD;
|
|
|
|
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<Buff> 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 TAG_SHLD = "SHLD";
|
|
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( TAG_SHLD, SHLD );
|
|
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 );
|
|
SHLD = bundle.getInt( TAG_SHLD );
|
|
|
|
for (Bundlable b : bundle.getCollection( BUFFS )) {
|
|
if (b != null) {
|
|
((Buff)b).attachTo( this );
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean attack( Char enemy ) {
|
|
|
|
if (enemy == null || !enemy.isAlive()) return false;
|
|
|
|
boolean visibleFight = Dungeon.visible[pos] || Dungeon.visible[enemy.pos];
|
|
|
|
if (hit( this, enemy, false )) {
|
|
|
|
// 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) {
|
|
|
|
Dungeon.fail( getClass() );
|
|
GLog.n( Messages.capitalize(Messages.get(Char.class, "kill", name)) );
|
|
|
|
} else if (this == Dungeon.hero) {
|
|
GLog.i( Messages.capitalize(Messages.get(Char.class, "defeat", enemy.name)) );
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
if (visibleFight) {
|
|
String defense = enemy.defenseVerb();
|
|
enemy.sprite.showStatus( CharSprite.NEUTRAL, defense );
|
|
|
|
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 Messages.get(this, "def_verb");
|
|
}
|
|
|
|
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 (!isAlive() || 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( Messages.get(Char.class, "out_of_paralysis", name) );
|
|
}
|
|
}
|
|
}
|
|
|
|
//FIXME: when I add proper damage properties, should add an IGNORES_SHIELDS property to use here.
|
|
if (src instanceof Hunger || SHLD == 0){
|
|
HP -= dmg;
|
|
} else if (SHLD >= dmg){
|
|
SHLD -= dmg;
|
|
} else if (SHLD > 0) {
|
|
HP -= (dmg - SHLD);
|
|
SHLD = 0;
|
|
}
|
|
|
|
if (dmg > 0 || src instanceof Char) {
|
|
sprite.showStatus( HP > HT / 2 ?
|
|
CharSprite.WARNING :
|
|
CharSprite.NEGATIVE,
|
|
Integer.toString( dmg ) );
|
|
}
|
|
|
|
if (HP < 0) HP = 0;
|
|
|
|
if (!isAlive()) {
|
|
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<Buff> buffs() {
|
|
return buffs;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public <T extends Buff> HashSet<T> buffs( Class<T> c ) {
|
|
HashSet<T> filtered = new HashSet<>();
|
|
for (Buff b : buffs) {
|
|
if (c.isInstance( b )) {
|
|
filtered.add( (T)b );
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public <T extends Buff> T buff( Class<T> 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<? extends Buff> buffClass ) {
|
|
for (Buff buff : buffs( buffClass )) {
|
|
remove( buff );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onRemove() {
|
|
for (Buff buff : buffs.toArray(new Buff[buffs.size()])) {
|
|
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<Class<?>> EMPTY = new HashSet<>();
|
|
|
|
public HashSet<Class<?>> resistances() {
|
|
return EMPTY;
|
|
}
|
|
|
|
public HashSet<Class<?>> immunities() {
|
|
return EMPTY;
|
|
}
|
|
|
|
protected HashSet<Property> properties = new HashSet<>();
|
|
|
|
public HashSet<Property> properties() { return properties; }
|
|
|
|
public enum Property{
|
|
BOSS,
|
|
MINIBOSS,
|
|
UNDEAD,
|
|
DEMONIC,
|
|
IMMOVABLE
|
|
}
|
|
}
|