Event actions now have a "Go to event" button.

This commit is contained in:
Daan Vanden Bosch 2021-06-19 10:36:43 +02:00
parent d7f0e536ec
commit 403e03b0ee
6 changed files with 220 additions and 96 deletions

View File

@ -7,6 +7,7 @@ import world.phantasmal.observable.cell.list.ListCell
import world.phantasmal.observable.cell.list.emptyListCell import world.phantasmal.observable.cell.list.emptyListCell
import world.phantasmal.observable.cell.list.flatMapToList import world.phantasmal.observable.cell.list.flatMapToList
import world.phantasmal.observable.cell.list.listCell import world.phantasmal.observable.cell.list.listCell
import world.phantasmal.observable.cell.map
import world.phantasmal.web.questEditor.actions.* import world.phantasmal.web.questEditor.actions.*
import world.phantasmal.web.questEditor.models.QuestEventActionModel import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel import world.phantasmal.web.questEditor.models.QuestEventModel
@ -165,6 +166,17 @@ class EventsController(private val store: QuestEditorStore) : Controller() {
store.executeAction(DeleteEventActionAction(::selectEvent, event, index, action)) store.executeAction(DeleteEventActionAction(::selectEvent, event, index, action))
} }
fun canGoToEvent(eventId: Cell<Int>): Cell<Boolean> =
map(enabled, events, eventId) { en, evts, id ->
en && evts.any { it.id.value == id }
}
fun goToEvent(eventId: Int) {
events.value.find { it.id.value == eventId }?.let { event ->
store.setSelectedEvent(event)
}
}
fun setActionSectionId( fun setActionSectionId(
event: QuestEventModel, event: QuestEventModel,
action: QuestEventActionModel.SpawnNpcs, action: QuestEventActionModel.SpawnNpcs,

View File

@ -38,7 +38,7 @@ class QuestModel(
private val _mapDesignations = mutableCell(mapDesignations) private val _mapDesignations = mutableCell(mapDesignations)
private val _npcs = SimpleListCell(npcs) { arrayOf(it.sectionInitialized, it.wave) } private val _npcs = SimpleListCell(npcs) { arrayOf(it.sectionInitialized, it.wave) }
private val _objects = SimpleListCell(objects) { arrayOf(it.sectionInitialized) } private val _objects = SimpleListCell(objects) { arrayOf(it.sectionInitialized) }
private val _events = SimpleListCell(events) private val _events = SimpleListCell(events) { arrayOf(it.id) }
val id: Cell<Int> = _id val id: Cell<Int> = _id
val language: Cell<Int> = _language val language: Cell<Int> = _language

View File

@ -0,0 +1,123 @@
package world.phantasmal.web.questEditor.widgets
import org.w3c.dom.Node
import world.phantasmal.observable.cell.cell
import world.phantasmal.web.questEditor.controllers.EventsController
import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.webui.dom.Icon
import world.phantasmal.webui.dom.td
import world.phantasmal.webui.dom.th
import world.phantasmal.webui.dom.tr
import world.phantasmal.webui.widgets.Button
import world.phantasmal.webui.widgets.IntInput
import world.phantasmal.webui.widgets.Widget
class EventActionWidget(
private val ctrl: EventsController,
private val event: QuestEventModel,
private val action: QuestEventActionModel,
) : Widget() {
override fun Node.createElement() =
tr {
className = "pw-quest-editor-event-action"
th { textContent = "${action.shortName}:" }
when (action) {
is QuestEventActionModel.SpawnNpcs -> {
td {
addChild(
IntInput(
enabled = ctrl.enabled,
tooltip = cell("Section"),
value = action.sectionId,
onChange = { ctrl.setActionSectionId(event, action, it) },
min = 0,
step = 1,
)
)
addChild(
IntInput(
enabled = ctrl.enabled,
tooltip = cell("Appear flag"),
value = action.appearFlag,
onChange = { ctrl.setActionAppearFlag(event, action, it) },
min = 0,
step = 1,
)
)
}
}
is QuestEventActionModel.Door -> {
td {
addChild(
IntInput(
enabled = ctrl.enabled,
tooltip = cell("Door"),
value = action.doorId,
onChange = { ctrl.setActionDoorId(event, action, it) },
min = 0,
step = 1,
)
)
}
}
is QuestEventActionModel.TriggerEvent -> {
td {
addChild(
IntInput(
enabled = ctrl.enabled,
value = action.eventId,
onChange = { ctrl.setActionEventId(event, action, it) },
min = 0,
step = 1,
)
)
}
}
}
td {
className = "pw-quest-editor-event-action-buttons"
addChild(
Button(
enabled = ctrl.enabled,
tooltip = cell("Remove this action from the event"),
iconLeft = Icon.Remove,
onClick = { ctrl.removeAction(event, action) }
)
)
if (action is QuestEventActionModel.TriggerEvent) {
addChild(
Button(
enabled = ctrl.canGoToEvent(action.eventId),
tooltip = cell("Go to event"),
iconLeft = Icon.ArrowRight,
onClick = { e ->
e.stopPropagation()
ctrl.goToEvent(action.eventId.value)
}
)
)
}
}
}
companion object {
init {
@Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty")
// language=css
style(
"""
.pw-quest-editor-event-action-buttons {
display: flex;
flex-direction: row;
}
""".trimIndent()
)
}
}
}

View File

@ -1,15 +1,10 @@
package world.phantasmal.web.questEditor.widgets package world.phantasmal.web.questEditor.widgets
import org.w3c.dom.* import org.w3c.dom.*
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.observable.cell.cell
import world.phantasmal.web.questEditor.controllers.EventsController import world.phantasmal.web.questEditor.controllers.EventsController
import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.webui.dom.* import world.phantasmal.webui.dom.*
import world.phantasmal.webui.obj import world.phantasmal.webui.obj
import world.phantasmal.webui.widgets.Button
import world.phantasmal.webui.widgets.Dropdown import world.phantasmal.webui.widgets.Dropdown
import world.phantasmal.webui.widgets.IntInput import world.phantasmal.webui.widgets.IntInput
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
@ -118,8 +113,8 @@ class EventWidget(
} }
} }
tbody { tbody {
bindDisposableChildrenTo(event.actions) { action, _ -> bindChildWidgetsTo(event.actions) { action, _ ->
createActionElement(action) EventActionWidget(ctrl, event, action)
} }
} }
tfoot { tfoot {
@ -139,91 +134,12 @@ class EventWidget(
} }
} }
private fun Node.createActionElement(action: QuestEventActionModel): Pair<Node, Disposable> {
val disposer = Disposer()
val node = tr {
th { textContent = "${action.shortName}:" }
when (action) {
is QuestEventActionModel.SpawnNpcs -> {
td {
addWidget(
disposer.add(IntInput(
enabled = ctrl.enabled,
tooltip = cell("Section"),
value = action.sectionId,
onChange = { ctrl.setActionSectionId(event, action, it) },
min = 0,
step = 1,
)),
addToDisposer = false,
)
addWidget(
disposer.add(IntInput(
enabled = ctrl.enabled,
tooltip = cell("Appear flag"),
value = action.appearFlag,
onChange = { ctrl.setActionAppearFlag(event, action, it) },
min = 0,
step = 1,
)),
addToDisposer = false,
)
}
}
is QuestEventActionModel.Door -> {
td {
addWidget(
disposer.add(IntInput(
enabled = ctrl.enabled,
tooltip = cell("Door"),
value = action.doorId,
onChange = { ctrl.setActionDoorId(event, action, it) },
min = 0,
step = 1,
)),
addToDisposer = false,
)
}
}
is QuestEventActionModel.TriggerEvent -> {
td {
addWidget(
disposer.add(IntInput(
enabled = ctrl.enabled,
value = action.eventId,
onChange = { ctrl.setActionEventId(event, action, it) },
min = 0,
step = 1,
)),
addToDisposer = false,
)
}
}
}
td {
addWidget(
disposer.add(Button(
enabled = ctrl.enabled,
tooltip = cell("Remove this action from the event"),
iconLeft = Icon.Remove,
onClick = { ctrl.removeAction(event, action) }
)),
addToDisposer = false,
)
}
}
return Pair(node, disposer)
}
companion object { companion object {
init { init {
@Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty") @Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty")
// language=css // language=css
style(""" style(
"""
.pw-quest-editor-event { .pw-quest-editor-event {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -250,11 +166,11 @@ class EventWidget(
} }
.pw-quest-editor-event-props { .pw-quest-editor-event-props {
width: 120px; width: 115px;
} }
.pw-quest-editor-event-actions { .pw-quest-editor-event-actions {
width: 150px; width: 165px;
} }
.pw-quest-editor-event > div > table { .pw-quest-editor-event > div > table {
@ -265,7 +181,8 @@ class EventWidget(
.pw-quest-editor-event th { .pw-quest-editor-event th {
text-align: left; text-align: left;
} }
""".trimIndent()) """.trimIndent()
)
} }
} }
} }

View File

@ -1,12 +1,10 @@
package world.phantasmal.web.questEditor.controllers package world.phantasmal.web.questEditor.controllers
import world.phantasmal.web.questEditor.models.QuestEventActionModel import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.web.test.WebTestSuite import world.phantasmal.web.test.WebTestSuite
import world.phantasmal.web.test.createQuestModel import world.phantasmal.web.test.createQuestModel
import kotlin.test.Test import kotlin.test.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class EventsControllerTests : WebTestSuite { class EventsControllerTests : WebTestSuite {
@Test @Test
@ -74,4 +72,76 @@ class EventsControllerTests : WebTestSuite {
assertEquals(1, event.actions.value.size) assertEquals(1, event.actions.value.size)
} }
@Test
fun canGoToEvent() = testAsync {
// Setup.
val store = components.questEditorStore
// Quest with two events, the first event triggers the second event.
val quest = createQuestModel(
mapDesignations = mapOf(1 to 0),
events = listOf(
QuestEventModel(
id = 100,
areaId = 1,
sectionId = 11,
waveId = 1,
delay = 50,
unknown = 0,
actions = mutableListOf(QuestEventActionModel.TriggerEvent(101)),
),
QuestEventModel(
id = 101,
areaId = 1,
sectionId = 11,
waveId = 2,
delay = 50,
unknown = 0,
actions = mutableListOf(QuestEventActionModel.Door.Unlock(7)),
),
),
)
store.setCurrentQuest(quest)
store.setCurrentArea(quest.areaVariants.value.first().area)
val ctrl = disposer.add(EventsController(store))
val canGoToEvent = ctrl.canGoToEvent(
(ctrl.events[0].actions[0] as QuestEventActionModel.TriggerEvent).eventId
)
// We test the observed value instead of the cell's value property.
var canGoToEventValue: Boolean? = null
disposer.add(canGoToEvent.observe(callNow = true) {
assertNull(canGoToEventValue)
canGoToEventValue = it.value
})
assertEquals(true, canGoToEventValue)
// Let event 100 point to nonexistent event 102.
canGoToEventValue = null
ctrl.setActionEventId(
ctrl.events[0],
ctrl.events[0].actions[0] as QuestEventActionModel.TriggerEvent,
102,
)
assertEquals(false, canGoToEventValue)
// Add event 102.
canGoToEventValue = null
ctrl.selectEvent(null) // Deselect so the next event will be added at the end of the list.
ctrl.addEvent()
ctrl.setId(ctrl.events.value.last(), 102)
assertEquals(true, canGoToEventValue)
// Remove event 102.
canGoToEventValue = null
ctrl.removeEvent(ctrl.events.value.last())
assertEquals(false, canGoToEventValue)
}
} }

View File

@ -100,6 +100,7 @@ fun getRoot(): HTMLElement = document.getElementById("pw-root") as HTMLElement
enum class Icon { enum class Icon {
ArrowDown, ArrowDown,
ArrowRight,
Eye, Eye,
File, File,
GitHub, GitHub,
@ -122,6 +123,7 @@ enum class Icon {
fun Node.icon(icon: Icon): HTMLElement { fun Node.icon(icon: Icon): HTMLElement {
val iconStr = when (icon) { val iconStr = when (icon) {
Icon.ArrowDown -> "fas fa-arrow-down" Icon.ArrowDown -> "fas fa-arrow-down"
Icon.ArrowRight -> "fas fa-arrow-right"
Icon.Eye -> "far fa-eye" Icon.Eye -> "far fa-eye"
Icon.File -> "fas fa-file" Icon.File -> "fas fa-file"
Icon.GitHub -> "fab fa-github" Icon.GitHub -> "fab fa-github"