aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/data/IslandGraphs.kt
blob: 02a065abe63a894c3cd3cdfb1f8398f46eb12bec (plain)
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package at.hannibal2.skyhanni.data

import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.data.model.Graph
import at.hannibal2.skyhanni.data.model.GraphNode
import at.hannibal2.skyhanni.data.model.findShortestPathAsGraphWithDistance
import at.hannibal2.skyhanni.data.repo.RepoUtils
import at.hannibal2.skyhanni.events.IslandChangeEvent
import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent
import at.hannibal2.skyhanni.events.LorenzTickEvent
import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
import at.hannibal2.skyhanni.events.RepositoryReloadEvent
import at.hannibal2.skyhanni.features.misc.IslandAreas
import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule
import at.hannibal2.skyhanni.test.SkyHanniDebugsAndTests
import at.hannibal2.skyhanni.utils.LocationUtils
import at.hannibal2.skyhanni.utils.LocationUtils.canBeSeen
import at.hannibal2.skyhanni.utils.LocationUtils.distanceSqToPlayer
import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer
import at.hannibal2.skyhanni.utils.LorenzColor
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.LorenzVec
import at.hannibal2.skyhanni.utils.RenderUtils.draw3DLine
import at.hannibal2.skyhanni.utils.RenderUtils.draw3DPathWithWaypoint
import at.hannibal2.skyhanni.utils.RenderUtils.drawWaypointFilled
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import java.awt.Color
import java.io.File
import kotlin.math.abs

/**
 * TODO
 * benefits of every island graphs:
 * global:
 * 	NEU's fairy souls
 * 	point of interest
 * 	slayer area
 * 	NEU's NPC's
 * 	races (end, park, winter, dungeon hub)
 * 	jump pads between servers
 * 	ring of love/romeo juliet quest
 * 	death location
 * hub:
 * 	12 starter NPC's
 * 	diana
 * farming:
 * 	pelt farming
 * rift:
 * 	enigma souls
 * 	eyes
 * 	big quests
 * spider:
 * 	relicts + throw spot
 * dwarven mines:
 * 	emissary
 * 	commssion areas
 * 	events: raffle, goblin slayer, donpieresso
 * deep
 * 	path to the bottom
 * end
 * 	golem spawn
 * 	dragon death spot
 * crimson
 *  vanquisher path
 *  area mini bosses
 *  daily quests
 *  intro tutorials with elle
 *
 * graph todo:
 * 	fix rename not using tick but input event we have (+ create the input event in the first place)
 * 	toggle distance to node by node path lengh, instead of eye of sight lenght
 * 	press test button again to enable "true test mode", with graph math and hiding other stuff
 * 	option to compare two graphs, and store multiple graphs in the edit mode in paralell
 */

@SkyHanniModule
object IslandGraphs {
    var currentIslandGraph: Graph? = null

    val existsForThisIsland get() = currentIslandGraph != null

    var closedNote: GraphNode? = null

    private var currentTarget: LorenzVec? = null
    private var color = Color.WHITE
    private var showGoalExact = false
    private var onFound: () -> Unit = {}
    private var goal: GraphNode? = null
        set(value) {
            prevGoal = field
            field = value
        }
    private var prevGoal: GraphNode? = null

    private var fastestPath: Graph? = null
    private var condition: () -> Boolean = { true }

    @SubscribeEvent
    fun onRepoReload(event: RepositoryReloadEvent) {
        if (!LorenzUtils.inSkyBlock) return

        reloadFromJson(LorenzUtils.skyBlockIsland)
    }

    @SubscribeEvent
    fun onIslandChange(event: IslandChangeEvent) {
        reloadFromJson(event.newIsland)
    }

    @SubscribeEvent
    fun onWorldChange(event: LorenzWorldChangeEvent) {
        reset()
    }

    private fun reloadFromJson(newIsland: IslandType) {
        val islandName = newIsland.name
        val constant = "island_graphs/$islandName"
        val name = "constants/$constant.json"
        val jsonFile = File(SkyHanniMod.repo.repoLocation, name)
        if (!jsonFile.isFile) {
            currentIslandGraph = null
            return
        }

        val graph = RepoUtils.getConstant(SkyHanniMod.repo.repoLocation, constant, Graph.gson, Graph::class.java)
        setNewGraph(graph)
    }

    fun setNewGraph(graph: Graph) {
        reset()
        currentIslandGraph = graph
    }

    private fun reset() {
        closedNote = null
        currentTarget = null
        goal = null
        fastestPath = null
    }

    @SubscribeEvent
    fun onTick(event: LorenzTickEvent) {
        if (!LorenzUtils.inSkyBlock) return
        val prevClosed = closedNote

        val graph = currentIslandGraph ?: return

        currentTarget?.let {
            if (it.distanceToPlayer() < 3) {
                onFound()
                reset()
            }
            if (!condition()) {
                reset()
            }
        }

        val newClosest = if (SkyHanniDebugsAndTests.c == 0.0) {
            graph.minBy { it.position.distanceSqToPlayer() }
        } else null
        if (closedNote == newClosest) return
        closedNote = newClosest
        onNewNote()
        val closest = closedNote ?: return
        val goal = goal ?: return
        if (closest == prevClosed) return

        val (path, distance) = graph.findShortestPathAsGraphWithDistance(closest, goal)
        val first = path.firstOrNull()
        val second = path.getOrNull(1)

        val playerPosition = LocationUtils.playerLocation()
        val nodeDistance = first?.let { playerPosition.distance(it.position) } ?: 0.0
        if (first != null && second != null) {
            val direct = playerPosition.distance(second.position)
            val firstPath = first.neighbours[second] ?: 0.0
            val around = nodeDistance + firstPath
            if (direct < around) {
                setFastestPath(Graph(path.drop(1)) to (distance - firstPath + direct))
                return
            }
        }
        setFastestPath(path to (distance + nodeDistance))
    }

    private fun setFastestPath(path: Pair<Graph, Double>) {
        fastestPath = path.takeIf { it.first.isNotEmpty() }?.first

        fastestPath?.let {
            fastestPath = Graph(cutByMaxDistance(it.nodes, 3.0))
        }
    }

    private fun onNewNote() {
        // TODO create an event
        IslandAreas.noteMoved()
    }

    fun stop() {
        currentTarget = null
        goal = null
        fastestPath = null
    }

    fun pathFind(
        location: LorenzVec,
        color: Color = LorenzColor.WHITE.toColor(),
        onFound: () -> Unit = {},
        showGoalExact: Boolean = false,
        condition: () -> Boolean = { true },
    ) {
        reset()
        currentTarget = location
        this.color = color
        this.onFound = onFound
        this.showGoalExact = showGoalExact
        this.condition = condition
        val graph = currentIslandGraph ?: return
        goal = graph.minBy { it.position.distance(currentTarget!!) }
    }

    @SubscribeEvent
    fun onRenderWorld(event: LorenzRenderWorldEvent) {
        if (!LorenzUtils.inSkyBlock) return
        val path = fastestPath ?: return

        var graph = path
        graph = skipNodes(graph) ?: graph

        event.draw3DPathWithWaypoint(
            graph,
            color,
            6,
            true,
            bezierPoint = 2.0,
            textSize = 1.0,
        )
        val lastNode = graph.nodes.last().position
        val targetLocation = currentTarget ?: return
        event.draw3DLine(lastNode.add(0.5, 0.5, 0.5), targetLocation.add(0.5, 0.5, 0.5), color, 4, true)

        if (showGoalExact) {
            event.drawWaypointFilled(targetLocation, color)
        }
    }

    // TODO move into new utils class
    private fun cutByMaxDistance(nodes: List<GraphNode>, maxDistance: Double): List<GraphNode> {
        var index = nodes.size * 10
        val locations = mutableListOf<LorenzVec>()
        var first = true
        for (node in nodes) {
            if (first) {
                first = false
            } else {
                var lastPosition = locations.last()
                val currentPosition = node.position
                val vector = (currentPosition - lastPosition).normalize()
                var distance = lastPosition.distance(currentPosition)
                while (distance > maxDistance) {
                    distance -= maxDistance
                    val nextStepDistance = if (distance < maxDistance / 2) {
                        (maxDistance + distance) / 2
                        break
                    } else maxDistance
                    val newPosition = lastPosition + (vector * (nextStepDistance))
                    locations.add(newPosition)
                    lastPosition = newPosition
                }
            }
            locations.add(node.position)
        }

        return locations.map { GraphNode(index++, it) }
    }

    // trying to find a faster node-path, if the future nodes are in line of sight and gratly beneift the current path
    private fun skipNodes(graph: Graph): Graph? {
        val closedNode = closedNote ?: return null

        val playerEyeLocation = LocationUtils.playerEyeLocation()
        val playerY = playerEyeLocation.y - 1

        val distanceToPlayer = closedNode.position.distanceToPlayer()
        val skipNodeDistance = distanceToPlayer > 8
        val maxSkipDistance = if (skipNodeDistance) 50.0 else 20.0

        val nodes = graph.nodes
        val potentialSkip =
            nodes.lastOrNull { it.position.canBeSeen(maxSkipDistance, -1.0) && abs(it.position.y - playerY) <= 2 }
                ?: return null

        val angleSkip = if (potentialSkip == nodes.first()) {
            false
        } else {
            val v1 = potentialSkip.position - playerEyeLocation
            val v2 = nodes.first().position - playerEyeLocation
            val v = v1.angleInRad(v2)
            v > 1
        }

        if (!skipNodeDistance && !angleSkip) return null

        val list = mutableListOf<GraphNode>()
        list.add(potentialSkip)

        var passed = false
        for (node in nodes) {
            if (passed) {
                list.add(node)
            } else {
                if (node == potentialSkip) {
                    passed = true
                }
            }
        }

        return Graph(list)
    }
}