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 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..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) } @@ -299,6 +300,18 @@ 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.toNonZeroLong(): Long { + val result = this.roundToLong() + if (result == 0L) + return if (this >= 0) 1L else -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/api/casting/eval/CastingEnvironment.java b/Common/src/main/java/at/petrak/hexcasting/api/casting/eval/CastingEnvironment.java index d12221cd5a..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 @@ -1,8 +1,8 @@ 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; 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; @@ -12,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; @@ -269,7 +268,8 @@ public boolean isEnlightened() { * positive. */ public long extractMedia(long cost, boolean simulate) { - cost = (long) (cost * costModifier); + if (cost == 0) return 0; + 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 292f404eba..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,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.toNonZeroLong 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).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 2882984ebd..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,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.toNonZeroLong 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).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 0bed07989c..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,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.toNonZeroLong 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.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 0bf708bce6..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,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.toNonZeroLong import at.petrak.hexcasting.api.misc.MediaConstants import at.petrak.hexcasting.api.pigment.FrozenPigment import at.petrak.hexcasting.api.player.FlightAbility @@ -23,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 @@ -37,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).roundToLong() + 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 6f7dc5a05e..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.toLong(), + cost.toNonZeroLong(), 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) }