From af945c837b673fcede25f27f4b1c9576d3f6acf2 Mon Sep 17 00:00:00 2001 From: Robotgiggle Date: Tue, 22 Sep 2026 18:58:37 -0400 Subject: [PATCH 1/5] Fix spells with player-chosen cost being free with extremely small inputs --- .../at/petrak/hexcasting/api/casting/ActionUtils.kt | 11 +++++++++++ .../common/casting/actions/spells/OpAddMotion.kt | 3 ++- .../common/casting/actions/spells/OpBlink.kt | 4 ++-- .../common/casting/actions/spells/OpExplode.kt | 3 ++- .../common/casting/actions/spells/OpPotionEffect.kt | 4 ++-- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt b/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt index c6dda37c9a..131eefd57d 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt @@ -299,6 +299,17 @@ fun evaluatable(datum: Iota, reverseIdx: Int): Either = fun Iota?.orNull() = this ?: NullIota() +/** + * Converts to long while ensuring that the final result is always nonzero. + * Used when a spell's cost depends on an arbitrary multiplier provided by + * the player, so that an extremely small value doesn't make the spell free. + */ +fun Double.toLongNonzero(): Long { + val result = this.roundToLong() + if (result == 0L) return 1L + return result +} + // TODO do we make this work on lists // there should probably be some way to abstract function application over lists, vecs, and numbers, // and i bet it's fucking monads diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt index 292f404eba..e10a775b1b 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt @@ -8,6 +8,7 @@ import at.petrak.hexcasting.api.casting.eval.vm.CastingImage import at.petrak.hexcasting.api.casting.getEntity import at.petrak.hexcasting.api.casting.getVec3 import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.casting.toLongNonzero import at.petrak.hexcasting.api.misc.MediaConstants import net.minecraft.nbt.CompoundTag import net.minecraft.world.entity.Entity @@ -40,7 +41,7 @@ object OpAddMotion : SpellAction { return SpellAction.Result( Spell(target, shrunkMotion), - (motionForCost * MediaConstants.DUST_UNIT).toLong(), + (motionForCost * MediaConstants.DUST_UNIT).toLongNonzero(), listOf( ParticleSpray( target.position().add(0.0, target.eyeHeight / 2.0, 0.0), diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt index 2882984ebd..cace0a2cfc 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt @@ -10,6 +10,7 @@ import at.petrak.hexcasting.api.casting.getEntity import at.petrak.hexcasting.api.casting.iota.Iota import at.petrak.hexcasting.api.casting.mishaps.MishapBadLocation import at.petrak.hexcasting.api.casting.mishaps.MishapImmuneEntity +import at.petrak.hexcasting.api.casting.toLongNonzero import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.api.mod.HexConfig import at.petrak.hexcasting.api.mod.HexTags @@ -17,7 +18,6 @@ import at.petrak.hexcasting.common.casting.actions.spells.great.OpTeleport import net.minecraft.world.entity.Entity import net.minecraft.world.phys.Vec3 import kotlin.math.absoluteValue -import kotlin.math.roundToLong object OpBlink : SpellAction { override val argc = 2 @@ -54,7 +54,7 @@ object OpBlink : SpellAction { return SpellAction.Result( Spell(target, dvec), - (MediaConstants.SHARD_UNIT * delta.absoluteValue * 0.5).roundToLong(), + (MediaConstants.SHARD_UNIT * delta.absoluteValue * 0.5).toLongNonzero(), listOf( ParticleSpray.cloud(targetMiddlePos, 2.0, 50), ParticleSpray.burst(targetMiddlePos.add(dvec), 2.0, 100) diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt index 0bed07989c..db71c5435f 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt @@ -7,6 +7,7 @@ import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.getPositiveDoubleUnderInclusive import at.petrak.hexcasting.api.casting.getVec3 import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.casting.toLongNonzero import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.common.casting.actions.selectors.OpGetEntitiesBy import net.minecraft.core.BlockPos @@ -42,7 +43,7 @@ class OpExplode(val fire: Boolean) : SpellAction { val cost = MediaConstants.DUST_UNIT * (3 * clampedStrength + if (fire) 1.0 else 0.125) return SpellAction.Result( Spell(pos, strength, this.fire), - cost.toLong(), + cost.toLongNonzero(), listOf(ParticleSpray.burst(pos, strength, 50)) ) } diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt index 6f7dc5a05e..ec626fab5e 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt @@ -36,7 +36,7 @@ class OpPotionEffect( } return SpellAction.Result( Spell(effect, target, duration, potency), - cost.toLong(), + cost.toLongNonzero(), listOf(ParticleSpray.cloud(target.position().add(0.0, target.eyeHeight / 2.0, 0.0), 1.0)) ) } @@ -44,7 +44,7 @@ class OpPotionEffect( private class Spell(val effect: MobEffect, val target: LivingEntity, val duration: Double, val potency: Double) : RenderedSpell { override fun cast(env: CastingEnvironment) { - if (duration > 1.0 / 20.0) { + if (duration >= 1.0 / 20.0) { val effectInst = MobEffectInstance(effect, (duration * 20).toInt(), potency.toInt() - 1) target.addEffect(effectInst) } From 94b275de2dc2a6d5f7ebb617b538ee13fc0b7f65 Mon Sep 17 00:00:00 2001 From: Robotgiggle Date: Tue, 22 Sep 2026 18:58:55 -0400 Subject: [PATCH 2/5] Also apply to cost scaling factors --- .../hexcasting/api/casting/eval/CastingEnvironment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java index d12221cd5a..beaac83247 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java @@ -1,5 +1,6 @@ package at.petrak.hexcasting.api.casting.eval; +import at.petrak.hexcasting.api.casting.OperatorUtils; import at.petrak.hexcasting.api.casting.ParticleSpray; import at.petrak.hexcasting.api.casting.PatternShapeMatch; import at.petrak.hexcasting.api.casting.eval.env.PlayerBasedCastEnv; @@ -269,7 +270,8 @@ public boolean isEnlightened() { * positive. */ public long extractMedia(long cost, boolean simulate) { - cost = (long) (cost * costModifier); + if (cost == 0) return 0; + cost = OperatorUtils.toLongNonzero(cost * costModifier); for (var extractMediaComponent : preMediaExtract) cost = extractMediaComponent.onExtractMedia(cost, simulate); cost = extractMediaEnvironment(cost, simulate); From adc3861e8e913f44a8aa2e14706f845173d2b351 Mon Sep 17 00:00:00 2001 From: Robotgiggle Date: Tue, 22 Sep 2026 19:17:10 -0400 Subject: [PATCH 3/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9235ad30..1d0084ed0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - Fixed Great Spells causing a mishap when drawn without enlightenment even while parenthesized ([#1290](https://github.com/FallingColors/HexMod/pull/1290)) @Robotgiggle +- Spells with a variable cost can no longer be made free by providing an extremely small input value ([#1308](https://github.com/FallingColors/HexMod/pull/1308)) @Robotgiggle ## `0.11.4` - 2026-08-26 From 10743abbcc16ef815bcf307936772264f43ec173 Mon Sep 17 00:00:00 2001 From: Robotgiggle Date: Tue, 22 Sep 2026 19:23:30 -0400 Subject: [PATCH 4/5] Oops, forgot the flight spells --- .../hexcasting/common/casting/actions/spells/OpFlight.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt index 0bf708bce6..0948b22a25 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt @@ -7,6 +7,7 @@ import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.getPlayer import at.petrak.hexcasting.api.casting.getPositiveDouble import at.petrak.hexcasting.api.casting.iota.Iota +import at.petrak.hexcasting.api.casting.toLongNonzero import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.api.pigment.FrozenPigment import at.petrak.hexcasting.api.player.FlightAbility @@ -37,7 +38,7 @@ class OpFlight(val type: Type) : SpellAction { // One block of radius, or one second of duration, costs 2 dust val costUnit = 2 * MediaConstants.DUST_UNIT - var cost = (theArg * costUnit).roundToLong() + var cost = (theArg * costUnit).toLongNonzero() // Cost for anchorite does not decrease below 1 meter if (type == Type.LimitRange) cost = max(cost, costUnit) From 8185c0c3e12d9cb38b0f73bff730408898c16f4c Mon Sep 17 00:00:00 2001 From: Robotgiggle Date: Tue, 22 Sep 2026 19:39:57 -0400 Subject: [PATCH 5/5] Rename method, make it work on negatives --- .../java/at/petrak/hexcasting/api/casting/ActionUtils.kt | 6 ++++-- .../hexcasting/api/casting/eval/CastingEnvironment.java | 4 +--- .../hexcasting/common/casting/actions/spells/OpAddMotion.kt | 4 ++-- .../hexcasting/common/casting/actions/spells/OpBlink.kt | 4 ++-- .../hexcasting/common/casting/actions/spells/OpExplode.kt | 4 ++-- .../hexcasting/common/casting/actions/spells/OpFlight.kt | 5 ++--- .../common/casting/actions/spells/OpPotionEffect.kt | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt b/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt index 131eefd57d..13fc993b65 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/ActionUtils.kt @@ -21,6 +21,7 @@ import java.util.function.DoubleUnaryOperator import kotlin.math.abs import kotlin.math.roundToInt import kotlin.math.roundToLong +import kotlin.math.sign fun List.getDouble(idx: Int, argc: Int = 0): Double { val x = this.getOrElse(idx) { throw MishapNotEnoughArgs(idx + 1, this.size) } @@ -304,9 +305,10 @@ fun Iota?.orNull() = this ?: NullIota() * Used when a spell's cost depends on an arbitrary multiplier provided by * the player, so that an extremely small value doesn't make the spell free. */ -fun Double.toLongNonzero(): Long { +fun Double.toNonZeroLong(): Long { val result = this.roundToLong() - if (result == 0L) return 1L + if (result == 0L) + return if (this >= 0) 1L else -1L return result } diff --git a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java index beaac83247..0078b25c02 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java +++ b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java @@ -3,7 +3,6 @@ import at.petrak.hexcasting.api.casting.OperatorUtils; import at.petrak.hexcasting.api.casting.ParticleSpray; import at.petrak.hexcasting.api.casting.PatternShapeMatch; -import at.petrak.hexcasting.api.casting.eval.env.PlayerBasedCastEnv; import at.petrak.hexcasting.api.casting.eval.vm.CastingImage; import at.petrak.hexcasting.api.casting.mishaps.Mishap; import at.petrak.hexcasting.api.casting.mishaps.MishapBadLocation; @@ -13,7 +12,6 @@ import at.petrak.hexcasting.api.mod.HexTags; import at.petrak.hexcasting.api.pigment.FrozenPigment; import at.petrak.hexcasting.api.utils.HexUtils; -import at.petrak.hexcasting.common.lib.HexAttributes; import at.petrak.hexcasting.xplat.IXplatAbstractions; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; @@ -271,7 +269,7 @@ public boolean isEnlightened() { */ public long extractMedia(long cost, boolean simulate) { if (cost == 0) return 0; - cost = OperatorUtils.toLongNonzero(cost * costModifier); + cost = OperatorUtils.toNonZeroLong(cost * costModifier); for (var extractMediaComponent : preMediaExtract) cost = extractMediaComponent.onExtractMedia(cost, simulate); cost = extractMediaEnvironment(cost, simulate); diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt index e10a775b1b..2ebc41382e 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpAddMotion.kt @@ -8,7 +8,7 @@ import at.petrak.hexcasting.api.casting.eval.vm.CastingImage import at.petrak.hexcasting.api.casting.getEntity import at.petrak.hexcasting.api.casting.getVec3 import at.petrak.hexcasting.api.casting.iota.Iota -import at.petrak.hexcasting.api.casting.toLongNonzero +import at.petrak.hexcasting.api.casting.toNonZeroLong import at.petrak.hexcasting.api.misc.MediaConstants import net.minecraft.nbt.CompoundTag import net.minecraft.world.entity.Entity @@ -41,7 +41,7 @@ object OpAddMotion : SpellAction { return SpellAction.Result( Spell(target, shrunkMotion), - (motionForCost * MediaConstants.DUST_UNIT).toLongNonzero(), + (motionForCost * MediaConstants.DUST_UNIT).toNonZeroLong(), listOf( ParticleSpray( target.position().add(0.0, target.eyeHeight / 2.0, 0.0), diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt index cace0a2cfc..f3a777c195 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpBlink.kt @@ -10,7 +10,7 @@ import at.petrak.hexcasting.api.casting.getEntity import at.petrak.hexcasting.api.casting.iota.Iota import at.petrak.hexcasting.api.casting.mishaps.MishapBadLocation import at.petrak.hexcasting.api.casting.mishaps.MishapImmuneEntity -import at.petrak.hexcasting.api.casting.toLongNonzero +import at.petrak.hexcasting.api.casting.toNonZeroLong import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.api.mod.HexConfig import at.petrak.hexcasting.api.mod.HexTags @@ -54,7 +54,7 @@ object OpBlink : SpellAction { return SpellAction.Result( Spell(target, dvec), - (MediaConstants.SHARD_UNIT * delta.absoluteValue * 0.5).toLongNonzero(), + (MediaConstants.SHARD_UNIT * delta.absoluteValue * 0.5).toNonZeroLong(), listOf( ParticleSpray.cloud(targetMiddlePos, 2.0, 50), ParticleSpray.burst(targetMiddlePos.add(dvec), 2.0, 100) diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt index db71c5435f..fb73023b11 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpExplode.kt @@ -7,7 +7,7 @@ import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.getPositiveDoubleUnderInclusive import at.petrak.hexcasting.api.casting.getVec3 import at.petrak.hexcasting.api.casting.iota.Iota -import at.petrak.hexcasting.api.casting.toLongNonzero +import at.petrak.hexcasting.api.casting.toNonZeroLong import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.common.casting.actions.selectors.OpGetEntitiesBy import net.minecraft.core.BlockPos @@ -43,7 +43,7 @@ class OpExplode(val fire: Boolean) : SpellAction { val cost = MediaConstants.DUST_UNIT * (3 * clampedStrength + if (fire) 1.0 else 0.125) return SpellAction.Result( Spell(pos, strength, this.fire), - cost.toLongNonzero(), + cost.toNonZeroLong(), listOf(ParticleSpray.burst(pos, strength, 50)) ) } diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt index 0948b22a25..bf7d8009bd 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpFlight.kt @@ -7,7 +7,7 @@ import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.getPlayer import at.petrak.hexcasting.api.casting.getPositiveDouble import at.petrak.hexcasting.api.casting.iota.Iota -import at.petrak.hexcasting.api.casting.toLongNonzero +import at.petrak.hexcasting.api.casting.toNonZeroLong import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.api.pigment.FrozenPigment import at.petrak.hexcasting.api.player.FlightAbility @@ -24,7 +24,6 @@ import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import kotlin.math.max import kotlin.math.roundToInt -import kotlin.math.roundToLong class OpFlight(val type: Type) : SpellAction { override val argc = 2 @@ -38,7 +37,7 @@ class OpFlight(val type: Type) : SpellAction { // One block of radius, or one second of duration, costs 2 dust val costUnit = 2 * MediaConstants.DUST_UNIT - var cost = (theArg * costUnit).toLongNonzero() + var cost = (theArg * costUnit).toNonZeroLong() // Cost for anchorite does not decrease below 1 meter if (type == Type.LimitRange) cost = max(cost, costUnit) diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt index ec626fab5e..64de54a96f 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPotionEffect.kt @@ -36,7 +36,7 @@ class OpPotionEffect( } return SpellAction.Result( Spell(effect, target, duration, potency), - cost.toLongNonzero(), + cost.toNonZeroLong(), listOf(ParticleSpray.cloud(target.position().add(0.0, target.eyeHeight / 2.0, 0.0), 1.0)) ) }