diff options
author | David Cole <40234707+DavidArthurCole@users.noreply.github.com> | 2024-09-10 15:53:30 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-10 21:53:30 +0200 |
commit | 2f6121bc209fb28e1d9f12fe2d10bfacfb12836d (patch) | |
tree | 1dba111a8d401d596e71c2a949e0c0986cbac76b | |
parent | e2795490be75dd6e4ddbfa8896d99db956719d3f (diff) | |
download | skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.tar.gz skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.tar.bz2 skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.zip |
Feature: Corpse Tracker (#2306)
13 files changed, 513 insertions, 5 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt index edfdb2d7a..7a17c1462 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -58,6 +58,7 @@ import at.hannibal2.skyhanni.features.garden.visitor.GardenVisitorDropStatistics import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker import at.hannibal2.skyhanni.features.mining.KingTalismanHelper import at.hannibal2.skyhanni.features.mining.MineshaftPityDisplay +import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseTracker import at.hannibal2.skyhanni.features.mining.fossilexcavator.ExcavatorProfitTracker import at.hannibal2.skyhanni.features.mining.powdertracker.PowderTracker import at.hannibal2.skyhanni.features.minion.MinionFeatures @@ -219,6 +220,7 @@ object Commands { registerCommand("shresetghostcounter", "Resets the ghost counter") { GhostUtil.reset() } registerCommand("shresetpowdertracker", "Resets the Powder Tracker") { PowderTracker.resetCommand() } registerCommand("shresetdicertracker", "Resets the Dicer Drop Tracker") { DicerRngDropTracker.resetCommand() } + registerCommand("shresetcorpsetracker", "Resets the Glacite Mineshaft Corpse Tracker") { CorpseTracker.resetCommand() } registerCommand( "shresetendernodetracker", "Resets the Ender Node Tracker", diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java new file mode 100644 index 000000000..61d3b1959 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java @@ -0,0 +1,24 @@ +package at.hannibal2.skyhanni.config.features.mining; + +import at.hannibal2.skyhanni.config.core.config.Position; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigLink; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; + +public class CorpseTrackerConfig { + + @Expose + @ConfigOption(name = "Enabled", desc = "Enable the Corpse Tracker overlay for Glacite Mineshafts.") + @ConfigEditorBoolean + public boolean enabled = false; + + @Expose + @ConfigOption(name = "Only when in Mineshaft", desc = "Only show the overlay while in a Glacite Mineshaft.") + @ConfigEditorBoolean + public boolean onlyInMineshaft = false; + + @Expose + @ConfigLink(owner = CorpseTrackerConfig.class, field = "enabled") + public Position position = new Position(-274, 0, false, true); +} diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java index b89a7d3d0..723cf9b90 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java @@ -18,6 +18,11 @@ public class GlaciteMineshaftConfig { public CorpseLocatorConfig corpseLocator = new CorpseLocatorConfig(); @Expose + @ConfigOption(name = "Corpse Tracker", desc = "") + @Accordion + public CorpseTrackerConfig corpseTracker = new CorpseTrackerConfig(); + + @Expose @ConfigOption(name = "Share Waypoint Location", desc = "Share the location of the nearest waypoint upon key press.\n" + "§eYou can share the location even if it has already been shared!") @ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE) diff --git a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java index d622d317a..5938eebdb 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -33,6 +33,7 @@ import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactor import at.hannibal2.skyhanni.features.inventory.wardrobe.WardrobeAPI; import at.hannibal2.skyhanni.features.mining.MineshaftPityDisplay; import at.hannibal2.skyhanni.features.mining.fossilexcavator.ExcavatorProfitTracker; +import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseTracker; import at.hannibal2.skyhanni.features.mining.powdertracker.PowderTracker; import at.hannibal2.skyhanni.features.misc.trevor.TrevorTracker; import at.hannibal2.skyhanni.features.rift.area.westvillage.VerminTracker; @@ -564,6 +565,9 @@ public class ProfileSpecificStorage { @Expose public List<MineshaftPityDisplay.PityData> blocksBroken = new ArrayList<>(); + + @Expose + public CorpseTracker.BucketData corpseProfitTracker = new CorpseTracker.BucketData(); } } diff --git a/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt index ea1e813d0..9e38d8e95 100644 --- a/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt +++ b/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt @@ -1,6 +1,6 @@ package at.hannibal2.skyhanni.events.mining import at.hannibal2.skyhanni.events.LorenzEvent -import at.hannibal2.skyhanni.features.mining.mineshaft.CorpseType +import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseType class CorpseLootedEvent(val corpseType: CorpseType, val loot: List<Pair<String, Int>>) : LorenzEvent() diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt index e49ca462f..c8ee81288 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt @@ -1,4 +1,4 @@ -package at.hannibal2.skyhanni.features.mining.mineshaft +package at.hannibal2.skyhanni.features.mining.glacitemineshaft import at.hannibal2.skyhanni.data.IslandType import at.hannibal2.skyhanni.events.LorenzChatEvent diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt new file mode 100644 index 000000000..77aabc1bd --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt @@ -0,0 +1,142 @@ +package at.hannibal2.skyhanni.features.mining.glacitemineshaft + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.IslandChangeEvent +import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.CollectionUtils.addOrPut +import at.hannibal2.skyhanni.utils.CollectionUtils.addSearchString +import at.hannibal2.skyhanni.utils.CollectionUtils.sumAllValues +import at.hannibal2.skyhanni.utils.ItemPriceUtils.getPrice +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.renderables.Searchable +import at.hannibal2.skyhanni.utils.renderables.toSearchable +import at.hannibal2.skyhanni.utils.tracker.BucketedItemTrackerData +import at.hannibal2.skyhanni.utils.tracker.ItemTrackerData.TrackedItem +import at.hannibal2.skyhanni.utils.tracker.SkyHanniBucketedItemTracker +import com.google.gson.annotations.Expose +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.EnumMap + +@SkyHanniModule +object CorpseTracker { + private val config get() = SkyHanniMod.feature.mining.glaciteMineshaft.corpseTracker + + private val tracker = SkyHanniBucketedItemTracker<CorpseType, BucketData>( + "Corpse Tracker", + { BucketData() }, + { it.mining.mineshaft.corpseProfitTracker }, + { drawDisplay(it) } + ) + + class BucketData : BucketedItemTrackerData<CorpseType>() { + override fun resetItems() { + corpsesLooted = EnumMap(CorpseType::class.java) + } + + override fun getDescription(timesGained: Long): List<String> { + val divisor = 1.coerceAtLeast(getSelectedBucket()?.let { corpsesLooted[it]?.toInt() } ?: corpsesLooted.sumAllValues().toInt()) + val percentage = timesGained.toDouble() / divisor + val dropRate = LorenzUtils.formatPercentage(percentage.coerceAtMost(1.0)) + return listOf( + "§7Dropped §e${timesGained.addSeparators()} §7times.", + "§7Your drop rate: §c$dropRate.", + ) + } + + override fun getCoinName(bucket: CorpseType?, item: TrackedItem) = "<no coins>" + override fun getCoinDescription(bucket: CorpseType?, item: TrackedItem): List<String> = listOf("<no coins>") + + @Expose + var corpsesLooted: MutableMap<CorpseType, Long> = EnumMap(CorpseType::class.java) + + fun getCorpseCount(): Long = getSelectedBucket()?.let { corpsesLooted[it] } ?: corpsesLooted.values.sum() + } + + private fun addLootedCorpse(type: CorpseType) = tracker.modify { it.corpsesLooted.addOrPut(type, 1) } + + @SubscribeEvent + fun onCorpseLoot(event: CorpseLootedEvent) { + addLootedCorpse(event.corpseType) + for ((itemName, amount) in event.loot) { + NEUInternalName.fromItemNameOrNull(itemName)?.let { item -> + tracker.modify { + it.addItem(event.corpseType, item, amount) + } + } + } + } + + private fun drawDisplay(bucketData: BucketData): List<Searchable> = buildList { + addSearchString("§b§lMineshaft Corpse Profit Tracker") + tracker.addBucketSelector(this, bucketData, "Corpse Type") + + if (bucketData.getCorpseCount() == 0L) return@buildList + + var profit = tracker.drawItems(bucketData, { true }, this) + val applicableKeys: List<CorpseType> = bucketData.getSelectedBucket()?.let { listOf(it) } + ?: enumValues<CorpseType>().toList().filter { bucketData.corpsesLooted[it] != null } + var totalKeyCost = 0.0 + var totalKeyCount = 0 + val keyCostStrings = buildList { + applicableKeys.forEach { keyData -> + keyData.key?.let { key -> + val keyName = key.itemName + val price = key.getPrice() + val count = bucketData.corpsesLooted[keyData] ?: 0 + val totalPrice = price * count + if (totalPrice > 0) { + profit -= totalPrice + totalKeyCost += totalPrice + totalKeyCount += count.toInt() + add("§7${count}x $keyName§7: §c-${totalPrice.shortFormat()}") + } + } + } + } + + if (totalKeyCount > 0) { + val specificKeyFormat = if (applicableKeys.count() == 1) applicableKeys.first().key!!.itemName else "§eCorpse Keys" + val keyFormat = "§7${totalKeyCount}x $specificKeyFormat§7: §c-${totalKeyCost.shortFormat()}" + add( + if (applicableKeys.count() == 1) Renderable.string(keyFormat).toSearchable() + else Renderable.hoverTips( + keyFormat, + keyCostStrings, + ).toSearchable(), + ) + } + + add(tracker.addTotalProfit(profit, bucketData.getCorpseCount(), "loot")) + + tracker.addPriceFromButton(this) + } + + @SubscribeEvent + fun onRenderOverlay(event: GuiRenderEvent) { + if (!isEnabled()) return + tracker.renderDisplay(config.position) + } + + @SubscribeEvent + fun onIslandChange(event: IslandChangeEvent) { + if (event.newIsland == IslandType.MINESHAFT || event.newIsland == IslandType.DWARVEN_MINES) { + tracker.firstUpdate() + } + } + + fun resetCommand() { + tracker.resetCommand() + } + + fun isEnabled() = + config.enabled && IslandType.DWARVEN_MINES.isInIsland() && (!config.onlyInMineshaft || IslandType.MINESHAFT.isInIsland()) +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt index 7ddfca867..42567d5f3 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt @@ -1,4 +1,4 @@ -package at.hannibal2.skyhanni.features.mining.mineshaft +package at.hannibal2.skyhanni.features.mining.glacitemineshaft import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName @@ -10,4 +10,6 @@ enum class CorpseType(val displayName: String, private val keyName: String? = nu ; val key by lazy { keyName?.asInternalName() } + + override fun toString(): String = displayName } diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt index 102391b87..eed5f2b9d 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt @@ -1,4 +1,4 @@ -package at.hannibal2.skyhanni.features.mining.mineshaft +package at.hannibal2.skyhanni.features.mining.glacitemineshaft import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent diff --git a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt index 991b1a53e..898466ce4 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt @@ -336,7 +336,7 @@ object CollectionUtils { for (entry in universe) { val display = getName(entry) if (isCurrent(entry)) { - addString("§a[$display]") + addString("§a[$display§a]") } else { addString("§e[") add( diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt new file mode 100644 index 000000000..3bedaf8a4 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt @@ -0,0 +1,101 @@ +package at.hannibal2.skyhanni.utils.tracker + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.tracker.ItemTrackerData.TrackedItem +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + +abstract class BucketedItemTrackerData<E : Enum<E>> : TrackerData() { + + private val config get() = SkyHanniMod.feature.misc.tracker + + abstract fun resetItems() + + abstract fun getDescription(timesGained: Long): List<String> + + abstract fun getCoinName(bucket: E?, item: TrackedItem): String + + abstract fun getCoinDescription(bucket: E?, item: TrackedItem): List<String> + + open fun getCustomPricePer(internalName: NEUInternalName) = SkyHanniTracker.getPricePer(internalName) + + override fun reset() { + bucketedItems.clear() + selectedBucket = null + resetItems() + } + + fun addItem(bucket: E, internalName: NEUInternalName, stackSize: Int) { + val bucketMap = bucketedItems.getOrPut(bucket) { HashMap() } + val item = bucketMap.getOrPut(internalName) { TrackedItem() } + + item.timesGained++ + item.totalAmount += stackSize + item.lastTimeUpdated = SimpleTimeMark.now() + } + + fun removeItem(bucket: E?, internalName: NEUInternalName) { + bucket?.let { + bucketedItems[bucket]?.remove(internalName) + } ?: bucketedItems.forEach { + it.value.remove(internalName) + } + } + + fun toggleItemHide(bucket: E?, internalName: NEUInternalName) { + bucket?.let { + bucketedItems[bucket]?.get(internalName)?.let { it.hidden = !it.hidden } + } ?: bucketedItems.forEach { + it.value[internalName]?.hidden = !it.value[internalName]?.hidden!! + } + } + + private val buckets: Array<E> by lazy { + @Suppress("UNCHECKED_CAST") + selectedBucket?.javaClass?.enumConstants + ?: (this.javaClass.genericSuperclass as? ParameterizedTypeImpl)?.actualTypeArguments?.firstOrNull()?.let { type -> + (type as? Class<E>)?.enumConstants + } ?: ErrorManager.skyHanniError( + "Unable to retrieve enum constants for E in BucketedItemTrackerData", + "selectedBucket" to selectedBucket, + "dataClass" to this.javaClass.superclass.name, + ) + } + + private var selectedBucket: E? = null + private var bucketedItems: MutableMap<E, MutableMap<NEUInternalName, TrackedItem>> = HashMap() + + private fun getBucket(bucket: E): MutableMap<NEUInternalName, TrackedItem> = bucketedItems[bucket]?.toMutableMap() ?: HashMap() + private fun getPoppedBuckets(): MutableList<E> = (bucketedItems.toMutableMap().filter { it.value.isNotEmpty() }.keys).toMutableList() + fun getItemsProp(): MutableMap<NEUInternalName, TrackedItem> = getSelectedBucket()?.let { getBucket(it) } ?: flattenBuckets() + fun getSelectedBucket() = selectedBucket + fun selectNextSequentialBucket() { + // Move to the next ordinal, or wrap to null if at the last value + val nextOrdinal = selectedBucket?.let { it.ordinal + 1 } // Only calculate if selectedBucket is non-null + selectedBucket = when { + selectedBucket == null -> buckets.first() // If selectedBucket is null, start with the first enum + nextOrdinal != null && nextOrdinal >= buckets.size -> null // Wrap to null if we've reached the end + nextOrdinal != null -> buckets[nextOrdinal] // Move to the next enum value + else -> selectedBucket // Fallback, shouldn't happen + } + } + + private fun flattenBuckets(): MutableMap<NEUInternalName, TrackedItem> { + val flatMap: MutableMap<NEUInternalName, TrackedItem> = HashMap() + getPoppedBuckets().distinct().forEach { bucket -> + getBucket(bucket).filter { !it.value.hidden }.entries.distinctBy { it.key }.forEach { (key, value) -> + flatMap.merge(key, value) { existing, new -> + existing.copy( + hidden = false, + totalAmount = existing.totalAmount + new.totalAmount, + timesGained = existing.timesGained + new.timesGained, + lastTimeUpdated = maxOf(existing.lastTimeUpdated, new.lastTimeUpdated), + ) + } + } + } + return flatMap.toMutableMap() + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt index bce63e2d1..a3976e44f 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt @@ -52,5 +52,19 @@ abstract class ItemTrackerData : TrackerData() { var hidden = false var lastTimeUpdated = SimpleTimeMark.farPast() + + fun copy( + timesGained: Long = this.timesGained, + totalAmount: Long = this.totalAmount, + hidden: Boolean = this.hidden, + lastTimeUpdated: SimpleTimeMark = this.lastTimeUpdated, + ): TrackedItem { + val copy = TrackedItem() + copy.timesGained = timesGained + copy.totalAmount = totalAmount + copy.hidden = hidden + copy.lastTimeUpdated = lastTimeUpdated + return copy + } } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt new file mode 100644 index 000000000..dcbab2b1b --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt @@ -0,0 +1,214 @@ +package at.hannibal2.skyhanni.utils.tracker + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage +import at.hannibal2.skyhanni.data.SlayerAPI +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.CollectionUtils.addSearchableSelector +import at.hannibal2.skyhanni.utils.CollectionUtils.sortedDesc +import at.hannibal2.skyhanni.utils.ItemPriceSource +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.KeyboardManager +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.renderables.RenderableUtils.addButton +import at.hannibal2.skyhanni.utils.renderables.Searchable +import at.hannibal2.skyhanni.utils.renderables.toSearchable +import kotlin.time.Duration.Companion.seconds + +class SkyHanniBucketedItemTracker<E : Enum<E>, BucketedData : BucketedItemTrackerData<E>>( + name: String, + createNewSession: () -> BucketedData, + getStorage: (ProfileSpecificStorage) -> BucketedData, + drawDisplay: (BucketedData) -> List<Searchable>, + vararg extraStorage: Pair<DisplayMode, (ProfileSpecificStorage) -> BucketedData>, +) : SkyHanniTracker<BucketedData>(name, createNewSession, getStorage, *extraStorage, drawDisplay = drawDisplay) { + + companion object { + val SKYBLOCK_COIN = NEUInternalName.SKYBLOCK_COIN + } + + fun addCoins(bucket: E, coins: Int) { + addItem(bucket, SKYBLOCK_COIN, coins) + } + + fun addItem(bucket: E, internalName: NEUInternalName, amount: Int) { + modify { + it.addItem(bucket, internalName, amount) + } + getSharedTracker()?.let { + val hidden = it.get(DisplayMode.TOTAL).getItemsProp()[internalName]!!.hidden + it.get(DisplayMode.SESSION).getItemsProp()[internalName]!!.hidden = hidden + } + + val (itemName, price) = SlayerAPI.getItemNameAndPrice(internalName, amount) + if (config.warnings.chat && price >= config.warnings.minimumChat) { + ChatUtils.chat("§a+Tracker Drop§7: §r$itemName") + } + if (config.warnings.title && price >= config.warnings.minimumTitle) { + LorenzUtils.sendTitle("§a+ $itemName", 5.seconds) + } + } + + fun addPriceFromButton(lists: MutableList<Searchable>) { + if (isInventoryOpen()) { + lists.addSearchableSelector<ItemPriceSource>( + "", + getName = { type -> type.sellName }, + isCurrent = { it?.ordinal == config.priceSource.ordinal }, // todo avoid ordinal + onChange = { + config.priceSource = it?.let { ItemPriceSource.entries[it.ordinal] } // todo avoid ordinal + update() + }, + ) + } + } + + fun addBucketSelector( + lists: MutableList<Searchable>, + data: BucketedData, + sourceStringPrefix: String, + nullBucketLabel: String = "All", + ) { + if (isInventoryOpen()) { + lists.addButton( + prefix = "§7$sourceStringPrefix: ", + getName = data.getSelectedBucket()?.toString() ?: nullBucketLabel, + onChange = { + data.selectNextSequentialBucket() + update() + }, + ) + } + } + + fun drawItems( + data: BucketedData, + filter: (NEUInternalName) -> Boolean, + lists: MutableList<Searchable>, + ): Double { + var profit = 0.0 + val dataItems = data.getItemsProp() + val items = mutableMapOf<NEUInternalName, Long>() + for ((internalName, itemProfit) in dataItems) { + if (!filter(internalName)) continue + + val amount = itemProfit.totalAmount + val pricePer = + if (internalName == SKYBLOCK_COIN) 1.0 else data.getCustomPricePer(internalName) + val price = (pricePer * amount).toLong() + val hidden = itemProfit.hidden + + if (isInventoryOpen() || !hidden) { + items[internalName] = price + } + if (!hidden || !config.excludeHiddenItemsInPrice) { + profit += price + } + } + + val limitList = config.hideCheapItems + var pos = 0 + val hiddenItemTexts = mutableListOf<String>() + for ((internalName, price) in items.sortedDesc()) { + val itemProfit = data.getItemsProp()[internalName] ?: error("Item not found for $internalName") + + val amount = itemProfit.totalAmount + val displayAmount = if (internalName == SKYBLOCK_COIN) itemProfit.timesGained else amount + + val cleanName = if (internalName == SKYBLOCK_COIN) { + data.getCoinName(data.getSelectedBucket(), itemProfit) + } else { + internalName.itemName + } + + val priceFormat = price.shortFormat() + val hidden = itemProfit.hidden + val newDrop = itemProfit.lastTimeUpdated.passedSince() < 10.seconds && config.showRecentDrops + val numberColor = if (newDrop) "§a§l" else "§7" + + var displayName = if (hidden) { + "§8§m" + cleanName.removeColor(keepFormatting = true).replace("§r", "") + } else cleanName + displayName = " $numberColor${displayAmount.addSeparators()}x $displayName§7: §6$priceFormat" + + pos++ + if (limitList.enabled.get()) { + if (pos > limitList.alwaysShowBest.get()) { + if (price < limitList.minPrice.get() * 1000) { + hiddenItemTexts += displayName + continue + } + } + } + + val lore = buildLore(data, itemProfit, hidden, newDrop, internalName) + val renderable = if (isInventoryOpen()) Renderable.clickAndHover( + displayName, lore, + onClick = { + if (KeyboardManager.isModifierKeyDown()) { + data.removeItem(data.getSelectedBucket(), internalName) + ChatUtils.chat("Removed $cleanName §efrom $name${if (data.getSelectedBucket() != null) " (${data.getSelectedBucket()})" else ""}") + } else { + modify { + it.toggleItemHide(data.getSelectedBucket(), internalName) + } + } + update() + + }, + ) else Renderable.string(displayName) + + lists.add(renderable.toSearchable(name)) + } + if (hiddenItemTexts.size > 0) { + val text = Renderable.hoverTips(" §7${hiddenItemTexts.size} cheap items are hidden.", hiddenItemTexts).toSearchable() + lists.add(text) + } + + return profit + } + + private fun buildLore( + data: BucketedData, + item: ItemTrackerData.TrackedItem, + hidden: Boolean, + newDrop: Boolean, + internalName: NEUInternalName, + ) = buildList { + if (internalName == SKYBLOCK_COIN) { + addAll(data.getCoinDescription(data.getSelectedBucket(), item)) + } else { + addAll(data.getDescription(item.timesGained)) + } + add("") + if (newDrop) { + add("§aYou obtained this item recently.") + add("") + } + add("§eClick to " + (if (hidden) "show" else "hide") + "!") + add("§eControl + Click to remove this item!") + if (SkyHanniMod.feature.dev.debug.enabled) { + add("") + add("§7${internalName}") + } + } + + fun addTotalProfit(profit: Double, totalAmount: Long, action: String): Searchable { + val profitFormat = profit.toLong().addSeparators() + val profitPrefix = if (profit < 0) "§c" else "§6" + + val tips = if (totalAmount > 0) { + val profitPerCatch = profit / totalAmount + val profitPerCatchFormat = profitPerCatch.shortFormat() + listOf("§7Profit per $action: $profitPrefix$profitPerCatchFormat") + } else emptyList() + + val text = "§eTotal Profit: $profitPrefix$profitFormat coins" + return Renderable.hoverTips(text, tips).toSearchable() + } +} |