mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
The 3D view now updates when a different area variant is configured for the current area. Undo/redo of entity translations now works more or less correctly even when a different area variant is now active. Fixed a bug that resulted in double onChange calls when editing an input field.
This commit is contained in:
parent
ecdf7cafb8
commit
fc64f62285
@ -161,8 +161,6 @@ Features that are in ***bold italics*** are planned but not yet implemented.
|
||||
- When a modal dialog is open, global keybindings should be disabled
|
||||
- The ASM editor is slow with big scripts, e.g. Seat of the Heart (#27)
|
||||
- Improve the default camera target for Crater Interior
|
||||
- The 3D doesn't update when a different variant is configured with a map designate instruction for
|
||||
the current area
|
||||
- Entities with rendering issues:
|
||||
- Caves 4 Button door
|
||||
- Pofuilly Slime
|
||||
|
@ -3,40 +3,31 @@ package world.phantasmal.web.questEditor.actions
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.SectionModel
|
||||
|
||||
class TranslateEntityAction(
|
||||
private val setSelectedEntity: (QuestEntityModel<*, *>) -> Unit,
|
||||
private val setEntitySection: (Int) -> Unit,
|
||||
private val entity: QuestEntityModel<*, *>,
|
||||
private val newSection: SectionModel?,
|
||||
private val oldSection: SectionModel?,
|
||||
private val newSection: Int?,
|
||||
private val oldSection: Int?,
|
||||
private val newPosition: Vector3,
|
||||
private val oldPosition: Vector3,
|
||||
private val world: Boolean,
|
||||
) : Action {
|
||||
override val description: String = "Move ${entity.type.simpleName}"
|
||||
|
||||
override fun execute() {
|
||||
setSelectedEntity(entity)
|
||||
|
||||
newSection?.let(entity::setSection)
|
||||
newSection?.let(setEntitySection)
|
||||
|
||||
if (world) {
|
||||
entity.setWorldPosition(newPosition)
|
||||
} else {
|
||||
entity.setPosition(newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
setSelectedEntity(entity)
|
||||
|
||||
oldSection?.let(entity::setSection)
|
||||
oldSection?.let(setEntitySection)
|
||||
|
||||
if (world) {
|
||||
entity.setWorldPosition(oldPosition)
|
||||
} else {
|
||||
entity.setPosition(oldPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,12 +123,12 @@ class EntityInfoController(
|
||||
|
||||
questEditorStore.executeAction(TranslateEntityAction(
|
||||
setSelectedEntity = questEditorStore::setSelectedEntity,
|
||||
setEntitySection = { /* Won't be called. */ },
|
||||
entity,
|
||||
entity.section.value,
|
||||
entity.section.value,
|
||||
Vector3(x, y, z),
|
||||
entity.position.value,
|
||||
false,
|
||||
newSection = null,
|
||||
oldSection = null,
|
||||
newPosition = Vector3(x, y, z),
|
||||
oldPosition = entity.position.value,
|
||||
))
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ class EntityInfoController(
|
||||
))
|
||||
}
|
||||
|
||||
fun setPropValue(prop:QuestEntityPropModel, value:Any) {
|
||||
fun setPropValue(prop: QuestEntityPropModel, value: Any) {
|
||||
questEditorStore.selectedEntity.value?.let { entity ->
|
||||
questEditorStore.executeAction(EditEntityPropAction(
|
||||
setSelectedEntity = questEditorStore::setSelectedEntity,
|
||||
|
@ -76,6 +76,9 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
|
||||
): Object3D =
|
||||
cache.get(EpisodeAndAreaVariant(episode, areaVariant)).collisionGeometry
|
||||
|
||||
fun getCachedSections(episode: Episode, areaVariant: AreaVariantModel): List<SectionModel>? =
|
||||
cache.getIfPresentNow(EpisodeAndAreaVariant(episode, areaVariant))?.sections
|
||||
|
||||
private suspend fun getAreaAsset(
|
||||
episode: Episode,
|
||||
areaVariant: AreaVariantModel,
|
||||
@ -506,6 +509,24 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
|
||||
"s_n_0_5i_1_iqqrft",
|
||||
"s_n_0_g_1_iipv9r",
|
||||
"s_n_0_c_1_ihboen",
|
||||
"s_n_0_3l_2_iljrhl",
|
||||
"s_n_0_5t_2_ill0ej",
|
||||
"s_n_0_4e_2_iobj4y", // Deletes useful walls.
|
||||
"s_n_0_6y_2_ipln11", // Deletes useful walls.
|
||||
"s_n_0_43_1_iqbzr4",
|
||||
"s_n_0_o_1_ikqpac",
|
||||
"s_n_0_c_1_ihrvdk",
|
||||
"s_n_0_c_1_ih2ob6",
|
||||
"s_n_0_c_1_ihwsxo",
|
||||
"s_n_0_c_1_igrh47",
|
||||
"s_n_0_j9_4_iqqrft", // Deletes useful walls.
|
||||
"s_n_0_p_2_ihe7ca",
|
||||
"s_n_0_l_2_igkyx3",
|
||||
"s_n_0_n_2_igubtb",
|
||||
"s_n_0_l_2_ihuczl",
|
||||
"s_n_0_o_1_ijn9y2",
|
||||
"s_n_0_f_1_ijpzol",
|
||||
"s_n_0_2n_1_ilgim5",
|
||||
),
|
||||
),
|
||||
// Cave 3
|
||||
|
@ -62,26 +62,10 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
open fun setSectionId(sectionId: Int) {
|
||||
entity.sectionId = sectionId.toShort()
|
||||
_sectionId.value = sectionId
|
||||
|
||||
if (sectionId != _section.value?.id) {
|
||||
_section.value = null
|
||||
}
|
||||
|
||||
fun initializeSection(section: SectionModel) {
|
||||
require(!sectionInitialized.value) {
|
||||
"Section is already initialized."
|
||||
}
|
||||
require(section.areaVariant.area.id == areaId) {
|
||||
"Section should lie within the entity's area."
|
||||
}
|
||||
|
||||
setSectionId(section.id)
|
||||
|
||||
_section.value = section
|
||||
|
||||
// Update world position and rotation by calling setPosition and setRotation with the
|
||||
// current position and rotation.
|
||||
setPosition(position.value)
|
||||
setRotation(rotation.value)
|
||||
|
||||
setSectionInitialized()
|
||||
}
|
||||
|
||||
fun setSectionInitialized() {
|
||||
@ -89,23 +73,34 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Will update the entity's relative transformation but keep its world transformation constant.
|
||||
* @param keepRelativeTransform If true, keep the entity's relative transform and update its
|
||||
* world transform. Otherwise keep its world transform and update its relative transform.
|
||||
*/
|
||||
fun setSection(section: SectionModel) {
|
||||
fun setSection(section: SectionModel, keepRelativeTransform: Boolean = false) {
|
||||
require(section.areaVariant.area.id == areaId) {
|
||||
"Quest entities can't be moved across areas."
|
||||
}
|
||||
|
||||
setSectionId(section.id)
|
||||
entity.sectionId = section.id.toShort()
|
||||
_sectionId.value = section.id
|
||||
|
||||
_section.value = section
|
||||
|
||||
// Update relative position and rotation by calling setWorldPosition and setWorldRotation
|
||||
// with the current world position and rotation.
|
||||
if (keepRelativeTransform) {
|
||||
// Update world position and rotation by calling setPosition and setRotation with the
|
||||
// current position and rotation.
|
||||
setPosition(position.value)
|
||||
setRotation(rotation.value)
|
||||
} else {
|
||||
// Update relative position and rotation by calling setWorldPosition and
|
||||
// setWorldRotation with the current world position and rotation.
|
||||
setWorldPosition(worldPosition.value)
|
||||
setWorldRotation(worldRotation.value)
|
||||
}
|
||||
|
||||
setSectionInitialized()
|
||||
}
|
||||
|
||||
fun setPosition(pos: Vector3) {
|
||||
entity.setPosition(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat())
|
||||
|
||||
|
@ -6,6 +6,8 @@ import world.phantasmal.lib.fileFormats.quest.DatUnknown
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.SimpleListVal
|
||||
import world.phantasmal.observable.value.list.flatMapToList
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
|
||||
@ -57,7 +59,7 @@ class QuestModel(
|
||||
/**
|
||||
* One variant per area.
|
||||
*/
|
||||
val areaVariants: Val<List<AreaVariantModel>>
|
||||
val areaVariants: ListVal<AreaVariantModel>
|
||||
|
||||
val npcs: ListVal<QuestNpcModel> = _npcs
|
||||
val objects: ListVal<QuestObjectModel> = _objects
|
||||
@ -88,10 +90,11 @@ class QuestModel(
|
||||
map
|
||||
}
|
||||
|
||||
areaVariants = map(entitiesPerArea, this.mapDesignations) { entitiesPerArea, mds ->
|
||||
areaVariants =
|
||||
flatMapToList(entitiesPerArea, this.mapDesignations) { entitiesPerArea, mds ->
|
||||
val variants = mutableMapOf<Int, AreaVariantModel>()
|
||||
|
||||
for (areaId in entitiesPerArea.values) {
|
||||
for (areaId in entitiesPerArea.keys) {
|
||||
getVariant(episode, areaId, 0)?.let {
|
||||
variants[areaId] = it
|
||||
}
|
||||
@ -103,7 +106,7 @@ class QuestModel(
|
||||
}
|
||||
}
|
||||
|
||||
variants.values.toList()
|
||||
listVal(*variants.values.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,10 +12,14 @@ class QuestEditorMeshManager(
|
||||
renderContext: QuestRenderContext,
|
||||
) : QuestMeshManager(areaAssetLoader, entityAssetLoader, questEditorStore, renderContext) {
|
||||
init {
|
||||
observe(questEditorStore.currentQuest, questEditorStore.currentArea) { quest, area ->
|
||||
val areaVariant = quest?.let {
|
||||
observe(
|
||||
questEditorStore.currentQuest,
|
||||
questEditorStore.currentQuest.flatMapNull { it?.areaVariants },
|
||||
questEditorStore.currentArea,
|
||||
) { quest, questAreaVariants, area ->
|
||||
val areaVariant = questAreaVariants?.let {
|
||||
area?.let {
|
||||
quest.areaVariants.value.find { it.area.id == area.id }
|
||||
questAreaVariants.find { it.area.id == area.id }
|
||||
?: area.areaVariants.first()
|
||||
}
|
||||
}
|
||||
|
@ -130,12 +130,12 @@ class StateContext(
|
||||
) {
|
||||
questEditorStore.executeAction(TranslateEntityAction(
|
||||
::setSelectedEntity,
|
||||
{ questEditorStore.setEntitySection(entity, it) },
|
||||
entity,
|
||||
newSection,
|
||||
oldSection,
|
||||
newSection?.id,
|
||||
oldSection?.id,
|
||||
newPosition,
|
||||
oldPosition,
|
||||
world = true,
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ class TranslationState(
|
||||
private val grabOffset: Vector3,
|
||||
) : State() {
|
||||
private val initialSection: SectionModel? = entity.section.value
|
||||
private val initialPosition: Vector3 = entity.worldPosition.value
|
||||
private val initialPosition: Vector3 = entity.position.value
|
||||
private val pointerDevicePosition = Vector2()
|
||||
private var shouldTranslate = false
|
||||
private var shouldTranslateVertically = false
|
||||
@ -46,7 +46,7 @@ class TranslationState(
|
||||
entity,
|
||||
entity.section.value,
|
||||
initialSection,
|
||||
entity.worldPosition.value,
|
||||
entity.position.value,
|
||||
initialPosition,
|
||||
)
|
||||
}
|
||||
@ -87,6 +87,6 @@ class TranslationState(
|
||||
entity.setSection(initialSection)
|
||||
}
|
||||
|
||||
entity.setWorldPosition(initialPosition)
|
||||
entity.setPosition(initialPosition)
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import world.phantasmal.webui.stores.Store
|
||||
import world.phantasmal.lib.fileFormats.quest.getAreasForEpisode as getAreasForEpisodeLib
|
||||
|
||||
class AreaStore(private val areaAssetLoader: AreaAssetLoader) : Store() {
|
||||
private val areas: Map<Episode, List<AreaModel>> = Episode.values()
|
||||
.map { episode ->
|
||||
private val areas: Map<Episode, List<AreaModel>> =
|
||||
Episode.values().associate { episode ->
|
||||
episode to getAreasForEpisodeLib(episode).map { area ->
|
||||
val variants = mutableListOf<AreaVariantModel>()
|
||||
val areaModel = AreaModel(area.id, area.name, area.order, variants)
|
||||
@ -22,7 +22,6 @@ class AreaStore(private val areaAssetLoader: AreaAssetLoader) : Store() {
|
||||
areaModel
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
|
||||
fun getAreasForEpisode(episode: Episode): List<AreaModel> =
|
||||
areas.getValue(episode)
|
||||
@ -42,4 +41,7 @@ class AreaStore(private val areaAssetLoader: AreaAssetLoader) : Store() {
|
||||
|
||||
suspend fun getSections(episode: Episode, variant: AreaVariantModel): List<SectionModel> =
|
||||
areaAssetLoader.loadSections(episode, variant)
|
||||
|
||||
fun getLoadedSections(episode: Episode, variant: AreaVariantModel): List<SectionModel>? =
|
||||
areaAssetLoader.getCachedSections(episode, variant)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package world.phantasmal.web.questEditor.stores
|
||||
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.launch
|
||||
import world.phantasmal.core.Severity
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
@ -68,7 +69,7 @@ class AsmStore(
|
||||
}
|
||||
|
||||
observe(asmAnalyser.mapDesignations) {
|
||||
questEditorStore.currentQuest.value?.setMapDesignations(it)
|
||||
scope.launch { questEditorStore.setMapDesignations(it) }
|
||||
}
|
||||
|
||||
observe(problems) { problems ->
|
||||
|
@ -37,7 +37,7 @@ class QuestEditorStore(
|
||||
|
||||
val devMode: Val<Boolean> = _devMode
|
||||
|
||||
val runner = QuestRunner()
|
||||
private val runner = QuestRunner()
|
||||
val currentQuest: Val<QuestModel?> = _currentQuest
|
||||
val currentArea: Val<AreaModel?> = _currentArea
|
||||
val selectedEvent: Val<QuestEventModel?> = _selectedEvent
|
||||
@ -134,12 +134,7 @@ class QuestEditorStore(
|
||||
_currentQuest.value = quest
|
||||
|
||||
// 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)
|
||||
}
|
||||
updateQuestEntitySections(quest)
|
||||
|
||||
// Ensure all entities have their section initialized.
|
||||
quest.npcs.value.forEach { it.setSectionInitialized() }
|
||||
@ -150,25 +145,6 @@ class QuestEditorStore(
|
||||
suspend fun getDefaultQuest(episode: Episode): QuestModel =
|
||||
convertQuestToModel(questLoader.loadDefaultQuest(episode), areaStore::getVariant)
|
||||
|
||||
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." }
|
||||
entity.setSectionInitialized()
|
||||
} else {
|
||||
entity.initializeSection(section)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentArea(area: AreaModel?) {
|
||||
val event = selectedEvent.value
|
||||
|
||||
@ -215,6 +191,30 @@ class QuestEditorStore(
|
||||
_selectedEntity.value = entity
|
||||
}
|
||||
|
||||
suspend fun setMapDesignations(mapDesignations: Map<Int, Int>) {
|
||||
currentQuest.value?.let { quest ->
|
||||
quest.setMapDesignations(mapDesignations)
|
||||
updateQuestEntitySections(quest)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEntitySection(entity: QuestEntityModel<*, *>, sectionId: Int) {
|
||||
currentQuest.value?.let { quest ->
|
||||
val variant = quest.areaVariants.value.find { it.area.id == entity.areaId }
|
||||
|
||||
variant?.let {
|
||||
val section = areaStore.getLoadedSections(quest.episode, variant)
|
||||
?.find { it.id == sectionId }
|
||||
|
||||
if (section == null) {
|
||||
entity.setSectionId(sectionId)
|
||||
} else {
|
||||
entity.setSection(section)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun executeAction(action: Action) {
|
||||
pushAction(action)
|
||||
action.execute()
|
||||
@ -239,4 +239,32 @@ class QuestEditorStore(
|
||||
fun questSaved() {
|
||||
undoManager.savePoint()
|
||||
}
|
||||
|
||||
private suspend fun updateQuestEntitySections(quest: QuestModel) {
|
||||
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." }
|
||||
entity.setSectionInitialized()
|
||||
} else {
|
||||
entity.setSection(section, keepRelativeTransform = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import kotlin.test.assertTrue
|
||||
|
||||
class UndoStackTests : WebTestSuite {
|
||||
@Test
|
||||
fun simple_properties_and_invariants() {
|
||||
fun simple_properties_and_invariants() = test {
|
||||
val stack = UndoStack(UndoManager())
|
||||
|
||||
assertFalse(stack.canUndo.value)
|
||||
@ -35,7 +35,7 @@ class UndoStackTests : WebTestSuite {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun undo() {
|
||||
fun undo() = test {
|
||||
val stack = UndoStack(UndoManager())
|
||||
|
||||
var value = 3
|
||||
@ -56,7 +56,7 @@ class UndoStackTests : WebTestSuite {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun redo() {
|
||||
fun redo() = test {
|
||||
val stack = UndoStack(UndoManager())
|
||||
|
||||
var value = 3
|
||||
@ -80,7 +80,7 @@ class UndoStackTests : WebTestSuite {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun push_then_undo_then_push_again() {
|
||||
fun push_then_undo_then_push_again() = test {
|
||||
val stack = UndoStack(UndoManager())
|
||||
|
||||
var value = 3
|
||||
|
@ -19,13 +19,13 @@ class QuestEntityModelTests : WebTestSuite {
|
||||
|
||||
assertTrue(entity.position.value.equals(entity.worldPosition.value))
|
||||
|
||||
// When section is initialized, relative position stays the same and world position changes.
|
||||
entity.initializeSection(SectionModel(
|
||||
// Initialize section and keep relative position the same so world position changes.
|
||||
entity.setSection(SectionModel(
|
||||
20,
|
||||
Vector3(7.0, 7.0, 7.0),
|
||||
euler(.0, .0, .0),
|
||||
components.areaStore.getVariant(Episode.I, 0, 0)!!,
|
||||
))
|
||||
), keepRelativeTransform = true)
|
||||
|
||||
assertEquals(5.0, entity.position.value.x)
|
||||
assertEquals(5.0, entity.position.value.y)
|
||||
|
@ -38,12 +38,6 @@ abstract class Input<T>(
|
||||
|
||||
onchange = { callOnChange(this) }
|
||||
|
||||
onkeydown = { e ->
|
||||
if (e.key == "Enter") {
|
||||
callOnChange(this)
|
||||
}
|
||||
}
|
||||
|
||||
interceptInputElement(this)
|
||||
|
||||
observe(this@Input.value) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLStyleElement
|
||||
@ -203,6 +205,10 @@ abstract class Widget(
|
||||
addDisposable(disposablePointerDrag(onPointerDown, onPointerMove, onPointerUp))
|
||||
}
|
||||
|
||||
protected fun launch(block: suspend CoroutineScope.() -> Unit) {
|
||||
scope.launch(block = block)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val STYLE_EL by lazy {
|
||||
val el = document.createElement("style") as HTMLStyleElement
|
||||
|
Loading…
Reference in New Issue
Block a user