Entities are now correctly rotated and positioned within their section.

This commit is contained in:
Daan Vanden Bosch 2020-11-08 16:07:08 +01:00
parent 346a2cb4f9
commit db1149ddc0
18 changed files with 418 additions and 75 deletions

View File

@ -0,0 +1,8 @@
package world.phantasmal.core.math
/**
* Returns the floored modulus of its arguments. The computed value will have the same sign as the
* [divisor].
*/
fun floorMod(dividend: Double, divisor: Double): Double =
((dividend % divisor) + divisor) % divisor

View File

@ -0,0 +1,94 @@
package world.phantasmal.lib.fileFormats
import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.fileFormats.ninja.NinjaObject
import world.phantasmal.lib.fileFormats.ninja.XjModel
import world.phantasmal.lib.fileFormats.ninja.angleToRad
import world.phantasmal.lib.fileFormats.ninja.parseXjObject
class RenderObject(
val sections: List<RenderSection>,
)
class RenderSection(
val id: Int,
val position: Vec3,
val rotation: Vec3,
val objects: List<NinjaObject<XjModel>>,
)
fun parseAreaGeometry(cursor: Cursor): RenderObject {
val sections = mutableListOf<RenderSection>()
cursor.seekEnd(16)
val dataOffset = parseRel(cursor, parseIndex = false).dataOffset
cursor.seekStart(dataOffset)
cursor.seek(8) // Format "fmt2" in UTF-16.
val sectionCount = cursor.int()
cursor.seek(4)
val sectionTableOffset = cursor.int()
// val textureNameOffset = cursor.int()
for (i in 0 until sectionCount) {
cursor.seekStart(sectionTableOffset + 52 * i)
val sectionId = cursor.int()
val sectionPosition = cursor.vec3Float()
val sectionRotation = Vec3(
angleToRad(cursor.int()),
angleToRad(cursor.int()),
angleToRad(cursor.int()),
)
cursor.seek(4)
val simpleGeometryOffsetTableOffset = cursor.int()
// val animatedGeometryOffsetTableOffset = cursor.int()
cursor.seek(4)
val simpleGeometryOffsetCount = cursor.int()
// val animatedGeometryOffsetCount = cursor.int()
// Ignore animatedGeometryOffsetCount and the last 4 bytes.
val objects = parseGeometryTable(
cursor,
simpleGeometryOffsetTableOffset,
simpleGeometryOffsetCount,
)
sections.add(RenderSection(
sectionId,
sectionPosition,
sectionRotation,
objects,
))
}
return RenderObject(sections)
}
// TODO: don't reparse the same objects multiple times. Create DAG instead of tree.
private fun parseGeometryTable(
cursor: Cursor,
tableOffset: Int,
tableEntryCount: Int,
): List<NinjaObject<XjModel>> {
val objects = mutableListOf<NinjaObject<XjModel>>()
for (i in 0 until tableEntryCount) {
cursor.seekStart(tableOffset + 16 * i)
var offset = cursor.int()
cursor.seek(8)
val flags = cursor.int()
if (flags and 0b100 != 0) {
offset = cursor.seekStart(offset).int()
}
cursor.seekStart(offset)
objects.addAll(parseXjObject(cursor))
}
return objects
}

View File

@ -16,6 +16,9 @@ fun parseNj(cursor: Cursor): PwResult<List<NinjaObject<NjModel>>> =
fun parseXj(cursor: Cursor): PwResult<List<NinjaObject<XjModel>>> = fun parseXj(cursor: Cursor): PwResult<List<NinjaObject<XjModel>>> =
parseNinja(cursor, { c, _ -> parseXjModel(c) }, Unit) parseNinja(cursor, { c, _ -> parseXjModel(c) }, Unit)
fun parseXjObject(cursor: Cursor): List<NinjaObject<XjModel>> =
parseSiblingObjects(cursor, { c, _ -> parseXjModel(c) }, Unit)
private fun <Model : NinjaModel, Context> parseNinja( private fun <Model : NinjaModel, Context> parseNinja(
cursor: Cursor, cursor: Cursor,
parseModel: (cursor: Cursor, context: Context) -> Model, parseModel: (cursor: Cursor, context: Context) -> Model,

View File

@ -7,6 +7,8 @@ interface QuestEntity<Type : EntityType> {
var areaId: Int var areaId: Int
var sectionId: Int
/** /**
* Section-relative position. * Section-relative position.
*/ */

View File

@ -69,6 +69,12 @@ class QuestNpc(
} }
} }
override var sectionId: Int
get() = data.getUShort(12).toInt()
set(value) {
data.setUShort(12, value.toUShort())
}
override var position: Vec3 override var position: Vec3
get() = Vec3(data.getFloat(20), data.getFloat(24), data.getFloat(28)) get() = Vec3(data.getFloat(20), data.getFloat(24), data.getFloat(28))
set(value) { set(value) {

View File

@ -19,6 +19,12 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<Obje
typeId = value.typeId ?: -1 typeId = value.typeId ?: -1
} }
override var sectionId: Int
get() = data.getUShort(12).toInt()
set(value) {
data.setUShort(12, value.toUShort())
}
override var position: Vec3 override var position: Vec3
get() = Vec3(data.getFloat(16), data.getFloat(20), data.getFloat(24)) get() = Vec3(data.getFloat(16), data.getFloat(20), data.getFloat(24))
set(value) { set(value) {

View File

@ -1,8 +1,13 @@
package world.phantasmal.web.core package world.phantasmal.web.core
import world.phantasmal.web.externals.babylon.Matrix import world.phantasmal.web.externals.babylon.Matrix
import world.phantasmal.web.externals.babylon.Quaternion
import world.phantasmal.web.externals.babylon.Vector3 import world.phantasmal.web.externals.babylon.Vector3
operator fun Vector3.plusAssign(other: Vector3) {
addInPlace(other)
}
operator fun Vector3.minus(other: Vector3): Vector3 = operator fun Vector3.minus(other: Vector3): Vector3 =
subtract(other) subtract(other)
@ -32,3 +37,7 @@ fun Matrix.multiply(v: Vector3) {
fun Matrix.multiply3x3(v: Vector3) { fun Matrix.multiply3x3(v: Vector3) {
Vector3.TransformNormalToRef(v, this, v) Vector3.TransformNormalToRef(v, this, v)
} }
operator fun Quaternion.timesAssign(other: Quaternion) {
multiplyInPlace(other)
}

View File

@ -8,3 +8,5 @@ import world.phantasmal.web.externals.babylon.Vector3
fun vec2ToBabylon(v: Vec2): Vector2 = Vector2(v.x.toDouble(), v.y.toDouble()) fun vec2ToBabylon(v: Vec2): Vector2 = Vector2(v.x.toDouble(), v.y.toDouble())
fun vec3ToBabylon(v: Vec3): Vector3 = Vector3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) fun vec3ToBabylon(v: Vec3): Vector3 = Vector3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble())
fun babylonToVec3(v: Vector3): Vec3 = Vec3(v.x.toFloat(), v.y.toFloat(), v.z.toFloat())

View File

@ -19,10 +19,16 @@ private val NO_TRANSLATION = Vector3.Zero()
private val NO_ROTATION = Quaternion.Identity() private val NO_ROTATION = Quaternion.Identity()
private val NO_SCALE = Vector3.One() private val NO_SCALE = Vector3.One()
// TODO: take into account different kinds of meshes/vertices (with or without normals, uv, etc.).
fun ninjaObjectToVertexData(ninjaObject: NinjaObject<*>): VertexData = fun ninjaObjectToVertexData(ninjaObject: NinjaObject<*>): VertexData =
NinjaToVertexDataConverter(VertexDataBuilder()).convert(ninjaObject) NinjaToVertexDataConverter(VertexDataBuilder()).convert(ninjaObject)
fun ninjaObjectToVertexDataBuilder(
ninjaObject: NinjaObject<*>,
builder: VertexDataBuilder,
): VertexData =
NinjaToVertexDataConverter(builder).convert(ninjaObject)
// TODO: take into account different kinds of meshes/vertices (with or without normals, uv, etc.).
private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) { private class NinjaToVertexDataConverter(private val builder: VertexDataBuilder) {
private val vertexHolder = VertexHolder() private val vertexHolder = VertexHolder()
private var boneIndex = 0 private var boneIndex = 0

View File

@ -82,13 +82,16 @@ external class Quaternion(
* @return the current quaternion * @return the current quaternion
*/ */
fun multiplyToRef(q1: Quaternion, result: Quaternion): Quaternion fun multiplyToRef(q1: Quaternion, result: Quaternion): Quaternion
fun toEulerAngles(): Vector3
fun toEulerAnglesToRef(result: Vector3): Quaternion
fun rotateByQuaternionToRef(quaternion: Quaternion, result: Vector3): Vector3
fun clone(): Quaternion fun clone(): Quaternion
fun copyFrom(other: Quaternion): Quaternion fun copyFrom(other: Quaternion): Quaternion
companion object { companion object {
fun Identity(): Quaternion fun Identity(): Quaternion
fun FromEulerAngles(x: Double, y: Double, z: Double): Quaternion fun FromEulerAngles(x: Double, y: Double, z: Double): Quaternion
fun FromEulerAnglesToRef(x: Double, y: Double, z: Double, result: Quaternion): Quaternion
fun RotationYawPitchRoll(yaw: Double, pitch: Double, roll: Double): Quaternion fun RotationYawPitchRoll(yaw: Double, pitch: Double, roll: Double): Quaternion
} }
} }

View File

@ -81,7 +81,7 @@ class QuestEditorToolbarController(
} }
} }
private fun setCurrentQuest(quest: Quest) { private suspend fun setCurrentQuest(quest: Quest) {
questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant)) questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant))
} }

View File

@ -5,28 +5,65 @@ import kotlinx.coroutines.async
import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.ArrayBuffer
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.fileFormats.CollisionObject
import world.phantasmal.lib.fileFormats.RenderObject
import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry
import world.phantasmal.lib.fileFormats.parseAreaGeometry
import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.web.core.loading.AssetLoader import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.rendering.conversion.VertexDataBuilder
import world.phantasmal.web.core.rendering.conversion.ninjaObjectToVertexDataBuilder
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.Scene
import world.phantasmal.web.externals.babylon.TransformNode import world.phantasmal.web.externals.babylon.TransformNode
import world.phantasmal.web.questEditor.models.AreaVariantModel import world.phantasmal.web.questEditor.models.AreaVariantModel
import world.phantasmal.web.questEditor.rendering.conversion.areaCollisionGeometryToTransformNode import world.phantasmal.web.questEditor.models.SectionModel
import world.phantasmal.webui.DisposableContainer import world.phantasmal.webui.DisposableContainer
/**
* Loads and caches area assets.
*/
class AreaAssetLoader( class AreaAssetLoader(
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val assetLoader: AssetLoader, private val assetLoader: AssetLoader,
private val scene: Scene, private val scene: Scene,
) : DisposableContainer() { ) : DisposableContainer() {
private val collisionObjectCache = /**
addDisposable(LoadingCache<Triple<Episode, Int, Int>, TransformNode> { it.dispose() }) * This cache's values consist of a TransformNode containing area render meshes and a list of
* that area's sections.
*/
private val renderObjectCache = addDisposable(
LoadingCache<CacheKey, Pair<TransformNode, List<SectionModel>>> { it.first.dispose() }
)
private val collisionObjectCache = addDisposable(
LoadingCache<CacheKey, TransformNode> { it.dispose() }
)
suspend fun loadSections(episode: Episode, areaVariant: AreaVariantModel): List<SectionModel> =
loadRenderGeometryAndSections(episode, areaVariant).second
suspend fun loadRenderGeometry(episode: Episode, areaVariant: AreaVariantModel): TransformNode =
loadRenderGeometryAndSections(episode, areaVariant).first
private suspend fun loadRenderGeometryAndSections(
episode: Episode,
areaVariant: AreaVariantModel,
): Pair<TransformNode, List<SectionModel>> =
renderObjectCache.getOrPut(CacheKey(episode, areaVariant.area.id, areaVariant.id)) {
scope.async {
val buffer = getAreaAsset(episode, areaVariant, AssetType.Render)
val obj = parseAreaGeometry(buffer.cursor(Endianness.Little))
areaGeometryToTransformNodeAndSections(scene, obj, areaVariant)
}
}.await()
suspend fun loadCollisionGeometry( suspend fun loadCollisionGeometry(
episode: Episode, episode: Episode,
areaVariant: AreaVariantModel, areaVariant: AreaVariantModel,
): TransformNode = ): TransformNode =
collisionObjectCache.getOrPut(Triple(episode, areaVariant.area.id, areaVariant.id)) { collisionObjectCache.getOrPut(CacheKey(episode, areaVariant.area.id, areaVariant.id)) {
scope.async { scope.async {
val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision) val buffer = getAreaAsset(episode, areaVariant, AssetType.Collision)
val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little)) val obj = parseAreaCollisionGeometry(buffer.cursor(Endianness.Little))
@ -47,11 +84,21 @@ class AreaAssetLoader(
return assetLoader.loadArrayBuffer(baseUrl + suffix) return assetLoader.loadArrayBuffer(baseUrl + suffix)
} }
enum class AssetType { private data class CacheKey(
val episode: Episode,
val areaId: Int,
val areaVariantId: Int,
)
private enum class AssetType {
Render, Collision Render, Collision
} }
} }
class AreaMetadata(
val section: SectionModel?,
)
private val AREA_BASE_NAMES: Map<Episode, List<Pair<String, Int>>> = mapOf( private val AREA_BASE_NAMES: Map<Episode, List<Pair<String, Int>>> = mapOf(
Episode.I to listOf( Episode.I to listOf(
Pair("city00_00", 1), Pair("city00_00", 1),
@ -135,3 +182,87 @@ private fun areaVersionToBaseUrl(episode: Episode, areaVariant: AreaVariantModel
return "/maps/map_${base_name}${variant}" return "/maps/map_${base_name}${variant}"
} }
private fun areaGeometryToTransformNodeAndSections(
scene: Scene,
renderObject: RenderObject,
areaVariant: AreaVariantModel,
): Pair<TransformNode, List<SectionModel>> {
val sections = mutableListOf<SectionModel>()
val node = TransformNode("Render Geometry", scene)
node.setEnabled(false)
for (section in renderObject.sections) {
val builder = VertexDataBuilder()
for (obj in section.objects) {
ninjaObjectToVertexDataBuilder(obj, builder)
}
val vertexData = builder.build()
val mesh = Mesh("Render Geometry", scene, node)
vertexData.applyToMesh(mesh)
// TODO: Material.
mesh.position = vec3ToBabylon(section.position)
mesh.rotation = vec3ToBabylon(section.rotation)
if (section.id >= 0) {
val sec = SectionModel(
section.id,
vec3ToBabylon(section.position),
vec3ToBabylon(section.rotation),
areaVariant,
)
sections.add(sec)
mesh.metadata = AreaMetadata(sec)
}
}
return Pair(node, sections)
}
private fun areaCollisionGeometryToTransformNode(
scene: Scene,
obj: CollisionObject,
): TransformNode {
val node = TransformNode("Collision Geometry", 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
}

View File

@ -1,29 +1,114 @@
package world.phantasmal.web.questEditor.models package world.phantasmal.web.questEditor.models
import world.phantasmal.core.math.floorMod
import world.phantasmal.lib.fileFormats.quest.EntityType import world.phantasmal.lib.fileFormats.quest.EntityType
import world.phantasmal.lib.fileFormats.quest.QuestEntity import world.phantasmal.lib.fileFormats.quest.QuestEntity
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.plusAssign
import world.phantasmal.web.core.rendering.conversion.babylonToVec3
import world.phantasmal.web.core.rendering.conversion.vec3ToBabylon import world.phantasmal.web.core.rendering.conversion.vec3ToBabylon
import world.phantasmal.web.core.timesAssign
import world.phantasmal.web.externals.babylon.Quaternion
import world.phantasmal.web.externals.babylon.Vector3 import world.phantasmal.web.externals.babylon.Vector3
import kotlin.math.PI
abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>( abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
private val entity: Entity, private val entity: Entity,
) { ) {
private val _sectionId = mutableVal(entity.sectionId)
private val _section = mutableVal<SectionModel?>(null)
private val _position = mutableVal(vec3ToBabylon(entity.position)) private val _position = mutableVal(vec3ToBabylon(entity.position))
private val _worldPosition = mutableVal(_position.value) private val _worldPosition = mutableVal(_position.value)
private val _rotation = mutableVal(vec3ToBabylon(entity.rotation))
private val _worldRotation = mutableVal(_rotation.value)
val type: Type get() = entity.type val type: Type get() = entity.type
val areaId: Int get() = entity.areaId val areaId: Int get() = entity.areaId
val sectionId: Val<Int> = _sectionId
val section: Val<SectionModel?> = _section
/** /**
* Section-relative position * Section-relative position
*/ */
val position: Val<Vector3> = _position val position: Val<Vector3> = _position
/**
* World position
*/
val worldPosition: Val<Vector3> = _worldPosition val worldPosition: Val<Vector3> = _worldPosition
/**
* Section-relative rotation
*/
val rotation: Val<Vector3> = _rotation
val worldRotation: Val<Vector3> = _worldRotation
fun setSection(section: SectionModel) {
require(section.areaVariant.area.id == areaId) {
"Quest entities can't be moved across areas."
}
entity.sectionId = section.id
_section.value = section
_sectionId.value = section.id
setPosition(position.value)
setRotation(rotation.value)
}
fun setPosition(pos: Vector3) {
entity.position = babylonToVec3(pos)
_position.value = pos
val section = section.value
_worldPosition.value =
if (section == null) pos
else Vector3.Zero().also { worldPos ->
pos.rotateByQuaternionToRef(section.rotationQuaternion, worldPos)
worldPos += section.position
}
}
fun setRotation(rot: Vector3) {
floorModEuler(rot)
entity.rotation = babylonToVec3(rot)
val section = section.value
if (section == null) {
_worldRotation.value = rot
} else {
Quaternion.FromEulerAnglesToRef(rot.x, rot.y, rot.z, q1)
Quaternion.FromEulerAnglesToRef(
section.rotation.x,
section.rotation.y,
section.rotation.z,
q2
)
q1 *= q2
val worldRot = q1.toEulerAngles()
floorModEuler(worldRot)
_worldRotation.value = worldRot
}
}
private fun floorModEuler(euler: Vector3) {
euler.set(
floorMod(euler.x, 2 * PI),
floorMod(euler.y, 2 * PI),
floorMod(euler.z, 2 * PI),
)
}
companion object {
// These quaternions are used as temporary variables to avoid memory allocation.
private val q1 = Quaternion()
private val q2 = Quaternion()
}
} }

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.models package world.phantasmal.web.questEditor.models
import world.phantasmal.web.externals.babylon.Quaternion
import world.phantasmal.web.externals.babylon.Vector3 import world.phantasmal.web.externals.babylon.Vector3
class SectionModel( class SectionModel(
@ -13,4 +14,7 @@ class SectionModel(
"id should be greater than or equal to -1 but was $id." "id should be greater than or equal to -1 but was $id."
} }
} }
val rotationQuaternion: Quaternion =
Quaternion.FromEulerAngles(rotation.x, rotation.y, rotation.z)
} }

View File

@ -146,13 +146,11 @@ private class LoadedEntity(
mesh.position = pos mesh.position = pos
} }
addDisposables( observe(entity.worldRotation) { rot ->
// TODO: Rotation. mesh.rotation = rot
// entity.worldRotation.observe { (value) -> }
// mesh.rotation.copy(value)
// renderer.schedule_render()
// },
addDisposables(
// TODO: Model. // TODO: Model.
// entity.model.observe { // entity.model.observe {
// remove(listOf(entity)) // remove(listOf(entity))

View File

@ -1,50 +0,0 @@
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
}

View File

@ -2,19 +2,23 @@ package world.phantasmal.web.questEditor.stores
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import world.phantasmal.lib.fileFormats.quest.Episode 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.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.models.AreaModel import world.phantasmal.web.questEditor.models.AreaModel
import world.phantasmal.web.questEditor.models.AreaVariantModel import world.phantasmal.web.questEditor.models.AreaVariantModel
import world.phantasmal.web.questEditor.models.SectionModel
import world.phantasmal.webui.stores.Store import world.phantasmal.webui.stores.Store
import world.phantasmal.lib.fileFormats.quest.getAreasForEpisode as getAreasForEpisodeLib
class AreaStore(scope: CoroutineScope, areaAssetLoader: AreaAssetLoader) : Store(scope) { class AreaStore(
scope: CoroutineScope,
private val areaAssetLoader: AreaAssetLoader,
) : Store(scope) {
private val areas: Map<Episode, List<AreaModel>> private val areas: Map<Episode, List<AreaModel>>
init { init {
areas = Episode.values() areas = Episode.values()
.map { episode -> .map { episode ->
episode to getAreasForEpisode(episode).map { area -> episode to getAreasForEpisodeLib(episode).map { area ->
val variants = mutableListOf<AreaVariantModel>() val variants = mutableListOf<AreaVariantModel>()
val areaModel = AreaModel(area.id, area.name, area.order, variants) val areaModel = AreaModel(area.id, area.name, area.order, variants)
@ -28,9 +32,15 @@ class AreaStore(scope: CoroutineScope, areaAssetLoader: AreaAssetLoader) : Store
.toMap() .toMap()
} }
fun getAreasForEpisode(episode: Episode): List<AreaModel> =
areas.getValue(episode)
fun getArea(episode: Episode, areaId: Int): AreaModel? = fun getArea(episode: Episode, areaId: Int): AreaModel? =
areas.getValue(episode).find { it.id == areaId } areas.getValue(episode).find { it.id == areaId }
fun getVariant(episode: Episode, areaId: Int, variantId: Int): AreaVariantModel? = fun getVariant(episode: Episode, areaId: Int, variantId: Int): AreaVariantModel? =
getArea(episode, areaId)?.areaVariants?.getOrNull(variantId) getArea(episode, areaId)?.areaVariants?.getOrNull(variantId)
suspend fun getSections(episode: Episode, variant: AreaVariantModel): List<SectionModel> =
areaAssetLoader.loadSections(episode, variant)
} }

View File

@ -1,14 +1,14 @@
package world.phantasmal.web.questEditor.stores package world.phantasmal.web.questEditor.stores
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import mu.KotlinLogging
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.models.AreaModel import world.phantasmal.web.questEditor.models.*
import world.phantasmal.web.questEditor.models.QuestEntityModel
import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.models.WaveModel
import world.phantasmal.webui.stores.Store import world.phantasmal.webui.stores.Store
private val logger = KotlinLogging.logger {}
class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) : 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)
@ -23,12 +23,38 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
// TODO: Take into account whether we're debugging or not. // TODO: Take into account whether we're debugging or not.
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null } val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
fun setCurrentQuest(quest: QuestModel?) { suspend fun setCurrentQuest(quest: QuestModel?) {
_currentArea.value = null _currentArea.value = null
_currentQuest.value = quest _currentQuest.value = quest
quest?.let { quest?.let {
_currentArea.value = areaStore.getArea(quest.episode, 0) _currentArea.value = areaStore.getArea(quest.episode, 0)
// Load section data.
quest.areaVariants.value.forEach { variant ->
val sections = areaStore.getSections(quest.episode, variant)
variant.setSections(sections)
setSectionOnQuestEntities(quest.npcs.value, variant, sections)
setSectionOnQuestEntities(quest.objects.value, variant, sections)
}
}
}
private fun setSectionOnQuestEntities(
entities: List<QuestEntityModel<*, *>>,
variant: AreaVariantModel,
sections: List<SectionModel>,
) {
entities.forEach { entity ->
if (entity.areaId == variant.area.id) {
val section = sections.find { it.id == entity.sectionId.value }
if (section == null) {
logger.warn { "Section ${entity.sectionId.value} not found." }
} else {
entity.setSection(section)
}
}
} }
} }