diff --git a/src/main/java/me/owdding/mortem/mixins/LivingEntityMixin.java b/src/main/java/me/owdding/mortem/mixins/LivingEntityMixin.java new file mode 100644 index 0000000..e4e24db --- /dev/null +++ b/src/main/java/me/owdding/mortem/mixins/LivingEntityMixin.java @@ -0,0 +1,20 @@ +package me.owdding.mortem.mixins; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import me.owdding.mortem.events.EntityDeathEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import tech.thatgravyboat.skyblockapi.api.SkyBlockAPI; + +@Mixin(LivingEntity.class) +public class LivingEntityMixin { + + @WrapMethod(method = "die") + private void onDieWrap(DamageSource damageSource, Operation original) { + new EntityDeathEvent((LivingEntity) (Object) this, damageSource).post(SkyBlockAPI.getEventBus()); + original.call(damageSource); + } + +} diff --git a/src/main/kotlin/me/owdding/mortem/config/category/OverlayConfig.kt b/src/main/kotlin/me/owdding/mortem/config/category/OverlayConfig.kt index b205036..5e9be1d 100644 --- a/src/main/kotlin/me/owdding/mortem/config/category/OverlayConfig.kt +++ b/src/main/kotlin/me/owdding/mortem/config/category/OverlayConfig.kt @@ -1,5 +1,6 @@ package me.owdding.mortem.config.category +import com.teamresourceful.resourcefulconfig.api.types.info.Translatable import com.teamresourceful.resourcefulconfigkt.api.CategoryKt import me.owdding.lib.overlays.ConfigPosition import me.owdding.mortem.config.separator @@ -23,10 +24,29 @@ object OverlayConfig : CategoryKt("overlays") { var dungeonBreakerShowWhenHolding by boolean(false) { translation = "$translation.dungeonbreaker_show_when_holding" } + + init { + separator("$translation.score_separator") + } + + var scoreOverlay by boolean(true) { + translation = "$translation.score_overlay" + } + + var displayMode by enum(DisplayMode.DETAILED) { + translation = "$translation.score_display_mode" + } +} + +enum class DisplayMode : Translatable { + DETAILED, COMPACT, SHORT; + + override fun getTranslationKey() = "mortem.config.overlays.display_mode.$name".lowercase() } object OverlayPositions : CategoryKt("overlaysPositions") { override val hidden: Boolean = true val dungeonBreaker by obj(ConfigPosition(100, 200)) + val score by obj(ConfigPosition(10, 500) ) } diff --git a/src/main/kotlin/me/owdding/mortem/events/EntityEvents.kt b/src/main/kotlin/me/owdding/mortem/events/EntityEvents.kt new file mode 100644 index 0000000..60d9ef6 --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/events/EntityEvents.kt @@ -0,0 +1,10 @@ +package me.owdding.mortem.events + +import net.minecraft.world.damagesource.DamageSource +import net.minecraft.world.entity.LivingEntity +import tech.thatgravyboat.skyblockapi.api.events.base.SkyBlockEvent + +data class EntityDeathEvent( + val entity: LivingEntity, + val damageSource: DamageSource, +) : SkyBlockEvent() diff --git a/src/main/kotlin/me/owdding/mortem/features/ScoreCalculator.kt b/src/main/kotlin/me/owdding/mortem/features/ScoreCalculator.kt new file mode 100644 index 0000000..c0e72eb --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/features/ScoreCalculator.kt @@ -0,0 +1,247 @@ +package me.owdding.mortem.features + +import me.owdding.ktmodules.Module +import me.owdding.mortem.events.EntityDeathEvent +import net.minecraft.ChatFormatting +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.monster.Zombie +import tech.thatgravyboat.skyblockapi.api.area.dungeon.DungeonAPI +import tech.thatgravyboat.skyblockapi.api.area.dungeon.DungeonFloor +import tech.thatgravyboat.skyblockapi.api.data.Perk +import tech.thatgravyboat.skyblockapi.api.events.base.Subscription +import tech.thatgravyboat.skyblockapi.api.events.base.predicates.OnlyIn +import tech.thatgravyboat.skyblockapi.api.events.base.predicates.OnlyWidget +import tech.thatgravyboat.skyblockapi.api.events.chat.ChatReceivedEvent +import tech.thatgravyboat.skyblockapi.api.events.hypixel.ServerChangeEvent +import tech.thatgravyboat.skyblockapi.api.events.info.ScoreboardUpdateEvent +import tech.thatgravyboat.skyblockapi.api.events.info.TabWidget +import tech.thatgravyboat.skyblockapi.api.events.info.TabWidgetChangeEvent +import tech.thatgravyboat.skyblockapi.api.events.location.ServerDisconnectEvent +import tech.thatgravyboat.skyblockapi.api.location.SkyBlockIsland +import tech.thatgravyboat.skyblockapi.helpers.McClient +import tech.thatgravyboat.skyblockapi.utils.extentions.enumMapOf +import tech.thatgravyboat.skyblockapi.utils.extentions.toFloatValue +import tech.thatgravyboat.skyblockapi.utils.extentions.toIntValue +import tech.thatgravyboat.skyblockapi.utils.regex.RegexUtils.anyMatch +import tech.thatgravyboat.skyblockapi.utils.regex.matchWhen +import tech.thatgravyboat.skyblockapi.utils.text.Text +import tech.thatgravyboat.skyblockapi.utils.text.Text.send +import tech.thatgravyboat.skyblockapi.utils.text.TextColor +import kotlin.math.floor +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/* +Discoveries: 4 + Secrets Found: 4 + Crypts: 0 + +Dungeon: Catacombs + Opened Rooms: 34 + Completed Rooms: 16 + Secrets Found: 7.8% + Time: 06m 39s + +Puzzles: (3) + Quiz: [✖] () + Three Weirdos: [✔] + ???: [✦] + */ + +// TODO +// Spirit Pet in Death +// Prince Kill +// Entrace req +@Module +object ScoreCalculator { + // + // --Chat-- + private val mimicKilledRegex = ".*(\$SKYTILS-DUNGEON-SCORE-MIMIC\$|Mimic [Dd]ead!|Mimic Killed!)$".toRegex() + // --Scoreboard-- + private val clearedPercentageRegex = "\\s*Cleared: (?[\\d,.]+)% \\((?[\\d.]+)\\)".toRegex() + // -- Tab-- + private val cryptsRegex = " Crypts: (?\\d+)".toRegex() + private val secretPercentageRegex = " Secrets Found: (?[\\d.]+)%".toRegex() + private val puzzleRegex = " [\\w\\s?]+: \\[[✖✦]](?: \\(.*\\))?".toRegex() + // + + private val requirements = enumMapOf( + DungeonFloor.E to Requirements(0.3, 600), + DungeonFloor.F1 to Requirements(0.3, 600), + DungeonFloor.F2 to Requirements(0.4, 600), + DungeonFloor.F3 to Requirements(0.5, 600), + DungeonFloor.F4 to Requirements(0.6, 720), + DungeonFloor.F5 to Requirements(0.7, 600), + DungeonFloor.F6 to Requirements(0.85, 720), + DungeonFloor.F7 to Requirements(1.0, 840), + DungeonFloor.M1 to Requirements(1.0, 480), + DungeonFloor.M2 to Requirements(1.0, 480), + DungeonFloor.M3 to Requirements(1.0, 480), + DungeonFloor.M4 to Requirements(1.0, 480), + DungeonFloor.M5 to Requirements(1.0, 480), + DungeonFloor.M6 to Requirements(1.0, 600), + DungeonFloor.M7 to Requirements(1.0, 840), + ) + private val speedDecreasePercentage = mapOf( + 0.0..0.2 to 0.02, + 0.2..0.4 to 0.04, + 0.4..0.5 to 0.05, + 0.5..0.6 to 0.06, + 0.6..Double.MAX_VALUE to 0.07, + ) + + private var deaths = 0 + private var secretPercentage = 0f + private var roomsClearedPercentage = 0f + private var mimicKilled = false + private var cryptsKilled = 0 + private var failedPuzzles = 0 + + fun getScore() = DungeonAPI.dungeonFloor?.let { getScore(DungeonAPI.time, it) } + fun getScore(time: Duration, floor: DungeonFloor): Score { + val req = requirements[floor] ?: return Score.ZERO + return Score( + getSkillScore(), + getExplorationScore(req), + getSpeedScore(time, req), + getBonusScore(), + ) + } + + private fun getSkillScore(): Int { + val roomsScore = floor((80 * roomsClearedPercentage)).toInt() + val puzzlePenalty = 10 * failedPuzzles + val deathPenalty = 2 * deaths + + return 20 + (roomsScore - puzzlePenalty - deathPenalty).coerceAtLeast(0) + } + + private fun getExplorationScore(req: Requirements): Int { + val roomsScore = floor(60 * roomsClearedPercentage).toInt() + val secretsScore = floor((40 * (secretPercentage / req.secretPercentNeeded).coerceAtMost(1.0))).toInt() + return roomsScore + secretsScore + } + + private fun getSpeedScore(time: Duration, req: Requirements): Int { + if (time <= req.speedTime) return 100 + + val percentOver = (time / req.speedTime) - 1 + val lost = speedDecreasePercentage.mapNotNull { (range, step) -> + if (percentOver < range.start) return@mapNotNull null + val delta = (percentOver - range.start).coerceAtMost(range.endInclusive - range.start) + delta / step + } + return 100 - floor(lost.sum()).toInt() + } + + private fun getBonusScore(): Int = buildList { + if (Perk.EZPZ.active) add(10) + if (mimicKilled) add(2) + add(cryptsKilled.coerceAtMost(5)) + }.sum() + + data class Score( + val skill: Int, + val exploration: Int, + val speed: Int, + val bonus: Int, + ) { + val total = skill + exploration + speed + bonus + val rank = Rank.getRank(total) + + companion object { + val ZERO = Score(0,0,0,0) + } + } + + private data class Requirements( + val secretPercentNeeded: Double, + val speedTime: Duration, + ) { + constructor(secretPercentNeeded: Double, speedTime: Int) : this(secretPercentNeeded, speedTime.seconds) + } + + enum class Rank(val minScore: Int, val component: Component) { + D(0, Text.of("D").withColor(TextColor.RED)), + C(100, Text.of("C").withColor(TextColor.BLUE)), + B(160, Text.of("B").withColor(TextColor.GREEN)), + A(230, Text.of("A").withColor(TextColor.DARK_PURPLE)), + S(270, Text.of("S").withColor(TextColor.YELLOW)), + S_PLUS(300, Text.of("S+").withColor(TextColor.GOLD).withStyle(ChatFormatting.BOLD)); + + companion object { + fun getRank(score: Int) = entries.lastOrNull { score >= it.minScore } ?: D + } + } + + private fun isMimicFloor() = DungeonAPI.dungeonFloor in listOf(DungeonFloor.F6, DungeonFloor.F7, DungeonFloor.M6, DungeonFloor.M7) + + @Subscription(ServerChangeEvent::class, ServerDisconnectEvent::class) + fun reset() { + deaths = 0 + secretPercentage = 0f + roomsClearedPercentage = 0f + mimicKilled = false + cryptsKilled = 0 + failedPuzzles = 0 + } + + @Subscription + @OnlyIn(SkyBlockIsland.THE_CATACOMBS) + fun onEntityDeath(event: EntityDeathEvent) { + if (event.entity !is Zombie || !event.entity.isBaby || !isMimicFloor()) return + McClient.runNextTick { + if (mimicKilled) return@runNextTick + McClient.sendCommand("pc Mimic Killed!") + mimicKilled = true + } + } + + @Subscription + @OnlyIn(SkyBlockIsland.THE_CATACOMBS) + fun onChat(event: ChatReceivedEvent.Pre) { + matchWhen(event.text) { + case(mimicKilledRegex) { + if (isMimicFloor()) mimicKilled = true + } + } + } + + @Subscription + fun onScoreboard(event: ScoreboardUpdateEvent) { + clearedPercentageRegex.anyMatch(event.new, "percentage") { (percentage) -> + roomsClearedPercentage = percentage.toFloatValue() / 100f + } + } + + @Subscription + @OnlyWidget(TabWidget.DISCOVERIES) + fun onDiscoveriesWidget(event: TabWidgetChangeEvent) { + cryptsRegex.anyMatch(event.new, "amount") { (amount) -> + cryptsKilled = amount.toIntValue() + } + } + + @Subscription + @OnlyWidget(TabWidget.AREA) + fun onAreaWidget(event: TabWidgetChangeEvent) { + secretPercentageRegex.anyMatch(event.new, "percentage") { (percentage) -> + this.secretPercentage = percentage.toFloatValue() / 100f + } + } + + @Subscription + @OnlyWidget(TabWidget.PUZZLES) + fun onPuzzlesWidget(event: TabWidgetChangeEvent) { + failedPuzzles = event.new.count { puzzleRegex.matches(it) } + } + + @Subscription + @OnlyWidget(TabWidget.TEAM_DEATHS) + fun onTeamDeathsWidget(event: TabWidgetChangeEvent) { + TabWidget.TEAM_DEATHS.regex.anyMatch(event.new, "amount") { (amount) -> + this.deaths = amount.toIntValue() + } + } +} diff --git a/src/main/kotlin/me/owdding/mortem/features/ScoreDisplay.kt b/src/main/kotlin/me/owdding/mortem/features/ScoreDisplay.kt new file mode 100644 index 0000000..493ce66 --- /dev/null +++ b/src/main/kotlin/me/owdding/mortem/features/ScoreDisplay.kt @@ -0,0 +1,83 @@ +package me.owdding.mortem.features + +import me.owdding.ktmodules.Module +import me.owdding.lib.builder.DisplayFactory +import me.owdding.lib.overlays.Position +import me.owdding.mortem.config.category.DisplayMode +import me.owdding.mortem.config.category.OverlayConfig +import me.owdding.mortem.config.category.OverlayPositions +import me.owdding.mortem.utils.CachedValue +import me.owdding.mortem.utils.MortemOverlay +import me.owdding.mortem.utils.Overlay +import me.owdding.mortem.utils.colors.CatppuccinColors +import me.owdding.mortem.utils.colors.MortemColors +import me.owdding.mortem.utils.ticks +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import tech.thatgravyboat.skyblockapi.api.location.SkyBlockIsland +import tech.thatgravyboat.skyblockapi.helpers.McFont +import tech.thatgravyboat.skyblockapi.utils.text.Text +import tech.thatgravyboat.skyblockapi.utils.text.TextBuilder.append +import tech.thatgravyboat.skyblockapi.utils.text.TextStyle.color + +@Module +@Overlay +object ScoreDisplay : MortemOverlay { + + override val name: Component = Text.of("Score Display") + override val enabled: Boolean get() = OverlayConfig.scoreOverlay && SkyBlockIsland.THE_CATACOMBS.inIsland() + override val position: Position = OverlayPositions.score + override val bounds: Pair + get() = display?.let { it.getWidth() to it.getHeight() } ?: (0 to 0) + + + private val display by CachedValue(2.ticks) { + val score = ScoreCalculator.getScore() ?: return@CachedValue null + + when (OverlayConfig.displayMode) { + DisplayMode.DETAILED -> getDetailed(score) + DisplayMode.COMPACT -> getCompact(score) + DisplayMode.SHORT -> getShort(score) + } + } + + private fun getDetailed(score: ScoreCalculator.Score) = DisplayFactory.vertical { + display(getCompact(score)) + spacer(McFont.height) + string("TODO MORE INFO") + } + + private fun getCompact(score: ScoreCalculator.Score) = DisplayFactory.vertical { + display(getShort(score)) + string(" - Skill: ") { + color = MortemColors.BASE_TEXT + append("${score.skill}") { color = CatppuccinColors.Mocha.pink } + } + string(" - Exploration: ") { + color = MortemColors.BASE_TEXT + append("${score.exploration}") { color = CatppuccinColors.Mocha.yellow } + } + string(" - Speed: ") { + color = MortemColors.BASE_TEXT + append("${score.speed}") { color = CatppuccinColors.Mocha.green } + } + string(" - Bonus: ") { + color = MortemColors.BASE_TEXT + append("${score.bonus}") { color = CatppuccinColors.Mocha.blue } + } + } + + private fun getShort(score: ScoreCalculator.Score) = DisplayFactory.vertical { + string("Score: ") { + color = MortemColors.BASE_TEXT + append("${score.total} ") { color = MortemColors.HIGHLIGHT } + append("(") { color = MortemColors.SEPARATOR } + append(score.rank.component) + append(")") { color = MortemColors.SEPARATOR } + } + } + + override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) { + display?.render(graphics) + } +} diff --git a/src/main/kotlin/me/owdding/mortem/utils/colors/MortemColors.kt b/src/main/kotlin/me/owdding/mortem/utils/colors/MortemColors.kt index d0134e6..efdf8c6 100644 --- a/src/main/kotlin/me/owdding/mortem/utils/colors/MortemColors.kt +++ b/src/main/kotlin/me/owdding/mortem/utils/colors/MortemColors.kt @@ -3,5 +3,5 @@ package me.owdding.mortem.utils.colors object MortemColors { const val BASE_TEXT = 0xcdd6f4 const val SEPARATOR = 0x585b70 - const val HIGHLIGHT = 0xcba6f7 + const val HIGHLIGHT = CatppuccinColors.Mocha.mauve } diff --git a/src/main/resources/mortem.mixins.json b/src/main/resources/mortem.mixins.json index 28b7ac7..23c0462 100644 --- a/src/main/resources/mortem.mixins.json +++ b/src/main/resources/mortem.mixins.json @@ -14,5 +14,8 @@ }, "mixinextras": { "minVersion": "0.5.0" - } + }, + "mixins": [ + "LivingEntityMixin" + ] }