mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-07 16:58:26 +08:00
Added area mesh loading.
This commit is contained in:
parent
8ec75f8b4a
commit
8de81c9cb4
@ -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."
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package world.phantasmal.lib.fileFormats.quest
|
||||
|
||||
class Area(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val order: Int,
|
||||
val areaVariants: List<AreaVariant>,
|
||||
)
|
||||
|
||||
class AreaVariant(
|
||||
val id: Int,
|
||||
val area: Area,
|
||||
)
|
||||
|
||||
fun getAreasForEpisode(episode: Episode): List<Area> =
|
||||
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<AreaVariant>()
|
||||
val area = Area(id, name, order, avs)
|
||||
|
||||
for (avId in 0 until variants) {
|
||||
avs.add(AreaVariant(avId, area))
|
||||
}
|
||||
|
||||
return area
|
||||
}
|
@ -5,6 +5,8 @@ import world.phantasmal.lib.fileFormats.Vec3
|
||||
interface QuestEntity<Type : EntityType> {
|
||||
val type: Type
|
||||
|
||||
var areaId: Int
|
||||
|
||||
/**
|
||||
* Section-relative position.
|
||||
*/
|
||||
|
@ -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<NpcType> {
|
||||
class QuestNpc(
|
||||
var episode: Episode,
|
||||
override var areaId: Int,
|
||||
val data: Buffer,
|
||||
) : QuestEntity<NpcType> {
|
||||
var typeId: Short
|
||||
get() = data.getShort(0)
|
||||
set(value) {
|
||||
|
@ -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<ObjectType> {
|
||||
class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<ObjectType> {
|
||||
var typeId: Int
|
||||
get() = data.getInt(0)
|
||||
set(value) {
|
||||
|
@ -11,6 +11,8 @@ interface MutableListVal<E> : ListVal<E>, MutableVal<List<E>> {
|
||||
|
||||
fun removeAt(index: Int): E
|
||||
|
||||
fun replaceAll(elements: Iterable<E>)
|
||||
|
||||
fun replaceAll(elements: Sequence<E>)
|
||||
|
||||
fun clear()
|
||||
|
@ -73,6 +73,13 @@ class SimpleListVal<E>(
|
||||
return removed
|
||||
}
|
||||
|
||||
override fun replaceAll(elements: Iterable<E>) {
|
||||
val removed = ArrayList(this.elements)
|
||||
this.elements.clear()
|
||||
this.elements.addAll(elements)
|
||||
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
|
||||
}
|
||||
|
||||
override fun replaceAll(elements: Sequence<E>) {
|
||||
val removed = ArrayList(this.elements)
|
||||
this.elements.clear()
|
||||
|
@ -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<PwTool> = 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()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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<PwTool, Val<Boolean>> = uiStore.toolToActive
|
||||
val tools: Map<PwToolType, Val<Boolean>> = uiStore.toolToActive
|
||||
}
|
||||
|
@ -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<PwTool, Val<Boolean>> = uiStore.toolToActive
|
||||
val tools: Map<PwToolType, Val<Boolean>> = uiStore.toolToActive
|
||||
|
||||
fun setCurrentTool(tool: PwTool) {
|
||||
fun setCurrentTool(tool: PwToolType) {
|
||||
uiStore.setCurrentTool(tool)
|
||||
}
|
||||
}
|
||||
|
@ -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<PwTool, (CoroutineScope) -> Widget>,
|
||||
private val toolViews: Map<PwToolType, (CoroutineScope) -> Widget>,
|
||||
) : Widget(scope) {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
|
@ -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<Boolean>,
|
||||
private val mouseDown: () -> Unit,
|
||||
) : Control(scope) {
|
||||
|
@ -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)
|
||||
}
|
18
web/src/main/kotlin/world/phantasmal/web/core/PwTool.kt
Normal file
18
web/src/main/kotlin/world/phantasmal/web/core/PwTool.kt
Normal file
@ -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
|
||||
}
|
@ -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"),
|
||||
}
|
@ -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<T : PathAwareTab>(
|
||||
private val uiStore: UiStore,
|
||||
private val tool: PwTool,
|
||||
private val tool: PwToolType,
|
||||
tabs: List<T>,
|
||||
) : TabController<T>(tabs) {
|
||||
init {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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,10 +72,12 @@ class VertexDataBuilder {
|
||||
normals[3 * i + 1] = normal.y.toFloat()
|
||||
normals[3 * i + 2] = normal.z.toFloat()
|
||||
|
||||
uvs?.let {
|
||||
val uv = this.uvs[i]
|
||||
uvs[2 * i] = uv.x.toFloat()
|
||||
uvs[2 * i + 1] = uv.y.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
val data = VertexData()
|
||||
data.positions = positions
|
||||
|
@ -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<String>
|
||||
|
||||
@ -27,8 +19,11 @@ interface ApplicationUrl {
|
||||
fun replaceUrl(url: String)
|
||||
}
|
||||
|
||||
class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) : Store(scope) {
|
||||
private val _currentTool: MutableVal<PwTool>
|
||||
class UiStore(
|
||||
scope: CoroutineScope,
|
||||
private val applicationUrl: ApplicationUrl,
|
||||
) : Store(scope) {
|
||||
private val _currentTool: MutableVal<PwToolType>
|
||||
|
||||
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<String> = mutableSetOf()
|
||||
|
||||
val tools: List<PwTool> = PwTool.values().toList()
|
||||
val tools: List<PwToolType> = 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<PwTool>
|
||||
val currentTool: Val<PwToolType>
|
||||
|
||||
/**
|
||||
* Map of tools to a boolean Val that says whether they are the current tool or not.
|
||||
*/
|
||||
val toolToActive: Map<PwTool, Val<Boolean>>
|
||||
val toolToActive: Map<PwToolType, Val<Boolean>>
|
||||
|
||||
/**
|
||||
* 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<String, String> =
|
||||
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<String, PwTool> =
|
||||
PwTool.values().map { it.slug to it }.toMap()
|
||||
private val SLUG_TO_PW_TOOL: Map<String, PwToolType> =
|
||||
PwToolType.values().map { it.slug to it }.toMap()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -149,6 +149,7 @@ external class Engine(
|
||||
) : ThinEngine
|
||||
|
||||
external class Scene(engine: Engine) {
|
||||
var useRightHandedSystem: Boolean
|
||||
var clearColor: Color4
|
||||
|
||||
fun render()
|
||||
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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<PathAwareTab>(
|
||||
uiStore,
|
||||
PwTool.HuntOptimizer,
|
||||
PwToolType.HuntOptimizer,
|
||||
listOf(
|
||||
PathAwareTab("Optimize", HuntOptimizerUrls.optimize),
|
||||
PathAwareTab("Methods", HuntOptimizerUrls.methods),
|
||||
|
@ -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<MethodsTab>(
|
||||
uiStore,
|
||||
PwTool.HuntOptimizer,
|
||||
PwToolType.HuntOptimizer,
|
||||
listOf(
|
||||
MethodsTab("Episode I", HuntOptimizerUrls.methodsEpisodeI, Episode.I),
|
||||
MethodsTab("Episode II", HuntOptimizerUrls.methodsEpisodeII, Episode.II),
|
||||
|
@ -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() {
|
||||
) : DisposableContainer(), PwTool {
|
||||
override val toolType = PwToolType.QuestEditor
|
||||
|
||||
override fun initialize(scope: CoroutineScope): Widget {
|
||||
// Renderer
|
||||
val canvas = document.createElement("CANVAS") as HTMLCanvasElement
|
||||
val renderer = addDisposable(QuestRenderer(canvas, createEngine(canvas)))
|
||||
|
||||
// Asset Loaders
|
||||
private val questLoader = addDisposable(QuestLoader(scope, assetLoader))
|
||||
val questLoader = addDisposable(QuestLoader(scope, assetLoader))
|
||||
val areaAssetLoader = addDisposable(AreaAssetLoader(scope, assetLoader, renderer.scene))
|
||||
val entityAssetLoader = addDisposable(EntityAssetLoader(scope, assetLoader, renderer.scene))
|
||||
|
||||
// Stores
|
||||
private val questEditorStore = addDisposable(QuestEditorStore(scope))
|
||||
val areaStore = addDisposable(AreaStore(scope, areaAssetLoader))
|
||||
val questEditorStore = addDisposable(QuestEditorStore(scope, areaStore))
|
||||
|
||||
// Controllers
|
||||
private val toolbarController =
|
||||
addDisposable(QuestEditorToolbarController(questLoader, questEditorStore))
|
||||
private val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
||||
private val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||
val toolbarController =
|
||||
addDisposable(QuestEditorToolbarController(questLoader, areaStore, questEditorStore))
|
||||
val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
||||
val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||
|
||||
fun createWidget(): Widget =
|
||||
QuestEditorWidget(
|
||||
// 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) }
|
||||
)
|
||||
|
||||
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)
|
||||
{ s -> QuestInfoWidget(s, questInfoController) },
|
||||
{ s -> NpcCountsWidget(s, npcCountsController) },
|
||||
{ s -> QuestEditorRendererWidget(s, canvas, renderer) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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<*>) {
|
||||
|
@ -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<Triple<Episode, Int, Int>, 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<Episode, List<Pair<String, Int>>> = 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}"
|
||||
}
|
@ -29,18 +29,24 @@ class EntityAssetLoader(
|
||||
private val assetLoader: AssetLoader,
|
||||
private val scene: Scene,
|
||||
) : DisposableContainer() {
|
||||
private val defaultMesh = MeshBuilder.CreateCylinder("Entity", obj {
|
||||
private val defaultMesh =
|
||||
MeshBuilder.CreateCylinder(
|
||||
"Entity",
|
||||
obj {
|
||||
diameter = 6.0
|
||||
height = 20.0
|
||||
}, scene).apply {
|
||||
},
|
||||
scene
|
||||
).apply {
|
||||
setEnabled(false)
|
||||
position = Vector3(0.0, 10.0, 0.0)
|
||||
}
|
||||
|
||||
private val meshCache = addDisposable(LoadingCache<Pair<EntityType, Int?>, Mesh>())
|
||||
private val meshCache =
|
||||
addDisposable(LoadingCache<Pair<EntityType, Int?>, 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)
|
||||
|
@ -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<K, V> : TrackedDisposable() {
|
||||
class LoadingCache<K, V>(private val disposeValue: (V) -> Unit) : TrackedDisposable() {
|
||||
private val map = mutableMapOf<K, Deferred<V>>()
|
||||
|
||||
operator fun set(key: K, value: Deferred<V>) {
|
||||
map[key] = value
|
||||
}
|
||||
|
||||
@Suppress("DeferredIsResult")
|
||||
fun getOrPut(key: K, defaultValue: () -> Deferred<V>): Deferred<V> =
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class QuestLoader(
|
||||
private val scope: CoroutineScope,
|
||||
private val assetLoader: AssetLoader,
|
||||
) : TrackedDisposable() {
|
||||
private val cache = LoadingCache<String, ArrayBuffer>()
|
||||
private val cache = LoadingCache<String, ArrayBuffer> {}
|
||||
|
||||
override fun internalDispose() {
|
||||
cache.dispose()
|
||||
|
@ -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<AreaVariantModel>,
|
||||
) {
|
||||
init {
|
||||
requireNonNegative(id, "id")
|
||||
}
|
||||
}
|
||||
|
@ -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<SectionModel>()
|
||||
|
||||
val sections: ListVal<SectionModel> = _sections
|
||||
|
||||
init {
|
||||
requireNonNegative(id, "id")
|
||||
}
|
||||
|
||||
fun setSections(sections: List<SectionModel>) {
|
||||
_sections.replaceAll(sections)
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
|
||||
val type: Type get() = entity.type
|
||||
|
||||
val areaId: Int get() = entity.areaId
|
||||
|
||||
/**
|
||||
* Section-relative position
|
||||
*/
|
||||
|
@ -13,14 +13,17 @@ class QuestModel(
|
||||
shortDescription: String,
|
||||
longDescription: String,
|
||||
val episode: Episode,
|
||||
mapDesignations: Map<Int, Int>,
|
||||
npcs: MutableList<QuestNpcModel>,
|
||||
objects: MutableList<QuestObjectModel>,
|
||||
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<String> = _name
|
||||
val shortDescription: Val<String> = _shortDescription
|
||||
val longDescription: Val<String> = _longDescription
|
||||
val mapDesignations: Val<Map<Int, Int>> = _mapDesignations
|
||||
|
||||
/**
|
||||
* Map of area IDs to entity counts.
|
||||
*/
|
||||
val entitiesPerArea: Val<Map<Int, Int>>
|
||||
|
||||
/**
|
||||
* One variant per area.
|
||||
*/
|
||||
val areaVariants: Val<List<AreaVariantModel>>
|
||||
|
||||
val npcs: ListVal<QuestNpcModel> = _npcs
|
||||
val objects: ListVal<QuestObjectModel> = _objects
|
||||
|
||||
@ -38,6 +53,39 @@ class QuestModel(
|
||||
setName(name)
|
||||
setShortDescription(shortDescription)
|
||||
setLongDescription(longDescription)
|
||||
|
||||
entitiesPerArea = this.npcs.map(this.objects) { ns, os ->
|
||||
val map = mutableMapOf<Int, Int>()
|
||||
|
||||
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<Int, AreaVariantModel>()
|
||||
|
||||
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 {
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
@ -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}."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<QuestModel?>,
|
||||
private val currentArea: Val<AreaModel?>,
|
||||
selectedWave: Val<WaveModel?>,
|
||||
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
|
||||
|
||||
val areaVariant: AreaVariantModel?
|
||||
val npcs: ListVal<QuestNpcModel>
|
||||
val objects: ListVal<QuestObjectModel>
|
||||
|
||||
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()
|
||||
private fun getAreaVariantDetails(quest: QuestModel?, area: AreaModel?): AreaVariantDetails {
|
||||
quest?.let {
|
||||
val areaVariant = area?.let {
|
||||
quest.areaVariants.value.find { it.area.id == area.id }
|
||||
}
|
||||
|
||||
return AreaVariantDetails(quest?.episode, areaVariant, npcs, objects)
|
||||
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(null, null, listVal(), listVal())
|
||||
}
|
||||
|
||||
private class AreaVariantDetails(
|
||||
val episode: Episode?,
|
||||
val areaVariant: AreaVariantModel?,
|
||||
val npcs: ListVal<QuestNpcModel>,
|
||||
val objects: ListVal<QuestObjectModel>,
|
||||
)
|
||||
}
|
||||
|
@ -1,54 +1,65 @@
|
||||
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<WaveModel?>,
|
||||
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()
|
||||
|
||||
// TODO: Load area mesh.
|
||||
protected fun loadMeshes(
|
||||
episode: Episode?,
|
||||
areaVariant: AreaVariantModel?,
|
||||
npcs: ListVal<QuestNpcModel>,
|
||||
objects: ListVal<QuestObjectModel>,
|
||||
) {
|
||||
loadJob?.cancel()
|
||||
loadJob = scope.launch {
|
||||
areaMeshManager.load(episode, areaVariant)
|
||||
|
||||
areaDisposer.disposeAll()
|
||||
npcMeshManager.removeAll()
|
||||
objectMeshManager.removeAll()
|
||||
renderer.resetEntityMeshes()
|
||||
|
||||
// Load entity meshes.
|
||||
areaDisposer.addAll(
|
||||
details.npcs.observeList(callNow = true, ::npcsChanged),
|
||||
details.objects.observeList(callNow = true, ::objectsChanged),
|
||||
npcs.observeList(callNow = true, ::npcsChanged),
|
||||
objects.observeList(callNow = true, ::objectsChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
disposer.dispose()
|
||||
@ -69,10 +80,3 @@ abstract class QuestMeshManager protected constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AreaVariantDetails(
|
||||
val episode: Episode?,
|
||||
val areaVariant: AreaVariantModel?,
|
||||
val npcs: ListVal<QuestNpcModel>,
|
||||
val objects: ListVal<QuestObjectModel>,
|
||||
)
|
||||
|
@ -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<QuestEntityModel<*, *>, 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()
|
||||
|
@ -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
|
||||
}
|
@ -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<Episode, List<AreaModel>>
|
||||
|
||||
init {
|
||||
areas = Episode.values()
|
||||
.map { episode ->
|
||||
episode to getAreasForEpisode(episode).map { area ->
|
||||
val variants = mutableListOf<AreaVariantModel>()
|
||||
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)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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<QuestModel?>(null)
|
||||
private val _currentArea = mutableVal<AreaModel?>(null)
|
||||
private val _selectedWave = mutableVal<WaveModel?>(null)
|
||||
@ -21,6 +21,11 @@ class QuestEditorStore(scope: CoroutineScope) : Store(scope) {
|
||||
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
|
||||
|
||||
fun setCurrentQuest(quest: QuestModel?) {
|
||||
_currentArea.value = null
|
||||
_currentQuest.value = quest
|
||||
|
||||
quest?.let {
|
||||
_currentArea.value = areaStore.getArea(quest.episode, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
) : DisposableContainer(), PwTool {
|
||||
override val toolType = PwToolType.Viewer
|
||||
|
||||
override fun initialize(scope: CoroutineScope): Widget {
|
||||
// Stores
|
||||
private val viewerStore = addDisposable(ViewerStore(scope))
|
||||
val viewerStore = addDisposable(ViewerStore(scope))
|
||||
|
||||
// Controllers
|
||||
private val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
|
||||
val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
|
||||
|
||||
fun createWidget(): Widget =
|
||||
ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), ::createViewerRenderer)
|
||||
// Rendering
|
||||
val canvas = document.createElement("CANVAS") as HTMLCanvasElement
|
||||
val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas)))
|
||||
|
||||
private fun createViewerRenderer(canvas: HTMLCanvasElement): MeshRenderer =
|
||||
MeshRenderer(viewerStore, canvas, createEngine(canvas))
|
||||
// Main Widget
|
||||
return ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), canvas, renderer)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user