aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/events
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/features/events')
-rw-r--r--src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt224
-rw-r--r--src/main/kotlin/features/events/carnival/CarnivalFeatures.kt17
-rw-r--r--src/main/kotlin/features/events/carnival/MinesweeperHelper.kt276
3 files changed, 517 insertions, 0 deletions
diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
new file mode 100644
index 0000000..8926a95
--- /dev/null
+++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
@@ -0,0 +1,224 @@
+
+package moe.nea.firmament.features.events.anniversity
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.jarvis.api.Point
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.entity.passive.PigEntity
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityInteractionEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.rei.SBItemEntryDefinition
+import moe.nea.firmament.repo.ItemNameLookup
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.parseShortNumber
+import moe.nea.firmament.util.useMatch
+
+object AnniversaryFeatures : FirmamentFeature {
+ override val identifier: String
+ get() = "anniversary"
+
+ object TConfig : ManagedConfig(identifier) {
+ val enableShinyPigTracker by toggle("shiny-pigs") {true}
+ val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ data class ClickedPig(
+ val clickedAt: TimeMark,
+ val startLocation: BlockPos,
+ val pigEntity: PigEntity
+ ) {
+ @Bind("timeLeft")
+ fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration
+ }
+
+ val clickedPigs = ObservableList<ClickedPig>(mutableListOf())
+ var lastClickedPig: PigEntity? = null
+
+ val pigDuration = 90.seconds
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration }
+ }
+
+ val pattern = "SHINY! You extracted (?<reward>.*) from the piglet's orb!".toPattern()
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ if(!TConfig.enableShinyPigTracker)return
+ if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") {
+ val pig = lastClickedPig ?: return
+ // TODO: store proper location based on the orb location, maybe
+ val startLocation = pig.blockPos ?: return
+ clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig))
+ lastClickedPig = null
+ }
+ if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") {
+ val player = MC.player ?: return
+ val lowest =
+ clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return
+ clickedPigs.remove(lowest)
+ }
+ pattern.useMatch(event.unformattedString) {
+ val reward = group("reward")
+ val parsedReward = parseReward(reward)
+ addReward(parsedReward)
+ PigCooldown.rewards.atOnce {
+ PigCooldown.rewards.clear()
+ rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) }
+ }
+ }
+ }
+
+ fun addReward(reward: Reward) {
+ val it = rewards.listIterator()
+ while (it.hasNext()) {
+ val merged = reward.mergeWith(it.next()) ?: continue
+ it.set(merged)
+ return
+ }
+ rewards.add(reward)
+ }
+
+ val rewards = mutableListOf<Reward>()
+
+ fun <T> ObservableList<T>.atOnce(block: () -> Unit) {
+ val oldObserver = observer
+ observer = null
+ block()
+ observer = oldObserver
+ update()
+ }
+
+ sealed interface Reward {
+ fun mergeWith(other: Reward): Reward?
+ data class EXP(val amount: Double, val skill: String) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is EXP && other.skill == skill)
+ return EXP(amount + other.amount, skill)
+ return null
+ }
+ }
+
+ data class Coins(val amount: Double) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is Coins)
+ return Coins(other.amount + amount)
+ return null
+ }
+ }
+
+ data class Items(val amount: Int, val item: SkyblockId) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is Items && other.item == item)
+ return Items(amount + other.amount, item)
+ return null
+ }
+ }
+
+ data class Unknown(val text: String) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ return null
+ }
+ }
+ }
+
+ val expReward = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern()
+ val coinReward = "\\+(?<amount>$SHORT_NUMBER_FORMAT) coins".toPattern()
+ val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern()
+ fun parseReward(string: String): Reward {
+ expReward.useMatch<Unit>(string) {
+ val exp = parseShortNumber(group("exp"))
+ val kind = group("kind")
+ return Reward.EXP(exp, kind)
+ }
+ coinReward.useMatch<Unit>(string) {
+ val coins = parseShortNumber(group("amount"))
+ return Reward.Coins(coins)
+ }
+ itemReward.useMatch(string) {
+ val amount = group("amount")?.toIntOrNull() ?: 1
+ val name = group("name")
+ val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch
+ return Reward.Items(amount, item)
+ }
+ return Reward.Unknown(string)
+ }
+
+ @Subscribe
+ fun onWorldClear(event: WorldReadyEvent) {
+ lastClickedPig = null
+ clickedPigs.clear()
+ }
+
+ @Subscribe
+ fun onEntityClick(event: EntityInteractionEvent) {
+ if (event.entity is PigEntity) {
+ lastClickedPig = event.entity
+ }
+ }
+
+ @Subscribe
+ fun init(event: WorldReadyEvent) {
+ PigCooldown.forceInit()
+ }
+
+ object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) {
+ override fun shouldRender(): Boolean {
+ return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker
+ }
+
+ @Bind("pigs")
+ fun getPigs() = clickedPigs
+
+ class DisplayReward(val backedBy: Reward) {
+ @Bind
+ fun count(): String {
+ return when (backedBy) {
+ is Reward.Coins -> backedBy.amount
+ is Reward.EXP -> backedBy.amount
+ is Reward.Items -> backedBy.amount
+ is Reward.Unknown -> 0
+ }.toString()
+ }
+
+ val itemStack = if (backedBy is Reward.Items) {
+ SBItemEntryDefinition.getEntry(backedBy.item, backedBy.amount)
+ } else {
+ SBItemEntryDefinition.getEntry(SkyblockId.NULL)
+ }
+
+ @Bind
+ fun name(): String {
+ return when (backedBy) {
+ is Reward.Coins -> "Coins"
+ is Reward.EXP -> backedBy.skill
+ is Reward.Items -> itemStack.value.asItemStack().name.string
+ is Reward.Unknown -> backedBy.text
+ }
+ }
+
+ @Bind
+ fun isKnown() = backedBy !is Reward.Unknown
+ }
+
+ @get:Bind("rewards")
+ val rewards = ObservableList<DisplayReward>(mutableListOf())
+
+ }
+
+}
diff --git a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
new file mode 100644
index 0000000..1e6d97a
--- /dev/null
+++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
@@ -0,0 +1,17 @@
+
+package moe.nea.firmament.features.events.carnival
+
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+
+object CarnivalFeatures : FirmamentFeature {
+ object TConfig : ManagedConfig(identifier) {
+ val enableBombSolver by toggle("bombs-solver") { true }
+ val displayTutorials by toggle("tutorials") { true }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+ override val identifier: String
+ get() = "carnival"
+}
diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
new file mode 100644
index 0000000..06caf86
--- /dev/null
+++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
@@ -0,0 +1,276 @@
+
+package moe.nea.firmament.features.events.carnival
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import java.util.UUID
+import net.minecraft.block.Blocks
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.text.ClickEvent
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import net.minecraft.world.WorldAccess
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.AttackBlockEvent
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.debug.DebugLogger
+import moe.nea.firmament.util.LegacyFormattingCode
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.item.createSkullItem
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.setSkyBlockFirmamentUiId
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.useMatch
+
+object MinesweeperHelper {
+ val sandBoxLow = BlockPos(-112, 72, -11)
+ val sandBoxHigh = BlockPos(-106, 72, -5)
+ val boardSize = Pair(sandBoxHigh.x - sandBoxLow.x, sandBoxHigh.z - sandBoxLow.z)
+
+ val gameStartMessage = "[NPC] Carnival Pirateman: Good luck, matey!"
+ val gameEndMessage = "Fruit Digging"
+ val bombPattern = "MINES! There (are|is) (?<bombCount>[0-8]) bombs? hidden nearby\\.".toPattern()
+ val startGameQuestion = "[NPC] Carnival Pirateman: Would ye like to do some Fruit Digging?"
+
+
+ enum class Piece(
+ @get:Bind("fruitName")
+ val fruitName: String,
+ val points: Int,
+ val specialAbility: String,
+ val totalPerBoard: Int,
+ val textureHash: String,
+ val fruitColor: LegacyFormattingCode,
+ ) {
+ COCONUT("Coconut",
+ 200,
+ "Prevents a bomb from exploding next turn",
+ 3,
+ "10ceb1455b471d016a9f06d25f6e468df9fcf223e2c1e4795b16e84fcca264ee",
+ LegacyFormattingCode.DARK_PURPLE),
+ APPLE("Apple",
+ 100,
+ "Gains 100 points for each apple dug up",
+ 8,
+ "17ea278d6225c447c5943d652798d0bbbd1418434ce8c54c54fdac79994ddd6c",
+ LegacyFormattingCode.GREEN),
+ WATERMELON("Watermelon",
+ 100,
+ "Blows up an adjacent fruit for half the points",
+ 4,
+ "efe4ef83baf105e8dee6cf03dfe7407f1911b3b9952c891ae34139560f2931d6",
+ LegacyFormattingCode.DARK_BLUE),
+ DURIAN("Durian",
+ 800,
+ "Halves the points earned in the next turn",
+ 2,
+ "ac268d36c2c6047ffeec00124096376b56dbb4d756a55329363a1b27fcd659cd",
+ LegacyFormattingCode.DARK_PURPLE),
+ MANGO("Mango",
+ 300,
+ "Just an ordinary fruit",
+ 10,
+ "f363a62126a35537f8189343a22660de75e810c6ac004a7d3da65f1c040a839",
+ LegacyFormattingCode.GREEN),
+ DRAGON_FRUIT("Dragonfruit",
+ 1200,
+ "Halves the points earned in the next turn",
+ 1,
+ "3cc761bcb0579763d9b8ab6b7b96fa77eb6d9605a804d838fec39e7b25f95591",
+ LegacyFormattingCode.LIGHT_PURPLE),
+ POMEGRANATE("Pomegranate",
+ 200,
+ "Grants an extra 50% more points in the next turn",
+ 4,
+ "40824d18079042d5769f264f44394b95b9b99ce689688cc10c9eec3f882ccc08",
+ LegacyFormattingCode.DARK_BLUE),
+ CHERRY("Cherry",
+ 200,
+ "The second cherry grants 300 bonus points",
+ 2,
+ "c92b099a62cd2fbf8ada09dec145c75d7fda4dc57b968bea3a8fa11e37aa48b2",
+ LegacyFormattingCode.DARK_PURPLE),
+ BOMB("Bomb",
+ -1,
+ "Destroys nearby fruit",
+ 15,
+ "a76a2811d1e176a07b6d0a657b910f134896ce30850f6e80c7c83732d85381ea",
+ LegacyFormattingCode.DARK_RED),
+ RUM("Rum",
+ -1,
+ "Stops your dowsing ability for one turn",
+ 5,
+ "407b275d28b927b1bf7f6dd9f45fbdad2af8571c54c8f027d1bff6956fbf3c16",
+ LegacyFormattingCode.YELLOW),
+ ;
+
+ val textureUrl = "http://textures.minecraft.net/texture/$textureHash"
+ val itemStack = createSkullItem(UUID.randomUUID(), textureUrl)
+ .setSkyBlockFirmamentUiId("MINESWEEPER_$name")
+
+ @Bind
+ fun getIcon() = ModernItemStack.of(itemStack)
+
+ @Bind
+ fun pieceLabel() = fruitColor.formattingCode + fruitName
+
+ @Bind
+ fun boardLabel() = "§a$totalPerBoard§7/§rboard"
+
+ @Bind("description")
+ fun getDescription() = buildString {
+ append(specialAbility)
+ if (points >= 0) {
+ append(" Default points: $points.")
+ }
+ }
+ }
+
+ object TutorialScreen {
+ @get:Bind("pieces")
+ val pieces = ObservableList(Piece.entries.toList().reversed())
+
+ @get:Bind("modes")
+ val modes = ObservableList(DowsingMode.entries.toList())
+ }
+
+ enum class DowsingMode(
+ val itemType: Item,
+ @get:Bind("feature")
+ val feature: String,
+ @get:Bind("description")
+ val description: String,
+ ) {
+ MINES(Items.IRON_SHOVEL, "Bomb detection", "Tells you how many bombs are near the block"),
+ ANCHOR(Items.DIAMOND_SHOVEL, "Lowest fruit", "Shows you which block nearby contains the lowest scoring fruit"),
+ TREASURE(Items.GOLDEN_SHOVEL, "Highest fruit", "Tells you which kind of fruit is the highest scoring nearby"),
+ ;
+
+ @Bind("itemType")
+ fun getItemStack() = ModernItemStack.of(ItemStack(itemType))
+
+ companion object {
+ val id = SkyblockId("CARNIVAL_SHOVEL")
+ fun fromItem(itemStack: ItemStack): DowsingMode? {
+ if (itemStack.skyBlockId != id) return null
+ return DowsingMode.entries.find { it.itemType == itemStack.item }
+ }
+ }
+ }
+
+ data class BoardPosition(
+ val x: Int,
+ val y: Int
+ ) {
+ fun toBlockPos() = BlockPos(sandBoxLow.x + x, sandBoxLow.y, sandBoxLow.z + y)
+
+ fun getBlock(world: WorldAccess) = world.getBlockState(toBlockPos()).block
+ fun isUnopened(world: WorldAccess) = getBlock(world) == Blocks.SAND
+ fun isOpened(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE
+ fun isScorched(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE_STAIRS
+
+ companion object {
+ fun fromBlockPos(blockPos: BlockPos): BoardPosition? {
+ if (blockPos.y != sandBoxLow.y) return null
+ val x = blockPos.x - sandBoxLow.x
+ val y = blockPos.z - sandBoxLow.z
+ if (x < 0 || x >= boardSize.first) return null
+ if (y < 0 || y >= boardSize.second) return null
+ return BoardPosition(x, y)
+ }
+ }
+ }
+
+ data class GameState(
+ val nearbyBombs: MutableMap<BoardPosition, Int> = mutableMapOf(),
+ val knownBombPositions: MutableSet<BoardPosition> = mutableSetOf(),
+ var lastClickedPosition: BoardPosition? = null,
+ var lastDowsingMode: DowsingMode? = null,
+ )
+
+ var gameState: GameState? = null
+ val log = DebugLogger("minesweeper")
+
+ @Subscribe
+ fun onCommand(event: CommandEvent.SubCommand) {
+ event.subcommand("minesweepertutorial") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("carnival/minesweeper_tutorial",
+ TutorialScreen,
+ null))
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldChange(event: WorldReadyEvent) {
+ gameState = null
+ }
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) {
+ MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled {
+ it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial"))
+ })
+ }
+ if (!CarnivalFeatures.TConfig.enableBombSolver) {
+ gameState = null // TODO: replace this which a watchable property
+ return
+ }
+ if (event.unformattedString == gameStartMessage) {
+ gameState = GameState()
+ log.log { "Game started" }
+ }
+ if (event.unformattedString.trim() == gameEndMessage) {
+ gameState = null // TODO: add a loot tracker maybe? probably not, i dont think people care
+ log.log { "Finished game" }
+ }
+ val gs = gameState ?: return
+ bombPattern.useMatch(event.unformattedString) {
+ val bombCount = group("bombCount").toInt()
+ log.log { "Marking ${gs.lastClickedPosition} as having $bombCount nearby" }
+ val pos = gs.lastClickedPosition ?: return
+ gs.nearbyBombs[pos] = bombCount
+ }
+ }
+
+ @Subscribe
+ fun onMobChange(event: EntityUpdateEvent) {
+ val gs = gameState ?: return
+ if (event !is EntityUpdateEvent.TrackedDataUpdate) return
+ // TODO: listen to state
+ }
+
+ @Subscribe
+ fun onBlockClick(event: AttackBlockEvent) {
+ val gs = gameState ?: return
+ val boardPosition = BoardPosition.fromBlockPos(event.blockPos)
+ log.log { "Breaking block at ${event.blockPos} ($boardPosition)" }
+ gs.lastClickedPosition = boardPosition
+ gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack)
+ }
+
+ @Subscribe
+ fun onRender(event: WorldRenderLastEvent) {
+ val gs = gameState ?: return
+ RenderInWorldContext.renderInWorld(event) {
+ for ((pos, bombCount) in gs.nearbyBombs) {
+ this.text(pos.toBlockPos().up().toCenterPos(), Text.literal("§a$bombCount \uD83D\uDCA3"))
+ }
+ }
+ }
+
+
+}