1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
package at.hannibal2.skyhanni.features.dungeon
import at.hannibal2.skyhanni.data.ProfileStorageData
import at.hannibal2.skyhanni.data.ScoreboardData
import at.hannibal2.skyhanni.events.DungeonBossRoomEnterEvent
import at.hannibal2.skyhanni.events.DungeonEnterEvent
import at.hannibal2.skyhanni.events.DungeonStartEvent
import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent
import at.hannibal2.skyhanni.events.LorenzChatEvent
import at.hannibal2.skyhanni.events.LorenzTickEvent
import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
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.addOrPut
import at.hannibal2.skyhanni.utils.LorenzUtils.equalsOneOf
import at.hannibal2.skyhanni.utils.LorenzUtils.getOrNull
import at.hannibal2.skyhanni.utils.NumberUtil.formatNumber
import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimalIfNeeded
import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
import at.hannibal2.skyhanni.utils.StringUtils.removeColor
import at.hannibal2.skyhanni.utils.TabListData
import net.minecraft.item.ItemStack
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
class DungeonAPI {
private val floorPattern = " §7⏣ §cThe Catacombs §7\\((?<floor>.*)\\)".toPattern()
private val uniqueClassBonus =
"^Your ([A-Za-z]+) stats are doubled because you are the only player using this class!$".toRegex()
private val bossPattern =
"View all your (?<name>\\w+) Collection".toPattern()
private val levelPattern =
" +(?<kills>\\d+).*".toPattern()
private val killPattern = " +☠ Defeated (?<boss>\\w+).*".toPattern()
private val totalKillsPattern = "§7Total Kills: §e(?<kills>.*)".toPattern()
companion object {
var dungeonFloor: String? = null
var started = false
var inBossRoom = false
var playerClass: DungeonClass? = null
var playerClassLevel = -1
var isUniqueClass = false
val bossStorage: MutableMap<DungeonFloor, Int>? get() = ProfileStorageData.profileSpecific?.dungeons?.bosses
private val timePattern =
"Time Elapsed:( )?(?:(?<minutes>\\d+)m)? (?<seconds>\\d+)s".toPattern() // Examples: Time Elapsed: 10m 10s, Time Elapsed: 2s
fun inDungeon() = dungeonFloor != null
fun isOneOf(vararg floors: String) = dungeonFloor?.equalsOneOf(floors) == true
fun handleBossMessage(rawMessage: String) {
if (!inDungeon()) return
val message = rawMessage.removeColor()
val bossName = message.substringAfter("[BOSS] ").substringBefore(":").trim()
if (bossName != "The Watcher" && dungeonFloor != null && checkBossName(dungeonFloor!!, bossName) &&
!inBossRoom) {
DungeonBossRoomEnterEvent().postAndCatch()
inBossRoom = true
}
}
private fun checkBossName(floor: String, bossName: String): Boolean {
val correctBoss = when (floor) {
"E" -> "The Watcher"
"F1", "M1" -> "Bonzo"
"F2", "M2" -> "Scarf"
"F3", "M3" -> "The Professor"
"F4", "M4" -> "Thorn"
"F5", "M5" -> "Livid"
"F6", "M6" -> "Sadan"
"F7", "M7" -> "Maxor"
else -> null
} ?: return false
// Livid has a prefix in front of the name, so we check ends with to cover all the livids
return bossName.endsWith(correctBoss)
}
fun getTime(): String {
loop@ for (line in ScoreboardData.sidebarLinesFormatted) {
timePattern.matchMatcher(line.removeColor()) {
if (!matches()) continue@loop
return "${group("minutes") ?: "00"}:${group("seconds")}" // 03:14
}
}
return ""
}
fun getCurrentBoss(): DungeonFloor? {
val floor = dungeonFloor ?: return null
return DungeonFloor.valueOf(floor.replace("M", "F"))
}
fun getRoomID() = ScoreboardData.sidebarLines.firstOrNull()?.removeColor()?.split(" ")?.getOrNull(2)
}
@SubscribeEvent
fun onTick(event: LorenzTickEvent) {
if (dungeonFloor == null) {
for (line in ScoreboardData.sidebarLinesFormatted) {
floorPattern.matchMatcher(line) {
val floor = group("floor")
dungeonFloor = floor
DungeonEnterEvent(floor).postAndCatch()
}
}
}
if (dungeonFloor != null && playerClass == null) {
val playerTeam =
TabListData.getTabList().firstOrNull {
it.contains(LorenzUtils.getPlayerName())
}?.removeColor() ?: ""
DungeonClass.entries.forEach {
if (playerTeam.contains("(${it.scoreboardName} ")) {
val level = playerTeam.split(" ").last().trimEnd(')').romanToDecimalIfNeeded()
playerClass = it
playerClassLevel = level
}
}
}
}
@SubscribeEvent
fun onWorldChange(event: LorenzWorldChangeEvent) {
dungeonFloor = null
started = false
inBossRoom = false
isUniqueClass = false
playerClass = null
playerClassLevel = -1
}
@SubscribeEvent
fun onChatMessage(event: LorenzChatEvent) {
val floor = dungeonFloor
if (floor != null) {
if (event.message == "§e[NPC] §bMort§f: §rHere, I found this map when I first entered the dungeon.") {
started = true
DungeonStartEvent(floor).postAndCatch()
}
if (event.message.removeColor().matches(uniqueClassBonus)) {
isUniqueClass = true
}
}
}
// This returns a map of boss name to the integer for the amount of kills the user has in the collection
@SubscribeEvent
fun onInventoryOpen(event: InventoryFullyOpenedEvent) {
val bossCollections = bossStorage ?: return
if (event.inventoryName == "Boss Collections") {
readallCollections(bossCollections, event.inventoryItems)
} else if (event.inventoryName.endsWith(" Collection")) {
readOneMaxCollection(bossCollections, event.inventoryItems, event.inventoryName)
}
}
private fun readOneMaxCollection(
bossCollections: MutableMap<DungeonFloor, Int>,
inventoryItems: Map<Int, ItemStack>,
inventoryName: String
) {
inventoryItems[48]?.let { item ->
if (item.name == "§aGo Back") {
item.getLore().getOrNull(0)?.let { firstLine ->
if (firstLine == "§7To Boss Collections") {
val name = inventoryName.split(" ").dropLast(1).joinToString(" ")
val floor = DungeonFloor.byBossName(name) ?: return
val lore = inventoryItems[4]?.getLore() ?: return
val line = lore.find { it.contains("Total Kills:") } ?: return
val kills = totalKillsPattern.matchMatcher(line) {
group("kills").formatNumber().toInt()
} ?: return
bossCollections[floor] = kills
}
}
}
}
}
private fun readallCollections(
bossCollections: MutableMap<DungeonFloor, Int>,
inventoryItems: Map<Int, ItemStack>,
) {
nextItem@ for (stack in inventoryItems.values) {
var name = ""
var kills = 0
nextLine@ for (line in stack.getLore()) {
val colorlessLine = line.removeColor()
bossPattern.matchMatcher(colorlessLine) {
if (matches()) {
name = group("name")
}
}
levelPattern.matchMatcher(colorlessLine) {
if (matches()) {
kills = group("kills").toInt()
break@nextLine
}
}
}
val floor = DungeonFloor.byBossName(name) ?: continue
bossCollections[floor] = kills
}
}
@SubscribeEvent
fun onChat(event: LorenzChatEvent) {
if (!LorenzUtils.inDungeons) return
killPattern.matchMatcher(event.message.removeColor()) {
val bossCollections = bossStorage ?: return
val boss =
|