mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-08 01:01:36 +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> {
|
interface QuestEntity<Type : EntityType> {
|
||||||
val type: Type
|
val type: Type
|
||||||
|
|
||||||
|
var areaId: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section-relative position.
|
* Section-relative position.
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +6,11 @@ import world.phantasmal.lib.fileFormats.ninja.angleToRad
|
|||||||
import world.phantasmal.lib.fileFormats.ninja.radToAngle
|
import world.phantasmal.lib.fileFormats.ninja.radToAngle
|
||||||
import kotlin.math.roundToInt
|
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
|
var typeId: Short
|
||||||
get() = data.getShort(0)
|
get() = data.getShort(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -6,7 +6,7 @@ import world.phantasmal.lib.fileFormats.ninja.angleToRad
|
|||||||
import world.phantasmal.lib.fileFormats.ninja.radToAngle
|
import world.phantasmal.lib.fileFormats.ninja.radToAngle
|
||||||
import kotlin.math.roundToInt
|
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
|
var typeId: Int
|
||||||
get() = data.getInt(0)
|
get() = data.getInt(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -11,6 +11,8 @@ interface MutableListVal<E> : ListVal<E>, MutableVal<List<E>> {
|
|||||||
|
|
||||||
fun removeAt(index: Int): E
|
fun removeAt(index: Int): E
|
||||||
|
|
||||||
|
fun replaceAll(elements: Iterable<E>)
|
||||||
|
|
||||||
fun replaceAll(elements: Sequence<E>)
|
fun replaceAll(elements: Sequence<E>)
|
||||||
|
|
||||||
fun clear()
|
fun clear()
|
||||||
|
@ -73,6 +73,13 @@ class SimpleListVal<E>(
|
|||||||
return removed
|
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>) {
|
override fun replaceAll(elements: Sequence<E>) {
|
||||||
val removed = ArrayList(this.elements)
|
val removed = ArrayList(this.elements)
|
||||||
this.elements.clear()
|
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.ApplicationWidget
|
||||||
import world.phantasmal.web.application.widgets.MainContentWidget
|
import world.phantasmal.web.application.widgets.MainContentWidget
|
||||||
import world.phantasmal.web.application.widgets.NavigationWidget
|
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.loading.AssetLoader
|
||||||
import world.phantasmal.web.core.stores.ApplicationUrl
|
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.core.stores.UiStore
|
||||||
import world.phantasmal.web.externals.babylon.Engine
|
import world.phantasmal.web.externals.babylon.Engine
|
||||||
import world.phantasmal.web.huntOptimizer.HuntOptimizer
|
import world.phantasmal.web.huntOptimizer.HuntOptimizer
|
||||||
@ -47,6 +47,13 @@ class Application(
|
|||||||
// Initialize core stores shared by several submodules.
|
// Initialize core stores shared by several submodules.
|
||||||
val uiStore = addDisposable(UiStore(scope, applicationUrl))
|
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.
|
// Controllers.
|
||||||
val navigationController = addDisposable(NavigationController(uiStore))
|
val navigationController = addDisposable(NavigationController(uiStore))
|
||||||
val mainContentController = addDisposable(MainContentController(uiStore))
|
val mainContentController = addDisposable(MainContentController(uiStore))
|
||||||
@ -56,28 +63,11 @@ class Application(
|
|||||||
ApplicationWidget(
|
ApplicationWidget(
|
||||||
scope,
|
scope,
|
||||||
NavigationWidget(scope, navigationController),
|
NavigationWidget(scope, navigationController),
|
||||||
MainContentWidget(scope, mainContentController, mapOf(
|
MainContentWidget(
|
||||||
PwTool.Viewer to { widgetScope ->
|
scope,
|
||||||
addDisposable(Viewer(
|
mainContentController,
|
||||||
widgetScope,
|
tools.map { it.toolType to it::initialize }.toMap()
|
||||||
createEngine,
|
)
|
||||||
)).createWidget()
|
|
||||||
},
|
|
||||||
PwTool.QuestEditor to { widgetScope ->
|
|
||||||
addDisposable(QuestEditor(
|
|
||||||
widgetScope,
|
|
||||||
assetLoader,
|
|
||||||
createEngine,
|
|
||||||
)).createWidget()
|
|
||||||
},
|
|
||||||
PwTool.HuntOptimizer to { widgetScope ->
|
|
||||||
addDisposable(HuntOptimizer(
|
|
||||||
widgetScope,
|
|
||||||
assetLoader,
|
|
||||||
uiStore,
|
|
||||||
)).createWidget()
|
|
||||||
},
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package world.phantasmal.web.application.controllers
|
package world.phantasmal.web.application.controllers
|
||||||
|
|
||||||
import world.phantasmal.observable.value.Val
|
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.web.core.stores.UiStore
|
||||||
import world.phantasmal.webui.controllers.Controller
|
import world.phantasmal.webui.controllers.Controller
|
||||||
|
|
||||||
class MainContentController(uiStore: UiStore) : 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
|
package world.phantasmal.web.application.controllers
|
||||||
|
|
||||||
import world.phantasmal.observable.value.Val
|
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.web.core.stores.UiStore
|
||||||
import world.phantasmal.webui.controllers.Controller
|
import world.phantasmal.webui.controllers.Controller
|
||||||
|
|
||||||
class NavigationController(private val uiStore: UiStore) : 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)
|
uiStore.setCurrentTool(tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import world.phantasmal.observable.value.not
|
import world.phantasmal.observable.value.not
|
||||||
import world.phantasmal.web.application.controllers.MainContentController
|
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.dom.div
|
||||||
import world.phantasmal.webui.widgets.LazyLoader
|
import world.phantasmal.webui.widgets.LazyLoader
|
||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
@ -12,7 +13,7 @@ import world.phantasmal.webui.widgets.Widget
|
|||||||
class MainContentWidget(
|
class MainContentWidget(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val ctrl: MainContentController,
|
private val ctrl: MainContentController,
|
||||||
private val toolViews: Map<PwTool, (CoroutineScope) -> Widget>,
|
private val toolViews: Map<PwToolType, (CoroutineScope) -> Widget>,
|
||||||
) : Widget(scope) {
|
) : Widget(scope) {
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
div {
|
div {
|
||||||
|
@ -3,7 +3,7 @@ package world.phantasmal.web.application.widgets
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import world.phantasmal.observable.Observable
|
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.input
|
||||||
import world.phantasmal.webui.dom.label
|
import world.phantasmal.webui.dom.label
|
||||||
import world.phantasmal.webui.dom.span
|
import world.phantasmal.webui.dom.span
|
||||||
@ -11,7 +11,7 @@ import world.phantasmal.webui.widgets.Control
|
|||||||
|
|
||||||
class PwToolButton(
|
class PwToolButton(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val tool: PwTool,
|
private val tool: PwToolType,
|
||||||
private val toggled: Observable<Boolean>,
|
private val toggled: Observable<Boolean>,
|
||||||
private val mouseDown: () -> Unit,
|
private val mouseDown: () -> Unit,
|
||||||
) : Control(scope) {
|
) : 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
|
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.web.core.stores.UiStore
|
||||||
import world.phantasmal.webui.controllers.Tab
|
import world.phantasmal.webui.controllers.Tab
|
||||||
import world.phantasmal.webui.controllers.TabController
|
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>(
|
open class PathAwareTabController<T : PathAwareTab>(
|
||||||
private val uiStore: UiStore,
|
private val uiStore: UiStore,
|
||||||
private val tool: PwTool,
|
private val tool: PwToolType,
|
||||||
tabs: List<T>,
|
tabs: List<T>,
|
||||||
) : TabController<T>(tabs) {
|
) : TabController<T>(tabs) {
|
||||||
init {
|
init {
|
||||||
|
@ -11,12 +11,17 @@ abstract class Renderer(
|
|||||||
protected val canvas: HTMLCanvasElement,
|
protected val canvas: HTMLCanvasElement,
|
||||||
protected val engine: Engine,
|
protected val engine: Engine,
|
||||||
) : DisposableContainer() {
|
) : DisposableContainer() {
|
||||||
protected val scene = Scene(engine)
|
val scene = Scene(engine)
|
||||||
private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 0.0), scene)
|
|
||||||
|
private val light = HemisphericLight("Light", Vector3(-1.0, 1.0, 1.0), scene)
|
||||||
|
|
||||||
protected abstract val camera: Camera
|
protected abstract val camera: Camera
|
||||||
|
|
||||||
init {
|
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() {
|
fun startRendering() {
|
||||||
@ -38,7 +43,7 @@ abstract class Renderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun render() {
|
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)
|
lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection)
|
||||||
light.direction = lightDirection
|
light.direction = lightDirection
|
||||||
scene.render()
|
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.NinjaObject
|
||||||
import world.phantasmal.lib.fileFormats.ninja.NjModel
|
import world.phantasmal.lib.fileFormats.ninja.NjModel
|
||||||
import world.phantasmal.lib.fileFormats.ninja.XjModel
|
import world.phantasmal.lib.fileFormats.ninja.XjModel
|
||||||
|
import world.phantasmal.web.core.*
|
||||||
import world.phantasmal.web.externals.babylon.*
|
import world.phantasmal.web.externals.babylon.*
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.sin
|
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.noRotate) NO_ROTATION else eulerToQuat(obj.rotation, ef.zxyRotationOrder),
|
||||||
if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position),
|
if (ef.noTranslate) NO_TRANSLATION else vec3ToBabylon(obj.position),
|
||||||
)
|
)
|
||||||
|
matrix.preMultiply(parentMatrix)
|
||||||
matrix.multiplyToRef(parentMatrix, matrix)
|
|
||||||
|
|
||||||
if (!ef.hidden) {
|
if (!ef.hidden) {
|
||||||
obj.model?.let { model ->
|
obj.model?.let { model ->
|
||||||
@ -72,8 +72,8 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
|||||||
val position = vec3ToBabylon(vertex.position)
|
val position = vec3ToBabylon(vertex.position)
|
||||||
val normal = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
val normal = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
||||||
|
|
||||||
Vector3.TransformCoordinatesToRef(position, matrix, position)
|
matrix.multiply(position)
|
||||||
Vector3.TransformNormalToRef(normal, normalMatrix, normal)
|
normalMatrix.multiply3x3(normal)
|
||||||
|
|
||||||
Vertex(
|
Vertex(
|
||||||
boneIndex,
|
boneIndex,
|
||||||
@ -158,10 +158,10 @@ private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder)
|
|||||||
|
|
||||||
for (vertex in model.vertices) {
|
for (vertex in model.vertices) {
|
||||||
val p = vec3ToBabylon(vertex.position)
|
val p = vec3ToBabylon(vertex.position)
|
||||||
Vector3.TransformCoordinatesToRef(p, matrix, p)
|
matrix.multiply(p)
|
||||||
|
|
||||||
val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
val n = vertex.normal?.let(::vec3ToBabylon) ?: Vector3.Up()
|
||||||
Vector3.TransformCoordinatesToRef(n, normalMatrix, n)
|
normalMatrix.multiply3x3(n)
|
||||||
|
|
||||||
val uv = vertex.uv?.let(::vec2ToBabylon) ?: DEFAULT_UV
|
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
|
// 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
|
// vertex normals point in the opposite direction. This hack fixes the winding for
|
||||||
// most models.
|
// most models.
|
||||||
val normal = pb.subtract(pa).cross(pc.subtract(pa))
|
val normal = (pb - pa) cross (pc - pa)
|
||||||
|
|
||||||
if (!clockwise) {
|
if (!clockwise) {
|
||||||
normal.negateInPlace()
|
normal.negateInPlace()
|
||||||
}
|
}
|
||||||
|
|
||||||
val oppositeCount =
|
val oppositeCount =
|
||||||
(if (Vector3.Dot(normal, na) < 0) 1 else 0) +
|
(if (normal dot na < 0) 1 else 0) +
|
||||||
(if (Vector3.Dot(normal, nb) < 0) 1 else 0) +
|
(if (normal dot nb < 0) 1 else 0) +
|
||||||
(if (Vector3.Dot(normal, nc) < 0) 1 else 0)
|
(if (normal dot nc < 0) 1 else 0)
|
||||||
|
|
||||||
if (oppositeCount >= 2) {
|
if (oppositeCount >= 2) {
|
||||||
clockwise = !clockwise
|
clockwise = !clockwise
|
||||||
|
@ -27,10 +27,10 @@ class VertexDataBuilder {
|
|||||||
fun getNormal(index: Int): Vector3 =
|
fun getNormal(index: Int): Vector3 =
|
||||||
normals[index]
|
normals[index]
|
||||||
|
|
||||||
fun addVertex(position: Vector3, normal: Vector3, uv: Vector2) {
|
fun addVertex(position: Vector3, normal: Vector3, uv: Vector2? = null) {
|
||||||
positions.add(position)
|
positions.add(position)
|
||||||
normals.add(normal)
|
normals.add(normal)
|
||||||
uvs.add(uv)
|
uv?.let { uvs.add(uv) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addIndex(index: Int) {
|
fun addIndex(index: Int) {
|
||||||
@ -55,11 +55,11 @@ class VertexDataBuilder {
|
|||||||
|
|
||||||
fun build(): VertexData {
|
fun build(): VertexData {
|
||||||
check(this.positions.size == this.normals.size)
|
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 positions = Float32Array(3 * positions.size)
|
||||||
val normals = Float32Array(3 * normals.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) {
|
for (i in this.positions.indices) {
|
||||||
val pos = this.positions[i]
|
val pos = this.positions[i]
|
||||||
@ -72,10 +72,12 @@ class VertexDataBuilder {
|
|||||||
normals[3 * i + 1] = normal.y.toFloat()
|
normals[3 * i + 1] = normal.y.toFloat()
|
||||||
normals[3 * i + 2] = normal.z.toFloat()
|
normals[3 * i + 2] = normal.z.toFloat()
|
||||||
|
|
||||||
|
uvs?.let {
|
||||||
val uv = this.uvs[i]
|
val uv = this.uvs[i]
|
||||||
uvs[2 * i] = uv.x.toFloat()
|
uvs[2 * i] = uv.x.toFloat()
|
||||||
uvs[2 * i + 1] = uv.y.toFloat()
|
uvs[2 * i + 1] = uv.y.toFloat()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val data = VertexData()
|
val data = VertexData()
|
||||||
data.positions = positions
|
data.positions = positions
|
||||||
|
@ -6,19 +6,11 @@ import org.w3c.dom.events.KeyboardEvent
|
|||||||
import world.phantasmal.observable.value.MutableVal
|
import world.phantasmal.observable.value.MutableVal
|
||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
import world.phantasmal.observable.value.mutableVal
|
import world.phantasmal.observable.value.mutableVal
|
||||||
|
import world.phantasmal.web.core.PwToolType
|
||||||
import world.phantasmal.web.core.models.Server
|
import world.phantasmal.web.core.models.Server
|
||||||
import world.phantasmal.webui.dom.disposableListener
|
import world.phantasmal.webui.dom.disposableListener
|
||||||
import world.phantasmal.webui.stores.Store
|
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 {
|
interface ApplicationUrl {
|
||||||
val url: Val<String>
|
val url: Val<String>
|
||||||
|
|
||||||
@ -27,8 +19,11 @@ interface ApplicationUrl {
|
|||||||
fun replaceUrl(url: String)
|
fun replaceUrl(url: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) : Store(scope) {
|
class UiStore(
|
||||||
private val _currentTool: MutableVal<PwTool>
|
scope: CoroutineScope,
|
||||||
|
private val applicationUrl: ApplicationUrl,
|
||||||
|
) : Store(scope) {
|
||||||
|
private val _currentTool: MutableVal<PwToolType>
|
||||||
|
|
||||||
private val _path = mutableVal("")
|
private val _path = mutableVal("")
|
||||||
private val _server = mutableVal(Server.Ephinea)
|
private val _server = mutableVal(Server.Ephinea)
|
||||||
@ -48,22 +43,22 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
|
|||||||
*/
|
*/
|
||||||
private val features: MutableSet<String> = mutableSetOf()
|
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.
|
* 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.
|
* 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.
|
* Application URL without the tool path prefix.
|
||||||
@ -92,7 +87,7 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
|
|||||||
observe(applicationUrl.url) { setDataFromUrl(it) }
|
observe(applicationUrl.url) { setDataFromUrl(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCurrentTool(tool: PwTool) {
|
fun setCurrentTool(tool: PwToolType) {
|
||||||
if (tool != currentTool.value) {
|
if (tool != currentTool.value) {
|
||||||
updateApplicationUrl(tool, path = "", replace = false)
|
updateApplicationUrl(tool, path = "", replace = false)
|
||||||
setCurrentTool(tool, path = "")
|
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
|
_path.value = path
|
||||||
_currentTool.value = tool
|
_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 fullPath = "/${tool.slug}${path}"
|
||||||
val params: MutableMap<String, String> =
|
val params: MutableMap<String, String> =
|
||||||
parameters[fullPath]?.let { HashMap(it) } ?: mutableMapOf()
|
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"
|
return "$tool -> $binding"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val SLUG_TO_PW_TOOL: Map<String, PwTool> =
|
private val SLUG_TO_PW_TOOL: Map<String, PwToolType> =
|
||||||
PwTool.values().map { it.slug to it }.toMap()
|
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.HTMLCanvasElement
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import world.phantasmal.web.core.rendering.Renderer
|
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 world.phantasmal.webui.widgets.Widget
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
class RendererWidget(
|
class RendererWidget(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val createRenderer: (HTMLCanvasElement) -> Renderer,
|
private val canvas: HTMLCanvasElement,
|
||||||
|
private val renderer: Renderer,
|
||||||
) : Widget(scope) {
|
) : Widget(scope) {
|
||||||
private var renderer: Renderer? = null
|
|
||||||
|
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
canvas {
|
div {
|
||||||
className = "pw-core-renderer"
|
className = "pw-core-renderer"
|
||||||
tabIndex = -1
|
tabIndex = -1
|
||||||
|
|
||||||
observeResize()
|
observeResize()
|
||||||
renderer = addDisposable(createRenderer(this))
|
|
||||||
|
|
||||||
observe(selfOrAncestorHidden) { hidden ->
|
observe(selfOrAncestorHidden) { hidden ->
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
renderer?.stopRendering()
|
renderer.stopRendering()
|
||||||
} else {
|
} else {
|
||||||
renderer?.startRendering()
|
renderer.startRendering()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
append(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
override fun resized(width: Double, height: Double) {
|
override fun resized(width: Double, height: Double) {
|
||||||
val canvas = (element as HTMLCanvasElement)
|
|
||||||
canvas.width = floor(width).toInt()
|
canvas.width = floor(width).toInt()
|
||||||
canvas.height = floor(height).toInt()
|
canvas.height = floor(height).toInt()
|
||||||
}
|
}
|
||||||
|
@ -149,6 +149,7 @@ external class Engine(
|
|||||||
) : ThinEngine
|
) : ThinEngine
|
||||||
|
|
||||||
external class Scene(engine: Engine) {
|
external class Scene(engine: Engine) {
|
||||||
|
var useRightHandedSystem: Boolean
|
||||||
var clearColor: Color4
|
var clearColor: Color4
|
||||||
|
|
||||||
fun render()
|
fun render()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package world.phantasmal.web.huntOptimizer
|
package world.phantasmal.web.huntOptimizer
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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.loading.AssetLoader
|
||||||
import world.phantasmal.web.core.stores.UiStore
|
import world.phantasmal.web.core.stores.UiStore
|
||||||
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
|
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
|
||||||
@ -12,20 +14,24 @@ import world.phantasmal.webui.DisposableContainer
|
|||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
|
|
||||||
class HuntOptimizer(
|
class HuntOptimizer(
|
||||||
private val scope: CoroutineScope,
|
private val assetLoader: AssetLoader,
|
||||||
assetLoader: AssetLoader,
|
private val uiStore: UiStore,
|
||||||
uiStore: UiStore,
|
) : DisposableContainer(), PwTool {
|
||||||
) : DisposableContainer() {
|
override val toolType = PwToolType.HuntOptimizer
|
||||||
private val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader))
|
|
||||||
|
|
||||||
private val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore))
|
override fun initialize(scope: CoroutineScope): Widget {
|
||||||
private val methodsController =
|
// Stores
|
||||||
addDisposable(MethodsController(uiStore, huntMethodStore))
|
val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader))
|
||||||
|
|
||||||
fun createWidget(): Widget =
|
// Controllers
|
||||||
HuntOptimizerWidget(
|
val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore))
|
||||||
|
val methodsController = addDisposable(MethodsController(uiStore, huntMethodStore))
|
||||||
|
|
||||||
|
// Main Widget
|
||||||
|
return HuntOptimizerWidget(
|
||||||
scope,
|
scope,
|
||||||
ctrl = huntOptimizerController,
|
ctrl = huntOptimizerController,
|
||||||
createMethodsWidget = { scope -> MethodsWidget(scope, methodsController) }
|
createMethodsWidget = { s -> MethodsWidget(s, methodsController) }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
package world.phantasmal.web.huntOptimizer.controllers
|
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.PathAwareTab
|
||||||
import world.phantasmal.web.core.controllers.PathAwareTabController
|
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.core.stores.UiStore
|
||||||
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
|
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
|
||||||
|
|
||||||
class HuntOptimizerController(uiStore: UiStore) :
|
class HuntOptimizerController(uiStore: UiStore) :
|
||||||
PathAwareTabController<PathAwareTab>(
|
PathAwareTabController<PathAwareTab>(
|
||||||
uiStore,
|
uiStore,
|
||||||
PwTool.HuntOptimizer,
|
PwToolType.HuntOptimizer,
|
||||||
listOf(
|
listOf(
|
||||||
PathAwareTab("Optimize", HuntOptimizerUrls.optimize),
|
PathAwareTab("Optimize", HuntOptimizerUrls.optimize),
|
||||||
PathAwareTab("Methods", HuntOptimizerUrls.methods),
|
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.ListVal
|
||||||
import world.phantasmal.observable.value.list.MutableListVal
|
import world.phantasmal.observable.value.list.MutableListVal
|
||||||
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.PathAwareTab
|
||||||
import world.phantasmal.web.core.controllers.PathAwareTabController
|
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.core.stores.UiStore
|
||||||
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
|
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
|
||||||
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
|
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
|
||||||
@ -19,7 +19,7 @@ class MethodsController(
|
|||||||
huntMethodStore: HuntMethodStore,
|
huntMethodStore: HuntMethodStore,
|
||||||
) : PathAwareTabController<MethodsTab>(
|
) : PathAwareTabController<MethodsTab>(
|
||||||
uiStore,
|
uiStore,
|
||||||
PwTool.HuntOptimizer,
|
PwToolType.HuntOptimizer,
|
||||||
listOf(
|
listOf(
|
||||||
MethodsTab("Episode I", HuntOptimizerUrls.methodsEpisodeI, Episode.I),
|
MethodsTab("Episode I", HuntOptimizerUrls.methodsEpisodeI, Episode.I),
|
||||||
MethodsTab("Episode II", HuntOptimizerUrls.methodsEpisodeII, Episode.II),
|
MethodsTab("Episode II", HuntOptimizerUrls.methodsEpisodeII, Episode.II),
|
||||||
|
@ -1,56 +1,68 @@
|
|||||||
package world.phantasmal.web.questEditor
|
package world.phantasmal.web.questEditor
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.w3c.dom.HTMLCanvasElement
|
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.core.loading.AssetLoader
|
||||||
import world.phantasmal.web.externals.babylon.Engine
|
import world.phantasmal.web.externals.babylon.Engine
|
||||||
import world.phantasmal.web.questEditor.controllers.NpcCountsController
|
import world.phantasmal.web.questEditor.controllers.NpcCountsController
|
||||||
import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController
|
import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController
|
||||||
import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
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.EntityAssetLoader
|
||||||
import world.phantasmal.web.questEditor.loading.QuestLoader
|
import world.phantasmal.web.questEditor.loading.QuestLoader
|
||||||
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
|
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
|
||||||
import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
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.stores.QuestEditorStore
|
||||||
import world.phantasmal.web.questEditor.widgets.*
|
import world.phantasmal.web.questEditor.widgets.*
|
||||||
import world.phantasmal.webui.DisposableContainer
|
import world.phantasmal.webui.DisposableContainer
|
||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
|
|
||||||
class QuestEditor(
|
class QuestEditor(
|
||||||
private val scope: CoroutineScope,
|
|
||||||
private val assetLoader: AssetLoader,
|
private val assetLoader: AssetLoader,
|
||||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
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
|
// 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
|
// Stores
|
||||||
private val questEditorStore = addDisposable(QuestEditorStore(scope))
|
val areaStore = addDisposable(AreaStore(scope, areaAssetLoader))
|
||||||
|
val questEditorStore = addDisposable(QuestEditorStore(scope, areaStore))
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
private val toolbarController =
|
val toolbarController =
|
||||||
addDisposable(QuestEditorToolbarController(questLoader, questEditorStore))
|
addDisposable(QuestEditorToolbarController(questLoader, areaStore, questEditorStore))
|
||||||
private val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
||||||
private val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||||
|
|
||||||
fun createWidget(): Widget =
|
// Rendering
|
||||||
QuestEditorWidget(
|
addDisposable(QuestEditorMeshManager(
|
||||||
|
scope,
|
||||||
|
questEditorStore,
|
||||||
|
renderer,
|
||||||
|
areaAssetLoader,
|
||||||
|
entityAssetLoader
|
||||||
|
))
|
||||||
|
|
||||||
|
// Main Widget
|
||||||
|
return QuestEditorWidget(
|
||||||
scope,
|
scope,
|
||||||
QuestEditorToolbar(scope, toolbarController),
|
QuestEditorToolbar(scope, toolbarController),
|
||||||
{ scope -> QuestInfoWidget(scope, questInfoController) },
|
{ s -> QuestInfoWidget(s, questInfoController) },
|
||||||
{ scope -> NpcCountsWidget(scope, npcCountsController) },
|
{ s -> NpcCountsWidget(s, npcCountsController) },
|
||||||
{ scope -> QuestEditorRendererWidget(scope, ::createQuestEditorRenderer) }
|
{ 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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import world.phantasmal.lib.fileFormats.quest.parseQstToQuest
|
|||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
import world.phantasmal.observable.value.mutableVal
|
import world.phantasmal.observable.value.mutableVal
|
||||||
import world.phantasmal.web.questEditor.loading.QuestLoader
|
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.QuestEditorStore
|
||||||
import world.phantasmal.web.questEditor.stores.convertQuestToModel
|
import world.phantasmal.web.questEditor.stores.convertQuestToModel
|
||||||
import world.phantasmal.webui.controllers.Controller
|
import world.phantasmal.webui.controllers.Controller
|
||||||
@ -21,6 +22,7 @@ private val logger = KotlinLogging.logger {}
|
|||||||
|
|
||||||
class QuestEditorToolbarController(
|
class QuestEditorToolbarController(
|
||||||
private val questLoader: QuestLoader,
|
private val questLoader: QuestLoader,
|
||||||
|
private val areaStore: AreaStore,
|
||||||
private val questEditorStore: QuestEditorStore,
|
private val questEditorStore: QuestEditorStore,
|
||||||
) : Controller() {
|
) : Controller() {
|
||||||
private val _resultDialogVisible = mutableVal(false)
|
private val _resultDialogVisible = mutableVal(false)
|
||||||
@ -31,7 +33,7 @@ class QuestEditorToolbarController(
|
|||||||
|
|
||||||
suspend fun createNewQuest(episode: Episode) {
|
suspend fun createNewQuest(episode: Episode) {
|
||||||
questEditorStore.setCurrentQuest(
|
questEditorStore.setCurrentQuest(
|
||||||
convertQuestToModel(questLoader.loadDefaultQuest(episode))
|
convertQuestToModel(questLoader.loadDefaultQuest(episode), areaStore::getVariant)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ class QuestEditorToolbarController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setCurrentQuest(quest: Quest) {
|
private fun setCurrentQuest(quest: Quest) {
|
||||||
questEditorStore.setCurrentQuest(convertQuestToModel(quest))
|
questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setResult(result: PwResult<*>) {
|
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 assetLoader: AssetLoader,
|
||||||
private val scene: Scene,
|
private val scene: Scene,
|
||||||
) : DisposableContainer() {
|
) : DisposableContainer() {
|
||||||
private val defaultMesh = MeshBuilder.CreateCylinder("Entity", obj {
|
private val defaultMesh =
|
||||||
|
MeshBuilder.CreateCylinder(
|
||||||
|
"Entity",
|
||||||
|
obj {
|
||||||
diameter = 6.0
|
diameter = 6.0
|
||||||
height = 20.0
|
height = 20.0
|
||||||
}, scene).apply {
|
},
|
||||||
|
scene
|
||||||
|
).apply {
|
||||||
setEnabled(false)
|
setEnabled(false)
|
||||||
position = Vector3(0.0, 10.0, 0.0)
|
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 {
|
suspend fun loadMesh(type: EntityType, model: Int?): Mesh =
|
||||||
return meshCache.getOrPut(Pair(type, model)) {
|
meshCache.getOrPut(Pair(type, model)) {
|
||||||
scope.async {
|
scope.async {
|
||||||
try {
|
try {
|
||||||
loadGeometry(type, model)?.let { vertexData ->
|
loadGeometry(type, model)?.let { vertexData ->
|
||||||
@ -60,7 +66,6 @@ class EntityAssetLoader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.await()
|
}.await()
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadGeometry(type: EntityType, model: Int?): VertexData? {
|
private suspend fun loadGeometry(type: EntityType, model: Int?): VertexData? {
|
||||||
val geomFormat = entityTypeToGeometryFormat(type)
|
val geomFormat = entityTypeToGeometryFormat(type)
|
||||||
|
@ -1,20 +1,30 @@
|
|||||||
package world.phantasmal.web.questEditor.loading
|
package world.phantasmal.web.questEditor.loading
|
||||||
|
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import world.phantasmal.core.disposable.TrackedDisposable
|
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>>()
|
private val map = mutableMapOf<K, Deferred<V>>()
|
||||||
|
|
||||||
operator fun set(key: K, value: Deferred<V>) {
|
operator fun set(key: K, value: Deferred<V>) {
|
||||||
map[key] = value
|
map[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DeferredIsResult")
|
||||||
fun getOrPut(key: K, defaultValue: () -> Deferred<V>): Deferred<V> =
|
fun getOrPut(key: K, defaultValue: () -> Deferred<V>): Deferred<V> =
|
||||||
map.getOrPut(key, defaultValue)
|
map.getOrPut(key, defaultValue)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun internalDispose() {
|
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()
|
super.internalDispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ class QuestLoader(
|
|||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val assetLoader: AssetLoader,
|
private val assetLoader: AssetLoader,
|
||||||
) : TrackedDisposable() {
|
) : TrackedDisposable() {
|
||||||
private val cache = LoadingCache<String, ArrayBuffer>()
|
private val cache = LoadingCache<String, ArrayBuffer> {}
|
||||||
|
|
||||||
override fun internalDispose() {
|
override fun internalDispose() {
|
||||||
cache.dispose()
|
cache.dispose()
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
package world.phantasmal.web.questEditor.models
|
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
|
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 type: Type get() = entity.type
|
||||||
|
|
||||||
|
val areaId: Int get() = entity.areaId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section-relative position
|
* Section-relative position
|
||||||
*/
|
*/
|
||||||
|
@ -13,14 +13,17 @@ class QuestModel(
|
|||||||
shortDescription: String,
|
shortDescription: String,
|
||||||
longDescription: String,
|
longDescription: String,
|
||||||
val episode: Episode,
|
val episode: Episode,
|
||||||
|
mapDesignations: Map<Int, Int>,
|
||||||
npcs: MutableList<QuestNpcModel>,
|
npcs: MutableList<QuestNpcModel>,
|
||||||
objects: MutableList<QuestObjectModel>,
|
objects: MutableList<QuestObjectModel>,
|
||||||
|
getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?,
|
||||||
) {
|
) {
|
||||||
private val _id = mutableVal(0)
|
private val _id = mutableVal(0)
|
||||||
private val _language = mutableVal(0)
|
private val _language = mutableVal(0)
|
||||||
private val _name = mutableVal("")
|
private val _name = mutableVal("")
|
||||||
private val _shortDescription = mutableVal("")
|
private val _shortDescription = mutableVal("")
|
||||||
private val _longDescription = mutableVal("")
|
private val _longDescription = mutableVal("")
|
||||||
|
private val _mapDesignations = mutableVal(mapDesignations)
|
||||||
private val _npcs = mutableListVal(npcs)
|
private val _npcs = mutableListVal(npcs)
|
||||||
private val _objects = mutableListVal(objects)
|
private val _objects = mutableListVal(objects)
|
||||||
|
|
||||||
@ -29,6 +32,18 @@ class QuestModel(
|
|||||||
val name: Val<String> = _name
|
val name: Val<String> = _name
|
||||||
val shortDescription: Val<String> = _shortDescription
|
val shortDescription: Val<String> = _shortDescription
|
||||||
val longDescription: Val<String> = _longDescription
|
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 npcs: ListVal<QuestNpcModel> = _npcs
|
||||||
val objects: ListVal<QuestObjectModel> = _objects
|
val objects: ListVal<QuestObjectModel> = _objects
|
||||||
|
|
||||||
@ -38,6 +53,39 @@ class QuestModel(
|
|||||||
setName(name)
|
setName(name)
|
||||||
setShortDescription(shortDescription)
|
setShortDescription(shortDescription)
|
||||||
setLongDescription(longDescription)
|
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 {
|
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
|
package world.phantasmal.web.questEditor.rendering
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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.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.loading.EntityAssetLoader
|
||||||
import world.phantasmal.web.questEditor.models.*
|
import world.phantasmal.web.questEditor.models.*
|
||||||
|
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||||
|
|
||||||
class QuestEditorMeshManager(
|
class QuestEditorMeshManager(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val currentQuest: Val<QuestModel?>,
|
questEditorStore: QuestEditorStore,
|
||||||
private val currentArea: Val<AreaModel?>,
|
|
||||||
selectedWave: Val<WaveModel?>,
|
|
||||||
renderer: QuestRenderer,
|
renderer: QuestRenderer,
|
||||||
|
areaAssetLoader: AreaAssetLoader,
|
||||||
entityAssetLoader: EntityAssetLoader,
|
entityAssetLoader: EntityAssetLoader,
|
||||||
) : QuestMeshManager(scope, selectedWave, renderer, entityAssetLoader) {
|
) : QuestMeshManager(scope, questEditorStore, renderer, areaAssetLoader, entityAssetLoader) {
|
||||||
init {
|
init {
|
||||||
disposer.addAll(
|
disposer.addAll(
|
||||||
currentQuest.observe { areaVariantChanged() },
|
questEditorStore.currentQuest.map(questEditorStore.currentArea, ::getAreaVariantDetails)
|
||||||
currentArea.observe { areaVariantChanged() },
|
.observe { (details) ->
|
||||||
|
loadMeshes(details.episode, details.areaVariant, details.npcs, details.objects)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAreaVariantDetails(): AreaVariantDetails {
|
private fun getAreaVariantDetails(quest: QuestModel?, area: AreaModel?): AreaVariantDetails {
|
||||||
val quest = currentQuest.value
|
quest?.let {
|
||||||
val area = currentArea.value
|
val areaVariant = area?.let {
|
||||||
|
quest.areaVariants.value.find { it.area.id == area.id }
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package world.phantasmal.web.questEditor.rendering
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import world.phantasmal.core.disposable.Disposer
|
import world.phantasmal.core.disposable.Disposer
|
||||||
import world.phantasmal.core.disposable.TrackedDisposable
|
import world.phantasmal.core.disposable.TrackedDisposable
|
||||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
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.ListVal
|
||||||
import world.phantasmal.observable.value.list.ListValChangeEvent
|
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.loading.EntityAssetLoader
|
||||||
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||||
import world.phantasmal.web.questEditor.models.QuestObjectModel
|
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].
|
* Loads the necessary area and entity 3D models into [QuestRenderer].
|
||||||
*/
|
*/
|
||||||
abstract class QuestMeshManager protected constructor(
|
abstract class QuestMeshManager protected constructor(
|
||||||
scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
selectedWave: Val<WaveModel?>,
|
questEditorStore: QuestEditorStore,
|
||||||
private val renderer: QuestRenderer,
|
private val renderer: QuestRenderer,
|
||||||
|
areaAssetLoader: AreaAssetLoader,
|
||||||
entityAssetLoader: EntityAssetLoader,
|
entityAssetLoader: EntityAssetLoader,
|
||||||
) : TrackedDisposable() {
|
) : TrackedDisposable() {
|
||||||
protected val disposer = Disposer()
|
protected val disposer = Disposer()
|
||||||
|
|
||||||
private val areaDisposer = disposer.add(Disposer())
|
private val areaDisposer = disposer.add(Disposer())
|
||||||
|
private val areaMeshManager = AreaMeshManager(areaAssetLoader)
|
||||||
private val npcMeshManager = disposer.add(
|
private val npcMeshManager = disposer.add(
|
||||||
EntityMeshManager(scope, selectedWave, renderer, entityAssetLoader)
|
EntityMeshManager(scope, questEditorStore.selectedWave, renderer, entityAssetLoader)
|
||||||
)
|
)
|
||||||
private val objectMeshManager = disposer.add(
|
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() {
|
protected fun loadMeshes(
|
||||||
val details = getAreaVariantDetails()
|
episode: Episode?,
|
||||||
|
areaVariant: AreaVariantModel?,
|
||||||
// TODO: Load area mesh.
|
npcs: ListVal<QuestNpcModel>,
|
||||||
|
objects: ListVal<QuestObjectModel>,
|
||||||
|
) {
|
||||||
|
loadJob?.cancel()
|
||||||
|
loadJob = scope.launch {
|
||||||
|
areaMeshManager.load(episode, areaVariant)
|
||||||
|
|
||||||
areaDisposer.disposeAll()
|
areaDisposer.disposeAll()
|
||||||
npcMeshManager.removeAll()
|
npcMeshManager.removeAll()
|
||||||
|
objectMeshManager.removeAll()
|
||||||
renderer.resetEntityMeshes()
|
renderer.resetEntityMeshes()
|
||||||
|
|
||||||
// Load entity meshes.
|
// Load entity meshes.
|
||||||
areaDisposer.addAll(
|
areaDisposer.addAll(
|
||||||
details.npcs.observeList(callNow = true, ::npcsChanged),
|
npcs.observeList(callNow = true, ::npcsChanged),
|
||||||
details.objects.observeList(callNow = true, ::objectsChanged),
|
objects.observeList(callNow = true, ::objectsChanged),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun internalDispose() {
|
override fun internalDispose() {
|
||||||
disposer.dispose()
|
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 world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
|
||||||
class QuestRenderer(
|
class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
|
||||||
canvas: HTMLCanvasElement,
|
|
||||||
engine: Engine,
|
|
||||||
createMeshManager: (QuestRenderer, Scene) -> QuestMeshManager,
|
|
||||||
) : Renderer(canvas, engine) {
|
|
||||||
private val meshManager = createMeshManager(this, scene)
|
|
||||||
private var entityMeshes = TransformNode("Entities", scene)
|
private var entityMeshes = TransformNode("Entities", scene)
|
||||||
private val entityToMesh = mutableMapOf<QuestEntityModel<*, *>, AbstractMesh>()
|
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 {
|
init {
|
||||||
with(camera) {
|
with(camera) {
|
||||||
@ -31,14 +26,13 @@ class QuestRenderer(
|
|||||||
angularSensibilityY = 200.0
|
angularSensibilityY = 200.0
|
||||||
panningInertia = 0.0
|
panningInertia = 0.0
|
||||||
panningSensibility = 3.0
|
panningSensibility = 3.0
|
||||||
panningAxis = Vector3(1.0, 0.0, 1.0)
|
panningAxis = Vector3(1.0, 0.0, -1.0)
|
||||||
pinchDeltaPercentage = 0.1
|
pinchDeltaPercentage = 0.1
|
||||||
wheelDeltaPercentage = 0.1
|
wheelDeltaPercentage = 0.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun internalDispose() {
|
override fun internalDispose() {
|
||||||
meshManager.dispose()
|
|
||||||
entityMeshes.dispose()
|
entityMeshes.dispose()
|
||||||
entityToMesh.clear()
|
entityToMesh.clear()
|
||||||
super.internalDispose()
|
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
|
package world.phantasmal.web.questEditor.stores
|
||||||
|
|
||||||
|
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||||
import world.phantasmal.lib.fileFormats.quest.Quest
|
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.QuestModel
|
||||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||||
import world.phantasmal.web.questEditor.models.QuestObjectModel
|
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(
|
return QuestModel(
|
||||||
quest.id,
|
quest.id,
|
||||||
quest.language,
|
quest.language,
|
||||||
@ -13,8 +18,10 @@ fun convertQuestToModel(quest: Quest): QuestModel {
|
|||||||
quest.shortDescription,
|
quest.shortDescription,
|
||||||
quest.longDescription,
|
quest.longDescription,
|
||||||
quest.episode,
|
quest.episode,
|
||||||
|
quest.mapDesignations,
|
||||||
// TODO: Add WaveModel to QuestNpcModel
|
// TODO: Add WaveModel to QuestNpcModel
|
||||||
quest.npcs.mapTo(mutableListOf()) { QuestNpcModel(it, null) },
|
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.web.questEditor.models.WaveModel
|
||||||
import world.phantasmal.webui.stores.Store
|
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 _currentQuest = mutableVal<QuestModel?>(null)
|
||||||
private val _currentArea = mutableVal<AreaModel?>(null)
|
private val _currentArea = mutableVal<AreaModel?>(null)
|
||||||
private val _selectedWave = mutableVal<WaveModel?>(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 }
|
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
|
||||||
|
|
||||||
fun setCurrentQuest(quest: QuestModel?) {
|
fun setCurrentQuest(quest: QuestModel?) {
|
||||||
|
_currentArea.value = null
|
||||||
_currentQuest.value = quest
|
_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(
|
class QuestEditorRendererWidget(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
createRenderer: (HTMLCanvasElement) -> QuestRenderer,
|
canvas: HTMLCanvasElement,
|
||||||
) : QuestRendererWidget(scope, createRenderer)
|
renderer: QuestRenderer,
|
||||||
|
) : QuestRendererWidget(scope, canvas, renderer)
|
||||||
|
@ -10,14 +10,15 @@ import world.phantasmal.webui.widgets.Widget
|
|||||||
|
|
||||||
abstract class QuestRendererWidget(
|
abstract class QuestRendererWidget(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val createRenderer: (HTMLCanvasElement) -> QuestRenderer,
|
private val canvas: HTMLCanvasElement,
|
||||||
|
private val renderer: QuestRenderer,
|
||||||
) : Widget(scope) {
|
) : Widget(scope) {
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
div {
|
div {
|
||||||
className = "pw-quest-editor-quest-renderer"
|
className = "pw-quest-editor-quest-renderer"
|
||||||
tabIndex = -1
|
tabIndex = -1
|
||||||
|
|
||||||
addChild(RendererWidget(scope, createRenderer))
|
addChild(RendererWidget(scope, canvas, renderer))
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package world.phantasmal.web.viewer
|
package world.phantasmal.web.viewer
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.w3c.dom.HTMLCanvasElement
|
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.externals.babylon.Engine
|
||||||
import world.phantasmal.web.viewer.controller.ViewerToolbarController
|
import world.phantasmal.web.viewer.controller.ViewerToolbarController
|
||||||
import world.phantasmal.web.viewer.rendering.MeshRenderer
|
import world.phantasmal.web.viewer.rendering.MeshRenderer
|
||||||
@ -12,18 +15,22 @@ import world.phantasmal.webui.DisposableContainer
|
|||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
|
|
||||||
class Viewer(
|
class Viewer(
|
||||||
private val scope: CoroutineScope,
|
|
||||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||||
) : DisposableContainer() {
|
) : DisposableContainer(), PwTool {
|
||||||
|
override val toolType = PwToolType.Viewer
|
||||||
|
|
||||||
|
override fun initialize(scope: CoroutineScope): Widget {
|
||||||
// Stores
|
// Stores
|
||||||
private val viewerStore = addDisposable(ViewerStore(scope))
|
val viewerStore = addDisposable(ViewerStore(scope))
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
private val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
|
val viewerToolbarController = addDisposable(ViewerToolbarController(viewerStore))
|
||||||
|
|
||||||
fun createWidget(): Widget =
|
// Rendering
|
||||||
ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), ::createViewerRenderer)
|
val canvas = document.createElement("CANVAS") as HTMLCanvasElement
|
||||||
|
val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas)))
|
||||||
|
|
||||||
private fun createViewerRenderer(canvas: HTMLCanvasElement): MeshRenderer =
|
// Main Widget
|
||||||
MeshRenderer(viewerStore, canvas, createEngine(canvas))
|
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.lib.fileFormats.ninja.NinjaObject
|
||||||
import world.phantasmal.web.core.rendering.Renderer
|
import world.phantasmal.web.core.rendering.Renderer
|
||||||
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexData
|
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 world.phantasmal.web.viewer.store.ViewerStore
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
|
||||||
@ -15,7 +18,7 @@ class MeshRenderer(
|
|||||||
) : Renderer(canvas, engine) {
|
) : Renderer(canvas, engine) {
|
||||||
private var mesh: Mesh? = null
|
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 {
|
init {
|
||||||
with(camera) {
|
with(camera) {
|
||||||
@ -29,7 +32,7 @@ class MeshRenderer(
|
|||||||
angularSensibilityX = 200.0
|
angularSensibilityX = 200.0
|
||||||
angularSensibilityY = 200.0
|
angularSensibilityY = 200.0
|
||||||
panningInertia = 0.0
|
panningInertia = 0.0
|
||||||
panningSensibility = 20.0
|
panningSensibility = 10.0
|
||||||
panningAxis = Vector3(1.0, 1.0, 0.0)
|
panningAxis = Vector3(1.0, 1.0, 0.0)
|
||||||
pinchDeltaPercentage = 0.1
|
pinchDeltaPercentage = 0.1
|
||||||
wheelDeltaPercentage = 0.1
|
wheelDeltaPercentage = 0.1
|
||||||
|
@ -11,7 +11,8 @@ import world.phantasmal.webui.widgets.Widget
|
|||||||
class ViewerWidget(
|
class ViewerWidget(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val toolbar: Widget,
|
private val toolbar: Widget,
|
||||||
private val createRenderer: (HTMLCanvasElement) -> Renderer,
|
private val canvas: HTMLCanvasElement,
|
||||||
|
private val renderer: Renderer,
|
||||||
) : Widget(scope) {
|
) : Widget(scope) {
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
div {
|
div {
|
||||||
@ -21,7 +22,7 @@ class ViewerWidget(
|
|||||||
div {
|
div {
|
||||||
className = "pw-viewer-viewer-container"
|
className = "pw-viewer-viewer-container"
|
||||||
|
|
||||||
addChild(RendererWidget(scope, createRenderer))
|
addChild(RendererWidget(scope, canvas, renderer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ class ViewerWidget(
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import world.phantasmal.core.disposable.disposable
|
|||||||
import world.phantasmal.core.disposable.use
|
import world.phantasmal.core.disposable.use
|
||||||
import world.phantasmal.testUtils.TestSuite
|
import world.phantasmal.testUtils.TestSuite
|
||||||
import world.phantasmal.web.core.loading.AssetLoader
|
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.externals.babylon.Engine
|
||||||
import world.phantasmal.web.test.TestApplicationUrl
|
import world.phantasmal.web.test.TestApplicationUrl
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package world.phantasmal.web.core.controllers
|
package world.phantasmal.web.core.controllers
|
||||||
|
|
||||||
import world.phantasmal.testUtils.TestSuite
|
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.core.stores.UiStore
|
||||||
import world.phantasmal.web.test.TestApplicationUrl
|
import world.phantasmal.web.test.TestApplicationUrl
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package world.phantasmal.web.core.store
|
package world.phantasmal.web.core.store
|
||||||
|
|
||||||
import world.phantasmal.testUtils.TestSuite
|
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.core.stores.UiStore
|
||||||
import world.phantasmal.web.test.TestApplicationUrl
|
import world.phantasmal.web.test.TestApplicationUrl
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
@ -7,7 +7,7 @@ import kotlinx.coroutines.cancel
|
|||||||
import world.phantasmal.core.disposable.disposable
|
import world.phantasmal.core.disposable.disposable
|
||||||
import world.phantasmal.testUtils.TestSuite
|
import world.phantasmal.testUtils.TestSuite
|
||||||
import world.phantasmal.web.core.loading.AssetLoader
|
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.core.stores.UiStore
|
||||||
import world.phantasmal.web.test.TestApplicationUrl
|
import world.phantasmal.web.test.TestApplicationUrl
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
Loading…
Reference in New Issue
Block a user