v3.0.0: big chaotic censer buff: now warns and only spews at enemies

This commit is contained in:
Evan Debenham
2025-01-09 13:18:57 -05:00
parent db70d8fa98
commit f9904d26c6
3 changed files with 129 additions and 116 deletions

View File

@@ -1,49 +1,67 @@
###blobs ###blobs
actors.blobs.blizzard.name=blizzard
actors.blobs.blizzard.desc=A blizzard is swirling here. actors.blobs.blizzard.desc=A blizzard is swirling here.
actors.blobs.confusiongas.name=confusion gas
actors.blobs.confusiongas.desc=A cloud of confusion gas is swirling here. actors.blobs.confusiongas.desc=A cloud of confusion gas is swirling here.
actors.blobs.electricity.name=electricity
actors.blobs.electricity.desc=A field of electricity is sparking brightly here. actors.blobs.electricity.desc=A field of electricity is sparking brightly here.
actors.blobs.electricity.rankings_desc=Electrocuted actors.blobs.electricity.rankings_desc=Electrocuted
actors.blobs.electricity.ondeath=You were shocked to death... actors.blobs.electricity.ondeath=You were shocked to death...
actors.blobs.fire.name=fire
actors.blobs.fire.desc=A fire is raging here. actors.blobs.fire.desc=A fire is raging here.
actors.blobs.foliage.name=foliage
actors.blobs.foliage.desc=Shafts of light pierce the gloom of the underground garden. actors.blobs.foliage.desc=Shafts of light pierce the gloom of the underground garden.
actors.blobs.freezing.name=freezing air
actors.blobs.freezing.desc=The air is unnaturally frigid here. actors.blobs.freezing.desc=The air is unnaturally frigid here.
actors.blobs.goowarn.name=dark energy
actors.blobs.goowarn.desc=Specks of dark energy are swarming here! actors.blobs.goowarn.desc=Specks of dark energy are swarming here!
actors.blobs.inferno.name=inferno
actors.blobs.inferno.desc=An inferno is raging here. actors.blobs.inferno.desc=An inferno is raging here.
actors.blobs.paralyticgas.name=paralytic gas
actors.blobs.paralyticgas.desc=A cloud of paralytic gas is swirling here. actors.blobs.paralyticgas.desc=A cloud of paralytic gas is swirling here.
actors.blobs.regrowth.name=regrowth
actors.blobs.sacrificialfire.name=sacrificial fire
actors.blobs.sacrificialfire.desc=There is an altar here with a sacrificial fire burning atop it. Any creature that's killed here will be consumed as an offering to the spirits of the dungeon.\n\nPerhaps a reward will be given if enough sacrifices are made? actors.blobs.sacrificialfire.desc=There is an altar here with a sacrificial fire burning atop it. Any creature that's killed here will be consumed as an offering to the spirits of the dungeon.\n\nPerhaps a reward will be given if enough sacrifices are made?
actors.blobs.sacrificialfire.worthy=The fire consumes your offering and grows stronger. actors.blobs.sacrificialfire.worthy=The fire consumes your offering and grows stronger.
actors.blobs.sacrificialfire.unworthy=The fire consumes your offering, but doesn't change. actors.blobs.sacrificialfire.unworthy=The fire consumes your offering, but doesn't change.
actors.blobs.sacrificialfire.reward=The fire flares and then dissipates, leaving a reward behind! actors.blobs.sacrificialfire.reward=The fire flares and then dissipates, leaving a reward behind!
actors.blobs.smokescreen.name=smokescreen
actors.blobs.smokescreen.desc=A cloud of thick black smoke is swirling here. actors.blobs.smokescreen.desc=A cloud of thick black smoke is swirling here.
actors.blobs.stenchgas.name=stench gas
actors.blobs.stenchgas.desc=A cloud of fetid stench is swirling here. actors.blobs.stenchgas.desc=A cloud of fetid stench is swirling here.
actors.blobs.stormcloud.name=storm clouds
actors.blobs.stormcloud.desc=A cloud of billowing water vapor is swirling here. actors.blobs.stormcloud.desc=A cloud of billowing water vapor is swirling here.
actors.blobs.toxicgas.name=toxic gas
actors.blobs.toxicgas.desc=A greenish cloud of toxic gas is swirling here. actors.blobs.toxicgas.desc=A greenish cloud of toxic gas is swirling here.
actors.blobs.toxicgas.rankings_desc=Suffocated actors.blobs.toxicgas.rankings_desc=Suffocated
actors.blobs.toxicgas.ondeath=You died from the toxic gas... actors.blobs.toxicgas.ondeath=You died from the toxic gas...
actors.blobs.corrosivegas.name=corrosive gas
actors.blobs.corrosivegas.desc=A cloud of deadly caustic gas is swirling here. actors.blobs.corrosivegas.desc=A cloud of deadly caustic gas is swirling here.
actors.blobs.waterofawareness.name=water of awareness
actors.blobs.waterofawareness.procced=As you take a sip, you feel knowledge pour into your mind. actors.blobs.waterofawareness.procced=As you take a sip, you feel knowledge pour into your mind.
actors.blobs.waterofawareness.desc=Power of knowledge radiates from the water of this well. Drinking from the well will fully identify all equipped items, identify curses on all items in your inventory, and reveal all items on the current floor. actors.blobs.waterofawareness.desc=Power of knowledge radiates from the water of this well. Drinking from the well will fully identify all equipped items, identify curses on all items in your inventory, and reveal all items on the current floor.
actors.blobs.waterofhealth.name=water of health
actors.blobs.waterofhealth.procced=As you take a sip, you feel your wounds heal completely. actors.blobs.waterofhealth.procced=As you take a sip, you feel your wounds heal completely.
actors.blobs.waterofhealth.desc=Power of health radiates from the water of this well. Drinking from this well will heal your wounds, satisfy hunger, and cleanse curses on any worn items. actors.blobs.waterofhealth.desc=Power of health radiates from the water of this well. Drinking from this well will heal your wounds, satisfy hunger, and cleanse curses on any worn items.
actors.blobs.wateroftransmutation.desc=Power of change radiates from the water of this well. Throw an item into the well to turn it into something else. actors.blobs.web.name=spider web
actors.blobs.web.desc=A thick web is covering everything here. Anything that touches or is thrown through the web will break it, but will also be stuck in place. actors.blobs.web.desc=A thick web is covering everything here. Anything that touches or is thrown through the web will break it, but will also be stuck in place.

View File

@@ -1341,9 +1341,10 @@ items.stones.stoneofshock.desc=This runestone unleashes a blast of electrical en
###trinkets ###trinkets
items.trinkets.chaoticcenser.name=chaotic censer items.trinkets.chaoticcenser.name=chaotic censer
items.trinkets.chaoticcenser.desc=After some time in the alchemy pot this incense-burning censer appears to be producing smoke all on its own! These gasses build up and will spew forth from the censer in random directions and semi-random intervals. It seems capable of producing all sorts of gasses, but the position they shoot out in seems to be more likely to be in your favour at least. items.trinkets.chaoticcenser.spew=Your censer is about to spew: %s.
items.trinkets.chaoticcenser.typical_stats_desc=Typically this trinket will spawn a harmful gas nearby roughly every _%d_ turns. The gas is more likely to appear when enemies are present, and less likely to appear in enclosed spaces. At higher levels these gases are more likely to be exotic and powerful. items.trinkets.chaoticcenser.desc=After some time in the alchemy pot this incense-burning censer appears to be producing smoke all on its own! These gasses build up and will spew forth from the censer toward enemies at semi-random intervals. It seems capable of producing all sorts of gasses, but you'll get a moment of warning just before the censer activates.
items.trinkets.chaoticcenser.stats_desc=At its current level, this trinket will spawn a harmful gas nearby roughly every _%d_ turns. The gas is more likely to appear when enemies are present, and less likely to appear in enclosed spaces. At higher levels these gases are more likely to be exotic and powerful. items.trinkets.chaoticcenser.typical_stats_desc=Typically this trinket will spawn a harmful gas near an enemy roughly every _%d_ turns. Gasses will only appear when enemies are present. At higher levels these gases are more likely to be exotic and powerful.
items.trinkets.chaoticcenser.stats_desc=At its current level, this trinket will spawn a harmful gas near an enemy roughly every _%d_ turns. Gasses will only appear when enemies are present. At higher levels these gases are more likely to be exotic and powerful.
items.trinkets.dimensionalsundial.name=dimensional sundial items.trinkets.dimensionalsundial.name=dimensional sundial
items.trinkets.dimensionalsundial.warning=Your sundial isn't casting a shadow, you feel uneasy. items.trinkets.dimensionalsundial.warning=Your sundial isn't casting a shadow, you feel uneasy.

View File

@@ -23,7 +23,6 @@ package com.shatteredpixel.shatteredpixeldungeon.items.trinkets;
import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char; import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blizzard; import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blizzard;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob; import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob;
@@ -36,15 +35,16 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.StenchGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.StormCloud; import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.StormCloud;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas; import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.FlavourBuff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Regeneration; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Regeneration;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.Shopkeeper;
import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile; import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile;
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
import com.shatteredpixel.shatteredpixeldungeon.mechanics.Ballistica; import com.shatteredpixel.shatteredpixeldungeon.effects.TargetedCell;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet;
import com.shatteredpixel.shatteredpixeldungeon.ui.TargetHealthIndicator; import com.shatteredpixel.shatteredpixeldungeon.ui.TargetHealthIndicator;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.watabou.noosa.audio.Sample; import com.watabou.noosa.audio.Sample;
import com.watabou.utils.BArray; import com.watabou.utils.BArray;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
@@ -99,91 +99,42 @@ public class ChaoticCenser extends Trinket {
if (avgTurns == -1){ if (avgTurns == -1){
spend(Random.NormalIntRange(1, 5)); spend(Random.NormalIntRange(1, 5));
return true; return true;
} else if (left > avgTurns*1.1f){ } else if (left > avgTurns*1.2f){
left = Random.IntRange((int) (avgTurns*0.9f), (int) (avgTurns*1.1f)); left = Random.IntRange((int) (avgTurns*0.833f), (int) (avgTurns*1.2f));
} }
float triggerChance = 0; if (left <= 0) {
if (left > 0 && left <= 30) {
Char enemy = null;
if (TargetHealthIndicator.instance != null && TargetHealthIndicator.instance.isVisible() if (TargetHealthIndicator.instance != null && TargetHealthIndicator.instance.isVisible()
&& TargetHealthIndicator.instance.target() != null && TargetHealthIndicator.instance.target() != null
&& TargetHealthIndicator.instance.target().alignment == Char.Alignment.ENEMY && TargetHealthIndicator.instance.target().alignment == Char.Alignment.ENEMY
&& TargetHealthIndicator.instance.target().isAlive()) { && TargetHealthIndicator.instance.target().isAlive()) {
triggerChance = 0.75f;
}
} else if (left > -30 && left <= 0) { if (produceGas(TargetHealthIndicator.instance.target())){
Sample.INSTANCE.play(Assets.Sounds.GAS, 0.5f);
if (TargetHealthIndicator.instance != null && TargetHealthIndicator.instance.isVisible() Dungeon.hero.interrupt();
&& TargetHealthIndicator.instance.target() != null left += Random.IntRange((int) (avgTurns * 0.9f), (int) (avgTurns * 1.1f));
&& TargetHealthIndicator.instance.target().alignment == Char.Alignment.ENEMY
&& TargetHealthIndicator.instance.target().isAlive()) {
triggerChance = 1f;
} else if (Dungeon.level.openSpace[target.pos]){
triggerChance = 0.2f;
}
} else if (left <= -30) {
triggerChance = 1f;
}
if (triggerChance > 0) {
if (safeAreaDelay >= 0) {
boolean safeArea = false;
//shops are a safe area
for (Char ch : Actor.chars()) {
if (ch instanceof Shopkeeper
&& Dungeon.level.distance(target.pos, ch.pos) <= 6
&& new Ballistica(target.pos, ch.pos, Ballistica.PROJECTILE).collisionPos == ch.pos) {
safeArea = true;
}
}
//enclosed spaces are a safe area if no enemies are present
if ((TargetHealthIndicator.instance == null || TargetHealthIndicator.instance.target() == null
|| TargetHealthIndicator.instance.target().alignment != Char.Alignment.ENEMY
|| !TargetHealthIndicator.instance.target().isAlive())
&& !Dungeon.level.openSpace[target.pos]) {
safeArea = true;
}
if (safeArea){
int delay = Random.NormalIntRange(1, 5);
spend(delay);
safeAreaDelay -= delay;
return true;
} }
} }
}
if (Random.Float() < triggerChance){
if (produceGas()) {
Sample.INSTANCE.play(Assets.Sounds.GAS);
Dungeon.hero.interrupt();
left += Random.IntRange((int) (avgTurns * 0.9f), (int) (avgTurns * 1.1f));
}
} }
//buff ticks an average of every 3 turns //buff ticks an average of every 3 turns
int delay = Random.NormalIntRange(1, 5); int delay = Random.NormalIntRange(1, 3);
spend(delay); spend(delay);
safeAreaDelay = Math.min(safeAreaDelay+2*delay, 100); left = (int)Math.max(left-delay, -avgTurns/3f);
left -= delay;
return true; return true;
} }
private static String LEFT = "left"; private static String LEFT = "left";
private static String SAFE_AREA_DELAY = "safe_area_delay";
@Override @Override
public void storeInBundle(Bundle bundle) { public void storeInBundle(Bundle bundle) {
super.storeInBundle(bundle); super.storeInBundle(bundle);
bundle.put(LEFT, left); bundle.put(LEFT, left);
bundle.put(SAFE_AREA_DELAY, safeAreaDelay);
} }
@Override @Override
@@ -191,12 +142,11 @@ public class ChaoticCenser extends Trinket {
super.restoreFromBundle(bundle); super.restoreFromBundle(bundle);
if (bundle.contains(LEFT)){ if (bundle.contains(LEFT)){
left = bundle.getInt(LEFT); left = bundle.getInt(LEFT);
safeAreaDelay = bundle.getInt(SAFE_AREA_DELAY);
} }
} }
} }
private static boolean produceGas(){ private static boolean produceGas( Char target ){
int level = trinketLevel(ChaoticCenser.class); int level = trinketLevel(ChaoticCenser.class);
if (level < 0 || level > 3){ if (level < 0 || level > 3){
@@ -222,69 +172,53 @@ public class ChaoticCenser extends Trinket {
break; break;
} }
Char target = null;
if (TargetHealthIndicator.instance != null && TargetHealthIndicator.instance.isVisible()
&& TargetHealthIndicator.instance.target() != null
&& TargetHealthIndicator.instance.target().alignment == Char.Alignment.ENEMY
&& TargetHealthIndicator.instance.target().isAlive()) {
target = TargetHealthIndicator.instance.target();
}
HashMap<Integer, Float> candidateCells = new HashMap<>(); HashMap<Integer, Float> candidateCells = new HashMap<>();
PathFinder.buildDistanceMap(Dungeon.hero.pos, BArray.not(Dungeon.level.solid, null), 5); PathFinder.buildDistanceMap(Dungeon.hero.pos, BArray.not(Dungeon.level.solid, null), 6);
//spawn gas in a random visible cell 2-5 tiles away, likelihood is 3,4 > 2,5 //spawn gas in a random visible cell 2-6 tiles away
for (int i = 0; i < Dungeon.level.length(); i++){ for (int i = 0; i < Dungeon.level.length(); i++){
if (Dungeon.level.heroFOV[i] && PathFinder.distance[i] < Integer.MAX_VALUE) { if (Dungeon.level.heroFOV[i] && PathFinder.distance[i] < Integer.MAX_VALUE) {
switch (PathFinder.distance[i]) { if (PathFinder.distance[i] >= 2 && PathFinder.distance[i] <= 6) {
case 3: candidateCells.put(i, 0f);
case 4:
candidateCells.put(i, 2f);
break;
case 2:
case 5:
candidateCells.put(i, 1f);
break;
} }
} }
} }
//unless we have a target, then strongly prefer cells closer to target //strongly prefer cells closer to target
if (target != null){ int targetpos = target.pos;
int targetpos = target.pos; if (Dungeon.level.trueDistance(target.pos, Dungeon.hero.pos) >= 4){
if (Dungeon.level.trueDistance(target.pos, Dungeon.hero.pos) >= 4){ //if target is a distance from the hero, aim in front of them instead
//if target is a distance from the hero, aim in front of them instead for (int i : PathFinder.NEIGHBOURS8){
for (int i : PathFinder.NEIGHBOURS8){ while (!Dungeon.level.solid[targetpos+i]
while (!Dungeon.level.solid[targetpos+i] && Dungeon.level.trueDistance(target.pos+i, Dungeon.hero.pos) < Dungeon.level.trueDistance(targetpos, Dungeon.hero.pos)){
&& Dungeon.level.trueDistance(target.pos+i, Dungeon.hero.pos) < Dungeon.level.trueDistance(targetpos, Dungeon.hero.pos)){ targetpos = target.pos+i;
targetpos = target.pos+i;
}
} }
} }
float closest = 100; }
for (int cell : candidateCells.keySet()){ float closest = 100;
float dist = Dungeon.level.distance(cell, targetpos); for (int cell : candidateCells.keySet()){
if (dist < closest){ float dist = Dungeon.level.distance(cell, targetpos);
closest = dist; if (dist < closest){
} closest = dist;
} }
for (int cell : candidateCells.keySet()){ }
float dist = Dungeon.level.distance(cell, targetpos); for (int cell : candidateCells.keySet()){
if (dist - closest == 0) { float dist = Dungeon.level.distance(cell, targetpos);
candidateCells.put(cell, 4f); if (dist - closest == 0) {
} else if (dist - closest <= 1) { candidateCells.put(cell, 8f);
candidateCells.put(cell, 1f); } else if (dist - closest <= 1) {
} else { candidateCells.put(cell, 1f);
candidateCells.put(cell, 0f); } else {
} candidateCells.put(cell, 0f);
} }
} }
if (!candidateCells.isEmpty()) { if (!candidateCells.isEmpty()) {
Integer targetCell = Random.chances(candidateCells); Integer targetCell = Random.chances(candidateCells);
if (targetCell != null) { if (targetCell != null) {
GameScene.add(Blob.seed(targetCell, (int) gasQuantity, gasToSpawn)); Buff.affect(Dungeon.hero, GasSpewer.class, Dungeon.hero.cooldown()).set(targetCell, gasToSpawn, (int)gasQuantity);
MagicMissile.boltFromChar(Dungeon.hero.sprite.parent, MISSILE_VFX.get(gasToSpawn), Dungeon.hero.sprite, targetCell, null); GLog.w(Messages.get(ChaoticCenser.class, "spew", Messages.titleCase(Messages.get(gasToSpawn, "name")) ));
target.sprite.parent.addToBack(new TargetedCell(targetCell, 0xFF0000));
return true; return true;
} }
} }
@@ -293,6 +227,66 @@ public class ChaoticCenser extends Trinket {
} }
public static class GasSpewer extends FlavourBuff {
private int targetCell;
private int depth;
private int branch;
private Class<?extends Blob> gasType;
private int gasQuantity;
public void set( int targetCell, Class<?extends Blob> gasType, int gasQuantity){
this.targetCell = targetCell;
depth = Dungeon.depth;
branch = Dungeon.branch;
this.gasType = gasType;
this.gasQuantity = gasQuantity;
}
@Override
public boolean act() {
if (depth == Dungeon.depth && branch == Dungeon.branch){
GameScene.add(Blob.seed(targetCell, gasQuantity, gasType));
MagicMissile.boltFromChar(Dungeon.hero.sprite.parent, MISSILE_VFX.get(gasType), Dungeon.hero.sprite, targetCell, null);
Sample.INSTANCE.play(Assets.Sounds.GAS);
}
detach();
return true;
}
private static final String CELL = "cell";
private static final String DEPTH = "depth";
private static final String BRANCH = "branch";
private static final String GAS_TYPE = "gas_type";
private static final String GAS_QUANTITY = "gas_quantity";
@Override
public void storeInBundle(Bundle bundle) {
super.storeInBundle(bundle);
bundle.put(CELL, targetCell);
bundle.put(DEPTH, depth);
bundle.put(BRANCH, branch);
bundle.put(GAS_TYPE, gasType);
bundle.put(GAS_QUANTITY, gasQuantity);
}
@Override
public void restoreFromBundle(Bundle bundle) {
super.restoreFromBundle(bundle);
targetCell = bundle.getInt(CELL);
depth = bundle.getInt(DEPTH);
branch = bundle.getInt(BRANCH);
gasType = bundle.getClass(GAS_TYPE);
gasQuantity = bundle.getInt(GAS_QUANTITY);
}
}
private static final float[][] GAS_CAT_CHANCES = new float[4][3]; private static final float[][] GAS_CAT_CHANCES = new float[4][3];
static { static {
GAS_CAT_CHANCES[0] = new float[]{70, 25, 5}; GAS_CAT_CHANCES[0] = new float[]{70, 25, 5};