diff --git a/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt b/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt index 4ea21fbd..611046b2 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt @@ -6,6 +6,10 @@ import world.phantasmal.web.externals.babylon.Vector3 operator fun Vector3.minus(other: Vector3): Vector3 = subtract(other) +operator fun Vector3.minusAssign(other: Vector3) { + subtractInPlace(other) +} + infix fun Vector3.dot(other: Vector3): Double = Vector3.Dot(this, other) diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt index 2620d8e4..c1b7c455 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/Renderer.kt @@ -8,20 +8,30 @@ import world.phantasmal.webui.DisposableContainer private val logger = KotlinLogging.logger {} abstract class Renderer( - protected val canvas: HTMLCanvasElement, + val canvas: HTMLCanvasElement, protected val engine: Engine, ) : DisposableContainer() { - val scene = Scene(engine) - - private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene) + private val light: HemisphericLight protected abstract val camera: Camera + val scene = Scene(engine) + init { with(scene) { useRightHandedSystem = true clearColor = Color4(0.09, 0.09, 0.09, 1.0) } + + light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene) + } + + override fun internalDispose() { + camera.dispose() + light.dispose() + scene.dispose() + engine.dispose() + super.internalDispose() } fun startRendering() { @@ -34,14 +44,6 @@ abstract class Renderer( engine.stopRenderLoop() } - override fun internalDispose() { - camera.dispose() - light.dispose() - scene.dispose() - engine.dispose() - super.internalDispose() - } - private fun render() { val lightDirection = Vector3(-1.0, 1.0, 1.0) lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection) diff --git a/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt b/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt index b6271d44..8c52aa4e 100644 --- a/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt +++ b/web/src/main/kotlin/world/phantasmal/web/externals/babylon/babylon.kt @@ -12,6 +12,7 @@ external class Vector2(x: Double, y: Double) { var x: Double var y: Double + fun set(x: Double, y: Double): Vector2 fun addInPlace(otherVector: Vector2): Vector2 fun addInPlaceFromFloats(x: Double, y: Double): Vector2 fun subtract(otherVector: Vector2): Vector2 @@ -32,10 +33,12 @@ external class Vector3(x: Double, y: Double, z: Double) { var y: Double var z: Double + fun set(x: Double, y: Double, z: Double): Vector2 fun toQuaternion(): Quaternion fun addInPlace(otherVector: Vector3): Vector3 fun addInPlaceFromFloats(x: Double, y: Double, z: Double): Vector3 fun subtract(otherVector: Vector3): Vector3 + fun subtractInPlace(otherVector: Vector3): Vector3 fun negate(): Vector3 fun negateInPlace(): Vector3 fun cross(other: Vector3): Vector3 @@ -148,9 +151,25 @@ external class Engine( antialias: Boolean = definedExternally, ) : ThinEngine +external class Ray + +external class PickingInfo { + val bu: Double + val bv: Double + val distance: Double + val faceId: Int + val hit: Boolean + val originMesh: AbstractMesh? + val pickedMesh: AbstractMesh? + val pickedPoint: Vector3? + val ray: Ray? +} + external class Scene(engine: Engine) { var useRightHandedSystem: Boolean var clearColor: Color4 + var pointerX: Double + var pointerY: Double fun render() fun addLight(light: Light) @@ -159,6 +178,15 @@ external class Scene(engine: Engine) { fun removeLight(toRemove: Light) fun removeMesh(toRemove: TransformNode, recursive: Boolean? = definedExternally) fun removeTransformNode(toRemove: TransformNode) + fun pick( + x: Double, + y: Double, + predicate: (AbstractMesh) -> Boolean = definedExternally, + fastCheck: Boolean = definedExternally, + camera: Camera? = definedExternally, + trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally, + ): PickingInfo? + fun dispose() } @@ -238,9 +266,13 @@ open external class TransformNode( var rotationQuaternion: Quaternion? val absoluteRotation: Quaternion var scaling: Vector3 + + fun locallyTranslate(vector3: Vector3): TransformNode } abstract external class AbstractMesh : TransformNode { + var showBoundingBox: Boolean + fun getBoundingInfo(): BoundingInfo } @@ -253,6 +285,9 @@ external class Mesh( clonePhysicsImpostor: Boolean = definedExternally, ) : AbstractMesh { fun createInstance(name: String): InstancedMesh + fun bakeCurrentTransformIntoVertices( + bakeIndependenlyOfChildren: Boolean = definedExternally, + ): Mesh } external class InstancedMesh : AbstractMesh diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt index 57350760..09293862 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt @@ -14,6 +14,7 @@ import world.phantasmal.web.questEditor.loading.AreaAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.loading.QuestLoader import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager +import world.phantasmal.web.questEditor.rendering.EntityManipulator import world.phantasmal.web.questEditor.rendering.QuestRenderer import world.phantasmal.web.questEditor.stores.AreaStore import world.phantasmal.web.questEditor.stores.QuestEditorStore @@ -48,13 +49,16 @@ class QuestEditor( val npcCountsController = addDisposable(NpcCountsController(questEditorStore)) // Rendering - addDisposable(QuestEditorMeshManager( - scope, - questEditorStore, - renderer, - areaAssetLoader, - entityAssetLoader - )) + addDisposables( + QuestEditorMeshManager( + scope, + questEditorStore, + renderer, + areaAssetLoader, + entityAssetLoader + ), + EntityManipulator(questEditorStore, renderer) + ) // Main Widget return QuestEditorWidget( diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt index 45f4d953..78e51237 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/EntityAssetLoader.kt @@ -33,32 +33,33 @@ class EntityAssetLoader( MeshBuilder.CreateCylinder( "Entity", obj { - diameter = 6.0 - height = 20.0 + diameter = 5.0 + height = 18.0 }, scene ).apply { setEnabled(false) - position = Vector3(0.0, 10.0, 0.0) + locallyTranslate(Vector3(0.0, 10.0, 0.0)) + bakeCurrentTransformIntoVertices() } private val meshCache = addDisposable(LoadingCache, Mesh> { it.dispose() }) + override fun internalDispose() { + defaultMesh.dispose() + super.internalDispose() + } + suspend fun loadMesh(type: EntityType, model: Int?): Mesh = meshCache.getOrPut(Pair(type, model)) { scope.async { try { loadGeometry(type, model)?.let { vertexData -> - // TODO: Remove this check when XJ models are parsed. - if (vertexData.indices == null || vertexData.indices!!.length == 0) { - defaultMesh - } else { - val mesh = Mesh("${type.uniqueName}${model?.let { "-$it" }}", scene) - mesh.setEnabled(false) - vertexData.applyToMesh(mesh) - mesh - } + val mesh = Mesh("${type.uniqueName}${model?.let { "-$it" }}", scene) + mesh.setEnabled(false) + vertexData.applyToMesh(mesh) + mesh } ?: defaultMesh } catch (e: Exception) { logger.error(e) { "Couldn't load mesh for $type (model: $model)." } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityManipulator.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityManipulator.kt new file mode 100644 index 00000000..443ddb1e --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityManipulator.kt @@ -0,0 +1,219 @@ +package world.phantasmal.web.questEditor.rendering + +import kotlinx.browser.document +import mu.KotlinLogging +import org.w3c.dom.events.Event +import org.w3c.dom.pointerevents.PointerEvent +import world.phantasmal.web.core.minusAssign +import world.phantasmal.web.externals.babylon.AbstractMesh +import world.phantasmal.web.externals.babylon.Vector2 +import world.phantasmal.web.externals.babylon.Vector3 +import world.phantasmal.web.questEditor.models.QuestEntityModel +import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata +import world.phantasmal.web.questEditor.stores.QuestEditorStore +import world.phantasmal.webui.DisposableContainer +import world.phantasmal.webui.dom.disposableListener + +private val logger = KotlinLogging.logger {} + +class EntityManipulator( + private val questEditorStore: QuestEditorStore, + private val renderer: QuestRenderer, +) : DisposableContainer() { + private val pointerPosition = Vector2.Zero() + private val lastPointerPosition = Vector2.Zero() + private var movedSinceLastPointerDown = false + private var state: State + + /** + * Whether entity transformations, deletions, etc. are enabled or not. + * Hover over and selection still work when this is set to false. + */ + var enabled: Boolean = true + set(enabled) { + field = enabled + state.cancel() + state = IdleState(questEditorStore, renderer, enabled) + } + + init { + state = IdleState(questEditorStore, renderer, enabled) + + observe(questEditorStore.selectedEntity, ::selectedEntityChanged) + + addDisposables( + disposableListener(renderer.canvas, "pointerdown", ::onPointerDown) + ) + } + + private fun selectedEntityChanged(entity: QuestEntityModel<*, *>?) { + state.cancel() + } + + private fun onPointerDown(e: PointerEvent) { + processPointerEvent(e) + + state = state.processEvent(PointerDownEvt( + e.buttons.toInt(), + movedSinceLastPointerDown + )) + + document.addEventListener("pointerup", ::onPointerUp) + } + + private fun onPointerUp(e: Event) { + try { + e as PointerEvent + processPointerEvent(e) + + state = state.processEvent(PointerUpEvt( + e.buttons.toInt(), + movedSinceLastPointerDown + )) + } finally { + document.removeEventListener("pointerup", ::onPointerUp) + } + } + + private fun processPointerEvent(e: PointerEvent) { + val rect = renderer.canvas.getBoundingClientRect() + pointerPosition.set(e.clientX - rect.left, e.clientY - rect.top) + + when (e.type) { + "pointerdown" -> { + movedSinceLastPointerDown = false + } + "pointermove", "pointerup" -> { + if (!pointerPosition.equals(lastPointerPosition)) { + movedSinceLastPointerDown = true + } + } + } + + lastPointerPosition.copyFrom(pointerPosition) + } +} + +private sealed class Evt + +private sealed class PointerEvt : Evt() { + abstract val buttons: Int + abstract val movedSinceLastPointerDown: Boolean +} + +private class PointerDownEvt( + override val buttons: Int, + override val movedSinceLastPointerDown: Boolean, +) : PointerEvt() + +private class PointerUpEvt( + override val buttons: Int, + override val movedSinceLastPointerDown: Boolean, +) : PointerEvt() + +private class Pick( + val entity: QuestEntityModel<*, *>, + val mesh: AbstractMesh, + + /** + * Vector that points from the grabbing point (somewhere on the model's surface) to the model's + * origin. + */ + val grabOffset: Vector3, + + /** + * Vector that points from the grabbing point to the terrain point directly under the model's + * origin. + */ +// val dragAdjust: Vector3, +) + +private abstract class State { + init { + logger.trace { "Transitioning to ${this::class.simpleName}." } + } + + abstract fun processEvent(event: Evt): State + + /** + * The state object should stop doing what it's doing and revert to the idle state as soon as + * possible. + */ + abstract fun cancel() +} + +private class IdleState( + private val questEditorStore: QuestEditorStore, + private val renderer: QuestRenderer, + private val enabled: Boolean, +) : State() { + override fun processEvent(event: Evt): State = + when (event) { + is PointerDownEvt -> { + pickEntity()?.let { pick -> + when (event.buttons) { + 1 -> { + questEditorStore.setSelectedEntity(pick.entity) + + if (enabled) { + // TODO: Enter TranslationState. + } + } + 2 -> { + questEditorStore.setSelectedEntity(pick.entity) + + if (enabled) { + // TODO: Enter RotationState. + } + } + } + } + + this + } + + is PointerUpEvt -> { + // If the user clicks on nothing, deselect the currently selected entity. + if (!event.movedSinceLastPointerDown && pickEntity() == null) { + questEditorStore.setSelectedEntity(null) + } + + this + } + } + + override fun cancel() { + // Do nothing. + } + + private fun pickEntity(): Pick? { + // Find the nearest object and NPC under the pointer. + val pickInfo = renderer.scene.pick(renderer.scene.pointerX, renderer.scene.pointerY) + if (pickInfo?.pickedMesh == null) return null + + val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity + ?: return null + val grabOffset = pickInfo.pickedMesh.position.clone() + grabOffset -= pickInfo.pickedPoint!! + + // TODO: dragAdjust. +// val dragAdjust = grabOffset.clone() +// +// // Find vertical distance to the ground. +// raycaster.set(intersection.object.position, DOWN_VECTOR) +// val [collision_geom_intersection] = raycaster.intersectObjects( +// this.renderer.collision_geometry.children, +// true, +// ) +// +// if (collision_geom_intersection) { +// dragAdjust.y -= collision_geom_intersection.distance +// } + + return Pick( + entity, + pickInfo.pickedMesh, + grabOffset, + ) + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt index 93c69f25..7abc490b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt @@ -3,31 +3,52 @@ package world.phantasmal.web.questEditor.rendering import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import mu.KotlinLogging -import world.phantasmal.core.disposable.Disposer -import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.observable.value.Val import world.phantasmal.web.externals.babylon.AbstractMesh +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.WaveModel import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata +import world.phantasmal.web.questEditor.stores.QuestEditorStore +import world.phantasmal.webui.DisposableContainer private val logger = KotlinLogging.logger {} -private class LoadedEntity(val entity: QuestEntityModel<*, *>, val disposer: Disposer) - class EntityMeshManager( private val scope: CoroutineScope, - private val selectedWave: Val, - private val renderer: QuestRenderer, + private val questEditorStore: QuestEditorStore, + renderer: QuestRenderer, private val entityAssetLoader: EntityAssetLoader, -) : TrackedDisposable() { +) : DisposableContainer() { private val queue: MutableList> = mutableListOf() - private val loadedEntities: MutableList = mutableListOf() + private val loadedEntities: MutableMap, LoadedEntity> = mutableMapOf() private var loading = false + private var entityMeshes = TransformNode("Entities", renderer.scene) + private var hoveredMesh: AbstractMesh? = null + private var selectedMesh: AbstractMesh? = null + + init { + observe(questEditorStore.selectedEntity) { entity -> + if (entity == null) { + unmarkSelected() + } else { + val loaded = loadedEntities[entity] + + // Mesh might not be loaded yet. + if (loaded == null) { + unmarkSelected() + } else { + markSelected(loaded.mesh) + } + } + } + } + override fun internalDispose() { + entityMeshes.dispose() removeAll() super.internalDispose() } @@ -63,26 +84,38 @@ class EntityMeshManager( for (entity in entities) { queue.remove(entity) - val loadedIndex = loadedEntities.indexOfFirst { it.entity == entity } - - if (loadedIndex != -1) { - val loaded = loadedEntities.removeAt(loadedIndex) - - renderer.removeEntityMesh(loaded.entity) - loaded.disposer.dispose() - } + loadedEntities.remove(entity)?.dispose() } } fun removeAll() { - for (loaded in loadedEntities) { - loaded.disposer.dispose() + for (loaded in loadedEntities.values) { + loaded.dispose() } loadedEntities.clear() queue.clear() } + private fun markSelected(entityMesh: AbstractMesh) { + if (entityMesh == hoveredMesh) { + hoveredMesh = null + } + + if (entityMesh != selectedMesh) { + selectedMesh?.let { it.showBoundingBox = false } + + entityMesh.showBoundingBox = true + } + + selectedMesh = entityMesh + } + + private fun unmarkSelected() { + selectedMesh?.let { it.showBoundingBox = false } + selectedMesh = null + } + private suspend fun load(entity: QuestEntityModel<*, *>) { // TODO val mesh = entityAssetLoader.loadMesh(entity.type, model = null) @@ -90,20 +123,30 @@ class EntityMeshManager( // Only add an instance of this mesh if the entity is still in the queue at this point. if (queue.remove(entity)) { val instance = mesh.createInstance(entity.type.uniqueName) - instance.metadata = EntityMetadata(entity) - instance.position = entity.worldPosition.value - updateEntityMesh(entity, instance) + instance.parent = entityMeshes + + if (entity == questEditorStore.selectedEntity.value) { + markSelected(instance) + } + + loadedEntities[entity] = LoadedEntity(entity, instance, questEditorStore.selectedWave) } } +} - private fun updateEntityMesh(entity: QuestEntityModel<*, *>, mesh: AbstractMesh) { - renderer.addEntityMesh(mesh) +private class LoadedEntity( + entity: QuestEntityModel<*, *>, + val mesh: AbstractMesh, + selectedWave: Val, +) : DisposableContainer() { + init { + mesh.metadata = EntityMetadata(entity) - val disposer = Disposer( - entity.worldPosition.observe { (pos) -> - mesh.position = pos - }, + observe(entity.worldPosition) { pos -> + mesh.position = pos + } + addDisposables( // TODO: Rotation. // entity.worldRotation.observe { (value) -> // mesh.rotation.copy(value) @@ -118,17 +161,21 @@ class EntityMeshManager( ) if (entity is QuestNpcModel) { - disposer.add( + addDisposable( selectedWave - .map(entity.wave) { selectedWave, entityWave -> - selectedWave == null || selectedWave == entityWave + .map(entity.wave) { sWave, entityWave -> + sWave == null || sWave == entityWave } .observe(callNow = true) { (visible) -> mesh.setEnabled(visible) }, ) } + } - loadedEntities.add(LoadedEntity(entity, disposer)) + override fun internalDispose() { + mesh.parent = null + mesh.dispose() + super.internalDispose() } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt index 4aeafc63..070d79ff 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt @@ -30,10 +30,10 @@ abstract class QuestMeshManager protected constructor( private val areaDisposer = disposer.add(Disposer()) private val areaMeshManager = AreaMeshManager(areaAssetLoader) private val npcMeshManager = disposer.add( - EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader) + EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader) ) private val objectMeshManager = disposer.add( - EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader) + EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader) ) private var loadJob: Job? = null @@ -51,7 +51,6 @@ abstract class QuestMeshManager protected constructor( areaDisposer.disposeAll() npcMeshManager.removeAll() objectMeshManager.removeAll() - renderer.resetEntityMeshes() // Load entity meshes. areaDisposer.addAll( diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt index 3d10d730..69843b6b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt @@ -2,15 +2,12 @@ package world.phantasmal.web.questEditor.rendering import org.w3c.dom.HTMLCanvasElement import world.phantasmal.web.core.rendering.Renderer -import world.phantasmal.web.externals.babylon.* -import world.phantasmal.web.questEditor.models.QuestEntityModel -import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata +import world.phantasmal.web.externals.babylon.ArcRotateCamera +import world.phantasmal.web.externals.babylon.Engine +import world.phantasmal.web.externals.babylon.Vector3 import kotlin.math.PI class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) { - private var entityMeshes = TransformNode("Entities", scene) - private val entityToMesh = mutableMapOf, AbstractMesh>() - override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene) init { @@ -31,41 +28,4 @@ class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas wheelDeltaPercentage = 0.1 } } - - override fun internalDispose() { - entityMeshes.dispose() - entityToMesh.clear() - super.internalDispose() - } - - fun resetEntityMeshes() { - entityMeshes.dispose(false) - entityToMesh.clear() - - entityMeshes = TransformNode("Entities", scene) - } - - fun addEntityMesh(mesh: AbstractMesh) { - val entity = (mesh.metadata as EntityMetadata).entity - mesh.parent = entityMeshes - - entityToMesh[entity]?.let { prevMesh -> - prevMesh.parent = null - prevMesh.dispose() - } - - entityToMesh[entity] = mesh - - // TODO: Mark selected entity. -// if (entity === this.selected_entity) { -// this.mark_selected(model) -// } - } - - fun removeEntityMesh(entity: QuestEntityModel<*, *>) { - entityToMesh.remove(entity)?.let { mesh -> - mesh.parent = null - mesh.dispose() - } - } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt index be31af16..56785f71 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.mutableVal import world.phantasmal.web.questEditor.models.AreaModel +import world.phantasmal.web.questEditor.models.QuestEntityModel import world.phantasmal.web.questEditor.models.QuestModel import world.phantasmal.web.questEditor.models.WaveModel import world.phantasmal.webui.stores.Store @@ -12,10 +13,12 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) private val _currentQuest = mutableVal(null) private val _currentArea = mutableVal(null) private val _selectedWave = mutableVal(null) + private val _selectedEntity = mutableVal?>(null) val currentQuest: Val = _currentQuest val currentArea: Val = _currentArea val selectedWave: Val = _selectedWave + val selectedEntity: Val?> = _selectedEntity // TODO: Take into account whether we're debugging or not. val questEditingDisabled: Val = currentQuest.map { it == null } @@ -28,4 +31,14 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) _currentArea.value = areaStore.getArea(quest.episode, 0) } } + + fun setSelectedEntity(entity: QuestEntityModel<*, *>?) { + entity?.let { + currentQuest.value?.let { quest -> + _currentArea.value = areaStore.getArea(quest.episode, entity.areaId) + } + } + + _selectedEntity.value = entity + } }