mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 14:38:32 +08:00
Hunt optimizer results are now calculated again.
This commit is contained in:
parent
e21bce7695
commit
478842ddab
@ -10,6 +10,8 @@ external interface JsArray<T> {
|
||||
fun slice(start: Int = definedExternally): JsArray<T>
|
||||
fun slice(start: Int, end: Int = definedExternally): JsArray<T>
|
||||
|
||||
fun some(callback: (element: T, index: Int) -> Boolean): Boolean
|
||||
|
||||
fun splice(start: Int, deleteCount: Int = definedExternally): JsArray<T>
|
||||
fun splice(start: Int, deleteCount: Int, vararg items: T): JsArray<T>
|
||||
}
|
||||
@ -31,3 +33,47 @@ inline fun <T> Array<T>.asJsArray(): JsArray<T> =
|
||||
|
||||
inline fun <T> List<T>.toJsArray(): JsArray<T> =
|
||||
toTypedArray().asJsArray()
|
||||
|
||||
@Suppress("unused")
|
||||
external interface JsPair<out A, out B>
|
||||
|
||||
inline val <T> JsPair<T, *>.first: T get() = asDynamic()[0].unsafeCast<T>()
|
||||
inline val <T> JsPair<*, T>.second: T get() = asDynamic()[1].unsafeCast<T>()
|
||||
|
||||
inline operator fun <T> JsPair<T, *>.component1(): T = first
|
||||
inline operator fun <T> JsPair<*, T>.component2(): T = second
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
inline fun objectKeys(jsObject: dynamic): Array<String> =
|
||||
js("Object.keys(jsObject)").unsafeCast<Array<String>>()
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
inline fun objectEntries(jsObject: dynamic): Array<JsPair<String, dynamic>> =
|
||||
js("Object.entries(jsObject)").unsafeCast<Array<JsPair<String, dynamic>>>()
|
||||
|
||||
external interface JsSet<T> {
|
||||
val size: Int
|
||||
|
||||
fun add(value: T): JsSet<T>
|
||||
fun clear()
|
||||
fun delete(value: T): Boolean
|
||||
fun has(value: T): Boolean
|
||||
fun forEach(callback: (value: T) -> Unit)
|
||||
}
|
||||
|
||||
inline fun <T> emptyJsSet(): JsSet<T> =
|
||||
js("new Set()").unsafeCast<JsSet<T>>()
|
||||
|
||||
external interface JsMap<K, V> {
|
||||
val size: Int
|
||||
|
||||
fun clear()
|
||||
fun delete(key: K): Boolean
|
||||
fun forEach(callback: (value: V, key: K) -> Unit)
|
||||
fun get(key: K): V?
|
||||
fun has(key: K): Boolean
|
||||
fun set(key: K, value: V): JsMap<K, V>
|
||||
}
|
||||
|
||||
inline fun <K, V> emptyJsMap(): JsMap<K, V> =
|
||||
js("new Map()").unsafeCast<JsMap<K, V>>()
|
||||
|
@ -0,0 +1,71 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.unsafeToNonNull
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
/**
|
||||
* Starts observing its dependencies when the first observer on this val is registered. Stops
|
||||
* observing its dependencies when the last observer on this val is disposed. This way no extra
|
||||
* disposables need to be managed when e.g. [map] is used.
|
||||
*/
|
||||
abstract class AbstractDependentVal<T>(
|
||||
private val dependencies: Iterable<Val<*>>,
|
||||
) : AbstractVal<T>() {
|
||||
/**
|
||||
* Is either empty or has a disposable per dependency.
|
||||
*/
|
||||
private val dependencyObservers = mutableListOf<Disposable>()
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
*/
|
||||
protected var hasObservers = false
|
||||
|
||||
protected var _value: T? = null
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
return _value.unsafeToNonNull()
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
if (dependencyObservers.isEmpty()) {
|
||||
hasObservers = true
|
||||
|
||||
dependencies.forEach { dependency ->
|
||||
dependencyObservers.add(
|
||||
dependency.observe {
|
||||
val oldValue = _value
|
||||
_value = computeValue()
|
||||
|
||||
if (_value != oldValue) {
|
||||
emit()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
|
||||
if (observers.isEmpty()) {
|
||||
hasObservers = false
|
||||
dependencyObservers.forEach { it.dispose() }
|
||||
dependencyObservers.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun computeValue(): T
|
||||
}
|
@ -1,71 +1,8 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.unsafeToNonNull
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
/**
|
||||
* Starts observing its dependencies when the first observer on this val is registered. Stops
|
||||
* observing its dependencies when the last observer on this val is disposed. This way no extra
|
||||
* disposables need to be managed when e.g. [map] is used.
|
||||
*/
|
||||
abstract class DependentVal<T>(
|
||||
private val dependencies: Iterable<Val<*>>,
|
||||
) : AbstractVal<T>() {
|
||||
/**
|
||||
* Is either empty or has a disposable per dependency.
|
||||
*/
|
||||
private val dependencyObservers = mutableListOf<Disposable>()
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
*/
|
||||
protected var hasObservers = false
|
||||
|
||||
protected var _value: T? = null
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
return _value.unsafeToNonNull()
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
if (dependencyObservers.isEmpty()) {
|
||||
hasObservers = true
|
||||
|
||||
dependencies.forEach { dependency ->
|
||||
dependencyObservers.add(
|
||||
dependency.observe {
|
||||
val oldValue = _value
|
||||
_value = computeValue()
|
||||
|
||||
if (_value != oldValue) {
|
||||
emit()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
|
||||
if (observers.isEmpty()) {
|
||||
hasObservers = false
|
||||
dependencyObservers.forEach { it.dispose() }
|
||||
dependencyObservers.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun computeValue(): T
|
||||
class DependentVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
private val compute: () -> T,
|
||||
) : AbstractDependentVal<T>(dependencies) {
|
||||
override fun computeValue(): T = compute()
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import world.phantasmal.observable.Observer
|
||||
class FlatMappedVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
private val compute: () -> Val<T>,
|
||||
) : DependentVal<T>(dependencies) {
|
||||
) : AbstractDependentVal<T>(dependencies) {
|
||||
private var computedVal: Val<T>? = null
|
||||
private var computedValObserver: Disposable? = null
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
class MappedVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
private val compute: () -> T,
|
||||
) : DependentVal<T>(dependencies) {
|
||||
override fun computeValue(): T = compute()
|
||||
}
|
@ -24,7 +24,7 @@ interface Val<out T> : Observable<T> {
|
||||
* @param transform called whenever this val changes
|
||||
*/
|
||||
fun <R> map(transform: (T) -> R): Val<R> =
|
||||
MappedVal(listOf(this)) { transform(value) }
|
||||
DependentVal(listOf(this)) { transform(value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function that returns a val over this val. The resulting val will change
|
||||
|
@ -40,7 +40,7 @@ fun <T1, T2, R> map(
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> R,
|
||||
): Val<R> =
|
||||
MappedVal(listOf(v1, v2)) { transform(v1.value, v2.value) }
|
||||
DependentVal(listOf(v1, v2)) { transform(v1.value, v2.value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function over 3 vals.
|
||||
@ -53,7 +53,7 @@ fun <T1, T2, T3, R> map(
|
||||
v3: Val<T3>,
|
||||
transform: (T1, T2, T3) -> R,
|
||||
): Val<R> =
|
||||
MappedVal(listOf(v1, v2, v3)) { transform(v1.value, v2.value, v3.value) }
|
||||
DependentVal(listOf(v1, v2, v3)) { transform(v1.value, v2.value, v3.value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function that returns a val over 2 vals. The resulting val will change when
|
||||
|
@ -1,5 +1,8 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.observable.value.list.DependentListVal
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
|
||||
infix fun <T> Val<T>.eq(value: T): Val<Boolean> =
|
||||
map { it == value }
|
||||
|
||||
@ -56,3 +59,6 @@ fun Val<String>.isBlank(): Val<Boolean> =
|
||||
|
||||
fun Val<String>.isNotBlank(): Val<Boolean> =
|
||||
map { it.isNotBlank() }
|
||||
|
||||
fun <T> Val<List<T>>.toListVal(): ListVal<T> =
|
||||
DependentListVal(listOf(this)) { value }
|
||||
|
@ -6,6 +6,8 @@ import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.AbstractVal
|
||||
import world.phantasmal.observable.value.DependentVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
abstract class AbstractListVal<E>(
|
||||
protected val elements: MutableList<E>,
|
||||
@ -59,6 +61,9 @@ abstract class AbstractListVal<E>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun firstOrNull(): Val<E?> =
|
||||
DependentVal(listOf(this)) { elements.firstOrNull() }
|
||||
|
||||
/**
|
||||
* Does the following in the given order:
|
||||
* - Updates element observers
|
||||
|
@ -37,7 +37,7 @@ class DependentListVal<E>(
|
||||
return elements
|
||||
}
|
||||
|
||||
override val sizeVal: Val<Int> = _sizeVal
|
||||
override val size: Val<Int> = _sizeVal
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
@ -35,7 +35,7 @@ class FilteredListVal<E>(
|
||||
return elements
|
||||
}
|
||||
|
||||
override val sizeVal: Val<Int> = _sizeVal
|
||||
override val size: Val<Int> = _sizeVal
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
@ -4,7 +4,7 @@ import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
interface ListVal<E> : Val<List<E>> {
|
||||
val sizeVal: Val<Int>
|
||||
val size: Val<Int>
|
||||
|
||||
operator fun get(index: Int): E
|
||||
|
||||
@ -18,4 +18,6 @@ interface ListVal<E> : Val<List<E>> {
|
||||
|
||||
fun filtered(predicate: (E) -> Boolean): ListVal<E> =
|
||||
FilteredListVal(this, predicate)
|
||||
|
||||
fun firstOrNull(): Val<E?>
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class SimpleListVal<E>(
|
||||
replaceAll(value)
|
||||
}
|
||||
|
||||
override val sizeVal: Val<Int> = _sizeVal
|
||||
override val size: Val<Int> = _sizeVal
|
||||
|
||||
override operator fun get(index: Int): E =
|
||||
elements[index]
|
||||
|
@ -4,11 +4,14 @@ import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.stubDisposable
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.StaticVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.value
|
||||
|
||||
class StaticListVal<E>(private val elements: List<E>) : ListVal<E> {
|
||||
override val sizeVal: Val<Int> = value(elements.size)
|
||||
private val firstOrNull = StaticVal(elements.firstOrNull())
|
||||
|
||||
override val size: Val<Int> = value(elements.size)
|
||||
|
||||
override val value: List<E> = elements
|
||||
|
||||
@ -32,4 +35,6 @@ class StaticListVal<E>(private val elements: List<E>) : ListVal<E> {
|
||||
|
||||
return stubDisposable()
|
||||
}
|
||||
|
||||
override fun firstOrNull(): Val<E?> = firstOrNull
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.observable.ObservableAndEmit
|
||||
|
||||
class DependentValTests : RegularValTests() {
|
||||
override fun create(): ObservableAndEmit<*, DependentVal<*>> {
|
||||
val v = SimpleVal(0)
|
||||
val value = DependentVal(listOf(v)) { 2 * v.value }
|
||||
return ObservableAndEmit(value) { v.value += 2 }
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): DependentVal<T> {
|
||||
val v = SimpleVal(value)
|
||||
return DependentVal(listOf(v)) { v.value }
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.observable.ObservableAndEmit
|
||||
|
||||
class MappedValTests : RegularValTests() {
|
||||
override fun create(): ObservableAndEmit<*, MappedVal<*>> {
|
||||
val v = SimpleVal(0)
|
||||
val value = MappedVal(listOf(v)) { 2 * v.value }
|
||||
return ObservableAndEmit(value) { v.value += 2 }
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): MappedVal<T> {
|
||||
val v = SimpleVal(value)
|
||||
return MappedVal(listOf(v)) { v.value }
|
||||
}
|
||||
}
|
@ -88,7 +88,7 @@ class FilteredListValTests : ListValTests() {
|
||||
event = it
|
||||
})
|
||||
|
||||
for (i in 0 until dep.sizeVal.value) {
|
||||
for (i in 0 until dep.size.value) {
|
||||
event = null
|
||||
|
||||
// Make an even number odd or an odd number even so that the . List should emit a Change event.
|
||||
@ -108,7 +108,7 @@ class FilteredListValTests : ListValTests() {
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until dep.sizeVal.value) {
|
||||
for (i in 0 until dep.size.value) {
|
||||
event = null
|
||||
|
||||
// Change a value, but keep even numbers even and odd numbers odd. List should emit an
|
||||
|
@ -18,21 +18,21 @@ abstract class ListValTests : ValTests() {
|
||||
abstract override fun create(): ListValAndAdd<*, ListVal<*>>
|
||||
|
||||
@Test
|
||||
fun listVal_updates_sizeVal_correctly() = test {
|
||||
fun listVal_updates_size_correctly() = test {
|
||||
val (list: ListVal<*>, add) = create()
|
||||
|
||||
assertEquals(0, list.sizeVal.value)
|
||||
assertEquals(0, list.size.value)
|
||||
|
||||
var observedSize = 0
|
||||
|
||||
disposer.add(
|
||||
list.sizeVal.observe { observedSize = it.value }
|
||||
list.size.observe { observedSize = it.value }
|
||||
)
|
||||
|
||||
for (i in 1..3) {
|
||||
add()
|
||||
|
||||
assertEquals(i, list.sizeVal.value)
|
||||
assertEquals(i, list.size.value)
|
||||
assertEquals(i, observedSize)
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ dependencies {
|
||||
implementation(npm("golden-layout", "^1.5.9"))
|
||||
implementation(npm("monaco-editor", "0.20.0"))
|
||||
implementation(npm("three", "^0.122.0"))
|
||||
implementation(npm("javascript-lp-solver", "0.4.17"))
|
||||
|
||||
implementation(devNpm("file-loader", "^6.0.0"))
|
||||
implementation(devNpm("monaco-editor-webpack-plugin", "1.9.0"))
|
||||
|
@ -1,8 +1,14 @@
|
||||
package world.phantasmal.web.core.dom
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.webui.dom.appendHtmlEl
|
||||
import world.phantasmal.webui.dom.span
|
||||
|
||||
private val ASSET_BASE_PATH: String = window.location.pathname.removeSuffix("/") + "/assets"
|
||||
|
||||
fun Node.externalLink(href: String, block: HTMLAnchorElement.() -> Unit) =
|
||||
appendHtmlEl<HTMLAnchorElement>("A") {
|
||||
@ -11,3 +17,13 @@ fun Node.externalLink(href: String, block: HTMLAnchorElement.() -> Unit) =
|
||||
this.href = href
|
||||
block()
|
||||
}
|
||||
|
||||
fun Node.sectionIdIcon(sectionId: SectionId, size: Int): HTMLElement =
|
||||
span {
|
||||
style.display = "inline-block"
|
||||
style.width = "${size}px"
|
||||
style.height = "${size}px"
|
||||
style.backgroundImage = "url($ASSET_BASE_PATH/images/sectionids/${sectionId}.png)"
|
||||
style.backgroundSize = "${size}px"
|
||||
title = sectionId.name
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package world.phantasmal.web.core.stores
|
||||
|
||||
import world.phantasmal.core.JsMap
|
||||
import world.phantasmal.core.emptyJsMap
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.models.Server
|
||||
@ -23,12 +25,34 @@ class ItemDropStore(
|
||||
private suspend fun loadEnemyDropTable(server: Server): EnemyDropTable {
|
||||
val drops = assetLoader.load<List<EnemyDrop>>("/enemy_drops.${server.slug}.json")
|
||||
|
||||
val table = mutableMapOf<Triple<Difficulty, SectionId, NpcType>, EnemyDrop>()
|
||||
val itemTypeToDrops = mutableMapOf<Int, MutableList<EnemyDrop>>()
|
||||
val table = emptyJsMap<Difficulty, JsMap<SectionId, JsMap<NpcType, EnemyDrop>>>()
|
||||
val itemTypeToDrops = emptyJsMap<Int, MutableList<EnemyDrop>>()
|
||||
|
||||
for (drop in drops) {
|
||||
table[Triple(drop.difficulty, drop.sectionId, drop.enemy)] = drop
|
||||
itemTypeToDrops.getOrPut(drop.itemTypeId) { mutableListOf() }.add(drop)
|
||||
var diffTable = table.get(drop.difficulty)
|
||||
|
||||
if (diffTable == null) {
|
||||
diffTable = emptyJsMap()
|
||||
table.set(drop.difficulty, diffTable)
|
||||
}
|
||||
|
||||
var sectionIdTable = diffTable.get(drop.sectionId)
|
||||
|
||||
if (sectionIdTable == null) {
|
||||
sectionIdTable = emptyJsMap()
|
||||
diffTable.set(drop.sectionId, sectionIdTable)
|
||||
}
|
||||
|
||||
sectionIdTable.set(drop.enemy, drop)
|
||||
|
||||
var itemTypeDrops = itemTypeToDrops.get(drop.itemTypeId)
|
||||
|
||||
if (itemTypeDrops == null) {
|
||||
itemTypeDrops = mutableListOf()
|
||||
itemTypeToDrops.set(drop.itemTypeId, itemTypeDrops)
|
||||
}
|
||||
|
||||
itemTypeDrops.add(drop)
|
||||
}
|
||||
|
||||
return EnemyDropTable(table, itemTypeToDrops)
|
||||
@ -36,15 +60,15 @@ class ItemDropStore(
|
||||
}
|
||||
|
||||
class EnemyDropTable(
|
||||
private val table: Map<Triple<Difficulty, SectionId, NpcType>, EnemyDrop>,
|
||||
private val table: JsMap<Difficulty, JsMap<SectionId, JsMap<NpcType, EnemyDrop>>>,
|
||||
/**
|
||||
* Mapping of [ItemType] ids to [EnemyDrop]s.
|
||||
*/
|
||||
private val itemTypeToDrops: Map<Int, List<EnemyDrop>>,
|
||||
private val itemTypeToDrops: JsMap<Int, MutableList<EnemyDrop>>,
|
||||
) {
|
||||
fun getDrop(difficulty: Difficulty, sectionId: SectionId, npcType: NpcType): EnemyDrop? =
|
||||
table[Triple(difficulty, sectionId, npcType)]
|
||||
table.get(difficulty)?.get(sectionId)?.get(npcType)
|
||||
|
||||
fun getDropsForItemType(itemType: ItemType): List<EnemyDrop> =
|
||||
itemTypeToDrops[itemType.id] ?: emptyList()
|
||||
itemTypeToDrops.get(itemType.id) ?: emptyList()
|
||||
}
|
||||
|
20
web/src/main/kotlin/world/phantasmal/web/externals/javascriptLpSolver/javascriptLpSolver.kt
vendored
Normal file
20
web/src/main/kotlin/world/phantasmal/web/externals/javascriptLpSolver/javascriptLpSolver.kt
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
@file:Suppress("FunctionName")
|
||||
|
||||
package world.phantasmal.web.externals.javascriptLpSolver
|
||||
|
||||
external interface IModel {
|
||||
var optimize: String
|
||||
|
||||
/**
|
||||
* "max" or "min".
|
||||
*/
|
||||
var opType: String
|
||||
var constraints: dynamic
|
||||
var variables: dynamic
|
||||
}
|
||||
|
||||
@JsModule("javascript-lp-solver")
|
||||
@JsNonModule
|
||||
external object Solver {
|
||||
fun Solve(model: IModel): dynamic
|
||||
}
|
@ -6,10 +6,7 @@ import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.stores.ItemDropStore
|
||||
import world.phantasmal.web.core.stores.ItemTypeStore
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
|
||||
import world.phantasmal.web.huntOptimizer.controllers.MethodsController
|
||||
import world.phantasmal.web.huntOptimizer.controllers.MethodsForEpisodeController
|
||||
import world.phantasmal.web.huntOptimizer.controllers.WantedItemsController
|
||||
import world.phantasmal.web.huntOptimizer.controllers.*
|
||||
import world.phantasmal.web.huntOptimizer.persistence.HuntMethodPersister
|
||||
import world.phantasmal.web.huntOptimizer.persistence.WantedItemPersister
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntMethodStore
|
||||
@ -43,22 +40,23 @@ class HuntOptimizer(
|
||||
itemDropStore,
|
||||
))
|
||||
|
||||
// Controllers
|
||||
val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore))
|
||||
val wantedItemsController = addDisposable(WantedItemsController(huntOptimizerStore))
|
||||
val methodsController = addDisposable(MethodsController(uiStore))
|
||||
|
||||
// Main Widget
|
||||
return HuntOptimizerWidget(
|
||||
ctrl = huntOptimizerController,
|
||||
ctrl = addDisposable(HuntOptimizerController(uiStore)),
|
||||
createOptimizerWidget = {
|
||||
OptimizerWidget(
|
||||
{ WantedItemsWidget(wantedItemsController) },
|
||||
{ OptimizationResultWidget() },
|
||||
createWantedItemsWidget = {
|
||||
WantedItemsWidget(addDisposable(WantedItemsController(huntOptimizerStore)))
|
||||
},
|
||||
createOptimizationResultWidget = {
|
||||
OptimizationResultWidget(
|
||||
addDisposable(OptimizationResultController(huntOptimizerStore))
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
createMethodsWidget = {
|
||||
MethodsWidget(methodsController) { episode ->
|
||||
MethodsWidget(addDisposable(MethodsController(uiStore))) { episode ->
|
||||
MethodsForEpisodeWidget(MethodsForEpisodeController(huntMethodStore, episode))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.toListVal
|
||||
import world.phantasmal.web.huntOptimizer.models.OptimalMethodModel
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntOptimizerStore
|
||||
import world.phantasmal.webui.controllers.Column
|
||||
import world.phantasmal.webui.controllers.TableController
|
||||
|
||||
class OptimizationResultController(
|
||||
huntOptimizerStore: HuntOptimizerStore,
|
||||
) : TableController<OptimalMethodModel>() {
|
||||
override val fixedColumns: Int = 4
|
||||
|
||||
override val values: ListVal<OptimalMethodModel> =
|
||||
huntOptimizerStore.optimizationResult.map { it.optimalMethods }.toListVal()
|
||||
|
||||
override val columns: ListVal<Column<OptimalMethodModel>> =
|
||||
huntOptimizerStore.optimizationResult.map {
|
||||
listOf<Column<OptimalMethodModel>>(
|
||||
Column(
|
||||
key = DIFF_COL,
|
||||
title = "Difficulty",
|
||||
width = 80,
|
||||
),
|
||||
Column(
|
||||
key = METHOD_COL,
|
||||
title = "Method",
|
||||
width = 250,
|
||||
),
|
||||
Column(
|
||||
key = EPISODE_COL,
|
||||
title = "Ep.",
|
||||
width = 40,
|
||||
),
|
||||
Column(
|
||||
key = SECTION_ID_COL,
|
||||
title = "Section ID",
|
||||
width = 90,
|
||||
),
|
||||
Column(
|
||||
key = TIME_PER_RUN_COL,
|
||||
title = "Time/Run",
|
||||
width = 90,
|
||||
textAlign = "center",
|
||||
),
|
||||
Column(
|
||||
key = RUNS_COL,
|
||||
title = "Runs",
|
||||
width = 60,
|
||||
textAlign = "right",
|
||||
tooltip = { it.runs.toString() }
|
||||
),
|
||||
Column(
|
||||
key = TOTAL_TIME_COL,
|
||||
title = "Total Hours",
|
||||
width = 60,
|
||||
textAlign = "right",
|
||||
tooltip = { it.totalTime.inHours.toString() }
|
||||
),
|
||||
)
|
||||
}.toListVal()
|
||||
|
||||
companion object {
|
||||
const val DIFF_COL = "diff"
|
||||
const val METHOD_COL = "method"
|
||||
const val EPISODE_COL = "episode"
|
||||
const val SECTION_ID_COL = "section_id"
|
||||
const val TIME_PER_RUN_COL = "time_per_run"
|
||||
const val RUNS_COL = "runs"
|
||||
const val TOTAL_TIME_COL = "total_time"
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ class WantedItemsController(
|
||||
huntOptimizerStore.huntableItems.filtered(filter)
|
||||
}
|
||||
|
||||
val wantedItems: ListVal<WantedItemModel> by lazy { huntOptimizerStore.wantedItems }
|
||||
val wantedItems: ListVal<WantedItemModel> = huntOptimizerStore.wantedItems
|
||||
|
||||
fun filterSelectableItems(text: String) {
|
||||
val sanitized = text.trim()
|
||||
|
@ -32,4 +32,12 @@ class HuntMethodModel(
|
||||
fun setUserTime(userTime: Duration?) {
|
||||
_userTime.value = userTime
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class.js != other::class.js) return false
|
||||
return id == (other as HuntMethodModel).id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
package world.phantasmal.web.huntOptimizer.models
|
||||
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.web.shared.dto.Difficulty
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import kotlin.time.Duration
|
||||
|
||||
class OptimalMethodModel(
|
||||
val difficulty: Difficulty,
|
||||
val sectionIds: List<SectionId>,
|
||||
val name: String,
|
||||
val episode: Episode,
|
||||
val methodTime: Duration,
|
||||
val runs: Double,
|
||||
val itemCounts: Map<ItemType, Double>,
|
||||
) {
|
||||
val totalTime: Duration = methodTime * runs
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package world.phantasmal.web.huntOptimizer.models
|
||||
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
|
||||
class OptimizationResultModel(
|
||||
val wantedItems: List<ItemType>,
|
||||
val optimalMethods: List<OptimalMethodModel>,
|
||||
)
|
@ -10,10 +10,10 @@ import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.shared.dto.QuestDto
|
||||
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
|
||||
import world.phantasmal.web.huntOptimizer.models.SimpleQuestModel
|
||||
import world.phantasmal.web.huntOptimizer.persistence.HuntMethodPersister
|
||||
import world.phantasmal.web.shared.dto.QuestDto
|
||||
import world.phantasmal.webui.stores.Store
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
@ -26,7 +26,7 @@ class HuntMethodStore(
|
||||
private val assetLoader: AssetLoader,
|
||||
private val huntMethodPersister: HuntMethodPersister,
|
||||
) : Store() {
|
||||
private val _methods = mutableListVal<HuntMethodModel>()
|
||||
private val _methods = mutableListVal<HuntMethodModel> { arrayOf(it.time) }
|
||||
|
||||
val methods: ListVal<HuntMethodModel> by lazy {
|
||||
observe(uiStore.server) { loadMethods(it) }
|
||||
|
@ -3,17 +3,32 @@ package world.phantasmal.web.huntOptimizer.stores
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.core.*
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.core.stores.EnemyDropTable
|
||||
import world.phantasmal.web.core.stores.ItemDropStore
|
||||
import world.phantasmal.web.core.stores.ItemTypeStore
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.externals.javascriptLpSolver.Solver
|
||||
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
|
||||
import world.phantasmal.web.huntOptimizer.models.OptimalMethodModel
|
||||
import world.phantasmal.web.huntOptimizer.models.OptimizationResultModel
|
||||
import world.phantasmal.web.huntOptimizer.models.WantedItemModel
|
||||
import world.phantasmal.web.huntOptimizer.persistence.WantedItemPersister
|
||||
import world.phantasmal.web.shared.dto.Difficulty
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.webui.obj
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
// TODO: take into account mothmants spawned from mothverts.
|
||||
// TODO: take into account split slimes.
|
||||
// TODO: Prefer methods that don't split pan arms over methods that do.
|
||||
@ -24,12 +39,13 @@ import world.phantasmal.webui.stores.Store
|
||||
class HuntOptimizerStore(
|
||||
private val wantedItemPersister: WantedItemPersister,
|
||||
private val uiStore: UiStore,
|
||||
private val huntMethodStore: HuntMethodStore,
|
||||
huntMethodStore: HuntMethodStore,
|
||||
private val itemTypeStore: ItemTypeStore,
|
||||
private val itemDropStore: ItemDropStore,
|
||||
) : Store() {
|
||||
private val _huntableItems = mutableListVal<ItemType>()
|
||||
private val _wantedItems = mutableListVal<WantedItemModel> { arrayOf(it.amount) }
|
||||
private val _optimizationResult = mutableVal(OptimizationResultModel(emptyList(), emptyList()))
|
||||
|
||||
val huntableItems: ListVal<ItemType> by lazy {
|
||||
observe(uiStore.server) { server ->
|
||||
@ -52,12 +68,20 @@ class HuntOptimizerStore(
|
||||
_wantedItems
|
||||
}
|
||||
|
||||
val optimizationResult: Val<OptimizationResultModel> = _optimizationResult
|
||||
|
||||
init {
|
||||
observe(wantedItems) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
wantedItemPersister.persistWantedItems(it, uiStore.server.value)
|
||||
}
|
||||
}
|
||||
|
||||
observe(wantedItems, huntMethodStore.methods) { wantedItems, huntMethods ->
|
||||
scope.launch(Dispatchers.Default) {
|
||||
_optimizationResult.value = optimize(wantedItems, huntMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addWantedItem(itemType: ItemType) {
|
||||
@ -79,4 +103,280 @@ class HuntOptimizerStore(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun optimize(
|
||||
wantedItems: List<WantedItemModel>,
|
||||
methods: List<HuntMethodModel>,
|
||||
): OptimizationResultModel {
|
||||
logger.debug { "Optimization start." }
|
||||
|
||||
val filteredWantedItems = wantedItems.filter { it.amount.value > 0 }
|
||||
|
||||
if (filteredWantedItems.isEmpty()) {
|
||||
logger.debug { "Optimization end, no wanted items to optimize for." }
|
||||
|
||||
return OptimizationResultModel(emptyList(), emptyList())
|
||||
}
|
||||
|
||||
val dropTable = itemDropStore.getEnemyDropTable(uiStore.server.value)
|
||||
|
||||
// Add a constraint per wanted item.
|
||||
val constraints: dynamic = obj {}
|
||||
|
||||
for (wanted in filteredWantedItems) {
|
||||
constraints[wanted.itemType.id] = obj { min = wanted.amount.value }
|
||||
}
|
||||
|
||||
// Add a variable to the LP model per method per difficulty per section ID.
|
||||
// When a method with pan arms is encountered, two variables are added. One for the method
|
||||
// with migiums and hidooms and one with pan arms.
|
||||
// Each variable has a time property to minimize and a property per item with the number of
|
||||
// enemies that drop the item multiplied by the corresponding drop rate as its value.
|
||||
val variables: dynamic = obj {}
|
||||
// Each variable has a matching FullMethod.
|
||||
val fullMethods = emptyJsMap<String, FullMethod>()
|
||||
|
||||
val wantedItemTypeIds = emptyJsSet<Int>()
|
||||
|
||||
for (wanted in filteredWantedItems) {
|
||||
wantedItemTypeIds.add(wanted.itemType.id)
|
||||
}
|
||||
|
||||
// TODO: Optimize this by not looping over every method, difficulty, section ID and enemy.
|
||||
// Instead, loop over wanted items, look up drops for the given item type and go from
|
||||
// there.
|
||||
for (method in methods) {
|
||||
// Calculate enemy counts including rare enemies
|
||||
// Counts include rare enemies, so they are fractional.
|
||||
val counts = emptyJsMap<NpcType, Double>()
|
||||
|
||||
for ((enemyType, count) in method.enemyCounts) {
|
||||
val rareEnemyType = enemyType.rareType
|
||||
val oldCount = counts.get(enemyType) ?: .0
|
||||
|
||||
if (rareEnemyType == null) {
|
||||
counts.set(enemyType, oldCount + count)
|
||||
} else {
|
||||
val rareRate: Double =
|
||||
if (rareEnemyType == NpcType.Kondrieu) KONDRIEU_PROB
|
||||
else RARE_ENEMY_PROB
|
||||
|
||||
counts.set(enemyType, oldCount + count * (1.0 - rareRate))
|
||||
counts.set(rareEnemyType, (counts.get(rareEnemyType) ?: .0) + count * rareRate)
|
||||
}
|
||||
}
|
||||
|
||||
// Create fully specified hunt methods and a variable for each of them.
|
||||
for (splitPanArms in arrayOf(false, true)) {
|
||||
createFullMethods(
|
||||
dropTable,
|
||||
wantedItemTypeIds,
|
||||
method,
|
||||
counts,
|
||||
splitPanArms,
|
||||
variables,
|
||||
fullMethods,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val optimalMethods = solve(filteredWantedItems, constraints, variables, fullMethods)
|
||||
|
||||
logger.debug { "Optimization end." }
|
||||
|
||||
return OptimizationResultModel(filteredWantedItems.map { it.itemType }, optimalMethods)
|
||||
}
|
||||
|
||||
private fun createFullMethods(
|
||||
dropTable: EnemyDropTable,
|
||||
wantedItemTypeIds: JsSet<Int>,
|
||||
method: HuntMethodModel,
|
||||
defaultCounts: JsMap<NpcType, Double>,
|
||||
splitPanArms: Boolean,
|
||||
variables: dynamic,
|
||||
fullMethods: JsMap<String, FullMethod>,
|
||||
) {
|
||||
val counts: JsMap<NpcType, Double>?
|
||||
|
||||
if (splitPanArms) {
|
||||
var splitPanArmsCounts: JsMap<NpcType, Double>? = null
|
||||
|
||||
// Create a secondary counts map if there are any pan arms that can be split
|
||||
// into migiums and hidooms.
|
||||
val panArmsCount = defaultCounts.get(NpcType.PanArms)
|
||||
val panArms2Count = defaultCounts.get(NpcType.PanArms2)
|
||||
|
||||
if (panArmsCount != null || panArms2Count != null) {
|
||||
splitPanArmsCounts = emptyJsMap()
|
||||
|
||||
if (panArmsCount != null) {
|
||||
splitPanArmsCounts.delete(NpcType.PanArms)
|
||||
splitPanArmsCounts.set(NpcType.Migium, panArmsCount)
|
||||
splitPanArmsCounts.set(NpcType.Hidoom, panArmsCount)
|
||||
}
|
||||
|
||||
if (panArms2Count != null) {
|
||||
splitPanArmsCounts.delete(NpcType.PanArms2)
|
||||
splitPanArmsCounts.set(NpcType.Migium2, panArms2Count)
|
||||
splitPanArmsCounts.set(NpcType.Hidoom2, panArms2Count)
|
||||
}
|
||||
}
|
||||
|
||||
counts = splitPanArmsCounts
|
||||
} else {
|
||||
counts = defaultCounts
|
||||
}
|
||||
|
||||
if (counts != null) {
|
||||
for (difficulty in Difficulty.VALUES) {
|
||||
for (sectionId in SectionId.VALUES) {
|
||||
// Will contain an entry per wanted item dropped by enemies in this method/
|
||||
// difficulty/section ID combo.
|
||||
val variable: dynamic = obj {
|
||||
time = method.time.value.inHours
|
||||
}
|
||||
// Only add the variable if the method provides at least 1 item we want.
|
||||
var addVariable = false
|
||||
|
||||
counts.forEach { count, npcType ->
|
||||
dropTable.getDrop(difficulty, sectionId, npcType)?.let { drop ->
|
||||
if (wantedItemTypeIds.has(drop.itemTypeId)) {
|
||||
val oldValue = variable[drop.itemTypeId] ?: .0
|
||||
variable[drop.itemTypeId] = oldValue + count * drop.dropRate
|
||||
addVariable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addVariable) {
|
||||
val fullMethod = FullMethod(method, difficulty, sectionId, splitPanArms)
|
||||
variables[fullMethod.variableName] = variable
|
||||
fullMethods.set(fullMethod.variableName, fullMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun solve(
|
||||
wantedItems: List<WantedItemModel>,
|
||||
constraints: dynamic,
|
||||
variables: dynamic,
|
||||
fullMethods: JsMap<String, FullMethod>,
|
||||
): List<OptimalMethodModel> {
|
||||
val result = Solver.Solve(obj {
|
||||
optimize = "time"
|
||||
opType = "min"
|
||||
this.constraints = constraints
|
||||
this.variables = variables
|
||||
})
|
||||
|
||||
if (!result.feasible.unsafeCast<Boolean>()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Loop over the entries in result, ignore standard properties that aren't variables.
|
||||
return objectEntries(result).mapNotNull { (variableName, runsOrOther) ->
|
||||
fullMethods.get(variableName)?.let { fullMethod ->
|
||||
val runs = runsOrOther as Double
|
||||
val variable = variables[variableName]
|
||||
|
||||
val itemCounts: Map<ItemType, Double> =
|
||||
objectEntries(variable)
|
||||
.mapNotNull { (itemTypeIdStr, expectedAmount) ->
|
||||
itemTypeIdStr.toIntOrNull()?.let { itemTypeId ->
|
||||
wantedItems
|
||||
.find { it.itemType.id == itemTypeId }
|
||||
?.let { wanted ->
|
||||
Pair(wanted.itemType, runs * (expectedAmount as Double))
|
||||
}
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
|
||||
check(itemCounts.isNotEmpty()) {
|
||||
"""Item counts map for variable "$variableName" was empty."""
|
||||
}
|
||||
|
||||
// Find all section IDs that provide the same items with the same expected amount.
|
||||
// E.g. if you need a spread needle and a bringer's right arm, using either
|
||||
// purplenum or yellowboze will give you the exact same probabilities.
|
||||
val sectionIds = mutableListOf<SectionId>()
|
||||
|
||||
for (sectionId in SectionId.VALUES) {
|
||||
var matchFound = true
|
||||
|
||||
if (sectionId != fullMethod.sectionId) {
|
||||
val v = variables[getVariableName(
|
||||
fullMethod.difficulty,
|
||||
sectionId,
|
||||
fullMethod.method,
|
||||
fullMethod.splitPanArms,
|
||||
)]
|
||||
|
||||
if (v == null) {
|
||||
matchFound = false
|
||||
} else {
|
||||
for ((itemName, expectedAmount) in objectEntries(variable)) {
|
||||
if (v[itemName] != expectedAmount.unsafeCast<Double>()) {
|
||||
matchFound = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchFound) {
|
||||
sectionIds.add(sectionId)
|
||||
}
|
||||
}
|
||||
|
||||
val method = fullMethod.method
|
||||
|
||||
val methodName = buildString {
|
||||
append(method.name)
|
||||
|
||||
if (fullMethod.splitPanArms) {
|
||||
append(" (Split Pan Arms)")
|
||||
}
|
||||
}
|
||||
|
||||
OptimalMethodModel(
|
||||
fullMethod.difficulty,
|
||||
sectionIds,
|
||||
methodName,
|
||||
method.episode,
|
||||
method.time.value,
|
||||
runs,
|
||||
itemCounts,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a fully specified hunt method.
|
||||
*/
|
||||
private data class FullMethod(
|
||||
val method: HuntMethodModel,
|
||||
val difficulty: Difficulty,
|
||||
val sectionId: SectionId,
|
||||
val splitPanArms: Boolean,
|
||||
) {
|
||||
val variableName: String =
|
||||
getVariableName(difficulty, sectionId, method, splitPanArms)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RARE_ENEMY_PROB = 1.0 / 512.0
|
||||
private const val KONDRIEU_PROB = 1.0 / 10.0
|
||||
|
||||
private fun getVariableName(
|
||||
difficulty: Difficulty,
|
||||
sectionId: SectionId,
|
||||
method: HuntMethodModel,
|
||||
splitPanArms: Boolean,
|
||||
): String =
|
||||
"$difficulty\t$sectionId\t${method.id}\t$splitPanArms"
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,54 @@
|
||||
package world.phantasmal.web.huntOptimizer.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.core.dom.sectionIdIcon
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.DIFF_COL
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.EPISODE_COL
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.METHOD_COL
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.RUNS_COL
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.SECTION_ID_COL
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.TIME_PER_RUN_COL
|
||||
import world.phantasmal.web.huntOptimizer.controllers.OptimizationResultController.Companion.TOTAL_TIME_COL
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.dom.dom
|
||||
import world.phantasmal.webui.dom.h2
|
||||
import world.phantasmal.webui.dom.span
|
||||
import world.phantasmal.webui.formatAsHoursAndMinutes
|
||||
import world.phantasmal.webui.toRoundedString
|
||||
import world.phantasmal.webui.widgets.Table
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class OptimizationResultWidget : Widget() {
|
||||
class OptimizationResultWidget(private val ctrl: OptimizationResultController) : Widget() {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-hunt-optimizer-optimization-result"
|
||||
|
||||
h2 { textContent = "Ideal Combination of Methods" }
|
||||
|
||||
addWidget(Table(
|
||||
ctrl = ctrl,
|
||||
renderCell = { optimalMethod, column ->
|
||||
when (column.key) {
|
||||
DIFF_COL -> optimalMethod.difficulty
|
||||
METHOD_COL -> optimalMethod.name
|
||||
EPISODE_COL -> optimalMethod.episode
|
||||
SECTION_ID_COL -> dom {
|
||||
span {
|
||||
style.display = "flex"
|
||||
|
||||
for (sectionId in optimalMethod.sectionIds) {
|
||||
sectionIdIcon(sectionId, size = 17)
|
||||
}
|
||||
}
|
||||
}
|
||||
TIME_PER_RUN_COL -> optimalMethod.methodTime.formatAsHoursAndMinutes()
|
||||
RUNS_COL -> optimalMethod.runs.toRoundedString(1)
|
||||
TOTAL_TIME_COL -> optimalMethod.totalTime.inHours.toRoundedString(1)
|
||||
else -> ""
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -0,0 +1,14 @@
|
||||
package world.phantasmal.webui
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Double.toRoundedString(decimals: Int): String =
|
||||
if (decimals <= 0) roundToInt().toString()
|
||||
else {
|
||||
var multiplied = this
|
||||
repeat(decimals) { multiplied *= 10 }
|
||||
val str = multiplied.roundToInt().toString()
|
||||
|
||||
if (this < 1) "0.$str"
|
||||
else "${str.dropLast(decimals)}.${str.takeLast(decimals)}"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package world.phantasmal.webui
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Duration.formatAsHoursAndMinutes(): String =
|
||||
toComponents { hours, minutes, _, _ ->
|
||||
"${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}"
|
||||
}
|
@ -4,6 +4,7 @@ import org.w3c.dom.HTMLInputElement
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.nullVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.webui.formatAsHoursAndMinutes
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.minutes
|
||||
|
||||
@ -50,9 +51,7 @@ class DurationInput(
|
||||
}
|
||||
|
||||
override fun setInputValue(input: HTMLInputElement, value: Duration) {
|
||||
input.value = value.toComponents { hours, minutes, _, _ ->
|
||||
"${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}"
|
||||
}
|
||||
input.value = value.formatAsHoursAndMinutes()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
Loading…
Reference in New Issue
Block a user