Entities are now shown per area and area selection is now possible. Fixed some bugs.

This commit is contained in:
Daan Vanden Bosch 2020-11-08 22:45:37 +01:00
parent db1149ddc0
commit 132cdccd0a
27 changed files with 478 additions and 263 deletions

View File

@ -70,9 +70,9 @@ class QuestNpc(
}
override var sectionId: Int
get() = data.getUShort(12).toInt()
get() = data.getShort(12).toInt()
set(value) {
data.setUShort(12, value.toUShort())
data.setShort(12, value.toShort())
}
override var position: Vec3

View File

@ -20,9 +20,9 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<Obje
}
override var sectionId: Int
get() = data.getUShort(12).toInt()
get() = data.getShort(12).toInt()
set(value) {
data.setUShort(12, value.toUShort())
data.setShort(12, value.toShort())
}
override var position: Vec3

View File

@ -2,19 +2,20 @@ package world.phantasmal.observable.value
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer
abstract class AbstractVal<T> : Val<T> {
protected val observers: MutableList<ValObserver<T>> = mutableListOf()
protected val observers: MutableList<Observer<T>> = mutableListOf()
final override fun observe(observer: Observer<T>): Disposable =
observe(callNow = false, observer)
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
observers.add(observer)
if (callNow) {
observer(ValChangeEvent(value, value))
observer(ChangeEvent(value))
}
return disposable {
@ -22,8 +23,8 @@ abstract class AbstractVal<T> : Val<T> {
}
}
protected fun emit(oldValue: T) {
val event = ValChangeEvent(value, oldValue)
protected fun emit() {
val event = ChangeEvent(value)
observers.forEach { it(event) }
}
}

View File

@ -11,7 +11,7 @@ class DelegatingVal<T>(
if (value != oldValue) {
setter(value)
emit(oldValue)
emit()
}
}
}

View File

@ -3,6 +3,7 @@ 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
@ -17,19 +18,26 @@ abstract class DependentVal<T>(
*/
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 (hasNoObservers()) {
if (!hasObservers) {
_value = computeValue()
}
return _value.unsafeToNonNull()
}
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
if (hasNoObservers()) {
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
if (dependencyObservers.isEmpty()) {
hasObservers = true
dependencies.forEach { dependency ->
dependencyObservers.add(
dependency.observe {
@ -37,7 +45,7 @@ abstract class DependentVal<T>(
_value = computeValue()
if (_value != oldValue) {
emit(oldValue.unsafeToNonNull())
emit()
}
}
)
@ -52,17 +60,12 @@ abstract class DependentVal<T>(
superDisposable.dispose()
if (observers.isEmpty()) {
hasObservers = false
dependencyObservers.forEach { it.dispose() }
dependencyObservers.clear()
}
}
}
protected fun hasObservers(): Boolean =
dependencyObservers.isNotEmpty()
protected fun hasNoObservers(): Boolean =
dependencyObservers.isEmpty()
protected abstract fun computeValue(): T
}

View File

@ -3,6 +3,7 @@ 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
class FlatMappedVal<T>(
dependencies: Iterable<Val<*>>,
@ -13,20 +14,20 @@ class FlatMappedVal<T>(
override val value: T
get() {
return if (hasNoObservers()) {
super.value
} else {
return if (hasObservers) {
computedVal.unsafeToNonNull().value
} else {
super.value
}
}
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
if (hasNoObservers()) {
if (!hasObservers) {
computedValObserver?.dispose()
computedValObserver = null
computedVal = null
@ -40,11 +41,10 @@ class FlatMappedVal<T>(
computedValObserver?.dispose()
if (hasObservers()) {
if (hasObservers) {
computedValObserver = computedVal.observe { (value) ->
val oldValue = _value.unsafeToNonNull<T>()
_value = value
emit(oldValue)
emit()
}
}

View File

@ -4,9 +4,8 @@ class SimpleVal<T>(value: T) : AbstractVal<T>(), MutableVal<T> {
override var value: T = value
set(value) {
if (value != field) {
val oldValue = field
field = value
emit(oldValue)
emit()
}
}
}

View File

@ -2,12 +2,13 @@ package world.phantasmal.observable.value
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.stubDisposable
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer
class StaticVal<T>(override val value: T) : Val<T> {
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
if (callNow) {
observer(ValChangeEvent(value, value))
observer(ChangeEvent(value))
}
return stubDisposable()

View File

@ -2,6 +2,7 @@ package world.phantasmal.observable.value
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.Observable
import world.phantasmal.observable.Observer
import kotlin.reflect.KProperty
/**
@ -15,7 +16,7 @@ interface Val<out T> : Observable<T> {
/**
* @param callNow Call [observer] immediately with the current [mutableVal].
*/
fun observe(callNow: Boolean = false, observer: ValObserver<T>): Disposable
fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable
fun <R> map(transform: (T) -> R): Val<R> =
MappedVal(listOf(this)) { transform(value) }
@ -23,6 +24,9 @@ interface Val<out T> : Observable<T> {
fun <T2, R> map(v2: Val<T2>, transform: (T, T2) -> R): Val<R> =
MappedVal(listOf(this, v2)) { transform(value, v2.value) }
fun <T2, T3, R> map(v2: Val<T2>, v3: Val<T3>, transform: (T, T2, T3) -> R): Val<R> =
MappedVal(listOf(this, v2, v3)) { transform(value, v2.value, v3.value) }
fun <R> flatMap(transform: (T) -> Val<R>): Val<R> =
FlatMappedVal(listOf(this)) { transform(value) }
}

View File

@ -1,9 +0,0 @@
package world.phantasmal.observable.value
import world.phantasmal.observable.ChangeEvent
class ValChangeEvent<out T>(value: T, val oldValue: T) : ChangeEvent<T>(value) {
operator fun component2() = oldValue
}
typealias ValObserver<T> = (event: ValChangeEvent<T>) -> Unit

View File

@ -0,0 +1,135 @@
package world.phantasmal.observable.value.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
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.MutableVal
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal
abstract class AbstractListVal<E>(
protected val elements: MutableList<E>,
private val extractObservables: ObservablesExtractor<E>? ,
): AbstractVal<List<E>>(), ListVal<E> {
/**
* Internal observers which observe observables related to this list's elements so that their
* changes can be propagated via ElementChange events.
*/
private val elementObservers = mutableListOf<ElementObserver>()
/**
* External list observers which are observing this list.
*/
protected val listObservers = mutableListOf<ListValObserver<E>>()
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
if (elementObservers.isEmpty() && extractObservables != null) {
replaceElementObservers(0, elementObservers.size, elements)
}
observers.add(observer)
if (callNow) {
observer(ChangeEvent(elements))
}
return disposable {
observers.remove(observer)
disposeElementObserversIfNecessary()
}
}
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
if (elementObservers.isEmpty() && extractObservables != null) {
replaceElementObservers(0, elementObservers.size, elements)
}
listObservers.add(observer)
if (callNow) {
observer(ListValChangeEvent.Change(0, emptyList(), elements))
}
return disposable {
listObservers.remove(observer)
disposeElementObserversIfNecessary()
}
}
/**
* Does the following in the given order:
* - Updates element observers
* - Emits ListValChangeEvent
* - Emits ValChangeEvent
*/
protected open fun finalizeUpdate(event: ListValChangeEvent<E>) {
if (
(listObservers.isNotEmpty() || observers.isNotEmpty()) &&
extractObservables != null &&
event is ListValChangeEvent.Change
) {
replaceElementObservers(event.index, event.removed.size, event.inserted)
}
listObservers.forEach { observer: ListValObserver<E> ->
observer(event)
}
emit()
}
private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List<E>) {
for (i in 1..amountRemoved) {
elementObservers.removeAt(from).observers.forEach { it.dispose() }
}
var index = from
elementObservers.addAll(
from,
insertedElements.map { element ->
ElementObserver(
index++,
element,
extractObservables!!(element)
)
}
)
val shift = insertedElements.size - amountRemoved
while (index < elementObservers.size) {
elementObservers[index++].index += shift
}
}
private fun disposeElementObserversIfNecessary() {
if (listObservers.isEmpty() && observers.isEmpty()) {
elementObservers.forEach { elementObserver: ElementObserver ->
elementObserver.observers.forEach { it.dispose() }
}
elementObservers.clear()
}
}
private inner class ElementObserver(
var index: Int,
element: E,
observables: Array<Observable<*>>,
) {
val observers: Array<Disposable> = Array(observables.size) {
observables[it].observe {
finalizeUpdate(
ListValChangeEvent.ElementChange(
index,
listOf(element)
)
)
}
}
}
}

View File

@ -0,0 +1,129 @@
package world.phantasmal.observable.value.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.observable.Observer
import world.phantasmal.observable.value.AbstractVal
import world.phantasmal.observable.value.Val
/**
* Starts observing its dependencies when the first observer on this property is registered.
* Stops observing its dependencies when the last observer on this property is disposed.
* This way no extra disposables need to be managed when e.g. [map] is used.
*/
class DependentListVal<E>(
private val dependencies: List<Val<*>>,
private val computeElements: () -> List<E>,
) : AbstractListVal<E>(mutableListOf(), extractObservables = null) {
private val _sizeVal = SizeVal()
/**
* Set to true right before actual observers are added.
*/
private var hasObservers = false
/**
* Is either empty or has a disposable per dependency.
*/
private val dependencyObservers = mutableListOf<Disposable>()
override val value: List<E>
get() {
if (!hasObservers) {
recompute()
}
return elements
}
override val sizeVal: Val<Int> = _sizeVal
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
initDependencyObservers()
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
}
}
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
initDependencyObservers()
val superDisposable = super.observeList(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
}
}
private fun recompute() {
elements.clear()
elements.addAll(computeElements())
}
private fun initDependencyObservers() {
if (dependencyObservers.isEmpty()) {
hasObservers = true
dependencies.forEach { dependency ->
dependencyObservers.add(
dependency.observe {
val removed = ArrayList(elements)
recompute()
finalizeUpdate(ListValChangeEvent.Change(0, removed, elements))
}
)
}
recompute()
}
}
private fun disposeDependencyObservers() {
if (observers.isEmpty() && listObservers.isEmpty() && _sizeVal.publicObservers.isEmpty()) {
hasObservers = false
dependencyObservers.forEach { it.dispose() }
dependencyObservers.clear()
}
}
override fun finalizeUpdate(event: ListValChangeEvent<E>) {
if (event is ListValChangeEvent.Change && event.removed.size != event.inserted.size) {
_sizeVal.publicEmit()
}
super.finalizeUpdate(event)
}
private inner class SizeVal : AbstractVal<Int>() {
override val value: Int
get() {
if (!hasObservers) {
recompute()
}
return elements.size
}
val publicObservers = super.observers
override fun observe(callNow: Boolean, observer: Observer<Int>): Disposable {
initDependencyObservers()
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
}
}
fun publicEmit() {
super.emit()
}
}
}

View File

@ -3,8 +3,8 @@ package world.phantasmal.observable.value.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.unsafeToNonNull
import world.phantasmal.observable.Observer
import world.phantasmal.observable.value.AbstractVal
import world.phantasmal.observable.value.ValObserver
class FoldedVal<T, R>(
private val dependency: ListVal<T>,
@ -23,16 +23,15 @@ class FoldedVal<T, R>(
}
}
override fun observe(callNow: Boolean, observer: ValObserver<R>): Disposable {
override fun observe(callNow: Boolean, observer: Observer<R>): Disposable {
val superDisposable = super.observe(callNow, observer)
if (dependencyDisposable == null) {
internalValue = computeValue()
dependencyDisposable = dependency.observe {
val oldValue = internalValue
internalValue = computeValue()
emit(oldValue.unsafeToNonNull())
emit()
}
}

View File

@ -13,4 +13,7 @@ interface ListVal<E> : Val<List<E>> {
fun <R> fold(initialValue: R, operation: (R, E) -> R): Val<R> =
FoldedVal(this, initialValue, operation)
fun filtered(predicate: (E) -> Boolean): ListVal<E> =
DependentListVal(listOf(this)) { value.filter(predicate) }
}

View File

@ -1,54 +1,29 @@
package world.phantasmal.observable.value.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.observable.Observable
import world.phantasmal.observable.Observer
import world.phantasmal.observable.value.*
import world.phantasmal.observable.value.MutableVal
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal
typealias ObservablesExtractor<E> = (element: E) -> Array<Observable<*>>
class SimpleListVal<E>(
private val elements: MutableList<E>,
/**
* Extractor function called on each element in this list. Changes to the returned observables
* will be propagated via ElementChange events.
/**
* @param elements The backing list for this ListVal
* @param extractObservables Extractor function called on each element in this list, changes to the
* returned observables will be propagated via ElementChange events
*/
private val extractObservables: ObservablesExtractor<E>? = null,
) : MutableListVal<E> {
class SimpleListVal<E>(
elements: MutableList<E>,
extractObservables: ObservablesExtractor<E>? = null,
) : AbstractListVal<E>(elements, extractObservables), MutableListVal<E> {
private val _sizeVal: MutableVal<Int> = mutableVal(elements.size)
override var value: List<E> = elements
set(value) {
val removed = ArrayList(elements)
elements.clear()
elements.addAll(value)
finalizeUpdate(
ListValChangeEvent.Change(
index = 0,
removed = removed,
inserted = value
)
)
replaceAll(value)
}
private val mutableSizeVal: MutableVal<Int> = mutableVal(elements.size)
override val sizeVal: Val<Int> = mutableSizeVal
/**
* Internal observers which observe observables related to this list's elements so that their
* changes can be propagated via ElementChange events.
*/
private val elementObservers = mutableListOf<ElementObserver>()
/**
* External list observers which are observing this list.
*/
private val listObservers = mutableListOf<ListValObserver<E>>()
/**
* External regular observers which are observing this list.
*/
private val observers = mutableListOf<ValObserver<List<E>>>()
override val sizeVal: Val<Int> = _sizeVal
override fun set(index: Int, element: E): E {
val removed = elements.set(index, element)
@ -93,121 +68,8 @@ class SimpleListVal<E>(
finalizeUpdate(ListValChangeEvent.Change(0, removed, emptyList()))
}
override fun observe(observer: Observer<List<E>>): Disposable =
observe(callNow = false, observer)
override fun observe(callNow: Boolean, observer: ValObserver<List<E>>): Disposable {
if (elementObservers.isEmpty() && extractObservables != null) {
replaceElementObservers(0, elementObservers.size, elements)
}
observers.add(observer)
if (callNow) {
observer(ValChangeEvent(elements, elements))
}
return disposable {
observers.remove(observer)
disposeElementObserversIfNecessary()
}
}
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
if (elementObservers.isEmpty() && extractObservables != null) {
replaceElementObservers(0, elementObservers.size, elements)
}
listObservers.add(observer)
if (callNow) {
observer(ListValChangeEvent.Change(0, emptyList(), elements))
}
return disposable {
listObservers.remove(observer)
disposeElementObserversIfNecessary()
}
}
/**
* Does the following in the given order:
* - Updates element observers
* - Emits size ValChangeEvent if necessary
* - Emits ListValChangeEvent
* - Emits ValChangeEvent
*/
private fun finalizeUpdate(event: ListValChangeEvent<E>) {
if (
(listObservers.isNotEmpty() || observers.isNotEmpty()) &&
extractObservables != null &&
event is ListValChangeEvent.Change
) {
replaceElementObservers(event.index, event.removed.size, event.inserted)
}
mutableSizeVal.value = elements.size
listObservers.forEach { observer: ListValObserver<E> ->
observer(event)
}
val regularEvent = ValChangeEvent(elements, elements)
observers.forEach { observer: ValObserver<List<E>> ->
observer(regularEvent)
}
}
private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List<E>) {
for (i in 1..amountRemoved) {
elementObservers.removeAt(from).observers.forEach { it.dispose() }
}
var index = from
elementObservers.addAll(
from,
insertedElements.map { element ->
ElementObserver(
index++,
element,
extractObservables!!(element)
)
}
)
val shift = insertedElements.size - amountRemoved
while (index < elementObservers.size) {
elementObservers[index++].index += shift
}
}
private fun disposeElementObserversIfNecessary() {
if (listObservers.isEmpty() && observers.isEmpty()) {
elementObservers.forEach { elementObserver: ElementObserver ->
elementObserver.observers.forEach { it.dispose() }
}
elementObservers.clear()
}
}
private inner class ElementObserver(
var index: Int,
element: E,
observables: Array<Observable<*>>,
) {
val observers: Array<Disposable> = Array(observables.size) {
observables[it].observe {
finalizeUpdate(
ListValChangeEvent.ElementChange(
index,
listOf(element)
)
)
}
}
override fun finalizeUpdate(event: ListValChangeEvent<E>) {
_sizeVal.value = elements.size
super.finalizeUpdate(event)
}
}

View File

@ -2,10 +2,9 @@ package world.phantasmal.observable.value.list
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.Val
import world.phantasmal.observable.value.ValChangeEvent
import world.phantasmal.observable.value.ValObserver
import world.phantasmal.observable.value.value
class StaticListVal<E>(elements: List<E>) : ListVal<E> {
@ -13,9 +12,9 @@ class StaticListVal<E>(elements: List<E>) : ListVal<E> {
override val value: List<E> = elements
override fun observe(callNow: Boolean, observer: ValObserver<List<E>>): Disposable {
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
if (callNow) {
observer(ValChangeEvent(value, value))
observer(ChangeEvent(value))
}
return stubDisposable()

View File

@ -0,0 +1,9 @@
package world.phantasmal.observable.value.list
class DependentListValTests : ListValTests() {
override fun create(): ListValAndAdd {
val l = SimpleListVal<Int>(mutableListOf())
val list = DependentListVal(listOf(l)) { l.value.map { 2 * it } }
return ListValAndAdd(list) { l.add(4) }
}
}

View File

@ -63,7 +63,7 @@ class QuestEditor(
// Main Widget
return QuestEditorWidget(
scope,
{ s -> QuestEditorToolbar(s, toolbarController) },
{ s -> QuestEditorToolbarWidget(s, toolbarController) },
{ s -> QuestInfoWidget(s, questInfoController) },
{ s -> NpcCountsWidget(s, npcCountsController) },
{ s -> QuestEditorRendererWidget(s, canvas, renderer) }

View File

@ -11,7 +11,9 @@ import world.phantasmal.lib.fileFormats.quest.parseBinDatToQuest
import world.phantasmal.lib.fileFormats.quest.parseQstToQuest
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.observable.value.value
import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.models.AreaModel
import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore
import world.phantasmal.web.questEditor.stores.convertQuestToModel
@ -20,6 +22,8 @@ import world.phantasmal.webui.readFile
private val logger = KotlinLogging.logger {}
class AreaAndLabel(val area: AreaModel, val label: String)
class QuestEditorToolbarController(
private val questLoader: QuestLoader,
private val areaStore: AreaStore,
@ -31,6 +35,28 @@ class QuestEditorToolbarController(
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
val result: Val<PwResult<*>?> = _result
// Ensure the areas list is updated when entities are added or removed (the count in the
// label should update).
val areas: Val<List<AreaAndLabel>> = questEditorStore.currentQuest.flatMap { quest ->
quest?.let {
quest.entitiesPerArea.map { entitiesPerArea ->
areaStore.getAreasForEpisode(quest.episode).map { area ->
val entityCount = entitiesPerArea[area.id]
AreaAndLabel(area, area.name + (entityCount?.let { " ($it)" } ?: ""))
}
}
} ?: value(emptyList())
}
val currentArea: Val<AreaAndLabel?> = areas.map(questEditorStore.currentArea) { areas, area ->
areas.find { it.area == area }
}
val areaSelectDisabled: Val<Boolean>
init {
val noQuestLoaded = questEditorStore.currentQuest.map { it == null }
areaSelectDisabled = noQuestLoaded
}
suspend fun createNewQuest(episode: Episode) {
questEditorStore.setCurrentQuest(
convertQuestToModel(questLoader.loadDefaultQuest(episode), areaStore::getVariant)
@ -81,6 +107,10 @@ class QuestEditorToolbarController(
}
}
fun setCurrentArea(areaAndLabel: AreaAndLabel) {
questEditorStore.setCurrentArea(areaAndLabel.area)
}
private suspend fun setCurrentQuest(quest: Quest) {
questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant))
}

View File

@ -18,6 +18,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
) {
private val _sectionId = mutableVal(entity.sectionId)
private val _section = mutableVal<SectionModel?>(null)
private val _sectionInitialized = mutableVal(false)
private val _position = mutableVal(vec3ToBabylon(entity.position))
private val _worldPosition = mutableVal(_position.value)
private val _rotation = mutableVal(vec3ToBabylon(entity.rotation))
@ -30,6 +31,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
val sectionId: Val<Int> = _sectionId
val section: Val<SectionModel?> = _section
val sectionInitialized: Val<Boolean> = _sectionInitialized
/**
* Section-relative position
@ -57,6 +59,12 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
setPosition(position.value)
setRotation(rotation.value)
setSectionInitialized()
}
fun setSectionInitialized() {
_sectionInitialized.value = true
}
fun setPosition(pos: Vector3) {

View File

@ -2,5 +2,17 @@ package world.phantasmal.web.questEditor.models
import world.phantasmal.lib.fileFormats.quest.ObjectType
import world.phantasmal.lib.fileFormats.quest.QuestObject
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.mutableVal
class QuestObjectModel(obj: QuestObject) : QuestEntityModel<ObjectType, QuestObject>(obj)
class QuestObjectModel(obj: QuestObject) : QuestEntityModel<ObjectType, QuestObject>(obj) {
private val _model = mutableVal(obj.model)
val model: Val<Int?> = _model
fun setModel(model: Int) {
_model.value = model
// TODO: Propagate to props.
}
}

View File

@ -9,6 +9,7 @@ import world.phantasmal.web.externals.babylon.TransformNode
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.models.QuestEntityModel
import world.phantasmal.web.questEditor.models.QuestNpcModel
import world.phantasmal.web.questEditor.models.QuestObjectModel
import world.phantasmal.web.questEditor.models.WaveModel
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
import world.phantasmal.web.questEditor.stores.QuestEditorStore
@ -117,8 +118,10 @@ class EntityMeshManager(
}
private suspend fun load(entity: QuestEntityModel<*, *>) {
// TODO
val mesh = entityAssetLoader.loadMesh(entity.type, model = null)
val mesh = entityAssetLoader.loadMesh(
type = entity.type,
model = (entity as? QuestObjectModel)?.model?.value
)
// Only add an instance of this mesh if the entity is still in the queue at this point.
if (queue.remove(entity)) {
@ -132,13 +135,12 @@ class EntityMeshManager(
loadedEntities[entity] = LoadedEntity(entity, instance, questEditorStore.selectedWave)
}
}
}
private class LoadedEntity(
private inner class LoadedEntity(
entity: QuestEntityModel<*, *>,
val mesh: AbstractMesh,
selectedWave: Val<WaveModel?>,
) : DisposableContainer() {
) : DisposableContainer() {
init {
mesh.metadata = EntityMetadata(entity)
@ -150,24 +152,29 @@ private class LoadedEntity(
mesh.rotation = rot
}
addDisposables(
// TODO: Model.
// entity.model.observe {
// remove(listOf(entity))
// add(listOf(entity))
// },
)
val isVisible: Val<Boolean>
if (entity is QuestNpcModel) {
addDisposable(
selectedWave
.map(entity.wave) { sWave, entityWave ->
sWave == null || sWave == entityWave
isVisible =
entity.sectionInitialized.map(
selectedWave,
entity.wave
) { sectionInitialized, sWave, entityWave ->
sectionInitialized && (sWave == null || sWave == entityWave)
}
.observe(callNow = true) { (visible) ->
} else {
isVisible = entity.section.map { section -> section != null }
if (entity is QuestObjectModel) {
addDisposable(entity.model.observe(callNow = false) {
remove(listOf(entity))
add(listOf(entity))
})
}
}
observe(isVisible) { visible ->
mesh.setEnabled(visible)
},
)
}
}
@ -176,4 +183,5 @@ private class LoadedEntity(
mesh.dispose()
super.internalDispose()
}
}
}

View File

@ -28,12 +28,12 @@ class QuestEditorMeshManager(
private fun getAreaVariantDetails(quest: QuestModel?, area: AreaModel?): AreaVariantDetails {
quest?.let {
val areaVariant = area?.let {
quest.areaVariants.value.find { it.area.id == area.id }
quest.areaVariants.value.find { it.area.id == area.id } ?: area.areaVariants.first()
}
areaVariant?.let {
val npcs = quest.npcs // TODO: Filter NPCs.
val objects = quest.objects // TODO: Filter objects.
val npcs = quest.npcs.filtered { it.areaId == area.id }
val objects = quest.objects.filtered { it.areaId == area.id }
return AreaVariantDetails(quest.episode, areaVariant, npcs, objects)
}
}

View File

@ -21,7 +21,7 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore
abstract class QuestMeshManager protected constructor(
private val scope: CoroutineScope,
questEditorStore: QuestEditorStore,
private val renderer: QuestRenderer,
renderer: QuestRenderer,
areaAssetLoader: AreaAssetLoader,
entityAssetLoader: EntityAssetLoader,
) : TrackedDisposable() {
@ -46,12 +46,14 @@ abstract class QuestMeshManager protected constructor(
) {
loadJob?.cancel()
loadJob = scope.launch {
areaMeshManager.load(episode, areaVariant)
// Reset models.
areaDisposer.disposeAll()
npcMeshManager.removeAll()
objectMeshManager.removeAll()
// Load area model.
areaMeshManager.load(episode, areaVariant)
// Load entity meshes.
areaDisposer.addAll(
npcs.observeList(callNow = true, ::npcsChanged),

View File

@ -24,11 +24,12 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
suspend fun setCurrentQuest(quest: QuestModel?) {
if (quest == null) {
_currentArea.value = null
_currentQuest.value = quest
quest?.let {
_currentQuest.value = null
} else {
_currentArea.value = areaStore.getArea(quest.episode, 0)
_currentQuest.value = quest
// Load section data.
quest.areaVariants.value.forEach { variant ->
@ -37,6 +38,10 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
setSectionOnQuestEntities(quest.npcs.value, variant, sections)
setSectionOnQuestEntities(quest.objects.value, variant, sections)
}
// Ensure all entities have their section initialized.
quest.npcs.value.forEach { it.setSectionInitialized() }
quest.objects.value.forEach { it.setSectionInitialized() }
}
}
@ -51,6 +56,7 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
if (section == null) {
logger.warn { "Section ${entity.sectionId.value} not found." }
entity.setSectionInitialized()
} else {
entity.setSection(section)
}
@ -58,6 +64,13 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
}
}
fun setCurrentArea(area: AreaModel?) {
// TODO: Set wave.
_selectedEntity.value = null
_currentArea.value = area
}
fun setSelectedEntity(entity: QuestEntityModel<*, *>?) {
entity?.let {
currentQuest.value?.let { quest ->

View File

@ -7,12 +7,9 @@ import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController
import world.phantasmal.webui.dom.Icon
import world.phantasmal.webui.dom.div
import world.phantasmal.webui.widgets.Button
import world.phantasmal.webui.widgets.FileButton
import world.phantasmal.webui.widgets.Toolbar
import world.phantasmal.webui.widgets.Widget
import world.phantasmal.webui.widgets.*
class QuestEditorToolbar(
class QuestEditorToolbarWidget(
scope: CoroutineScope,
private val ctrl: QuestEditorToolbarController,
) : Widget(scope) {
@ -36,6 +33,14 @@ class QuestEditorToolbar(
accept = ".bin, .dat, .qst",
multiple = true,
filesSelected = { files -> scope.launch { ctrl.openFiles(files) } }
),
Select(
scope,
disabled = ctrl.areaSelectDisabled,
itemsVal = ctrl.areas,
itemToString = { it.label },
selectedVal = ctrl.currentArea,
onSelect = ctrl::setCurrentArea
)
)
))

View File

@ -37,8 +37,7 @@ class Select<T : Any>(
private val items: Val<List<T>> = itemsVal ?: value(items ?: emptyList())
private val selected: Val<T?> = selectedVal ?: value(selected)
// Default to a single space so the inner text part won't be hidden.
private val buttonText = mutableVal(this.selected.value?.let(itemToString) ?: " ")
private val buttonText = mutableVal(" ")
private val menuHidden = mutableVal(true)
private lateinit var menu: Menu<T>
@ -48,6 +47,9 @@ class Select<T : Any>(
div {
className = "pw-select"
// Default to a single space so the inner text part won't be hidden.
observe(selected) { buttonText.value = it?.let(itemToString) ?: " " }
addWidget(Button(
scope,
disabled = disabled,