diff options
Diffstat (limited to 'src/main/kotlin/features/events')
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")) + } + } + } + + +} |