diff --git a/core/src/commonMain/kotlin/world/phantasmal/core/Preconditions.kt b/core/src/commonMain/kotlin/world/phantasmal/core/Preconditions.kt new file mode 100644 index 00000000..fadcfef3 --- /dev/null +++ b/core/src/commonMain/kotlin/world/phantasmal/core/Preconditions.kt @@ -0,0 +1,7 @@ +package world.phantasmal.core + +fun requireNonNegative(value: Int, name: String) { + require(value >= 0) { + "$name should be non-negative but was $value." + } +} diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Areas.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Areas.kt new file mode 100644 index 00000000..54a00e43 --- /dev/null +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Areas.kt @@ -0,0 +1,103 @@ +package world.phantasmal.lib.fileFormats.quest + +class Area( + val id: Int, + val name: String, + val order: Int, + val areaVariants: List, +) + +class AreaVariant( + val id: Int, + val area: Area, +) + +fun getAreasForEpisode(episode: Episode): List = + AREAS.getValue(episode) + +fun getAreaVariant(episode: Episode, areaId: Int, variantId: Int): AreaVariant? = + AREAS.getValue(episode).find { it.id == areaId }?.areaVariants?.getOrNull(variantId) + +private val AREAS by lazy { + var order = 0 + + @Suppress("UNUSED_CHANGED_VALUE") + val ep1 = listOf( + createArea(0, "Pioneer II", order++, 1), + createArea(1, "Forest 1", order++, 1), + createArea(2, "Forest 2", order++, 1), + createArea(11, "Under the Dome", order++, 1), + createArea(3, "Cave 1", order++, 6), + createArea(4, "Cave 2", order++, 5), + createArea(5, "Cave 3", order++, 6), + createArea(12, "Underground Channel", order++, 1), + createArea(6, "Mine 1", order++, 6), + createArea(7, "Mine 2", order++, 6), + createArea(13, "Monitor Room", order++, 1), + createArea(8, "Ruins 1", order++, 5), + createArea(9, "Ruins 2", order++, 5), + createArea(10, "Ruins 3", order++, 5), + createArea(14, "Dark Falz", order++, 1), + // TODO: + // createArea(15, "BA Ruins", order++, 3), + // createArea(16, "BA Spaceship", order++, 3), + // createArea(17, "Lobby", order++, 15), + ) + + order = 0 + + @Suppress("UNUSED_CHANGED_VALUE") + val ep2 = listOf( + createArea(0, "Lab", order++, 1), + createArea(1, "VR Temple Alpha", order++, 3), + createArea(2, "VR Temple Beta", order++, 3), + createArea(14, "VR Temple Final", order++, 1), + createArea(3, "VR Spaceship Alpha", order++, 3), + createArea(4, "VR Spaceship Beta", order++, 3), + createArea(15, "VR Spaceship Final", order++, 1), + createArea(5, "Central Control Area", order++, 1), + createArea(6, "Jungle Area East", order++, 1), + createArea(7, "Jungle Area North", order++, 1), + createArea(8, "Mountain Area", order++, 3), + createArea(9, "Seaside Area", order++, 1), + createArea(12, "Cliffs of Gal Da Val", order++, 1), + createArea(10, "Seabed Upper Levels", order++, 3), + createArea(11, "Seabed Lower Levels", order++, 3), + createArea(13, "Test Subject Disposal Area", order++, 1), + createArea(16, "Seaside Area at Night", order++, 2), + createArea(17, "Control Tower", order++, 5), + ) + + order = 0 + + @Suppress("UNUSED_CHANGED_VALUE") + val ep4 = listOf( + createArea(0, "Pioneer II (Ep. IV)", order++, 1), + createArea(1, "Crater Route 1", order++, 1), + createArea(2, "Crater Route 2", order++, 1), + createArea(3, "Crater Route 3", order++, 1), + createArea(4, "Crater Route 4", order++, 1), + createArea(5, "Crater Interior", order++, 1), + createArea(6, "Subterranean Desert 1", order++, 3), + createArea(7, "Subterranean Desert 2", order++, 3), + createArea(8, "Subterranean Desert 3", order++, 3), + createArea(9, "Meteor Impact Site", order++, 1), + ) + + mapOf( + Episode.I to ep1, + Episode.II to ep2, + Episode.IV to ep4, + ) +} + +private fun createArea(id: Int, name: String, order: Int, variants: Int): Area { + val avs = mutableListOf() + val area = Area(id, name, order, avs) + + for (avId in 0 until variants) { + avs.add(AreaVariant(avId, area)) + } + + return area +} diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestEntity.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestEntity.kt index 6abb12b3..c23e080c 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestEntity.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestEntity.kt @@ -5,6 +5,8 @@ import world.phantasmal.lib.fileFormats.Vec3 interface QuestEntity { val type: Type + var areaId: Int + /** * Section-relative position. */ diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt index 6a470e5e..eeec6512 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt @@ -6,7 +6,11 @@ import world.phantasmal.lib.fileFormats.ninja.angleToRad import world.phantasmal.lib.fileFormats.ninja.radToAngle import kotlin.math.roundToInt -class QuestNpc(var episode: Episode, var areaId: Int, val data: Buffer) : QuestEntity { +class QuestNpc( + var episode: Episode, + override var areaId: Int, + val data: Buffer, +) : QuestEntity { var typeId: Short get() = data.getShort(0) set(value) { diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt index 812dbfae..b4109012 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt @@ -6,7 +6,7 @@ import world.phantasmal.lib.fileFormats.ninja.angleToRad import world.phantasmal.lib.fileFormats.ninja.radToAngle import kotlin.math.roundToInt -class QuestObject(var areaId: Int, val data: Buffer) : QuestEntity { +class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity { var typeId: Int get() = data.getInt(0) set(value) { diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt index e7e9184e..6a6ebd2d 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt @@ -11,6 +11,8 @@ interface MutableListVal : ListVal, MutableVal> { fun removeAt(index: Int): E + fun replaceAll(elements: Iterable) + fun replaceAll(elements: Sequence) fun clear() diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt index f0d5294f..41515120 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt @@ -73,6 +73,13 @@ class SimpleListVal( return removed } + override fun replaceAll(elements: Iterable) { + val removed = ArrayList(this.elements) + this.elements.clear() + this.elements.addAll(elements) + finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements)) + } + override fun replaceAll(elements: Sequence) { val removed = ArrayList(this.elements) this.elements.clear() diff --git a/web/src/main/kotlin/world/phantasmal/web/application/Application.kt b/web/src/main/kotlin/world/phantasmal/web/application/Application.kt index c9bb5636..029a54a2 100644 --- a/web/src/main/kotlin/world/phantasmal/web/application/Application.kt +++ b/web/src/main/kotlin/world/phantasmal/web/application/Application.kt @@ -12,9 +12,9 @@ import world.phantasmal.web.application.controllers.NavigationController import world.phantasmal.web.application.widgets.ApplicationWidget import world.phantasmal.web.application.widgets.MainContentWidget import world.phantasmal.web.application.widgets.NavigationWidget +import world.phantasmal.web.core.PwTool import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.stores.ApplicationUrl -import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.huntOptimizer.HuntOptimizer @@ -47,6 +47,13 @@ class Application( // Initialize core stores shared by several submodules. val uiStore = addDisposable(UiStore(scope, applicationUrl)) + // The various tools Phantasmal World consists of. + val tools: List = listOf( + Viewer(createEngine), + QuestEditor(assetLoader, createEngine), + HuntOptimizer(assetLoader, uiStore), + ) + // Controllers. val navigationController = addDisposable(NavigationController(uiStore)) val mainContentController = addDisposable(MainContentController(uiStore)) @@ -56,28 +63,11 @@ class Application( ApplicationWidget( scope, NavigationWidget(scope, navigationController), - MainContentWidget(scope, mainContentController, mapOf( - PwTool.Viewer to { widgetScope -> - addDisposable(Viewer( - widgetScope, - createEngine, - )).createWidget() - }, - PwTool.QuestEditor to { widgetScope -> - addDisposable(QuestEditor( - widgetScope, - assetLoader, - createEngine, - )).createWidget() - }, - PwTool.HuntOptimizer to { widgetScope -> - addDisposable(HuntOptimizer( - widgetScope, - assetLoader, - uiStore, - )).createWidget() - }, - )) + MainContentWidget( + scope, + mainContentController, + tools.map { it.toolType to it::initialize }.toMap() + ) ) ) diff --git a/web/src/main/kotlin/world/phantasmal/web/application/controllers/MainContentController.kt b/web/src/main/kotlin/world/phantasmal/web/application/controllers/MainContentController.kt index df5d48da..911d0575 100644 --- a/web/src/main/kotlin/world/phantasmal/web/application/controllers/MainContentController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/application/controllers/MainContentController.kt @@ -1,10 +1,10 @@ package world.phantasmal.web.application.controllers import world.phantasmal.observable.value.Val -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.stores.UiStore import world.phantasmal.webui.controllers.Controller class MainContentController(uiStore: UiStore) : Controller() { - val tools: Map> = uiStore.toolToActive + val tools: Map> = uiStore.toolToActive } diff --git a/web/src/main/kotlin/world/phantasmal/web/application/controllers/NavigationController.kt b/web/src/main/kotlin/world/phantasmal/web/application/controllers/NavigationController.kt index 8b9351e7..6a4dbdfe 100644 --- a/web/src/main/kotlin/world/phantasmal/web/application/controllers/NavigationController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/application/controllers/NavigationController.kt @@ -1,14 +1,14 @@ package world.phantasmal.web.application.controllers import world.phantasmal.observable.value.Val -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.stores.UiStore import world.phantasmal.webui.controllers.Controller class NavigationController(private val uiStore: UiStore) : Controller() { - val tools: Map> = uiStore.toolToActive + val tools: Map> = uiStore.toolToActive - fun setCurrentTool(tool: PwTool) { + fun setCurrentTool(tool: PwToolType) { uiStore.setCurrentTool(tool) } } diff --git a/web/src/main/kotlin/world/phantasmal/web/application/widgets/MainContentWidget.kt b/web/src/main/kotlin/world/phantasmal/web/application/widgets/MainContentWidget.kt index 5004bae3..b8fbed4e 100644 --- a/web/src/main/kotlin/world/phantasmal/web/application/widgets/MainContentWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/application/widgets/MainContentWidget.kt @@ -4,7 +4,8 @@ import kotlinx.coroutines.CoroutineScope import org.w3c.dom.Node import world.phantasmal.observable.value.not import world.phantasmal.web.application.controllers.MainContentController -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.webui.dom.div import world.phantasmal.webui.widgets.LazyLoader import world.phantasmal.webui.widgets.Widget @@ -12,7 +13,7 @@ import world.phantasmal.webui.widgets.Widget class MainContentWidget( scope: CoroutineScope, private val ctrl: MainContentController, - private val toolViews: Map Widget>, + private val toolViews: Map Widget>, ) : Widget(scope) { override fun Node.createElement() = div { diff --git a/web/src/main/kotlin/world/phantasmal/web/application/widgets/PwToolButton.kt b/web/src/main/kotlin/world/phantasmal/web/application/widgets/PwToolButton.kt index 1d4108aa..a8053bb6 100644 --- a/web/src/main/kotlin/world/phantasmal/web/application/widgets/PwToolButton.kt +++ b/web/src/main/kotlin/world/phantasmal/web/application/widgets/PwToolButton.kt @@ -3,7 +3,7 @@ package world.phantasmal.web.application.widgets import kotlinx.coroutines.CoroutineScope import org.w3c.dom.Node import world.phantasmal.observable.Observable -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.webui.dom.input import world.phantasmal.webui.dom.label import world.phantasmal.webui.dom.span @@ -11,7 +11,7 @@ import world.phantasmal.webui.widgets.Control class PwToolButton( scope: CoroutineScope, - private val tool: PwTool, + private val tool: PwToolType, private val toggled: Observable, private val mouseDown: () -> Unit, ) : Control(scope) { diff --git a/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt b/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt new file mode 100644 index 00000000..4ea21fbd --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/core/BabylonExtensions.kt @@ -0,0 +1,30 @@ +package world.phantasmal.web.core + +import world.phantasmal.web.externals.babylon.Matrix +import world.phantasmal.web.externals.babylon.Vector3 + +operator fun Vector3.minus(other: Vector3): Vector3 = + subtract(other) + +infix fun Vector3.dot(other: Vector3): Double = + Vector3.Dot(this, other) + +infix fun Vector3.cross(other: Vector3): Vector3 = + cross(other) + +operator fun Matrix.timesAssign(other: Matrix) { + other.preMultiply(this) +} + +fun Matrix.preMultiply(other: Matrix) { + // Multiplies this by other. + multiplyToRef(other, this) +} + +fun Matrix.multiply(v: Vector3) { + Vector3.TransformCoordinatesToRef(v, this, v) +} + +fun Matrix.multiply3x3(v: Vector3) { + Vector3.TransformNormalToRef(v, this, v) +} diff --git a/web/src/main/kotlin/world/phantasmal/web/core/PwTool.kt b/web/src/main/kotlin/world/phantasmal/web/core/PwTool.kt new file mode 100644 index 00000000..6597b723 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/core/PwTool.kt @@ -0,0 +1,18 @@ +package world.phantasmal.web.core + +import kotlinx.coroutines.CoroutineScope +import world.phantasmal.webui.widgets.Widget + +/** + * Phantasmal World consists of several tools. + * An instance of PwTool should do as little work as possible in its constructor and defer any + * initialization work until [initialize] is called. + */ +interface PwTool { + val toolType: PwToolType + + /** + * The caller of this method takes ownership of the returned widget. + */ + fun initialize(scope: CoroutineScope): Widget +} diff --git a/web/src/main/kotlin/world/phantasmal/web/core/PwToolType.kt b/web/src/main/kotlin/world/phantasmal/web/core/PwToolType.kt new file mode 100644 index 00000000..6cf30793 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/core/PwToolType.kt @@ -0,0 +1,7 @@ +package world.phantasmal.web.core + +enum class PwToolType(val uiName: String, val slug: String) { + Viewer("Viewer", "viewer"), + QuestEditor("Quest Editor", "quest_editor"), + HuntOptimizer("Hunt Optimizer", "hunt_optimizer"), +} diff --git a/web/src/main/kotlin/world/phantasmal/web/core/controllers/PathAwareTabController.kt b/web/src/main/kotlin/world/phantasmal/web/core/controllers/PathAwareTabController.kt index e0f08fd8..0a8b5bc5 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/controllers/PathAwareTabController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/controllers/PathAwareTabController.kt @@ -1,6 +1,6 @@ package world.phantasmal.web.core.controllers -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.stores.UiStore import world.phantasmal.webui.controllers.Tab import world.phantasmal.webui.controllers.TabController @@ -9,7 +9,7 @@ open class PathAwareTab(override val title: String, val path: String) : Tab open class PathAwareTabController( private val uiStore: UiStore, - private val tool: PwTool, + private val tool: PwToolType, tabs: List, ) : TabController(tabs) { init { 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 bf96b339..2620d8e4 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 @@ -11,12 +11,17 @@ abstract class Renderer( protected val canvas: HTMLCanvasElement, protected val engine: Engine, ) : DisposableContainer() { - protected val scene = Scene(engine) - private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 0.0), scene) + val scene = Scene(engine) + + private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene) + protected abstract val camera: Camera init { - scene.clearColor = Color4(0.09, 0.09, 0.09, 1.0) + with(scene) { + useRightHandedSystem = true + clearColor = Color4(0.09, 0.09, 0.09, 1.0) + } } fun startRendering() { @@ -38,7 +43,7 @@ abstract class Renderer( } private fun render() { - val lightDirection = Vector3(-1.0, 1.0, 0.0) + val lightDirection = Vector3(-1.0, 1.0, 1.0) lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection) light.direction = lightDirection scene.render() diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt index 2d6b66c9..839f71ed 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt @@ -6,6 +6,7 @@ import world.phantasmal.lib.fileFormats.ninja.NinjaModel import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.lib.fileFormats.ninja.NjModel import world.phantasmal.lib.fileFormats.ninja.XjModel +import world.phantasmal.web.core.* import world.phantasmal.web.externals.babylon.* import kotlin.math.cos import kotlin.math.sin @@ -39,8 +40,7 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) if (ef.noRotate) NO_ROTATION else eulerToQuat(obj.rotation, ef.zxyRotationOrder), if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position), ) - - matrix.multiplyToRef(parentMatrix, matrix) + matrix.preMultiply(parentMatrix) if (!ef.hidden) { obj.model?.let { model -> @@ -72,8 +72,8 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) val position = vec3ToBabylon(vertex.position) val normal = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up() - Vector3.TransformCoordinatesToRef(position, matrix, position) - Vector3.TransformNormalToRef(normal, normalMatrix, normal) + matrix.multiply(position) + normalMatrix.multiply3x3(normal) Vertex( boneIndex, @@ -158,10 +158,10 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) for (vertex in model.vertices) { val p = vec3ToBabylon(vertex.position) - Vector3.TransformCoordinatesToRef(p, matrix, p) + matrix.multiply(p) val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up() - Vector3.TransformCoordinatesToRef(n, normalMatrix, n) + normalMatrix.multiply3x3(n) val uv = vertex.uv?.let(::vec2ToBabylon) ?: DEFAULT_UV @@ -190,16 +190,16 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) // Calculate a surface normal and reverse the vertex winding if at least 2 of the // vertex normals point in the opposite direction. This hack fixes the winding for // most models. - val normal = pb.subtract(pa).cross(pc.subtract(pa)) + val normal = (pb - pa) cross (pc - pa) if (!clockwise) { normal.negateInPlace() } val oppositeCount = - (if (Vector3.Dot(normal, na) < 0) 1 else 0) + - (if (Vector3.Dot(normal, nb) < 0) 1 else 0) + - (if (Vector3.Dot(normal, nc) < 0) 1 else 0) + (if (normal dot na < 0) 1 else 0) + + (if (normal dot nb < 0) 1 else 0) + + (if (normal dot nc < 0) 1 else 0) if (oppositeCount >= 2) { clockwise = !clockwise diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/VertexDataBuilder.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/VertexDataBuilder.kt index 6518c555..f8aefb5c 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/VertexDataBuilder.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/VertexDataBuilder.kt @@ -27,10 +27,10 @@ class VertexDataBuilder { fun getNormal(index: Int): Vector3 = normals[index] - fun addVertex(position: Vector3, normal: Vector3, uv: Vector2) { + fun addVertex(position: Vector3, normal: Vector3, uv: Vector2? = null) { positions.add(position) normals.add(normal) - uvs.add(uv) + uv?.let { uvs.add(uv) } } fun addIndex(index: Int) { @@ -55,11 +55,11 @@ class VertexDataBuilder { fun build(): VertexData { check(this.positions.size == this.normals.size) - check(this.positions.size == this.uvs.size) + check(this.uvs.isEmpty() || this.positions.size == this.uvs.size) val positions = Float32Array(3 * positions.size) val normals = Float32Array(3 * normals.size) - val uvs = Float32Array(2 * uvs.size) + val uvs = if (uvs.isEmpty()) null else Float32Array(2 * uvs.size) for (i in this.positions.indices) { val pos = this.positions[i] @@ -72,9 +72,11 @@ class VertexDataBuilder { normals[3 * i + 1] = normal.y.toFloat() normals[3 * i + 2] = normal.z.toFloat() - val uv = this.uvs[i] - uvs[2 * i] = uv.x.toFloat() - uvs[2 * i + 1] = uv.y.toFloat() + uvs?.let { + val uv = this.uvs[i] + uvs[2 * i] = uv.x.toFloat() + uvs[2 * i + 1] = uv.y.toFloat() + } } val data = VertexData() diff --git a/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt b/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt index e225059b..1ed709c3 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt @@ -6,19 +6,11 @@ import org.w3c.dom.events.KeyboardEvent import world.phantasmal.observable.value.MutableVal import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.mutableVal +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.models.Server import world.phantasmal.webui.dom.disposableListener import world.phantasmal.webui.stores.Store -/** - * Phantasmal World consists of several tools. - */ -enum class PwTool(val uiName: String, val slug: String) { - Viewer("Viewer", "viewer"), - QuestEditor("Quest Editor", "quest_editor"), - HuntOptimizer("Hunt Optimizer", "hunt_optimizer"), -} - interface ApplicationUrl { val url: Val @@ -27,8 +19,11 @@ interface ApplicationUrl { fun replaceUrl(url: String) } -class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) : Store(scope) { - private val _currentTool: MutableVal +class UiStore( + scope: CoroutineScope, + private val applicationUrl: ApplicationUrl, +) : Store(scope) { + private val _currentTool: MutableVal private val _path = mutableVal("") private val _server = mutableVal(Server.Ephinea) @@ -48,22 +43,22 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) */ private val features: MutableSet = mutableSetOf() - val tools: List = PwTool.values().toList() + val tools: List = PwToolType.values().toList() /** * The default tool that is loaded. */ - val defaultTool: PwTool = PwTool.Viewer + val defaultTool: PwToolType = PwToolType.Viewer /** - * The tool that is current visible. + * The tool that is currently visible. */ - val currentTool: Val + val currentTool: Val /** * Map of tools to a boolean Val that says whether they are the current tool or not. */ - val toolToActive: Map> + val toolToActive: Map> /** * Application URL without the tool path prefix. @@ -92,7 +87,7 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) observe(applicationUrl.url) { setDataFromUrl(it) } } - fun setCurrentTool(tool: PwTool) { + fun setCurrentTool(tool: PwToolType) { if (tool != currentTool.value) { updateApplicationUrl(tool, path = "", replace = false) setCurrentTool(tool, path = "") @@ -150,12 +145,12 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) } } - private fun setCurrentTool(tool: PwTool, path: String) { + private fun setCurrentTool(tool: PwToolType, path: String) { _path.value = path _currentTool.value = tool } - private fun updateApplicationUrl(tool: PwTool, path: String, replace: Boolean) { + private fun updateApplicationUrl(tool: PwToolType, path: String, replace: Boolean) { val fullPath = "/${tool.slug}${path}" val params: MutableMap = parameters[fullPath]?.let { HashMap(it) } ?: mutableMapOf() @@ -195,12 +190,12 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) } } - private fun handlerKey(tool: PwTool, binding: String): String { + private fun handlerKey(tool: PwToolType, binding: String): String { return "$tool -> $binding" } companion object { - private val SLUG_TO_PW_TOOL: Map = - PwTool.values().map { it.slug to it }.toMap() + private val SLUG_TO_PW_TOOL: Map = + PwToolType.values().map { it.slug to it }.toMap() } } diff --git a/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt b/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt index 89215562..6c2ad5ca 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/widgets/RendererWidget.kt @@ -4,35 +4,35 @@ import kotlinx.coroutines.CoroutineScope import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.Node import world.phantasmal.web.core.rendering.Renderer -import world.phantasmal.webui.dom.canvas +import world.phantasmal.webui.dom.div import world.phantasmal.webui.widgets.Widget import kotlin.math.floor class RendererWidget( scope: CoroutineScope, - private val createRenderer: (HTMLCanvasElement) -> Renderer, + private val canvas: HTMLCanvasElement, + private val renderer: Renderer, ) : Widget(scope) { - private var renderer: Renderer? = null override fun Node.createElement() = - canvas { + div { className = "pw-core-renderer" tabIndex = -1 observeResize() - renderer = addDisposable(createRenderer(this)) observe(selfOrAncestorHidden) { hidden -> if (hidden) { - renderer?.stopRendering() + renderer.stopRendering() } else { - renderer?.startRendering() + renderer.startRendering() } } + + append(canvas) } override fun resized(width: Double, height: Double) { - val canvas = (element as HTMLCanvasElement) canvas.width = floor(width).toInt() canvas.height = floor(height).toInt() } 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 18858b81..b6271d44 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 @@ -149,6 +149,7 @@ external class Engine( ) : ThinEngine external class Scene(engine: Engine) { + var useRightHandedSystem: Boolean var clearColor: Color4 fun render() diff --git a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizer.kt b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizer.kt index e825abbc..b3557440 100644 --- a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizer.kt @@ -1,6 +1,8 @@ package world.phantasmal.web.huntOptimizer import kotlinx.coroutines.CoroutineScope +import world.phantasmal.web.core.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController @@ -12,20 +14,24 @@ import world.phantasmal.webui.DisposableContainer import world.phantasmal.webui.widgets.Widget class HuntOptimizer( - private val scope: CoroutineScope, - assetLoader: AssetLoader, - uiStore: UiStore, -) : DisposableContainer() { - private val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader)) + private val assetLoader: AssetLoader, + private val uiStore: UiStore, +) : DisposableContainer(), PwTool { + override val toolType = PwToolType.HuntOptimizer - private val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore)) - private val methodsController = - addDisposable(MethodsController(uiStore, huntMethodStore)) + override fun initialize(scope: CoroutineScope): Widget { + // Stores + val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader)) - fun createWidget(): Widget = - HuntOptimizerWidget( + // Controllers + val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore)) + val methodsController = addDisposable(MethodsController(uiStore, huntMethodStore)) + + // Main Widget + return HuntOptimizerWidget( scope, ctrl = huntOptimizerController, - createMethodsWidget = { scope -> MethodsWidget(scope, methodsController) } + createMethodsWidget = { s -> MethodsWidget(s, methodsController) } ) + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/HuntOptimizerController.kt b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/HuntOptimizerController.kt index 707ac6fd..83925514 100644 --- a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/HuntOptimizerController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/HuntOptimizerController.kt @@ -1,15 +1,15 @@ package world.phantasmal.web.huntOptimizer.controllers +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.controllers.PathAwareTab import world.phantasmal.web.core.controllers.PathAwareTabController -import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls class HuntOptimizerController(uiStore: UiStore) : PathAwareTabController( uiStore, - PwTool.HuntOptimizer, + PwToolType.HuntOptimizer, listOf( PathAwareTab("Optimize", HuntOptimizerUrls.optimize), PathAwareTab("Methods", HuntOptimizerUrls.methods), diff --git a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/MethodsController.kt b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/MethodsController.kt index 13e3e82c..17a829b6 100644 --- a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/MethodsController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/controllers/MethodsController.kt @@ -4,9 +4,9 @@ import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.MutableListVal import world.phantasmal.observable.value.list.mutableListVal +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.controllers.PathAwareTab import world.phantasmal.web.core.controllers.PathAwareTabController -import world.phantasmal.web.core.stores.PwTool import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls import world.phantasmal.web.huntOptimizer.models.HuntMethodModel @@ -19,7 +19,7 @@ class MethodsController( huntMethodStore: HuntMethodStore, ) : PathAwareTabController( uiStore, - PwTool.HuntOptimizer, + PwToolType.HuntOptimizer, listOf( MethodsTab("Episode I", HuntOptimizerUrls.methodsEpisodeI, Episode.I), MethodsTab("Episode II", HuntOptimizerUrls.methodsEpisodeII, Episode.II), 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 bf785539..57350760 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt @@ -1,56 +1,68 @@ package world.phantasmal.web.questEditor +import kotlinx.browser.document import kotlinx.coroutines.CoroutineScope import org.w3c.dom.HTMLCanvasElement +import world.phantasmal.web.core.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.questEditor.controllers.NpcCountsController import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController import world.phantasmal.web.questEditor.controllers.QuestInfoController +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.QuestRenderer +import world.phantasmal.web.questEditor.stores.AreaStore import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.widgets.* import world.phantasmal.webui.DisposableContainer import world.phantasmal.webui.widgets.Widget class QuestEditor( - private val scope: CoroutineScope, private val assetLoader: AssetLoader, private val createEngine: (HTMLCanvasElement) -> Engine, -) : DisposableContainer() { - // Asset Loaders - private val questLoader = addDisposable(QuestLoader(scope, assetLoader)) +) : DisposableContainer(), PwTool { + override val toolType = PwToolType.QuestEditor - // Stores - private val questEditorStore = addDisposable(QuestEditorStore(scope)) + override fun initialize(scope: CoroutineScope): Widget { + // Renderer + val canvas = document.createElement("CANVAS") as HTMLCanvasElement + val renderer = addDisposable(QuestRenderer(canvas, createEngine(canvas))) - // Controllers - private val toolbarController = - addDisposable(QuestEditorToolbarController(questLoader, questEditorStore)) - private val questInfoController = addDisposable(QuestInfoController(questEditorStore)) - private val npcCountsController = addDisposable(NpcCountsController(questEditorStore)) + // Asset Loaders + val questLoader = addDisposable(QuestLoader(scope, assetLoader)) + val areaAssetLoader = addDisposable(AreaAssetLoader(scope, assetLoader, renderer.scene)) + val entityAssetLoader = addDisposable(EntityAssetLoader(scope, assetLoader, renderer.scene)) - fun createWidget(): Widget = - QuestEditorWidget( + // Stores + val areaStore = addDisposable(AreaStore(scope, areaAssetLoader)) + val questEditorStore = addDisposable(QuestEditorStore(scope, areaStore)) + + // Controllers + val toolbarController = + addDisposable(QuestEditorToolbarController(questLoader, areaStore, questEditorStore)) + val questInfoController = addDisposable(QuestInfoController(questEditorStore)) + val npcCountsController = addDisposable(NpcCountsController(questEditorStore)) + + // Rendering + addDisposable(QuestEditorMeshManager( + scope, + questEditorStore, + renderer, + areaAssetLoader, + entityAssetLoader + )) + + // Main Widget + return QuestEditorWidget( scope, QuestEditorToolbar(scope, toolbarController), - { scope -> QuestInfoWidget(scope, questInfoController) }, - { scope -> NpcCountsWidget(scope, npcCountsController) }, - { scope -> QuestEditorRendererWidget(scope, ::createQuestEditorRenderer) } + { s -> QuestInfoWidget(s, questInfoController) }, + { s -> NpcCountsWidget(s, npcCountsController) }, + { s -> QuestEditorRendererWidget(s, canvas, renderer) } ) - - private fun createQuestEditorRenderer(canvas: HTMLCanvasElement): QuestRenderer = - QuestRenderer(canvas, createEngine(canvas)) { renderer, scene -> - QuestEditorMeshManager( - scope, - questEditorStore.currentQuest, - questEditorStore.currentArea, - questEditorStore.selectedWave, - renderer, - EntityAssetLoader(scope, assetLoader, scene) - ) - } + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt index 9388ed47..b7fa7a39 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt @@ -12,6 +12,7 @@ import world.phantasmal.lib.fileFormats.quest.parseQstToQuest import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.mutableVal import world.phantasmal.web.questEditor.loading.QuestLoader +import world.phantasmal.web.questEditor.stores.AreaStore import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.stores.convertQuestToModel import world.phantasmal.webui.controllers.Controller @@ -21,6 +22,7 @@ private val logger = KotlinLogging.logger {} class QuestEditorToolbarController( private val questLoader: QuestLoader, + private val areaStore: AreaStore, private val questEditorStore: QuestEditorStore, ) : Controller() { private val _resultDialogVisible = mutableVal(false) @@ -31,7 +33,7 @@ class QuestEditorToolbarController( suspend fun createNewQuest(episode: Episode) { questEditorStore.setCurrentQuest( - convertQuestToModel(questLoader.loadDefaultQuest(episode)) + convertQuestToModel(questLoader.loadDefaultQuest(episode), areaStore::getVariant) ) } @@ -80,7 +82,7 @@ class QuestEditorToolbarController( } private fun setCurrentQuest(quest: Quest) { - questEditorStore.setCurrentQuest(convertQuestToModel(quest)) + questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant)) } private fun setResult(result: PwResult<*>) { diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt new file mode 100644 index 00000000..695e1b7b --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/AreaAssetLoader.kt @@ -0,0 +1,137 @@ +package world.phantasmal.web.questEditor.loading + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import org.khronos.webgl.ArrayBuffer +import world.phantasmal.lib.Endianness +import world.phantasmal.lib.cursor.cursor +import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry +import world.phantasmal.lib.fileFormats.quest.Episode +import world.phantasmal.web.core.loading.AssetLoader +import world.phantasmal.web.externals.babylon.Scene +import world.phantasmal.web.externals.babylon.TransformNode +import world.phantasmal.web.questEditor.models.AreaVariantModel +import world.phantasmal.web.questEditor.rendering.conversion.areaCollisionGeometryToTransformNode +import world.phantasmal.webui.DisposableContainer + +class AreaAssetLoader( + private val scope: CoroutineScope, + private val assetLoader: AssetLoader, + private val scene: Scene, +) : DisposableContainer() { + private val collisionObjectCache = + addDisposable(LoadingCache, TransformNode> { it.dispose() }) + + suspend fun loadCollisionGeometry( + episode: Episode, + areaVariant: AreaVariantModel, + ): TransformNode = + collisionObjectCache.getOrPut(Triple(episode, areaVariant.area.id, areaVariant.id)) { + scope.async { + val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision) + val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little)) + areaCollisionGeometryToTransformNode(scene, obj) + } + }.await() + + private suspend fun getAreaAsset( + episode: Episode, + areaVariant: AreaVariantModel, + type: AssetType, + ): ArrayBuffer { + val baseUrl = areaVersionToBaseUrl(episode, areaVariant) + val suffix = when (type) { + AssetType.Render -> "n.rel" + AssetType.Collision -> "c.rel" + } + return assetLoader.loadArrayBuffer(baseUrl + suffix) + } + + enum class AssetType { + Render, Collision + } +} + +private val AREA_BASE_NAMES: Map>> = mapOf( + Episode.I to listOf( + Pair("city00_00", 1), + Pair("forest01", 1), + Pair("forest02", 1), + Pair("cave01_", 6), + Pair("cave02_", 5), + Pair("cave03_", 6), + Pair("machine01_", 6), + Pair("machine02_", 6), + Pair("ancient01_", 5), + Pair("ancient02_", 5), + Pair("ancient03_", 5), + Pair("boss01", 1), + Pair("boss02", 1), + Pair("boss03", 1), + Pair("darkfalz00", 1), + ), + Episode.II to listOf( + Pair("labo00_00", 1), + Pair("ruins01_", 3), + Pair("ruins02_", 3), + Pair("space01_", 3), + Pair("space02_", 3), + Pair("jungle01_00", 1), + Pair("jungle02_00", 1), + Pair("jungle03_00", 1), + Pair("jungle04_", 3), + Pair("jungle05_00", 1), + Pair("seabed01_", 3), + Pair("seabed02_", 3), + Pair("boss05", 1), + Pair("boss06", 1), + Pair("boss07", 1), + Pair("boss08", 1), + Pair("jungle06_00", 1), + Pair("jungle07_", 5), + ), + Episode.IV to listOf( + Pair("city02_00", 1), + Pair("wilds01_00", 1), + Pair("wilds01_01", 1), + Pair("wilds01_02", 1), + Pair("wilds01_03", 1), + Pair("crater01_00", 1), + Pair("desert01_", 3), + Pair("desert02_", 3), + Pair("desert03_", 3), + Pair("boss09_00", 1), + ) +) + +private fun areaVersionToBaseUrl(episode: Episode, areaVariant: AreaVariantModel): String { + var areaId = areaVariant.area.id + var areaVariantId = areaVariant.id + + // Exception for Seaside area at night, variant 1. + // Phantasmal World 4 and Lost heart breaker use this to have two tower maps. + if (areaId == 16 && areaVariantId == 1) { + areaId = 17 + areaVariantId = 1 + } + + val episodeBaseNames = AREA_BASE_NAMES.getValue(episode) + + require(areaId in episodeBaseNames.indices) { + "Unknown episode $episode area $areaId." + } + + val (base_name, variants) = episodeBaseNames[areaId] + + require(areaVariantId in 0 until variants) { + "Unknown variant $areaVariantId of area $areaId in episode $episode." + } + + val variant = if (variants == 1) { + "" + } else { + areaVariantId.toString().padStart(2, '0') + } + + return "/maps/map_${base_name}${variant}" +} 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 b0cdf386..45f4d953 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 @@ -29,18 +29,24 @@ class EntityAssetLoader( private val assetLoader: AssetLoader, private val scene: Scene, ) : DisposableContainer() { - private val defaultMesh = MeshBuilder.CreateCylinder("Entity", obj { - diameter = 6.0 - height = 20.0 - }, scene).apply { - setEnabled(false) - position = Vector3(0.0, 10.0, 0.0) - } + private val defaultMesh = + MeshBuilder.CreateCylinder( + "Entity", + obj { + diameter = 6.0 + height = 20.0 + }, + scene + ).apply { + setEnabled(false) + position = Vector3(0.0, 10.0, 0.0) + } - private val meshCache = addDisposable(LoadingCache, Mesh>()) + private val meshCache = + addDisposable(LoadingCache, Mesh> { it.dispose() }) - suspend fun loadMesh(type: EntityType, model: Int?): Mesh { - return meshCache.getOrPut(Pair(type, model)) { + suspend fun loadMesh(type: EntityType, model: Int?): Mesh = + meshCache.getOrPut(Pair(type, model)) { scope.async { try { loadGeometry(type, model)?.let { vertexData -> @@ -60,7 +66,6 @@ class EntityAssetLoader( } } }.await() - } private suspend fun loadGeometry(type: EntityType, model: Int?): VertexData? { val geomFormat = entityTypeToGeometryFormat(type) diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/LoadingCache.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/LoadingCache.kt index 29bb2847..85c6996c 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/LoadingCache.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/LoadingCache.kt @@ -1,20 +1,30 @@ package world.phantasmal.web.questEditor.loading import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi import world.phantasmal.core.disposable.TrackedDisposable -class LoadingCache : TrackedDisposable() { +class LoadingCache(private val disposeValue: (V) -> Unit) : TrackedDisposable() { private val map = mutableMapOf>() operator fun set(key: K, value: Deferred) { map[key] = value } + @Suppress("DeferredIsResult") fun getOrPut(key: K, defaultValue: () -> Deferred): Deferred = map.getOrPut(key, defaultValue) + @OptIn(ExperimentalCoroutinesApi::class) override fun internalDispose() { - map.values.forEach { it.cancel() } + map.values.forEach { + if (it.isActive) { + it.cancel() + } else if (it.isCompleted) { + disposeValue(it.getCompleted()) + } + } + super.internalDispose() } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/QuestLoader.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/QuestLoader.kt index 9097da42..64530b39 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/QuestLoader.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/loading/QuestLoader.kt @@ -15,7 +15,7 @@ class QuestLoader( private val scope: CoroutineScope, private val assetLoader: AssetLoader, ) : TrackedDisposable() { - private val cache = LoadingCache() + private val cache = LoadingCache {} override fun internalDispose() { cache.dispose() diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaModel.kt index 02e8123b..cca3dbe1 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaModel.kt @@ -1,3 +1,17 @@ package world.phantasmal.web.questEditor.models -class AreaModel +import world.phantasmal.core.requireNonNegative + +class AreaModel( + /** + * Matches the PSO ID. + */ + val id: Int, + val name: String, + val order: Int, + val areaVariants: List, +) { + init { + requireNonNegative(id, "id") + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaVariantModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaVariantModel.kt index a690dd29..f8d4489b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaVariantModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/AreaVariantModel.kt @@ -1,3 +1,19 @@ package world.phantasmal.web.questEditor.models -class AreaVariantModel +import world.phantasmal.core.requireNonNegative +import world.phantasmal.observable.value.list.ListVal +import world.phantasmal.observable.value.list.mutableListVal + +class AreaVariantModel(val id: Int, val area: AreaModel) { + private val _sections = mutableListVal() + + val sections: ListVal = _sections + + init { + requireNonNegative(id, "id") + } + + fun setSections(sections: List) { + _sections.replaceAll(sections) + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt index fb7f8230..c70c0f47 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt @@ -15,6 +15,8 @@ abstract class QuestEntityModel>( val type: Type get() = entity.type + val areaId: Int get() = entity.areaId + /** * Section-relative position */ diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt index a25638fc..daeb4366 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestModel.kt @@ -13,14 +13,17 @@ class QuestModel( shortDescription: String, longDescription: String, val episode: Episode, + mapDesignations: Map, npcs: MutableList, objects: MutableList, + getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?, ) { private val _id = mutableVal(0) private val _language = mutableVal(0) private val _name = mutableVal("") private val _shortDescription = mutableVal("") private val _longDescription = mutableVal("") + private val _mapDesignations = mutableVal(mapDesignations) private val _npcs = mutableListVal(npcs) private val _objects = mutableListVal(objects) @@ -29,6 +32,18 @@ class QuestModel( val name: Val = _name val shortDescription: Val = _shortDescription val longDescription: Val = _longDescription + val mapDesignations: Val> = _mapDesignations + + /** + * Map of area IDs to entity counts. + */ + val entitiesPerArea: Val> + + /** + * One variant per area. + */ + val areaVariants: Val> + val npcs: ListVal = _npcs val objects: ListVal = _objects @@ -38,6 +53,39 @@ class QuestModel( setName(name) setShortDescription(shortDescription) setLongDescription(longDescription) + + entitiesPerArea = this.npcs.map(this.objects) { ns, os -> + val map = mutableMapOf() + + for (npc in ns) { + map[npc.areaId] = (map[npc.areaId] ?: 0) + 1 + } + + for (obj in os) { + map[obj.areaId] = (map[obj.areaId] ?: 0) + 1 + } + + map + } + + areaVariants = + entitiesPerArea.map(this.mapDesignations) { entitiesPerArea, mds -> + val variants = mutableMapOf() + + for (areaId in entitiesPerArea.values) { + getVariant(episode, areaId, 0)?.let { + variants[areaId] = it + } + } + + for ((areaId, variantId) in mds) { + getVariant(episode, areaId, variantId)?.let { + variants[areaId] = it + } + } + + variants.values.toList() + } } fun setId(id: Int): QuestModel { diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/SectionModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/SectionModel.kt new file mode 100644 index 00000000..7c368ff4 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/SectionModel.kt @@ -0,0 +1,16 @@ +package world.phantasmal.web.questEditor.models + +import world.phantasmal.web.externals.babylon.Vector3 + +class SectionModel( + val id: Int, + val position: Vector3, + val rotation: Vector3, + val areaVariant: AreaVariantModel, +) { + init { + require(id >= -1) { + "id should be greater than or equal to -1 but was $id." + } + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt new file mode 100644 index 00000000..7fed7cf9 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/AreaMeshManager.kt @@ -0,0 +1,31 @@ +package world.phantasmal.web.questEditor.rendering + +import mu.KotlinLogging +import world.phantasmal.lib.fileFormats.quest.Episode +import world.phantasmal.web.externals.babylon.TransformNode +import world.phantasmal.web.questEditor.loading.AreaAssetLoader +import world.phantasmal.web.questEditor.models.AreaVariantModel + +private val logger = KotlinLogging.logger {} + +class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) { + private var currentGeometry: TransformNode? = null + + suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) { + currentGeometry?.setEnabled(false) + + if (episode == null || areaVariant == null) { + return + } + + try { + val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant) + geom.setEnabled(true) + currentGeometry = geom + } catch (e: Exception) { + logger.error(e) { + "Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}." + } + } + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt index e9209f41..e9e246c2 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt @@ -1,46 +1,50 @@ package world.phantasmal.web.questEditor.rendering import kotlinx.coroutines.CoroutineScope -import world.phantasmal.observable.value.Val +import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.listVal +import world.phantasmal.web.questEditor.loading.AreaAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.models.* +import world.phantasmal.web.questEditor.stores.QuestEditorStore class QuestEditorMeshManager( scope: CoroutineScope, - private val currentQuest: Val, - private val currentArea: Val, - selectedWave: Val, + questEditorStore: QuestEditorStore, renderer: QuestRenderer, + areaAssetLoader: AreaAssetLoader, entityAssetLoader: EntityAssetLoader, -) : QuestMeshManager(scope, selectedWave, renderer, entityAssetLoader) { +) : QuestMeshManager(scope, questEditorStore, renderer, areaAssetLoader, entityAssetLoader) { init { disposer.addAll( - currentQuest.observe { areaVariantChanged() }, - currentArea.observe { areaVariantChanged() }, + questEditorStore.currentQuest.map(questEditorStore.currentArea, ::getAreaVariantDetails) + .observe { (details) -> + loadMeshes(details.episode, details.areaVariant, details.npcs, details.objects) + }, ) } - override fun getAreaVariantDetails(): AreaVariantDetails { - val quest = currentQuest.value - val area = currentArea.value + private fun getAreaVariantDetails(quest: QuestModel?, area: AreaModel?): AreaVariantDetails { + quest?.let { + val areaVariant = area?.let { + quest.areaVariants.value.find { it.area.id == area.id } + } - val areaVariant: AreaVariantModel? - val npcs: ListVal - val objects: ListVal - - if (quest != null /*&& area != null*/) { - // TODO: Set areaVariant. - areaVariant = null - npcs = quest.npcs // TODO: Filter NPCs. - objects = listVal() // TODO: Filter objects. - } else { - areaVariant = null - npcs = listVal() - objects = listVal() + areaVariant?.let { + val npcs = quest.npcs // TODO: Filter NPCs. + val objects = quest.objects // TODO: Filter objects. + return AreaVariantDetails(quest.episode, areaVariant, npcs, objects) + } } - return AreaVariantDetails(quest?.episode, areaVariant, npcs, objects) + return AreaVariantDetails(null, null, listVal(), listVal()) } + + private class AreaVariantDetails( + val episode: Episode?, + val areaVariant: AreaVariantModel?, + val npcs: ListVal, + val objects: ListVal, + ) } 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 7d630f5d..4aeafc63 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 @@ -1,53 +1,64 @@ package world.phantasmal.web.questEditor.rendering import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import world.phantasmal.core.disposable.Disposer import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.lib.fileFormats.quest.Episode -import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.ListValChangeEvent +import world.phantasmal.web.questEditor.loading.AreaAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.models.AreaVariantModel 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.stores.QuestEditorStore /** * Loads the necessary area and entity 3D models into [QuestRenderer]. */ abstract class QuestMeshManager protected constructor( - scope: CoroutineScope, - selectedWave: Val, + private val scope: CoroutineScope, + questEditorStore: QuestEditorStore, private val renderer: QuestRenderer, + areaAssetLoader: AreaAssetLoader, entityAssetLoader: EntityAssetLoader, ) : TrackedDisposable() { protected val disposer = Disposer() private val areaDisposer = disposer.add(Disposer()) + private val areaMeshManager = AreaMeshManager(areaAssetLoader) private val npcMeshManager = disposer.add( - EntityMeshManager(scope, selectedWave, renderer, entityAssetLoader) + EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader) ) private val objectMeshManager = disposer.add( - EntityMeshManager(scope, selectedWave, renderer, entityAssetLoader) + EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader) ) - protected abstract fun getAreaVariantDetails(): AreaVariantDetails + private var loadJob: Job? = null - protected fun areaVariantChanged() { - val details = getAreaVariantDetails() + protected fun loadMeshes( + episode: Episode?, + areaVariant: AreaVariantModel?, + npcs: ListVal, + objects: ListVal, + ) { + loadJob?.cancel() + loadJob = scope.launch { + areaMeshManager.load(episode, areaVariant) - // TODO: Load area mesh. + areaDisposer.disposeAll() + npcMeshManager.removeAll() + objectMeshManager.removeAll() + renderer.resetEntityMeshes() - areaDisposer.disposeAll() - npcMeshManager.removeAll() - renderer.resetEntityMeshes() - - // Load entity meshes. - areaDisposer.addAll( - details.npcs.observeList(callNow = true, ::npcsChanged), - details.objects.observeList(callNow = true, ::objectsChanged), - ) + // Load entity meshes. + areaDisposer.addAll( + npcs.observeList(callNow = true, ::npcsChanged), + objects.observeList(callNow = true, ::objectsChanged), + ) + } } override fun internalDispose() { @@ -69,10 +80,3 @@ abstract class QuestMeshManager protected constructor( } } } - -class AreaVariantDetails( - val episode: Episode?, - val areaVariant: AreaVariantModel?, - val npcs: ListVal, - val objects: ListVal, -) 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 21c14657..3d10d730 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 @@ -7,16 +7,11 @@ import world.phantasmal.web.questEditor.models.QuestEntityModel import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata import kotlin.math.PI -class QuestRenderer( - canvas: HTMLCanvasElement, - engine: Engine, - createMeshManager: (QuestRenderer, Scene) -> QuestMeshManager, -) : Renderer(canvas, engine) { - private val meshManager = createMeshManager(this, scene) +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", 0.0, PI / 6, 500.0, Vector3.Zero(), scene) + override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene) init { with(camera) { @@ -31,14 +26,13 @@ class QuestRenderer( angularSensibilityY = 200.0 panningInertia = 0.0 panningSensibility = 3.0 - panningAxis = Vector3(1.0, 0.0, 1.0) + panningAxis = Vector3(1.0, 0.0, -1.0) pinchDeltaPercentage = 0.1 wheelDeltaPercentage = 0.1 } } override fun internalDispose() { - meshManager.dispose() entityMeshes.dispose() entityToMesh.clear() super.internalDispose() diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/conversion/Areas.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/conversion/Areas.kt new file mode 100644 index 00000000..f2f69b52 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/conversion/Areas.kt @@ -0,0 +1,50 @@ +package world.phantasmal.web.questEditor.rendering.conversion + +import world.phantasmal.lib.fileFormats.CollisionObject +import world.phantasmal.web.core.rendering.conversion.VertexDataBuilder +import world.phantasmal.web.core.rendering.conversion.vec3ToBabylon +import world.phantasmal.web.externals.babylon.Mesh +import world.phantasmal.web.externals.babylon.Scene +import world.phantasmal.web.externals.babylon.TransformNode + +fun areaCollisionGeometryToTransformNode(scene: Scene, obj: CollisionObject): TransformNode { + val node = TransformNode("", scene) + + for (collisionMesh in obj.meshes) { + val builder = VertexDataBuilder() + + for (triangle in collisionMesh.triangles) { + val isSectionTransition = (triangle.flags and 0b1000000) != 0 + val isVegetation = (triangle.flags and 0b10000) != 0 + val isGround = (triangle.flags and 0b1) != 0 + val colorIndex = when { + isSectionTransition -> 3 + isVegetation -> 2 + isGround -> 1 + else -> 0 + } + + // Filter out walls. + if (colorIndex != 0) { + val p1 = vec3ToBabylon(collisionMesh.vertices[triangle.index1]) + val p2 = vec3ToBabylon(collisionMesh.vertices[triangle.index2]) + val p3 = vec3ToBabylon(collisionMesh.vertices[triangle.index3]) + val n = vec3ToBabylon(triangle.normal) + + builder.addIndex(builder.vertexCount) + builder.addVertex(p1, n) + builder.addIndex(builder.vertexCount) + builder.addVertex(p3, n) + builder.addIndex(builder.vertexCount) + builder.addVertex(p2, n) + } + } + + if (builder.vertexCount > 0) { + val mesh = Mesh("Collision Geometry", scene, parent = node) + builder.build().applyToMesh(mesh) + } + } + + return node +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AreaStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AreaStore.kt new file mode 100644 index 00000000..a784f787 --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AreaStore.kt @@ -0,0 +1,36 @@ +package world.phantasmal.web.questEditor.stores + +import kotlinx.coroutines.CoroutineScope +import world.phantasmal.lib.fileFormats.quest.Episode +import world.phantasmal.lib.fileFormats.quest.getAreasForEpisode +import world.phantasmal.web.questEditor.loading.AreaAssetLoader +import world.phantasmal.web.questEditor.models.AreaModel +import world.phantasmal.web.questEditor.models.AreaVariantModel +import world.phantasmal.webui.stores.Store + +class AreaStore(scope: CoroutineScope, areaAssetLoader: AreaAssetLoader) : Store(scope) { + private val areas: Map> + + init { + areas = Episode.values() + .map { episode -> + episode to getAreasForEpisode(episode).map { area -> + val variants = mutableListOf() + val areaModel = AreaModel(area.id, area.name, area.order, variants) + + area.areaVariants.forEach { variant -> + variants.add(AreaVariantModel(variant.id, areaModel)) + } + + areaModel + } + } + .toMap() + } + + fun getArea(episode: Episode, areaId: Int): AreaModel? = + areas.getValue(episode).find { it.id == areaId } + + fun getVariant(episode: Episode, areaId: Int, variantId: Int): AreaVariantModel? = + getArea(episode, areaId)?.areaVariants?.getOrNull(variantId) +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt index 5279b20f..f210dd14 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/ModelConversion.kt @@ -1,11 +1,16 @@ package world.phantasmal.web.questEditor.stores +import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.lib.fileFormats.quest.Quest +import world.phantasmal.web.questEditor.models.AreaVariantModel import world.phantasmal.web.questEditor.models.QuestModel import world.phantasmal.web.questEditor.models.QuestNpcModel import world.phantasmal.web.questEditor.models.QuestObjectModel -fun convertQuestToModel(quest: Quest): QuestModel { +fun convertQuestToModel( + quest: Quest, + getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?, +): QuestModel { return QuestModel( quest.id, quest.language, @@ -13,8 +18,10 @@ fun convertQuestToModel(quest: Quest): QuestModel { quest.shortDescription, quest.longDescription, quest.episode, + quest.mapDesignations, // TODO: Add WaveModel to QuestNpcModel quest.npcs.mapTo(mutableListOf()) { QuestNpcModel(it, null) }, - quest.objects.mapTo(mutableListOf()) { QuestObjectModel(it) } + quest.objects.mapTo(mutableListOf()) { QuestObjectModel(it) }, + getVariant ) } 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 3be789ac..be31af16 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 @@ -8,7 +8,7 @@ import world.phantasmal.web.questEditor.models.QuestModel import world.phantasmal.web.questEditor.models.WaveModel import world.phantasmal.webui.stores.Store -class QuestEditorStore(scope: CoroutineScope) : Store(scope) { +class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) : Store(scope) { private val _currentQuest = mutableVal(null) private val _currentArea = mutableVal(null) private val _selectedWave = mutableVal(null) @@ -21,6 +21,11 @@ class QuestEditorStore(scope: CoroutineScope) : Store(scope) { val questEditingDisabled: Val = currentQuest.map { it == null } fun setCurrentQuest(quest: QuestModel?) { + _currentArea.value = null _currentQuest.value = quest + + quest?.let { + _currentArea.value = areaStore.getArea(quest.episode, 0) + } } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorRendererWidget.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorRendererWidget.kt index 9482da51..ef9ac348 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorRendererWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorRendererWidget.kt @@ -6,5 +6,6 @@ import world.phantasmal.web.questEditor.rendering.QuestRenderer class QuestEditorRendererWidget( scope: CoroutineScope, - createRenderer: (HTMLCanvasElement) -> QuestRenderer, -) : QuestRendererWidget(scope, createRenderer) + canvas: HTMLCanvasElement, + renderer: QuestRenderer, +) : QuestRendererWidget(scope, canvas, renderer) diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestRendererWidget.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestRendererWidget.kt index 036fbbff..c77c82d9 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestRendererWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestRendererWidget.kt @@ -10,14 +10,15 @@ import world.phantasmal.webui.widgets.Widget abstract class QuestRendererWidget( scope: CoroutineScope, - private val createRenderer: (HTMLCanvasElement) -> QuestRenderer, + private val canvas: HTMLCanvasElement, + private val renderer: QuestRenderer, ) : Widget(scope) { override fun Node.createElement() = div { className = "pw-quest-editor-quest-renderer" tabIndex = -1 - addChild(RendererWidget(scope, createRenderer)) + addChild(RendererWidget(scope, canvas, renderer)) } companion object { diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/Viewer.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/Viewer.kt index 004c6925..38447275 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/Viewer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/Viewer.kt @@ -1,7 +1,10 @@ package world.phantasmal.web.viewer +import kotlinx.browser.document import kotlinx.coroutines.CoroutineScope import org.w3c.dom.HTMLCanvasElement +import world.phantasmal.web.core.PwTool +import world.phantasmal.web.core.PwToolType import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.viewer.controller.ViewerToolbarController import world.phantasmal.web.viewer.rendering.MeshRenderer @@ -12,18 +15,22 @@ import world.phantasmal.webui.DisposableContainer import world.phantasmal.webui.widgets.Widget class Viewer( - private val scope: CoroutineScope, private val createEngine: (HTMLCanvasElement) -> Engine, -) : DisposableContainer() { - // Stores - private val viewerStore = addDisposable(ViewerStore(scope)) +) : DisposableContainer(), PwTool { + override val toolType = PwToolType.Viewer - // Controllers - private val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore)) + override fun initialize(scope: CoroutineScope): Widget { + // Stores + val viewerStore = addDisposable(ViewerStore(scope)) - fun createWidget(): Widget = - ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), ::createViewerRenderer) + // Controllers + val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore)) - private fun createViewerRenderer(canvas: HTMLCanvasElement): MeshRenderer = - MeshRenderer(viewerStore, canvas, createEngine(canvas)) + // Rendering + val canvas = document.createElement("CANVAS") as HTMLCanvasElement + val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas))) + + // Main Widget + return ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), canvas, renderer) + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt index 5b5c6e6e..c45253d4 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/rendering/MeshRenderer.kt @@ -4,7 +4,10 @@ import org.w3c.dom.HTMLCanvasElement import world.phantasmal.lib.fileFormats.ninja.NinjaObject import world.phantasmal.web.core.rendering.Renderer import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData -import world.phantasmal.web.externals.babylon.* +import world.phantasmal.web.externals.babylon.ArcRotateCamera +import world.phantasmal.web.externals.babylon.Engine +import world.phantasmal.web.externals.babylon.Mesh +import world.phantasmal.web.externals.babylon.Vector3 import world.phantasmal.web.viewer.store.ViewerStore import kotlin.math.PI @@ -15,7 +18,7 @@ class MeshRenderer( ) : Renderer(canvas, engine) { private var mesh: Mesh? = null - override val camera = ArcRotateCamera("Camera", 0.0, PI / 3, 70.0, Vector3.Zero(), scene) + override val camera = ArcRotateCamera("Camera", PI / 2, PI / 3, 70.0, Vector3.Zero(), scene) init { with(camera) { @@ -29,7 +32,7 @@ class MeshRenderer( angularSensibilityX = 200.0 angularSensibilityY = 200.0 panningInertia = 0.0 - panningSensibility = 20.0 + panningSensibility = 10.0 panningAxis = Vector3(1.0, 1.0, 0.0) pinchDeltaPercentage = 0.1 wheelDeltaPercentage = 0.1 diff --git a/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerWidget.kt b/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerWidget.kt index 6e59ff95..3b5a8f49 100644 --- a/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/viewer/widgets/ViewerWidget.kt @@ -11,7 +11,8 @@ import world.phantasmal.webui.widgets.Widget class ViewerWidget( scope: CoroutineScope, private val toolbar: Widget, - private val createRenderer: (HTMLCanvasElement) -> Renderer, + private val canvas: HTMLCanvasElement, + private val renderer: Renderer, ) : Widget(scope) { override fun Node.createElement() = div { @@ -21,7 +22,7 @@ class ViewerWidget( div { className = "pw-viewer-viewer-container" - addChild(RendererWidget(scope, createRenderer)) + addChild(RendererWidget(scope, canvas, renderer)) } } @@ -38,6 +39,7 @@ class ViewerWidget( flex-grow: 1; display: flex; flex-direction: row; + overflow: hidden; } """.trimIndent()) } diff --git a/web/src/test/kotlin/world/phantasmal/web/application/ApplicationTests.kt b/web/src/test/kotlin/world/phantasmal/web/application/ApplicationTests.kt index bad7ca7e..db6726ca 100644 --- a/web/src/test/kotlin/world/phantasmal/web/application/ApplicationTests.kt +++ b/web/src/test/kotlin/world/phantasmal/web/application/ApplicationTests.kt @@ -10,7 +10,7 @@ import world.phantasmal.core.disposable.disposable import world.phantasmal.core.disposable.use import world.phantasmal.testUtils.TestSuite import world.phantasmal.web.core.loading.AssetLoader -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwTool import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.test.TestApplicationUrl import kotlin.test.Test diff --git a/web/src/test/kotlin/world/phantasmal/web/core/controllers/PathAwareTabControllerTests.kt b/web/src/test/kotlin/world/phantasmal/web/core/controllers/PathAwareTabControllerTests.kt index 20729000..38a52487 100644 --- a/web/src/test/kotlin/world/phantasmal/web/core/controllers/PathAwareTabControllerTests.kt +++ b/web/src/test/kotlin/world/phantasmal/web/core/controllers/PathAwareTabControllerTests.kt @@ -1,7 +1,7 @@ package world.phantasmal.web.core.controllers import world.phantasmal.testUtils.TestSuite -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwTool import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.test.TestApplicationUrl import kotlin.test.Test diff --git a/web/src/test/kotlin/world/phantasmal/web/core/store/UiStoreTests.kt b/web/src/test/kotlin/world/phantasmal/web/core/store/UiStoreTests.kt index 877f4ad4..64a9a5ca 100644 --- a/web/src/test/kotlin/world/phantasmal/web/core/store/UiStoreTests.kt +++ b/web/src/test/kotlin/world/phantasmal/web/core/store/UiStoreTests.kt @@ -1,7 +1,7 @@ package world.phantasmal.web.core.store import world.phantasmal.testUtils.TestSuite -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwTool import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.test.TestApplicationUrl import kotlin.test.Test diff --git a/web/src/test/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizerTests.kt b/web/src/test/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizerTests.kt index 831c6fde..87286034 100644 --- a/web/src/test/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizerTests.kt +++ b/web/src/test/kotlin/world/phantasmal/web/huntOptimizer/HuntOptimizerTests.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.cancel import world.phantasmal.core.disposable.disposable import world.phantasmal.testUtils.TestSuite import world.phantasmal.web.core.loading.AssetLoader -import world.phantasmal.web.core.stores.PwTool +import world.phantasmal.web.core.PwTool import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.test.TestApplicationUrl import kotlin.test.Test