v2.5.0: implemented the chaotic censer trinket

This commit is contained in:
Evan Debenham
2024-09-04 14:04:55 -04:00
parent 8b42e550f3
commit df1f67b160
4 changed files with 259 additions and 3 deletions

View File

@@ -1320,7 +1320,7 @@ items.stones.stoneofshock.desc=This runestone unleashes a blast of electrical en
###trinkets
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 direction 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.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.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.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.

View File

@@ -25,6 +25,7 @@ import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.ChaliceOfBlood;
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfEnergy;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.ChaoticCenser;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.SaltCube;
public class Regeneration extends Buff {
@@ -41,6 +42,12 @@ public class Regeneration extends Buff {
public boolean act() {
if (target.isAlive()) {
//if other trinkets ever get buffs like this should probably make the buff attaching
// behaviour more like wands/rings/artifacts
if (ChaoticCenser.averageTurnsUntilGas() != -1){
Buff.affect(Dungeon.hero, ChaoticCenser.CenserGasTracker.class);
}
//cancel regenning entirely in thie case
if (SaltCube.healthRegenMultiplier() == 0){
spend(REGENERATION_DELAY);

View File

@@ -107,6 +107,7 @@ import com.shatteredpixel.shatteredpixeldungeon.items.stones.StoneOfFear;
import com.shatteredpixel.shatteredpixeldungeon.items.stones.StoneOfFlock;
import com.shatteredpixel.shatteredpixeldungeon.items.stones.StoneOfIntuition;
import com.shatteredpixel.shatteredpixeldungeon.items.stones.StoneOfShock;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.ChaoticCenser;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.DimensionalSundial;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.ExoticCrystals;
import com.shatteredpixel.shatteredpixeldungeon.items.trinkets.EyeOfNewt;
@@ -580,9 +581,10 @@ public class Generator {
EyeOfNewt.class,
SaltCube.class,
VialOfBlood.class,
ShardOfOblivion.class
ShardOfOblivion.class,
ChaoticCenser.class
};
TRINKET.defaultProbs = new float[]{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
TRINKET.defaultProbs = new float[]{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
TRINKET.probs = TRINKET.defaultProbs.clone();
for (Category cat : Category.values()){

View File

@@ -0,0 +1,247 @@
/*
* Pixel Dungeon
* Copyright (C) 2012-2015 Oleg Dolya
*
* Shattered Pixel Dungeon
* Copyright (C) 2014-2024 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.items.trinkets;
import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blizzard;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ConfusionGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.CorrosiveGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Inferno;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Regrowth;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.SmokeScreen;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.StenchGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.StormCloud;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile;
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet;
import com.shatteredpixel.shatteredpixeldungeon.ui.TargetHealthIndicator;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.BArray;
import com.watabou.utils.PathFinder;
import com.watabou.utils.Random;
import java.util.HashMap;
public class ChaoticCenser extends Trinket {
{
image = ItemSpriteSheet.CHAOTIC_CENSER;
}
@Override
protected int upgradeEnergyCost() {
//6 -> 8(14) -> 10(24) -> 12(36)
return 6+2*level();
}
@Override
public String statsDesc() {
if (isIdentified()){
return Messages.get(this, "stats_desc", averageTurnsUntilGas(buffedLvl()));
} else {
return Messages.get(this, "stats_desc", averageTurnsUntilGas(0));
}
}
public static int averageTurnsUntilGas(){
return averageTurnsUntilGas(trinketLevel(ChaoticCenser.class));
}
public static int averageTurnsUntilGas(int level){
if (level <= -1){
return -1;
} else {
return 300 / (level + 1);
}
}
public static class CenserGasTracker extends Buff {
private int left = Integer.MAX_VALUE;
@Override
public boolean act() {
int avgTurns = averageTurnsUntilGas();
if (avgTurns == -1){
detach();
return true;
}
if (left > avgTurns*1.1f){
left = Random.IntRange((int) (avgTurns*0.9f), (int) (avgTurns*1.1f));
}
if (left < avgTurns/5){
//scales quadratically from ~4% at avgTurns/5, to 36% at 0, to 100% at -avgTurns/5
float triggerChance = (float) Math.pow( 1f - (left + avgTurns/5f)/(avgTurns/2f), 2);
//trigger chance is linearly increased by 5% for each open adjacent cell after 2
int openCells = 0;
for (int i : PathFinder.NEIGHBOURS8){
if (!Dungeon.level.solid[target.pos+i]){
openCells++;
}
}
if (openCells > 2){
triggerChance += (openCells-2)*0.05f;
}
//if no target is present, quadratically scale back trigger chance again, strongly
if (TargetHealthIndicator.instance.target() != null){
triggerChance = (float)Math.pow(triggerChance, 3);
}
if (Random.Float() < triggerChance){
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 5 turns
int delay = Random.NormalIntRange(2, 8);
spend(delay);
left -= delay;
return true;
}
}
private static void produceGas(){
int level = trinketLevel(ChaoticCenser.class);
if (level < 0 || level > 3){
return;
}
Class<?extends Blob> gasToSpawn;
float gasQuantity;
switch (Random.chances(GAS_CAT_CHANCES[level])){
case 0: default:
gasToSpawn = Random.element(COMMON_GASSES.keySet());
gasQuantity = COMMON_GASSES.get(gasToSpawn);
break;
case 1:
gasToSpawn = Random.element(UNCOMMON_GASSES.keySet());
gasQuantity = UNCOMMON_GASSES.get(gasToSpawn);
break;
case 2:
gasToSpawn = Random.element(RARE_GASSES.keySet());
gasQuantity = RARE_GASSES.get(gasToSpawn);
break;
}
Char target = null;
if (TargetHealthIndicator.instance != null){
target = TargetHealthIndicator.instance.target();
}
HashMap<Integer, Float> candidateCells = new HashMap<>();
PathFinder.buildDistanceMap(Dungeon.hero.pos, BArray.not(Dungeon.level.solid, null), 3);
//spawn gas in a random cell 1-3 tiles away, likelihood is 2>3>1
for (int i = 0; i < Dungeon.level.length(); i++){
switch (PathFinder.distance[i]){
case 0: default: break; //do nothing
case 1: candidateCells.put(i, 1f); break;
case 2: candidateCells.put(i, 3f); break;
case 3: candidateCells.put(i, 2f); break;
}
}
//unless we have a target, then strongly prefer cells closer to target
if (target != null){
float furthest = 0;
for (int cell : candidateCells.keySet()){
float dist = Dungeon.level.trueDistance(cell, target.pos);
if (dist > furthest){
furthest = dist;
}
}
for (int cell : candidateCells.keySet()){
float dist = Dungeon.level.trueDistance(cell, target.pos);
candidateCells.put(cell, furthest - dist);
}
}
if (!candidateCells.isEmpty()) {
int targetCell = Random.chances(candidateCells);
GameScene.add(Blob.seed(targetCell, (int) gasQuantity, gasToSpawn));
MagicMissile.boltFromChar(Dungeon.hero.sprite.parent, MISSILE_VFX.get(gasToSpawn), Dungeon.hero.sprite, targetCell, null);
}
}
private static final float[][] GAS_CAT_CHANCES = new float[4][3];
static {
GAS_CAT_CHANCES[0] = new float[]{70, 25, 5};
GAS_CAT_CHANCES[1] = new float[]{60, 30, 10};
GAS_CAT_CHANCES[2] = new float[]{50, 35, 15};
GAS_CAT_CHANCES[3] = new float[]{40, 40, 20};
}
private static final HashMap<Class<? extends Blob>, Float> COMMON_GASSES = new HashMap<>();
static {
COMMON_GASSES.put(ToxicGas.class, 500f);
COMMON_GASSES.put(ConfusionGas.class, 500f);
COMMON_GASSES.put(Regrowth.class, 250f);
}
private static final HashMap<Class<? extends Blob>, Float> UNCOMMON_GASSES = new HashMap<>();
static {
UNCOMMON_GASSES.put(StormCloud.class, 500f);
UNCOMMON_GASSES.put(SmokeScreen.class, 500f);
UNCOMMON_GASSES.put(StenchGas.class, 250f);
}
private static final HashMap<Class<? extends Blob>, Float> RARE_GASSES = new HashMap<>();
static {
RARE_GASSES.put(Inferno.class, 500f);
RARE_GASSES.put(Blizzard.class, 500f);
RARE_GASSES.put(CorrosiveGas.class, 250f);
}
private static final HashMap<Class<? extends Blob>, Integer> MISSILE_VFX = new HashMap<>();
static {
MISSILE_VFX.put(ToxicGas.class, MagicMissile.SPECK + Speck.TOXIC);
MISSILE_VFX.put(ConfusionGas.class, MagicMissile.SPECK + Speck.CONFUSION);
MISSILE_VFX.put(Regrowth.class, MagicMissile.FOLIAGE);
MISSILE_VFX.put(StormCloud.class, MagicMissile.SPECK + Speck.STORM);
MISSILE_VFX.put(SmokeScreen.class, MagicMissile.SPECK + Speck.SMOKE);
MISSILE_VFX.put(StenchGas.class, MagicMissile.SPECK + Speck.STENCH);
MISSILE_VFX.put(Inferno.class, MagicMissile.SPECK + Speck.INFERNO);
MISSILE_VFX.put(Blizzard.class, MagicMissile.SPECK + Speck.BLIZZARD);
MISSILE_VFX.put(CorrosiveGas.class, MagicMissile.SPECK + Speck.CORROSION);
}
}