diff options
author | hannibal2 <24389977+hannibal002@users.noreply.github.com> | 2024-03-17 18:23:46 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-17 18:23:46 +0100 |
commit | f72cf9906a49736948a353fceb135e49b3f041b5 (patch) | |
tree | 012116f3b0594b01ac95b2a8c863fa4c39c14502 /src/main | |
parent | ccf598ce287b3f179e65827c3a4caa05b75d3beb (diff) | |
download | skyhanni-f72cf9906a49736948a353fceb135e49b3f041b5.tar.gz skyhanni-f72cf9906a49736948a353fceb135e49b3f041b5.tar.bz2 skyhanni-f72cf9906a49736948a353fceb135e49b3f041b5.zip |
Feature: Thaumaturgy Tuning in Custom Scoreboard (#1201)
Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com>
Diffstat (limited to 'src/main')
8 files changed, 222 insertions, 57 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/gui/customscoreboard/DisplayConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/gui/customscoreboard/DisplayConfig.java index 4f7a7c91a..5485dcf32 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/gui/customscoreboard/DisplayConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/gui/customscoreboard/DisplayConfig.java @@ -103,6 +103,16 @@ public class DisplayConfig { public boolean colorArrowAmount = false; @Expose + @ConfigOption(name = "Compact Tuning", desc = "Show tuning stats compact") + @ConfigEditorBoolean + public boolean compactTuning = false; + + @Expose + @ConfigOption(name = "Tuning Amount", desc = "Only show the first # tunings.\n§cDoes not work with Compact Tuning.") + @ConfigEditorSlider(minValue = 1, maxValue = 8, minStep = 1) + public int tuningAmount = 2; + + @Expose @ConfigOption(name = "Line Spacing", desc = "The amount of space between each line.") @ConfigEditorSlider(minValue = 0, maxValue = 20, minStep = 1) public int lineSpacing = 10; 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 43b2cbfbc..b69420ab3 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -1,6 +1,7 @@ package at.hannibal2.skyhanni.config.storage; import at.hannibal2.skyhanni.api.SkillAPI; +import at.hannibal2.skyhanni.data.MaxwellAPI; import at.hannibal2.skyhanni.data.model.ComposterUpgrade; import at.hannibal2.skyhanni.features.combat.endernodetracker.EnderNodeTracker; import at.hannibal2.skyhanni.features.combat.ghostcounter.GhostData; @@ -52,6 +53,9 @@ public class ProfileSpecificStorage { @Expose public int magicalPower = -1; + + @Expose + public List<MaxwellAPI.ThaumaturgyPowerTuning> tunings = new ArrayList<>(); } @Expose diff --git a/src/main/java/at/hannibal2/skyhanni/data/MaxwellAPI.kt b/src/main/java/at/hannibal2/skyhanni/data/MaxwellAPI.kt index a40918e8d..98c410b86 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/MaxwellAPI.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/MaxwellAPI.kt @@ -1,13 +1,17 @@ package at.hannibal2.skyhanni.data import at.hannibal2.skyhanni.data.jsonobjects.repo.MaxwellPowersJson -import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.events.InventoryOpenEvent import at.hannibal2.skyhanni.events.LorenzChatEvent import at.hannibal2.skyhanni.events.RepositoryReloadEvent +import at.hannibal2.skyhanni.features.gui.customscoreboard.CustomScoreboard +import at.hannibal2.skyhanni.features.gui.customscoreboard.ScoreboardElement import at.hannibal2.skyhanni.test.command.ErrorManager import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.ItemUtils.name import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.groupOrNull import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland import at.hannibal2.skyhanni.utils.NumberUtil.formatInt import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher @@ -16,8 +20,11 @@ import at.hannibal2.skyhanni.utils.StringUtils.removeColor import at.hannibal2.skyhanni.utils.StringUtils.removeResets import at.hannibal2.skyhanni.utils.StringUtils.trimWhiteSpace import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import com.google.gson.annotations.Expose import net.minecraft.item.ItemStack +import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.regex.Pattern object MaxwellAPI { @@ -28,12 +35,19 @@ object MaxwellAPI { set(value) { storage?.maxwell?.currentPower = value ?: return } + var magicalPower: Int? get() = storage?.maxwell?.magicalPower set(value) { storage?.maxwell?.magicalPower = value ?: return } + var tunings: List<ThaumaturgyPowerTuning>? + get() = storage?.maxwell?.tunings + set(value) { + storage?.maxwell?.tunings = value ?: return + } + private var powers = mutableListOf<String>() private val group = RepoPattern.group("data.maxwell") @@ -53,6 +67,26 @@ object MaxwellAPI { "gui.thaumaturgy", "Accessory Bag Thaumaturgy" ) + private val thaumaturgyStartPattern by group.pattern( + "gui.thaumaturgy.start", + "§7Your tuning:" + ) + private val thaumaturgyDataPattern by group.pattern( + "gui.thaumaturgy.data", + "§(?<color>.)\\+(?<amount>[^ ]+)(?<icon>.) (?<name>.+)" + ) + private val statsTuningGuiPattern by group.pattern( + "gui.thaumaturgy.statstuning", + "Stats Tuning" + ) + private val statsTuningDataPattern by group.pattern( + "thaumaturgy.statstuning", + "§7You have: .+ §7\\+ §(?<color>.)(?<amount>[^ ]+) (?<icon>.)" + ) + private val tuningAutoAssignedPattern by group.pattern( + "tuningpoints.chat.autoassigned", + "§aYour §r§eTuning Points §r§awere auto-assigned as convenience!" + ) private val yourBagsGuiPattern by group.pattern( "gui.yourbags", "Your Bags" @@ -70,6 +104,8 @@ object MaxwellAPI { "(?:§.)*Requires (?:§.)*Redstone Collection I+(?:§.)*\\." ) + fun isThaumaturgyInventory(inventoryName: String) = thaumaturgyGuiPattern.matches(inventoryName) + @SubscribeEvent fun onChat(event: LorenzChatEvent) { if (!isEnabled()) return @@ -85,37 +121,103 @@ object MaxwellAPI { "message" to message ) } + tuningAutoAssignedPattern.matchMatcher(event.message) { + if (tunings?.isNotEmpty() == true) { + val tuningsInScoreboard = ScoreboardElement.TUNING in CustomScoreboard.config.scoreboardEntries + if (tuningsInScoreboard) { + ChatUtils.chat("Talk to Maxwell again to update the tuning data in scoreboard.") + } + } + } } - @SubscribeEvent - fun onInventoryFullyLoaded(event: InventoryFullyOpenedEvent) { + // load earler, so that other features can already use the api in this event + @SubscribeEvent(priority = EventPriority.HIGH) + fun onInventoryFullyLoaded(event: InventoryOpenEvent) { if (!isEnabled()) return - if (thaumaturgyGuiPattern.matches(event.inventoryName)) { - val selectedPowerStack = - event.inventoryItems.values.find { - powerSelectedPattern.matches(it.getLore().lastOrNull()) - } ?: return - val displayName = selectedPowerStack.displayName.removeColor().trim() - - currentPower = getPowerByNameOrNull(displayName) - ?: return ErrorManager.logErrorWithData( - UnknownMaxwellPower("Unknown power: $displayName"), - "Unknown power: $displayName", - "displayName" to displayName, - "lore" to selectedPowerStack.getLore(), - noStackTrace = true - ) - return + if (isThaumaturgyInventory(event.inventoryName)) { + loadThaumaturgyCurrentPower(event.inventoryItems) + loadThaumaturgyTunings(event.inventoryItems) } if (yourBagsGuiPattern.matches(event.inventoryName)) { - val stacks = event.inventoryItems - - for (stack in stacks.values) { + for (stack in event.inventoryItems.values) { if (accessoryBagStack.matches(stack.displayName)) processStack(stack) } } + if (statsTuningGuiPattern.matches(event.inventoryName)) { + loadThaumaturgyTuningsFromTuning(event.inventoryItems) + } + } + + private fun loadThaumaturgyTuningsFromTuning(inventoryItems: Map<Int, ItemStack>) { + val map = mutableListOf<ThaumaturgyPowerTuning>() + for (stack in inventoryItems.values) { + for (line in stack.getLore()) { + statsTuningDataPattern.readTuningFromLine(line)?.let { + it.name = "§.. (?<name>.+)".toPattern().matchMatcher(stack.name) { + group("name") + } ?: ErrorManager.skyHanniError( + "found no name in thaumaturgy", + "stack name" to stack.name, + "line" to line + ) + map.add(it) + } + } + } + tunings = map + } + + private fun Pattern.readTuningFromLine(line: String): ThaumaturgyPowerTuning? { + return matchMatcher(line) { + val color = "§" + group("color") + val icon = group("icon") + val name = groupOrNull("name") ?: "<missing>" + val value = group("amount") + ThaumaturgyPowerTuning(value, color, name, icon) + } + } + + private fun loadThaumaturgyCurrentPower(inventoryItems: Map<Int, ItemStack>) { + val selectedPowerStack = + inventoryItems.values.find { + powerSelectedPattern.matches(it.getLore().lastOrNull()) + } ?: return + val displayName = selectedPowerStack.displayName.removeColor().trim() + + currentPower = getPowerByNameOrNull(displayName) + ?: return ErrorManager.logErrorWithData( + UnknownMaxwellPower("Unknown power: $displayName"), + "Unknown power: $displayName", + "displayName" to displayName, + "lore" to selectedPowerStack.getLore(), + noStackTrace = true + ) + } + + private fun loadThaumaturgyTunings(inventoryItems: Map<Int, ItemStack>) { + val tunings = tunings ?: return + + // Only load those rounded values if we dont have any valurs at all + if (tunings.isNotEmpty()) return + + val item = inventoryItems[51] ?: return + var active = false + val map = mutableListOf<ThaumaturgyPowerTuning>() + for (line in item.getLore()) { + if (thaumaturgyStartPattern.matches(line)) { + active = true + continue + } + if (!active) continue + if (line.isEmpty()) break + thaumaturgyDataPattern.readTuningFromLine(line)?.let { + map.add(it) + } + } + this.tunings = map } private fun processStack(stack: ItemStack) { @@ -153,7 +255,7 @@ object MaxwellAPI { private fun getPowerByNameOrNull(name: String) = powers.find { it == name } - fun isEnabled() = LorenzUtils.inSkyBlock && storage != null + private fun isEnabled() = LorenzUtils.inSkyBlock && storage != null // Load powers from repo @SubscribeEvent @@ -163,4 +265,11 @@ object MaxwellAPI { } class UnknownMaxwellPower(message: String) : Exception(message) + + class ThaumaturgyPowerTuning( + @Expose val value: String, + @Expose val color: String, + @Expose var name: String, + @Expose val icon: String, + ) } diff --git a/src/main/java/at/hannibal2/skyhanni/features/dungeon/DungeonFinderFeatures.kt b/src/main/java/at/hannibal2/skyhanni/features/dungeon/DungeonFinderFeatures.kt index 964b4653e..7037743ef 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/dungeon/DungeonFinderFeatures.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/dungeon/DungeonFinderFeatures.kt @@ -167,7 +167,7 @@ class DungeonFinderFeatures { if (stack.getLore().firstOrNull()?.removeColor()?.startsWith("Dungeon:") == false) return if (classNames.contains(selectedClass)) selectedClass = "§a${selectedClass}§7" event.toolTip.add("") - event.toolTip.add("§cMissing: §7" + createCommaSeparatedList(classNames)) + event.toolTip.add("§cMissing: §7" + classNames.createCommaSeparatedList()) } @SubscribeEvent diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/pests/SprayDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/pests/SprayDisplay.kt index 754398106..fa69ac79d 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/garden/pests/SprayDisplay.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/pests/SprayDisplay.kt @@ -14,7 +14,7 @@ import at.hannibal2.skyhanni.features.garden.GardenPlotAPI.name import at.hannibal2.skyhanni.features.garden.GardenPlotAPI.plots import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.RenderUtils.renderString -import at.hannibal2.skyhanni.utils.StringUtils +import at.hannibal2.skyhanni.utils.StringUtils.createCommaSeparatedList import at.hannibal2.skyhanni.utils.TimeUtils.format import at.hannibal2.skyhanni.utils.TimeUtils.timerColor import net.minecraftforge.fml.common.eventhandler.SubscribeEvent @@ -61,7 +61,7 @@ class SprayDisplay { expiredPlots.forEach { it.markExpiredSprayAsNotified() } val wasAwayString = if (wasAway) "§7While you were away, your" else "§7Your" - val plotString = StringUtils.createCommaSeparatedList(expiredPlots.map { "§b${it.name}" }, "§7") + val plotString = expiredPlots.map { "§b${it.name}" }.createCommaSeparatedList("§7") val sprayString = if (expiredPlots.size > 1) "sprays" else "spray" val out = "$wasAwayString $sprayString on §aPlot §7- $plotString §7expired." ChatUtils.chat(out) diff --git a/src/main/java/at/hannibal2/skyhanni/features/gui/customscoreboard/ScoreboardElements.kt b/src/main/java/at/hannibal2/skyhanni/features/gui/customscoreboard/ScoreboardElements.kt index 0b9d7b1be..d0524fd4d 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/gui/customscoreboard/ScoreboardElements.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/gui/customscoreboard/ScoreboardElements.kt @@ -28,6 +28,7 @@ import at.hannibal2.skyhanni.utils.LorenzUtils.inDungeons import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators import at.hannibal2.skyhanni.utils.NumberUtil.percentageColor import at.hannibal2.skyhanni.utils.RenderUtils.HorizontalAlignment +import at.hannibal2.skyhanni.utils.StringUtils import at.hannibal2.skyhanni.utils.StringUtils.firstLetterUppercase import at.hannibal2.skyhanni.utils.StringUtils.matches import at.hannibal2.skyhanni.utils.TabListData @@ -42,7 +43,7 @@ internal var amountOfUnknownLines = 0 enum class ScoreboardElement( private val displayPair: Supplier<List<ScoreboardElementType>>, val showWhen: () -> Boolean, - private val configLine: String + private val configLine: String, ) { TITLE( ::getTitleDisplayPair, @@ -138,6 +139,11 @@ enum class ScoreboardElement( ::getPowerShowWhen, "Power: §aSighted §7(§61.263§7)" ), + TUNING( + ::getTuningDisplayPair, + ::getPowerShowWhen, + "Tuning: §c❁34§7, §e⚔20§7, and §9☣7" + ), COOKIE( ::getCookieDisplayPair, ::getCookieShowWhen, @@ -234,7 +240,6 @@ enum class ScoreboardElement( } } - private fun getTitleDisplayPair() = if (displayConfig.titleAndFooter.useHypixelTitleAnimation) { listOf(ScoreboardData.objectiveTitle to displayConfig.titleAndFooter.alignTitleAndFooter) } else { @@ -428,7 +433,6 @@ private fun getDateDisplayPair() = SkyBlockTime.now().formatted(yearElement = false, hoursAndMinutesElement = false) to HorizontalAlignment.LEFT ) - private fun getTimeDisplayPair(): List<ScoreboardElementType> { var symbol = getGroupFromPattern(ScoreboardData.sidebarLinesFormatted, ScoreboardPattern.timePattern, "symbol") if (symbol == "0") symbol = "" @@ -462,6 +466,48 @@ private fun getPowerDisplayPair() = listOf( ?: "§cOpen \"Your Bags\"!") to HorizontalAlignment.LEFT ) +private fun getTuningDisplayPair(): List<Pair<String, HorizontalAlignment>> { + val tunings = MaxwellAPI.tunings ?: return listOf("§cTalk to \"Maxwell\"!" to HorizontalAlignment.LEFT) + if (tunings.isEmpty()) return listOf("§cNo Maxwell Tunings :(" to HorizontalAlignment.LEFT) + + val title = StringUtils.pluralize(tunings.size, "Tuning", "Tunings") + return if (displayConfig.compactTuning) { + val tuning = tunings + .take(3) + .joinToString("§7, ") { tuning -> + with(tuning) { + if (displayConfig.displayNumbersFirst) { + "$color$value$icon" + } else { + "$color$icon$value" + } + } + + } + listOf( + if (displayConfig.displayNumbersFirst) { + "$tuning §f$title" + } else { + "$title: $tuning" + } to HorizontalAlignment.LEFT + ) + } else { + val tuning = tunings + .take(displayConfig.tuningAmount.coerceAtLeast(1)) + .map { tuning -> + with(tuning) { + " §7- §f" + if (displayConfig.displayNumbersFirst) { + "$color$value $icon $name" + } else { + "$name: $color$value$icon" + } + } + + }.toTypedArray() + listOf("$title:", *tuning).map { it to HorizontalAlignment.LEFT } + } +} + private fun getPowerShowWhen() = !inAnyIsland(IslandType.THE_RIFT) private fun getCookieDisplayPair(): List<ScoreboardElementType> { @@ -491,7 +537,8 @@ private fun getCookieShowWhen(): Boolean { } private fun getObjectiveDisplayPair() = buildList { - val objective = ScoreboardData.sidebarLinesFormatted.first { ScoreboardPattern.objectivePattern.matches(it) } + val objective = + ScoreboardData.sidebarLinesFormatted.first { ScoreboardPattern.objectivePattern.matches(it) } add(objective to HorizontalAlignment.LEFT) add((ScoreboardData.sidebarLinesFormatted.nextAfter(objective) ?: "<hidden>") to HorizontalAlignment.LEFT) @@ -508,7 +555,6 @@ private fun getObjectiveShowWhen(): Boolean = !inAnyIsland(IslandType.KUUDRA_ARENA) && ScoreboardData.sidebarLinesFormatted.none { ScoreboardPattern.objectivePattern.matches(it) } - private fun getSlayerDisplayPair(): List<ScoreboardElementType> = listOf( (if (SlayerAPI.hasActiveSlayerQuest()) "Slayer Quest" else "<hidden>") to HorizontalAlignment.LEFT, (" §7- §e${SlayerAPI.latestSlayerCategory.trim()}" to HorizontalAlignment.LEFT), diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/StatsTuning.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/StatsTuning.kt index 208be373d..7d625bd60 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/StatsTuning.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/StatsTuning.kt @@ -2,6 +2,7 @@ package at.hannibal2.skyhanni.features.inventory import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator +import at.hannibal2.skyhanni.data.MaxwellAPI import at.hannibal2.skyhanni.events.GuiContainerEvent import at.hannibal2.skyhanni.events.RenderInventoryItemTipEvent import at.hannibal2.skyhanni.utils.InventoryUtils @@ -10,6 +11,7 @@ import at.hannibal2.skyhanni.utils.ItemUtils.name import at.hannibal2.skyhanni.utils.LorenzColor import at.hannibal2.skyhanni.utils.LorenzUtils import at.hannibal2.skyhanni.utils.RenderUtils.highlight +import at.hannibal2.skyhanni.utils.StringUtils.createCommaSeparatedList import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern import net.minecraft.item.ItemStack @@ -32,7 +34,11 @@ class StatsTuning { val stack = event.stack if (config.templateStats && inventoryName == "Stats Tuning") if (templateStats(stack, event)) return - if (config.selectedStats && inventoryName == "Accessory Bag Thaumaturgy" && selectedStats(stack, event)) return + if (config.selectedStats && MaxwellAPI.isThaumaturgyInventory(inventoryName) && renderTunings( + stack, + event + ) + ) return if (config.points && inventoryName == "Stats Tuning") points(stack, event) } @@ -64,26 +70,17 @@ class StatsTuning { return true } - private fun selectedStats(stack: ItemStack, event: RenderInventoryItemTipEvent): Boolean { + private fun renderTunings(stack: ItemStack, event: RenderInventoryItemTipEvent): Boolean { if (stack.name != "§aStats Tuning") return false + val tunings = MaxwellAPI.tunings ?: return false - var grab = false - val list = mutableListOf<String>() - for (line in stack.getLore()) { - if (line == "§7Your tuning:") { - grab = true - continue - } - if (!grab) continue - if (line == "") { - grab = false - continue + event.stackTip = tunings + .map { tuning -> + with(tuning) { + "$color$value$icon" + } } - val text = line.split(":")[0].split(" ")[0] + "§7" - list.add(text) - } - if (list.isEmpty()) return false - event.stackTip = list.joinToString(" + ") + .createCommaSeparatedList("§7") event.offsetX = 3 event.offsetY = -5 event.alignLeft = false diff --git a/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt index f0456304e..c8863fcea 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt @@ -95,7 +95,6 @@ object StringUtils { return toString().replace("-", "") } - inline fun <T> Pattern.matchMatcher(text: String, consumer: Matcher.() -> T) = matcher(text).let { if (it.matches()) consumer(it) else null } @@ -181,13 +180,13 @@ object StringUtils { * @param delimiterColor - the color code of the delimiter, inserted before each delimiter (commas and "and"). * @return a string representing the list joined with the Oxford comma and the word "and". */ - fun createCommaSeparatedList(list: List<String>, delimiterColor: String = ""): String { - if (list.isEmpty()) return "" - if (list.size == 1) return list[0] - if (list.size == 2) return "${list[0]}$delimiterColor and ${list[1]}" - val lastIndex = list.size - 1 - val allButLast = list.subList(0, lastIndex).joinToString("$delimiterColor, ") - return "$allButLast$delimiterColor, and ${list[lastIndex]}" + fun List<String>.createCommaSeparatedList(delimiterColor: String = ""): String { + if (this.isEmpty()) return "" + if (this.size == 1) return this[0] + if (this.size == 2) return "${this[0]}$delimiterColor and ${this[1]}" + val lastIndex = this.size - 1 + val allButLast = this.subList(0, lastIndex).joinToString("$delimiterColor, ") + return "$allButLast$delimiterColor, and ${this[lastIndex]}" } fun pluralize(number: Int, singular: String, plural: String? = null, withNumber: Boolean = false): String { |