diff --git a/core/src/main/assets/messages/items/items.properties b/core/src/main/assets/messages/items/items.properties index 42c5f2fcb..45da67ca3 100644 --- a/core/src/main/assets/messages/items/items.properties +++ b/core/src/main/assets/messages/items/items.properties @@ -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. diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Regeneration.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Regeneration.java index 59bc3f2db..4802abc4e 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Regeneration.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Regeneration.java @@ -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); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/Generator.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/Generator.java index cd8713fad..cefc10418 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/Generator.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/Generator.java @@ -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()){ diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/trinkets/ChaoticCenser.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/trinkets/ChaoticCenser.java new file mode 100644 index 000000000..ec26bcf68 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/trinkets/ChaoticCenser.java @@ -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 + */ + +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 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 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, 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, 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, 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, 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); + } + +}