Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Wisps can now consume themselves for free to cleanly self-destruct.
- Moved certain lang keys for type iotas from Hexal into MoreIotas, since MoreIotas is what actually implements type iotas.
- Phase Block now ensures that the caster has permission to break the target block.
- Trade now mishaps if the list contains more than 2 motes, and makes it clear that the list should match a single trade offer.

### Fixed

Expand All @@ -44,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Fixed an error when a playerless caster tries to create a link.
- Fixed an error when passing null or an empty mote to Use Item On.
- Fixed a variety of broken lang keys in the config menu.
- Fixed Trade allowing trades even when not enough input items were present.

## `0.3.1` - 2025-10-30

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ram.talia.hexal.api.casting.mishaps

import at.petrak.hexcasting.api.casting.eval.CastingEnvironment
import at.petrak.hexcasting.api.casting.iota.Iota
import at.petrak.hexcasting.api.casting.mishaps.Mishap
import at.petrak.hexcasting.api.pigment.FrozenPigment
import net.minecraft.network.chat.Component
import net.minecraft.world.item.DyeColor

class MishapBadTrade(val problem: String, val moteList: Iota) : Mishap() {
override fun accentColor(env: CastingEnvironment, errorCtx: Context): FrozenPigment = dyeColor(DyeColor.BROWN)

override fun errorMessage(env: CastingEnvironment, errorCtx: Context): Component = error("bad_trade.$problem", moteList.display())

override fun execute(env: CastingEnvironment, errorCtx: Context, stack: MutableList<Iota>) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import net.minecraft.stats.Stats
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.trading.MerchantOffer
import net.minecraft.world.item.trading.MerchantOffers
import net.minecraft.server.level.ServerPlayer
import ram.talia.hexal.api.HexalAPI
import ram.talia.hexal.api.casting.castables.VarargConstMediaAction
import ram.talia.hexal.api.casting.iota.MoteIota
import ram.talia.hexal.api.casting.mishaps.MishapBadTrade
import ram.talia.hexal.api.casting.mishaps.MishapNoBoundStorage
import ram.talia.hexal.api.casting.mishaps.MishapStorageFull
import ram.talia.hexal.api.config.HexalConfig
Expand All @@ -37,25 +39,20 @@ object OpTradeMote : VarargConstMediaAction {
val toTradeItemIotas = args.getMoteOrMoteList(1, argc)?.map({ listOf(it) }, { it }) ?: return emptyList<Iota>().asActionResult
val tradeIndex = if (args.size == 3) args.getPositiveIntUnder(2, villager.offers.size, argc) else null

if (toTradeItemIotas.isEmpty())
if (toTradeItemIotas.isEmpty() || toTradeItemIotas.size > 2)
throw MishapInvalidIota.of(args[1], if (args.size == 3) 1 else 0, "villager_trade")

if (toTradeItemIotas.size > 1) {
for (i in toTradeItemIotas.indices) {
for (j in toTradeItemIotas.indices) {
if (i != j && toTradeItemIotas[i].itemIndex == toTradeItemIotas[j].itemIndex)
throw MishapInvalidIota.of(args[1], if (args.size == 3) 1 else 0, "mote_duplicated")
}
}
if (toTradeItemIotas.size == 2 && toTradeItemIotas[0].itemIndex == toTradeItemIotas[1].itemIndex) {
throw MishapInvalidIota.of(args[1], if (args.size == 3) 1 else 0, "mote_duplicated")
}

env.assertEntityInRange(villager)

val storage = if (userData.contains(MoteIota.TAG_TEMP_STORAGE))
userData.getUUID(MoteIota.TAG_TEMP_STORAGE)
else
env.caster?.let { MediafiedItemManager.getBoundStorage(it) }
?: throw MishapNoBoundStorage()
(env.castingEntity as? ServerPlayer)?.let { MediafiedItemManager.getBoundStorage(it) }
?: throw MishapNoBoundStorage()
if (!MediafiedItemManager.isStorageLoaded(storage))
throw MishapNoBoundStorage("storage_unloaded")

Expand All @@ -67,8 +64,8 @@ object OpTradeMote : VarargConstMediaAction {
if (villager.offers.isEmpty())
return emptyList<Iota>().asActionResult

env.caster?.let { villager.updateSpecialPrices(it) }
villager.tradingPlayer = env.caster
(env.castingEntity as? ServerPlayer)?.let { villager.updateSpecialPrices(it) }
villager.tradingPlayer = env.castingEntity as? ServerPlayer

var outRecord: ItemRecord? = null

Expand All @@ -82,12 +79,19 @@ object OpTradeMote : VarargConstMediaAction {
val merchantoffer = if (tradeIndex != null)
offers.getRecipeFor(toTrade0, toTrade1, tradeIndex) ?: offers.getRecipeFor(toTrade1, toTrade0, tradeIndex) ?: break
else getFirstMatchingInStockOffer(offers, toTrade0, toTrade1) ?: break
if (merchantoffer.isOutOfStock)
break
if (outRecord == null) { // if this is the first attempt, mishap if the trade isn't possible
if (!enoughToPay(merchantoffer, toTradeItemIotas))
throw MishapBadTrade("not_enough_payment", args[1])
if (merchantoffer.isOutOfStock)
throw MishapBadTrade("out_of_stock", args[1])
} else { // if we've already traded at least once, just break the loop if the trade isn't possible
if (!enoughToPay(merchantoffer, toTradeItemIotas) || merchantoffer.isOutOfStock)
break
}

if (merchantoffer.take(toTrade0, toTrade1) || merchantoffer.take(toTrade1, toTrade0)) {
villager.notifyTrade(merchantoffer)
env.caster?.awardStat(Stats.TRADED_WITH_VILLAGER)
(env.castingEntity as? ServerPlayer)?.awardStat(Stats.TRADED_WITH_VILLAGER)

if (outRecord == null)
outRecord = ItemRecord(merchantoffer.result)
Expand All @@ -109,6 +113,12 @@ object OpTradeMote : VarargConstMediaAction {
return outRecord?.let { record -> MoteIota.makeIfStorageLoaded(record, storage)?.let{ listOf(it) } } ?: null.asActionResult
}

private fun enoughToPay(offer: MerchantOffer, toTradeItemIotas: List<MoteIota>): Boolean {
val enoughA = (toTradeItemIotas.getOrNull(0)?.count ?: 0) >= offer.costA.count
val enoughB = (toTradeItemIotas.getOrNull(1)?.count ?: 0) >= offer.costB.count
return enoughA && enoughB
}

private fun getFirstMatchingInStockOffer(offers: MerchantOffers, toTrade0: ItemStack, toTrade1: ItemStack): MerchantOffer? {
for (index in 0 until offers.size) {
val offer = offers.getRecipeFor(toTrade0, toTrade1, index) ?: offers.getRecipeFor(toTrade1, toTrade0, index) ?: continue
Expand Down
5 changes: 4 additions & 1 deletion Common/src/main/resources/assets/hexal/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"hexcasting.mishap.storage_unloaded": "Bound Mote Nexus is unloaded.",
"hexcasting.mishap.excessive_reproduction": "Wisp %s tried to reproduce excessively; only 1 child per tick.",
"hexcasting.mishap.illegal_interworld_iota": "Attempted to bring iota %s interworld, this type of iota cannot be added to the Everbook.",
"hexcasting.mishap.entity_gate_with_no_entity" : "%s is anchored to an entity that is unloaded or doesn't exist.",
"hexcasting.mishap.entity_gate_with_no_entity": "%s is anchored to an entity that is unloaded or doesn't exist.",
"hexcasting.mishap.bad_trade.not_enough_payment": "%s is not enough to pay for the selected trade.",
"hexcasting.mishap.bad_trade.out_of_stock": "The selected trade is out of stock.",

"hexcasting.mishap.bad_block.phaseable": "a phaseable block",

Expand All @@ -42,6 +44,7 @@
"hexcasting.mishap.invalid_value.cant_combine_motes": "two motes that can be combined",
"hexcasting.mishap.invalid_value.mote_empty": "a non-empty mote",
"hexcasting.mishap.invalid_value.mote_not_size_one": "a mote with no NBT data or of size one",
"hexcasting.mishap.invalid_value.villager_trade": "a list of motes matching a single trade offer",
"hexcasting.mishap.invalid_value.gate.offset": "an offset of at most %f blocks",

"hexcasting.iota.hexal:gate": "Gate",
Expand Down
Loading