v0.9.0: implemented a new challenge: hostile champions

This commit is contained in:
Evan Debenham
2020-09-22 11:55:40 -04:00
parent afb53fe171
commit 316e2f46d6
10 changed files with 356 additions and 15 deletions

View File

@@ -96,6 +96,20 @@ actors.buffs.burning.ondeath=You burned to death...
actors.buffs.burning.rankings_desc=Burned to Ash
actors.buffs.burning.desc=Few things are more distressing than being engulfed in flames.\n\nFire will deal damage every turn until it is put out by water or it expires. Fire can be extinquished by stepping into water, or from the splash of a shattering potion.\n\nAdditionally, the fire may ignite flammable terrain or items that it comes into contact with.\n\nTurns of burning remaining: %s.
actors.buffs.championenemy.warn=You sense a deadly presence.
actors.buffs.championenemy$blazing.name=blazing champion
actors.buffs.championenemy$blazing.desc=Blazing champions deal 25% more melee damage, ignite enemies they attack, are immune to fire, and spread fire around them as they die.
actors.buffs.championenemy$projecting.name=projecting champion
actors.buffs.championenemy$projecting.desc=Projecting champions deal 25% more melee damage, and are able to attack any enemy that they can see.
actors.buffs.championenemy$antimagic.name=antimagic champion
actors.buffs.championenemy$antimagic.desc=Antimagic champions take 25% less damage and are completely immune to magical effects.
actors.buffs.championenemy$giant.name=giant champion
actors.buffs.championenemy$giant.desc=giant champions take 75% less damage and have +1 attack range, but cannot move through enclosed spaces.
actors.buffs.championenemy$blessed.name=blessed champion
actors.buffs.championenemy$blessed.desc=Blessed champions have 200% more accuracy and evasion.
actors.buffs.championenemy$growing.name=growing champion
actors.buffs.championenemy$growing.desc=Growing champions gain a steadily increasing bonus to accuracy, evasion, melee damage, and a reduction to damage taken.\n\nCurrent Accuracy/Evasion/Damage boost: %1$d%%\nCurrent damage reduction: %2$d%%
actors.buffs.charm.name=Charmed
actors.buffs.charm.heromsg=You are charmed!
actors.buffs.charm.desc=A charm is manipulative magic that can make enemies temporarily adore each other.\n\nCharacters affected by charm are unable to directly attack the enemy they are charmed by. Attacking other targets is still possible however. The shock of pain will lessen the duration of charm.\n\nTurns of charm remaining: %s.

View File

@@ -83,5 +83,7 @@ challenges.darkness=Into darkness
challenges.darkness_desc=It is a dungeon after all!\n\n- Regular visible distance dramatically reduced\n- A torch appears on each floor\n- Light buff lasts for less time
challenges.no_scrolls=Forbidden runes
challenges.no_scrolls_desc=A certain rune is harder to find. Unfortunately, it's always the most useful one.\n\n- Half of the dungeon's upgrades scrolls are removed
challenges.champion_enemies=Hostile champions
challenges.champion_enemies_desc=You're not the only one who can level up!\n\n- Regular enemies have a 1/15 chance to spawn with a special champion buff.\n- Champions wake up if they spawn asleep\n- The hero knows when a champion spawns\n- Champions are immune to corruption\n\nThere are six types of champion enemy:\n_Blazing (orange):_ +25% melee damage, ignites on hit, immune to fire, spreads flames on death\n_Projecting (purple):_ +25% melee damage, +8 melee range\n_Antimagic (green):_ -25% damage taken, immune to magical effects\n_Giant (blue):_ -75% damage taken, +1 melee range, cannot move into tunnels\n_Blessed (yellow):_ +200% accuracy, +200% evasion\n_Growing (red):_ +25% accuracy, evasion, damage, and effective HP. Increases by 1% every 2 turns.
rankings$record.something=Killed by Something

View File

@@ -34,21 +34,23 @@ public class Challenges {
public static final int SWARM_INTELLIGENCE = 16;
public static final int DARKNESS = 32;
public static final int NO_SCROLLS = 64;
public static final int CHAMPION_ENEMIES = 128;
public static final int MAX_VALUE = 127;
public static final int MAX_VALUE = 255;
public static final String[] NAME_IDS = {
"champion_enemies",
"no_food",
"no_armor",
"no_healing",
"no_herbalism",
"swarm_intelligence",
"darkness",
"swarm_intelligence",
"no_scrolls"
};
public static final int[] MASKS = {
NO_FOOD, NO_ARMOR, NO_HEALING, NO_HERBALISM, SWARM_INTELLIGENCE, DARKNESS, NO_SCROLLS
CHAMPION_ENEMIES, NO_FOOD, NO_ARMOR, NO_HEALING, NO_HERBALISM, SWARM_INTELLIGENCE, DARKNESS, NO_SCROLLS
};
public static boolean isItemBlocked( Item item ){

View File

@@ -32,6 +32,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Bleeding;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Bless;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Burning;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ChampionEnemy;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Charm;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Chill;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Corrosion;
@@ -132,7 +133,7 @@ public abstract class Char extends Actor {
Dungeon.level.updateFieldOfView( this, fieldOfView );
//throw any items that are on top of an immovable char
if (properties.contains(Property.IMMOVABLE)){
if (properties().contains(Property.IMMOVABLE)){
throwItems();
}
return false;
@@ -172,8 +173,8 @@ public abstract class Char extends Actor {
}
//can't swap into a space without room
if (properties.contains(Property.LARGE) && !Dungeon.level.openSpace[c.pos]
|| c.properties.contains(Property.LARGE) && !Dungeon.level.openSpace[pos]){
if (properties().contains(Property.LARGE) && !Dungeon.level.openSpace[c.pos]
|| c.properties().contains(Property.LARGE) && !Dungeon.level.openSpace[pos]){
return true;
}
@@ -375,10 +376,16 @@ public abstract class Char extends Actor {
float acuRoll = Random.Float( acuStat );
if (attacker.buff(Bless.class) != null) acuRoll *= 1.25f;
if (attacker.buff( Hex.class) != null) acuRoll *= 0.8f;
for (ChampionEnemy buff : attacker.buffs(ChampionEnemy.class)){
acuRoll *= buff.evasionAndAccuracyFactor();
}
float defRoll = Random.Float( defStat );
if (defender.buff(Bless.class) != null) defRoll *= 1.25f;
if (defender.buff( Hex.class) != null) defRoll *= 0.8f;
for (ChampionEnemy buff : defender.buffs(ChampionEnemy.class)){
defRoll *= buff.evasionAndAccuracyFactor();
}
return (magic ? acuRoll * 2 : acuRoll) >= defRoll;
}
@@ -410,6 +417,10 @@ public abstract class Char extends Actor {
if ( buff(Weakness.class) != null ){
damage *= 0.67f;
}
for (ChampionEnemy buff : buffs(ChampionEnemy.class)){
damage *= buff.meleeDamageFactor();
buff.onAttackProc( enemy );
}
return damage;
}
@@ -454,6 +465,10 @@ public abstract class Char extends Actor {
return;
}
for (ChampionEnemy buff : buffs(ChampionEnemy.class)){
dmg *= buff.damageTakenFactor();
}
if (!(src instanceof LifeLink) && buff(LifeLink.class) != null){
HashSet<LifeLink> links = buffs(LifeLink.class);
for (LifeLink link : links.toArray(new LifeLink[0])){
@@ -656,7 +671,7 @@ public abstract class Char extends Actor {
sprite.interruptMotion();
int newPos = pos + PathFinder.NEIGHBOURS8[Random.Int( 8 )];
if (!(Dungeon.level.passable[newPos] || Dungeon.level.avoid[newPos])
|| (properties.contains(Property.LARGE) && !Dungeon.level.openSpace[pos])
|| (properties().contains(Property.LARGE) && !Dungeon.level.openSpace[pos])
|| Actor.findChar( newPos ) != null)
return;
else {
@@ -746,7 +761,12 @@ public abstract class Char extends Actor {
protected HashSet<Property> properties = new HashSet<>();
public HashSet<Property> properties() {
return new HashSet<>(properties);
HashSet<Property> props = new HashSet<>(properties);
//TODO any more of these and we should make it a property of the buff, like with resistances/immunities
if (buff(ChampionEnemy.Giant.class) != null) {
props.add(Property.LARGE);
}
return props;
}
public enum Property{
@@ -794,6 +814,6 @@ public abstract class Char extends Actor {
}
public static boolean hasProp( Char ch, Property p){
return (ch != null && ch.properties.contains(p));
return (ch != null && ch.properties().contains(p));
}
}

View File

@@ -0,0 +1,255 @@
/*
* Pixel Dungeon
* Copyright (C) 2012-2015 Oleg Dolya
*
* Shattered Pixel Dungeon
* Copyright (C) 2014-2021 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.buffs;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Fire;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator;
import com.watabou.noosa.Image;
import com.watabou.utils.Bundle;
import com.watabou.utils.PathFinder;
import com.watabou.utils.Random;
public abstract class ChampionEnemy extends Buff {
{
type = buffType.POSITIVE;
}
protected int color;
@Override
public int icon() {
return BuffIndicator.CORRUPT;
}
@Override
public void tintIcon(Image icon) {
icon.hardlight(color);
}
@Override
public void fx(boolean on) {
if (on) target.sprite.aura( color );
else target.sprite.clearAura();
}
@Override
public String toString() {
return Messages.get(this, "name");
}
@Override
public String desc() {
return Messages.get(this, "desc");
}
public void onAttackProc(Char enemy ){
}
public boolean canAttackWithExtraReach( Char enemy ){
return false;
}
public float meleeDamageFactor(){
return 1f;
}
public float damageTakenFactor(){
return 1f;
}
public float evasionAndAccuracyFactor(){
return 1f;
}
{
immunities.add(Corruption.class);
}
public static void rollForChampion( Mob m ){
if (Random.Int(15) == 0){
switch (Random.Int(6)){
case 0: default: Buff.affect(m, Blazing.class); break;
case 1: Buff.affect(m, Projecting.class); break;
case 2: Buff.affect(m, AntiMagic.class); break;
case 3: Buff.affect(m, Giant.class); break;
case 4: Buff.affect(m, Blessed.class); break;
case 5: Buff.affect(m, Growing.class); break;
}
m.state = m.WANDERING;
}
}
public static class Blazing extends ChampionEnemy {
{
color = 0xFF8800;
}
@Override
public void onAttackProc(Char enemy) {
Buff.affect(enemy, Burning.class).reignite(enemy);
}
@Override
public void detach() {
for (int i : PathFinder.NEIGHBOURS9){
if (!Dungeon.level.solid[target.pos+i]){
GameScene.add(Blob.seed(target.pos+i, 8, Fire.class));
}
}
super.detach();
}
@Override
public float meleeDamageFactor() {
return 1.25f;
}
{
immunities.add(Burning.class);
}
}
public static class Projecting extends ChampionEnemy {
{
color = 0x8800FF;
}
@Override
public float meleeDamageFactor() {
return 1.25f;
}
@Override
public boolean canAttackWithExtraReach( Char enemy ) {
return target.fieldOfView[enemy.pos]; //if it can see it, it can attack it.
}
}
public static class AntiMagic extends ChampionEnemy {
{
color = 0x00FF00;
}
@Override
public float damageTakenFactor() {
return 0.75f;
}
{
immunities.addAll(com.shatteredpixel.shatteredpixeldungeon.items.armor.glyphs.AntiMagic.RESISTS);
}
}
//Also makes target large, see Char.properties()
public static class Giant extends ChampionEnemy {
{
color = 0x0088FF;
}
@Override
public float damageTakenFactor() {
return 0.25f;
}
@Override
public boolean canAttackWithExtraReach(Char enemy) {
//attack range of 2
return target.fieldOfView[enemy.pos] && Dungeon.level.distance(target.pos, enemy.pos) <= 2;
}
}
public static class Blessed extends ChampionEnemy {
{
color = 0xFFFF00;
}
@Override
public float evasionAndAccuracyFactor() {
return 3f;
}
}
public static class Growing extends ChampionEnemy {
{
color = 0xFF0000;
}
private float multiplier = 1.24f;
@Override
public boolean act() {
multiplier += 0.01f;
spend(2*TICK);
return true;
}
@Override
public float meleeDamageFactor() {
return multiplier;
}
@Override
public float damageTakenFactor() {
return 1f/multiplier;
}
@Override
public float evasionAndAccuracyFactor() {
return multiplier;
}
@Override
public String desc() {
return Messages.get(this, "desc", (int)(100*(multiplier-1)), (int)(100*(1 - 1f/multiplier)));
}
private static final String MULTIPLIER = "multiplier";
@Override
public void storeInBundle(Bundle bundle) {
super.storeInBundle(bundle);
bundle.put(MULTIPLIER, multiplier);
}
@Override
public void restoreFromBundle(Bundle bundle) {
super.restoreFromBundle(bundle);
multiplier = bundle.getFloat(MULTIPLIER);
}
}
}

View File

@@ -31,6 +31,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Adrenaline;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ChampionEnemy;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Charm;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Corruption;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Hunger;
@@ -346,7 +347,15 @@ public abstract class Mob extends Char {
}
protected boolean canAttack( Char enemy ) {
return Dungeon.level.adjacent( pos, enemy.pos );
if (Dungeon.level.adjacent( pos, enemy.pos )){
return true;
}
for (ChampionEnemy buff : buffs(ChampionEnemy.class)){
if (buff.canAttackWithExtraReach( enemy )){
return true;
}
}
return false;
}
protected boolean getCloser( int target ) {

View File

@@ -35,6 +35,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.WellWater;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Awareness;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Blindness;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ChampionEnemy;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.MagicalSight;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.MindVision;
@@ -445,8 +446,12 @@ public abstract class Level implements Bundlable {
if (mobsToSpawn == null || mobsToSpawn.isEmpty()) {
mobsToSpawn = Bestiary.getMobRotation(Dungeon.depth);
}
return Reflection.newInstance(mobsToSpawn.remove(0));
Mob m = Reflection.newInstance(mobsToSpawn.remove(0));
if (Dungeon.isChallenged(Challenges.CHAMPION_ENEMIES)){
ChampionEnemy.rollForChampion(m);
}
return m;
}
abstract protected void createMobs();
@@ -539,6 +544,9 @@ public abstract class Level implements Bundlable {
if (Statistics.amuletObtained) {
mob.beckon( Dungeon.hero.pos );
}
if (!mob.buffs(ChampionEnemy.class).isEmpty()){
GLog.w(Messages.get(ChampionEnemy.class, "warn"));
}
spend(Dungeon.level.respawnCooldown());
} else {
//try again in 1 turn

View File

@@ -30,6 +30,7 @@ import com.shatteredpixel.shatteredpixeldungeon.Statistics;
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ChampionEnemy;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.DemonSpawner;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
import com.shatteredpixel.shatteredpixeldungeon.effects.BannerSprites;
@@ -496,6 +497,12 @@ public class GameScene extends PixelScene {
GLog.w(Messages.get(this, "secrets"));
}
for (Mob mob : Dungeon.level.mobs) {
if (!mob.buffs(ChampionEnemy.class).isEmpty()) {
GLog.w(Messages.get(ChampionEnemy.class, "warn"));
}
}
InterlevelScene.mode = InterlevelScene.Mode.NONE;

View File

@@ -26,6 +26,7 @@ import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.effects.DarkBlock;
import com.shatteredpixel.shatteredpixeldungeon.effects.EmoIcon;
import com.shatteredpixel.shatteredpixeldungeon.effects.Flare;
import com.shatteredpixel.shatteredpixeldungeon.effects.FloatingText;
import com.shatteredpixel.shatteredpixeldungeon.effects.IceBlock;
import com.shatteredpixel.shatteredpixeldungeon.effects.ShieldHalo;
@@ -104,6 +105,7 @@ public class CharSprite extends MovieClip implements Tweener.Listener, MovieClip
protected TorchHalo light;
protected ShieldHalo shield;
protected AlphaTweener invisible;
protected Flare aura;
protected EmoIcon emo;
protected CharHealthIndicator health;
@@ -460,6 +462,24 @@ public class CharSprite extends MovieClip implements Tweener.Listener, MovieClip
break;
}
}
public void aura( int color ){
if (aura != null){
aura.killAndErase();
}
float size = Math.max(width(), height());
size = Math.max(size+4, 16);
aura = new Flare(5, size);
aura.angularSpeed = 90;
aura.color(color, true).show(this, 0);
}
public void clearAura(){
if (aura != null){
aura.killAndErase();
aura = null;
}
}
@Override
public void update() {
@@ -489,6 +509,10 @@ public class CharSprite extends MovieClip implements Tweener.Listener, MovieClip
if (marked != null) {
marked.visible = visible;
}
if (aura != null){
aura.visible = visible;
aura.point(center());
}
if (sleeping) {
showSleep();
} else {

View File

@@ -37,8 +37,8 @@ import java.util.ArrayList;
public class WndChallenges extends Window {
private static final int WIDTH = 120;
private static final int TTL_HEIGHT = 18;
private static final int BTN_HEIGHT = 18;
private static final int TTL_HEIGHT = 16;
private static final int BTN_HEIGHT = 16;
private static final int GAP = 1;
private boolean editable;
@@ -66,7 +66,7 @@ public class WndChallenges extends Window {
final String challenge = Challenges.NAME_IDS[i];
CheckBox cb = new CheckBox( Messages.get(Challenges.class, challenge) );
CheckBox cb = new CheckBox( Messages.titleCase(Messages.get(Challenges.class, challenge)) );
cb.checked( (checked & Challenges.MASKS[i]) != 0 );
cb.active = editable;