[Editor] Add support for different Tile Sets on each Tile Layer #3

This commit is contained in:
2022-08-26 17:27:05 +02:00
parent 859886be79
commit 547b79c9bf
29 changed files with 397 additions and 192 deletions

View File

@@ -1,12 +1,16 @@
package com.bartlomiejpluta.base.editor.asset.view.select
import com.bartlomiejpluta.base.editor.asset.model.Asset
import javafx.beans.property.BooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.StringProperty
import javafx.collections.ObservableList
import tornadofx.*
class SelectGraphicAssetFragment<T : Asset> : Fragment("Select Asset") {
val assets: ObservableList<T> by param()
val cancelable: BooleanProperty by param(true.toProperty())
val comment: StringProperty by param("".toProperty())
private val asset = SimpleObjectProperty<T>()
@@ -22,6 +26,12 @@ class SelectGraphicAssetFragment<T : Asset> : Fragment("Select Asset") {
}
override val root = form {
if(comment.isNotEmpty.value) {
label(comment) {
visibleWhen(comment.isNotEmpty)
}
}
fieldset {
this += selectGraphicAssetView.root
}
@@ -37,6 +47,7 @@ class SelectGraphicAssetFragment<T : Asset> : Fragment("Select Asset") {
}
button("Cancel") {
visibleWhen(cancelable)
action { close() }
}
}

View File

@@ -4,16 +4,17 @@ import com.bartlomiejpluta.base.editor.animation.view.importing.ImportAnimationF
import com.bartlomiejpluta.base.editor.animation.viewmodel.AnimationAssetDataVM
import com.bartlomiejpluta.base.editor.asset.model.Asset
import com.bartlomiejpluta.base.editor.asset.model.GraphicAsset
import com.bartlomiejpluta.base.editor.asset.view.select.SelectGraphicAssetFragment
import com.bartlomiejpluta.base.editor.asset.viewmodel.GraphicAssetVM
import com.bartlomiejpluta.base.editor.audio.view.importing.ImportSoundFragment
import com.bartlomiejpluta.base.editor.audio.viewmodel.SoundAssetDataVM
import com.bartlomiejpluta.base.editor.characterset.view.importing.ImportCharacterSetFragment
import com.bartlomiejpluta.base.editor.characterset.viewmodel.CharacterSetAssetDataVM
import com.bartlomiejpluta.base.editor.code.model.CodeScope
import com.bartlomiejpluta.base.editor.code.viewmodel.CodeVM
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.database.model.data.Query
import com.bartlomiejpluta.base.editor.database.viewmodel.QueryVM
import com.bartlomiejpluta.base.editor.characterset.view.importing.ImportCharacterSetFragment
import com.bartlomiejpluta.base.editor.characterset.viewmodel.CharacterSetAssetDataVM
import com.bartlomiejpluta.base.editor.event.SelectMainViewTabEvent
import com.bartlomiejpluta.base.editor.file.model.FileNode
import com.bartlomiejpluta.base.editor.file.model.ScriptAssetFileNode
@@ -26,18 +27,20 @@ import com.bartlomiejpluta.base.editor.image.view.importing.ImportImageFragment
import com.bartlomiejpluta.base.editor.image.viewmodel.ImageAssetDataVM
import com.bartlomiejpluta.base.editor.map.asset.GameMapAsset
import com.bartlomiejpluta.base.editor.map.model.map.GameMap
import com.bartlomiejpluta.base.editor.map.view.wizard.MapCreationWizard
import com.bartlomiejpluta.base.editor.map.view.wizard.MapImportWizard
import com.bartlomiejpluta.base.editor.map.view.wizard.MapCreationFragment
import com.bartlomiejpluta.base.editor.map.view.wizard.MapImportFragment
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapBuilderVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import com.bartlomiejpluta.base.editor.project.model.Project
import com.bartlomiejpluta.base.editor.project.view.ProjectSettingsFragment
import com.bartlomiejpluta.base.editor.project.viewmodel.ProjectVM
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import com.bartlomiejpluta.base.editor.tileset.view.importing.ImportTileSetFragment
import com.bartlomiejpluta.base.editor.tileset.viewmodel.TileSetAssetDataVM
import javafx.scene.control.TextInputDialog
import javafx.stage.FileChooser
import javafx.stage.StageStyle
import org.springframework.stereotype.Component
import tornadofx.*
import java.io.File
@@ -68,10 +71,9 @@ class MainController : Controller() {
val vm = GameMapBuilderVM()
setInScope(vm, scope)
find<MapCreationWizard>(scope).apply {
find<MapCreationFragment>(scope).apply {
onComplete {
val tileSet = projectContext.loadTileSet(vm.tileSetAsset.uid)
val map = GameMap(tileSet).apply {
val map = GameMap(vm.tileWidth.toDouble(), vm.tileHeight.toDouble()).apply {
rows = vm.rows
columns = vm.columns
handler = vm.handler
@@ -88,10 +90,28 @@ class MainController : Controller() {
val scope = UndoableScope()
val vm = GameMapBuilderVM()
setInScope(vm, scope)
find<MapImportWizard>(scope).apply {
find<MapImportFragment>(scope).apply {
onComplete {
val tileSet = projectContext.loadTileSet(vm.tileSetAsset.uid)
val map = projectContext.importMapFromFile(vm.name, vm.handler, File(vm.file), tileSet)
val map = projectContext.importMapFromFile(vm.name, vm.handler, File(vm.file)) { name, uid ->
var newUid = ""
find<SelectGraphicAssetFragment<TileSetAsset>>(
Scope(),
SelectGraphicAssetFragment<TileSetAsset>::assets to projectContext.project?.tileSets!!,
SelectGraphicAssetFragment<TileSetAsset>::comment to "You are importing a tile layer which originally was defined\nwith an other Tile Set asset data with UID: [$uid].\nPlease select asset you would like to apply for layer $name.\n".toProperty(),
SelectGraphicAssetFragment<TileSetAsset>::cancelable to false.toProperty()
).apply {
title = "Select Tile Set for layer $name"
onComplete {
newUid = it.uid
}
openModal(block = true, resizable = false, stageStyle = StageStyle.UNDECORATED)
}
newUid
}
openItems[scope] = GameMapVM(map)
}
@@ -121,11 +141,7 @@ class MainController : Controller() {
}
fun openScript(
fsNode: FileNode,
line: Int = 1,
column: Int = 1,
execute: ((String) -> Unit)? = null,
saveable: Boolean = true
fsNode: FileNode, line: Int = 1, column: Int = 1, execute: ((String) -> Unit)? = null, saveable: Boolean = true
) {
val findScript = { script: CodeVM -> script.fileNode.absolutePath == fsNode.absolutePath }
val updateExistingScope = { scope: CodeScope -> scope.setCaretPosition(line, column) }
@@ -170,11 +186,9 @@ class MainController : Controller() {
updateViewModel: (VM) -> Unit = {},
createItem: () -> Pair<Scope, VM>
) {
@Suppress("UNCHECKED_CAST")
val pair = openItems.entries
.filter { (_, item) -> item is VM }
.map { (scope, item) -> (scope as S) to (item as VM) }
.firstOrNull { (_, item) -> findItem(item) }
@Suppress("UNCHECKED_CAST") val pair =
openItems.entries.filter { (_, item) -> item is VM }.map { (scope, item) -> (scope as S) to (item as VM) }
.firstOrNull { (_, item) -> findItem(item) }
if (pair == null) {
val (scope, item) = createItem()
@@ -277,11 +291,7 @@ class MainController : Controller() {
width = 300.0
contentText = "Widget name"
title = "New Widget"
}
.showAndWait()
.map(::WidgetAssetData)
.map(projectContext::createWidget)
.ifPresent(this::openScript)
}.showAndWait().map(::WidgetAssetData).map(projectContext::createWidget).ifPresent(this::openScript)
}
fun importSound() {

View File

@@ -11,7 +11,6 @@ import javafx.scene.paint.Color
class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, private val painter: MapPainter) : Renderable {
var tileSet = map.tileSet
private var tileWidth = map.tileWidth
private var tileHeight = map.tileHeight

View File

@@ -21,8 +21,8 @@ class MapPainter(
private val editorStateVM: EditorStateVM,
private val paintingCallback: (PaintingTrace) -> Unit
) : Renderable, MapMouseEventHandler {
private val tileWidth = mapVM.tileSet.tileWidth.toDouble()
private val tileHeight = mapVM.tileSet.tileHeight.toDouble()
private val tileWidth = mapVM.tileWidth.toDouble()
private val tileHeight = mapVM.tileHeight.toDouble()
private var cursor: PaintingCursor? = null
private var currentTrace: PaintingTrace? = null

View File

@@ -47,7 +47,7 @@ class MapPane(
override fun handle(event: MouseEvent?) {
if (event != null) {
painter.handleMouseInput(MapMouseEvent.of(event, mapVM.tileSet))
painter.handleMouseInput(MapMouseEvent.of(event, mapVM))
}
mapCanvas.render(graphicsContext2D)

View File

@@ -1,14 +1,20 @@
package com.bartlomiejpluta.base.editor.map.model.layer
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import com.bartlomiejpluta.base.editor.tileset.model.Tile
import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleStringProperty
import javafx.scene.image.Image
import tornadofx.getValue
import tornadofx.setValue
import tornadofx.toProperty
class TileLayer(
name: String,
rows: Int,
columns: Int,
tileSetAsset: TileSetAsset,
layer: Array<Array<Tile?>> = Array(rows) { Array(columns) { null } }
) : Layer {
var layer = layer
@@ -17,6 +23,21 @@ class TileLayer(
override val nameProperty = SimpleStringProperty(name)
override var name: String by nameProperty
val tileSetAssetProperty = tileSetAsset.toProperty()
var tileSetAsset by tileSetAssetProperty
val tileSetProperty = Bindings.createObjectBinding({
tileSetAssetProperty.value.file.inputStream().use { fis ->
TileSet(
tileSetAsset.uid,
tileSetAsset.name,
Image(fis),
tileSetAsset.rows,
tileSetAsset.columns
)
}
}, tileSetAssetProperty)
override fun resize(rows: Int, columns: Int) {
layer = Array(rows) { row ->
Array(columns) { column ->

View File

@@ -10,15 +10,12 @@ import tornadofx.getValue
import tornadofx.setValue
class GameMap(val tileSet: TileSet) {
class GameMap(val tileWidth: Double, val tileHeight: Double) {
val uidProperty = SimpleStringProperty()
var uid by uidProperty
val layers = FXCollections.observableArrayList(Layer.extractor())
val tileWidth = tileSet.tileWidth.toDouble()
val tileHeight = tileSet.tileHeight.toDouble()
val rowsProperty = SimpleIntegerProperty(INITIAL_ROWS)
var rows by rowsProperty

View File

@@ -1,6 +1,7 @@
package com.bartlomiejpluta.base.editor.map.model.map
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
@@ -9,12 +10,15 @@ import tornadofx.setValue
import java.io.File
class GameMapBuilder {
val tileSetAssetProperty = SimpleObjectProperty<TileSetAsset>()
var tileSetAsset by tileSetAssetProperty
val nameProperty = SimpleStringProperty("")
var name by nameProperty
val tileWidthProperty = SimpleIntegerProperty(32)
var tileWidth by tileWidthProperty
val tileHeightProperty = SimpleIntegerProperty(32)
var tileHeight by tileHeightProperty
val rowsProperty = SimpleIntegerProperty(20)
var rows by rowsProperty

View File

@@ -0,0 +1,27 @@
package com.bartlomiejpluta.base.editor.map.parameter.layer
import com.bartlomiejpluta.base.editor.common.parameter.model.GraphicAssetParameter
import com.bartlomiejpluta.base.editor.common.parameter.model.Parameter
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.project.model.Project
import javafx.collections.ObservableList
import org.springframework.stereotype.Component
@Component
class TileLayerParametersBinder : LayerParametersBinder<TileLayer> {
override fun bind(
layer: TileLayer,
parameters: ObservableList<Parameter<*>>,
project: Project,
onCommit: () -> Unit
) {
val tileSet = GraphicAssetParameter("tileSet", layer.tileSetAsset, true, project.tileSets) { _, _, submit ->
onCommit()
submit()
}
tileSet.bindBidirectional(layer.tileSetAssetProperty)
parameters.addAll(tileSet)
}
}

View File

@@ -2,9 +2,10 @@ package com.bartlomiejpluta.base.editor.map.serial
import com.bartlomiejpluta.base.editor.common.serial.Deserializer
import com.bartlomiejpluta.base.editor.map.model.map.GameMap
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import java.io.InputStream
interface MapDeserializer : Deserializer<GameMap> {
fun deserialize(input: InputStream, tileSet: TileSet): GameMap
fun deserialize(input: InputStream, replaceTileSet: (String, String) -> String): GameMap
}

View File

@@ -6,6 +6,7 @@ import com.bartlomiejpluta.base.editor.map.model.layer.*
import com.bartlomiejpluta.base.editor.map.model.map.GameMap
import com.bartlomiejpluta.base.editor.map.model.obj.MapObject
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import com.bartlomiejpluta.base.editor.tileset.model.Tile
import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import com.bartlomiejpluta.base.proto.GameMapProto
@@ -24,25 +25,11 @@ class ProtobufMapDeserializer : MapDeserializer {
appContext.getBean(ProjectContext::class.java)
}
override fun deserialize(input: InputStream): GameMap {
override fun deserialize(input: InputStream) = deserialize(input) { _, uid -> uid }
override fun deserialize(input: InputStream, replaceTileSet: (String, String) -> String): GameMap {
val proto = GameMapProto.GameMap.parseFrom(input)
val tileSet = projectContext.loadTileSet(proto.tileSetUID)
val map = GameMap(tileSet)
map.uid = proto.uid
map.rows = proto.rows
map.columns = proto.columns
map.handler = proto.handler
proto.layersList.forEach {
map.layers.add(deserializeLayer(map.rows, map.columns, tileSet, it))
}
return map
}
override fun deserialize(input: InputStream, tileSet: TileSet): GameMap {
val proto = GameMapProto.GameMap.parseFrom(input)
val map = GameMap(tileSet)
val map = GameMap(proto.tileWidth.toDouble(), proto.tileHeight.toDouble())
map.uid = proto.uid
map.rows = proto.rows
map.columns = proto.columns
@@ -50,14 +37,14 @@ class ProtobufMapDeserializer : MapDeserializer {
proto.layersList
.filter { it.hasTileLayer() || it.hasObjectLayer() || it.hasColorLayer() }
.forEach { map.layers.add(deserializeLayer(map.rows, map.columns, tileSet, it)) }
.forEach { map.layers.add(deserializeLayer(map.rows, map.columns, it, replaceTileSet)) }
return map
}
private fun deserializeLayer(rows: Int, columns: Int, tileSet: TileSet, proto: GameMapProto.Layer): Layer {
private fun deserializeLayer(rows: Int, columns: Int, proto: GameMapProto.Layer, replaceTileSet: (String, String) -> String): Layer {
return when {
proto.hasTileLayer() -> deserializeTileLayer(rows, columns, tileSet, proto)
proto.hasTileLayer() -> deserializeTileLayer(rows, columns, proto, replaceTileSet)
proto.hasObjectLayer() -> deserializeObjectLayer(rows, columns, proto)
proto.hasColorLayer() -> deserializeColorLayer(proto)
proto.hasImageLayer() -> deserializeImageLayer(proto)
@@ -66,8 +53,10 @@ class ProtobufMapDeserializer : MapDeserializer {
}
}
private fun deserializeTileLayer(rows: Int, columns: Int, tileSet: TileSet, proto: GameMapProto.Layer): Layer {
private fun deserializeTileLayer(rows: Int, columns: Int, proto: GameMapProto.Layer, replaceTileSet: (String, String) -> String): Layer {
val layer: Array<Array<Tile?>> = Array(rows) { Array(columns) { null } }
val tileSetAsset = projectContext.findTileSetAsset(replaceTileSet(proto.name, proto.tileLayer.tilesetUID))
val tileSet = projectContext.loadTileSet(tileSetAsset.uid)
proto.tileLayer.tilesList.forEachIndexed { index, tile ->
layer[index / columns][index % columns] = when (tile) {
@@ -76,7 +65,7 @@ class ProtobufMapDeserializer : MapDeserializer {
}
}
return TileLayer(proto.name, rows, columns, layer)
return TileLayer(proto.name, rows, columns, tileSetAsset, layer)
}
private fun deserializeObjectLayer(rows: Int, columns: Int, proto: GameMapProto.Layer): Layer {

View File

@@ -16,7 +16,8 @@ class ProtobufMapSerializer : MapSerializer {
protoMap.uid = item.uid
protoMap.rows = item.rows
protoMap.columns = item.columns
protoMap.tileSetUID = item.tileSet.uid
protoMap.tileWidth = item.tileWidth.toInt()
protoMap.tileHeight = item.tileHeight.toInt()
protoMap.handler = item.handler
item.layers.forEach { layer -> protoMap.addLayers(serializeLayer(layer)) }
@@ -28,6 +29,7 @@ class ProtobufMapSerializer : MapSerializer {
return when (layer) {
is TileLayer -> layer.layer.flatMap { it.asIterable() }
.fold(GameMapProto.TileLayer.newBuilder()) { acc, tile -> acc.addTiles((tile?.id?.plus(1)) ?: 0) }
.setTilesetUID(layer.tileSetAsset.uid)
.build()
.let { GameMapProto.Layer.newBuilder().setName(layer.name).setTileLayer(it).build() }

View File

@@ -5,8 +5,10 @@ import com.bartlomiejpluta.base.editor.common.parameter.view.ParametersTableFrag
import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent
import com.bartlomiejpluta.base.editor.map.model.layer.ColorLayer
import com.bartlomiejpluta.base.editor.map.model.layer.ImageLayer
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.parameter.layer.ColorLayerParametersBinder
import com.bartlomiejpluta.base.editor.map.parameter.layer.ImageLayerParametersBinder
import com.bartlomiejpluta.base.editor.map.parameter.layer.TileLayerParametersBinder
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import tornadofx.View
@@ -20,6 +22,7 @@ class MapLayerParameters : View() {
// of LayerParametersBinder<> type
private val colorLayerParametersBinder: ColorLayerParametersBinder by di()
private val imageLayerParametersBinder: ImageLayerParametersBinder by di()
private val tileLayerParametersBinder: TileLayerParametersBinder by di()
private val parameters = observableListOf<Parameter<*>>()
@@ -29,15 +32,16 @@ class MapLayerParameters : View() {
parameters.clear()
when (layer) {
is ColorLayer -> colorLayerParametersBinder.bind(layer, parameters, projectContext.project!!) {
fire(
RedrawMapRequestEvent
)
is TileLayer -> tileLayerParametersBinder.bind(layer, parameters, projectContext.project!!) {
fire(RedrawMapRequestEvent)
}
is ColorLayer -> colorLayerParametersBinder.bind(layer, parameters, projectContext.project!!) {
fire(RedrawMapRequestEvent)
}
is ImageLayer -> imageLayerParametersBinder.bind(layer, parameters, projectContext.project!!) {
fire(
RedrawMapRequestEvent
)
fire(RedrawMapRequestEvent)
}
}
}

View File

@@ -13,6 +13,7 @@ import com.bartlomiejpluta.base.editor.map.model.layer.*
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import javafx.collections.ObservableList
import javafx.scene.control.ListCell
import javafx.scene.control.ListView
@@ -55,11 +56,21 @@ class MapLayersView : View() {
menubutton(graphic = FontIcon("fa-plus")) {
item("Tile Layer", graphic = FontIcon("fa-th-large")) {
action {
val layer = TileLayer("Layer ${mapVM.layers.size + 1}", mapVM.rows, mapVM.columns)
val command = CreateLayerCommand(mapVM.item, layer)
command.execute()
layersPane.selectionModel.select(mapVM.layers.size - 1)
undoRedoService.push(command, scope)
val scope = UndoableScope()
find<SelectGraphicAssetFragment<TileSetAsset>>(
scope,
SelectGraphicAssetFragment<TileSetAsset>::assets to projectContext.project?.tileSets!!
).apply {
onComplete {
val layer = TileLayer("Layer ${mapVM.layers.size + 1}", mapVM.rows, mapVM.columns, it)
val command = CreateLayerCommand(mapVM.item, layer)
command.execute()
layersPane.selectionModel.select(mapVM.layers.size - 1)
undoRedoService.push(command, scope)
}
openModal(block = true, resizable = false)
}
}
}
@@ -164,7 +175,7 @@ class MapLayersView : View() {
override fun fromString(string: String?): Layer = cell.item.apply {
string
?.takeIf { str -> str != name }
?.takeIf { str -> layers.none { it.name == str }}
?.takeIf { str -> layers.none { it.name == str } }
?.let { onUpdate(this, it) }
}
}

View File

@@ -40,7 +40,7 @@ class MapView : View() {
}
init {
brushVM.item = mapVM.tileSet.baseBrush
// brushVM.item = mapVM.tileSet.baseBrush
brushVM.commit()
subscribe<RedrawMapRequestEvent> { mapPane.render() }

View File

@@ -1,11 +1,13 @@
package com.bartlomiejpluta.base.editor.map.view.wizard
import com.bartlomiejpluta.base.editor.map.model.map.GameMapBuilder
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapBuilderVM
import com.bartlomiejpluta.base.editor.util.fx.TextFieldUtil
import tornadofx.*
class MapCreationBasicDataView : View("Basic Data") {
class MapCreationFragment : Fragment("Basic Data") {
private val mapBuilderVM = find<GameMapBuilderVM>()
private var onCompleteConsumer: ((GameMapBuilder) -> Unit)? = null
override val complete = mapBuilderVM.valid(
mapBuilderVM.nameProperty,
@@ -14,6 +16,11 @@ class MapCreationBasicDataView : View("Basic Data") {
mapBuilderVM.handlerProperty
)
fun onComplete(consumer: (GameMapBuilder) -> Unit) {
this.onCompleteConsumer = consumer
}
override val root = form {
fieldset("Map Settings") {
field("Map name") {
@@ -23,6 +30,36 @@ class MapCreationBasicDataView : View("Basic Data") {
}
}
field("Tile Width") {
spinner(min = 1, property = mapBuilderVM.tileWidthProperty, editable = true) {
required()
editor.textFormatter = TextFieldUtil.integerFormatter(mapBuilderVM.tileWidth)
validator {
if (it?.toInt()?.let { value -> value <= 0 } != false) {
error("The tile width must be greater than 0")
}
null
}
}
}
field("Tile Height") {
spinner(min = 1, property = mapBuilderVM.tileHeightProperty, editable = true) {
required()
editor.textFormatter = TextFieldUtil.integerFormatter(mapBuilderVM.tileHeight)
validator {
if (it?.toInt()?.let { value -> value <= 0 } != false) {
error("The tile height must be greater than 0")
}
null
}
}
}
field("Rows") {
spinner(min = 1, max = 100, property = mapBuilderVM.rowsProperty, editable = true) {
required()
@@ -60,5 +97,25 @@ class MapCreationBasicDataView : View("Basic Data") {
}
}
}
buttonbar {
button("Create") {
action {
if (mapBuilderVM.commit()) {
onCompleteConsumer?.let { it(mapBuilderVM.item) }
close()
}
}
}
button("Reset") {
action { mapBuilderVM.rollback() }
}
button("Cancel") {
action { close() }
}
}
}
}

View File

@@ -1,18 +0,0 @@
package com.bartlomiejpluta.base.editor.map.view.wizard
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapBuilderVM
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.Wizard
class MapCreationWizard : Wizard("New Map", "Provide map information") {
private val mapBuilderVM = find<GameMapBuilderVM>()
init {
graphic = FontIcon("fa-map").apply {
iconSize = 40
}
add(MapCreationBasicDataView::class)
add(MapTileSetSelectionView::class)
}
}

View File

@@ -1,13 +1,18 @@
package com.bartlomiejpluta.base.editor.map.view.wizard
import com.bartlomiejpluta.base.editor.map.model.map.GameMapBuilder
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapBuilderVM
import javafx.beans.binding.Bindings
import javafx.stage.FileChooser
import tornadofx.*
import java.io.File
class MapImportBasicDataView : View("Basic Data") {
class MapImportFragment : Fragment("Basic Data") {
private val mapBuilderVM = find<GameMapBuilderVM>()
private var onCompleteConsumer: ((GameMapBuilder) -> Unit)? = null
fun onComplete(consumer: (GameMapBuilder) -> Unit) {
this.onCompleteConsumer = consumer
}
override val complete = mapBuilderVM.valid(
mapBuilderVM.fileProperty,
@@ -61,5 +66,25 @@ class MapImportBasicDataView : View("Basic Data") {
}
}
}
buttonbar {
button("Create") {
action {
if (mapBuilderVM.commit()) {
onCompleteConsumer?.let { it(mapBuilderVM.item) }
close()
}
}
}
button("Reset") {
action { mapBuilderVM.rollback() }
}
button("Cancel") {
action { close() }
}
}
}
}

View File

@@ -1,18 +0,0 @@
package com.bartlomiejpluta.base.editor.map.view.wizard
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapBuilderVM
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.Wizard
class MapImportWizard : Wizard("Import Map", "Provide map information") {
private val mapBuilderVM = find<GameMapBuilderVM>()
init {
graphic = FontIcon("fa-map").apply {
iconSize = 40
}
add(MapImportBasicDataView::class)
add(MapTileSetSelectionView::class)
}
}

View File

@@ -1,28 +0,0 @@
package com.bartlomiejpluta.base.editor.map.view.wizard
import com.bartlomiejpluta.base.editor.asset.view.select.SelectGraphicAssetView
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapBuilderVM
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import tornadofx.View
class MapTileSetSelectionView : View("Tile Set") {
private val mapBuilderVM = find<GameMapBuilderVM>()
private val projectContext: ProjectContext by di()
private val selectGraphicAssetView = find<SelectGraphicAssetView<TileSetAsset>>(
SelectGraphicAssetView<TileSetAsset>::assets to projectContext.project!!.tileSets,
SelectGraphicAssetView<TileSetAsset>::asset to mapBuilderVM.tileSetAssetProperty
)
// FIXME
// It's kind of ugly solution because for some reason
// the custom validator on tileSetsListView does not work here.
// Desired solution should use mapBuilderVM.valid(mapBuilderVM.tileSetProperty)
// as in the previous step of the wizard as well as the feedback for user
// saying, that tile set field is required.
override val complete = mapBuilderVM.tileSetAssetProperty.isNotNull
override val root = selectGraphicAssetView.root
}

View File

@@ -6,12 +6,15 @@ import tornadofx.getValue
import tornadofx.setValue
class GameMapBuilderVM : ItemViewModel<GameMapBuilder>(GameMapBuilder()) {
val tileSetAssetProperty = bind(GameMapBuilder::tileSetAssetProperty, autocommit = true)
var tileSetAsset by tileSetAssetProperty
val nameProperty = bind(GameMapBuilder::nameProperty, autocommit = true)
var name by nameProperty
val tileWidthProperty = bind(GameMapBuilder::tileWidthProperty, autocommit = true)
var tileWidth by tileWidthProperty
val tileHeightProperty = bind(GameMapBuilder::tileHeightProperty, autocommit = true)
var tileHeight by tileHeightProperty
val rowsProperty = bind(GameMapBuilder::rowsProperty, autocommit = true)
var rows by rowsProperty

View File

@@ -13,9 +13,6 @@ class GameMapVM(map: GameMap) : ItemViewModel<GameMap>(map) {
val uidProperty = bind(GameMap::uidProperty)
val uid by uidProperty
val tileSetProperty = bind(GameMap::tileSet)
val tileSet by tileSetProperty
val rowsProperty = bind(GameMap::rowsProperty)
var rows by rowsProperty

View File

@@ -123,9 +123,14 @@ class DefaultProjectContext : ProjectContext {
}
}
override fun importMapFromFile(name: String, handler: String, file: File, tileSet: TileSet) =
override fun importMapFromFile(
name: String,
handler: String,
file: File,
replaceTileSet: (String, String) -> String
) =
project?.let { project ->
val map = file.inputStream().use { mapDeserializer.deserialize(it, tileSet) }
val map = file.inputStream().use { mapDeserializer.deserialize(it, replaceTileSet) }
UID.next(project.maps.map(Asset::uid)).let { uid ->
val asset = GameMapAsset(project, uid, name)
map.uid = uid
@@ -183,6 +188,11 @@ class DefaultProjectContext : ProjectContext {
} ?: throw IllegalStateException("There is no open project in the context")
}
override fun findTileSetAsset(uid: String) = project?.let {
it.tileSets.firstOrNull { tileSets -> tileSets.uid == uid }
?: throw IllegalStateException("The Tile Set with uid [$uid] does not exist ")
} ?: throw IllegalStateException("There is no open project in the context")
override fun importImage(data: ImageAssetData) {
project?.let {
UID.next(it.images.map(Asset::uid)).let { uid ->

View File

@@ -14,6 +14,7 @@ import com.bartlomiejpluta.base.editor.image.asset.ImageAsset
import com.bartlomiejpluta.base.editor.image.asset.ImageAssetData
import com.bartlomiejpluta.base.editor.map.model.map.GameMap
import com.bartlomiejpluta.base.editor.project.model.Project
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAssetData
import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import javafx.beans.property.ObjectProperty
@@ -29,15 +30,17 @@ interface ProjectContext {
fun createNewProject(project: Project)
fun importMap(name: String, map: GameMap)
fun importMapFromFile(name: String, handler: String, file: File, tileSet: TileSet): GameMap
fun importMapFromFile(name: String, handler: String, file: File, replaceTileSet: (String, String) -> String): GameMap
fun loadMap(uid: String): GameMap
fun saveMap(map: GameMap)
fun importTileSet(data: TileSetAssetData)
fun loadTileSet(uid: String): TileSet
fun findTileSetAsset(uid: String): TileSetAsset
fun importImage(data: ImageAssetData)
fun findImageAsset(uid: String): ImageAsset
fun loadImage(uid: String): Image
fun importCharacterSet(data: CharacterSetAssetData)
@@ -49,7 +52,6 @@ interface ProjectContext {
fun importFont(data: FontAssetData)
fun createWidget(data: WidgetAssetData): WidgetAsset
fun importSound(data: SoundAssetData)
fun deleteAsset(asset: Asset)
fun loadScript(fileNode: FileNode, execute: ((String) -> Unit)?, saveable: Boolean): Code

View File

@@ -1,6 +1,6 @@
package com.bartlomiejpluta.base.editor.render.input
import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import javafx.scene.input.MouseEvent
class MapMouseEvent(val row: Int, val column: Int, val event: MouseEvent) {
@@ -8,9 +8,9 @@ class MapMouseEvent(val row: Int, val column: Int, val event: MouseEvent) {
val button = event.button
companion object {
fun of(event: MouseEvent, tileSet: TileSet) = MapMouseEvent(
(event.y / tileSet.tileHeight).toInt(),
(event.x / tileSet.tileWidth).toInt(),
fun of(event: MouseEvent, mapVM: GameMapVM) = MapMouseEvent(
(event.y / mapVM.tileHeight).toInt(),
(event.x / mapVM.tileWidth).toInt(),
event
)
}

View File

@@ -1,38 +1,108 @@
package com.bartlomiejpluta.base.editor.tileset.canvas
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent
import com.bartlomiejpluta.base.editor.render.input.MapMouseEventHandler
import com.bartlomiejpluta.base.editor.render.model.Renderable
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import javafx.scene.canvas.GraphicsContext
import javafx.scene.image.WritableImage
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.paint.Color
import kotlin.math.roundToInt
class TileSetCanvas(private val gameMapVM: GameMapVM, private val selection: TileSetSelection) : Renderable, MapMouseEventHandler {
class TileSetCanvas(private val editorStateVM: EditorStateVM, private val selection: TileSetSelection) : Renderable,
MapMouseEventHandler {
private var mouseRow = -1
private var mouseColumn = -1
override fun render(gc: GraphicsContext) {
gc.clearRect(0.0, 0.0, gc.canvas.width, gc.canvas.height)
renderTiles(gc)
selection.render(gc)
if (editorStateVM.selectedLayer is TileLayer) {
val tileSet = (editorStateVM.selectedLayer as TileLayer).tileSetProperty.value
renderBackground(gc)
renderTiles(gc, tileSet)
renderGrid(gc, tileSet)
selection.render(gc)
}
}
private fun renderTiles(gc: GraphicsContext) {
gameMapVM.tileSet.forEach { row, column, tile ->
gc.fill = if ((row + column) % 2 == 0) BACKGROUND_COLOR1 else BACKGROUND_COLOR2
gc.fillRect(
column * tile.image.width,
row * tile.image.height,
gameMapVM.tileSet.tileWidth.toDouble(),
gameMapVM.tileSet.tileHeight.toDouble()
)
private fun renderBackground(gc: GraphicsContext) {
for (x in 0 until (gc.canvas.width / BACKGROUND_TILE_WIDTH).roundToInt()) {
for (y in 0 until (gc.canvas.height / BACKGROUND_TILE_HEIGHT).roundToInt()) {
gc.fill = when ((x + y) % 2) {
0 -> BACKGROUND_COLOR1
else -> BACKGROUND_COLOR2
}
gc.fillRect(x * BACKGROUND_TILE_WIDTH, y * BACKGROUND_TILE_HEIGHT, BACKGROUND_TILE_WIDTH, BACKGROUND_TILE_HEIGHT)
}
}
}
private fun renderTiles(gc: GraphicsContext, tileSet: TileSet) {
tileSet.forEach { row, column, tile ->
gc.drawImage(tile.image, column * tile.image.width, row * tile.image.height)
}
}
private fun renderGrid(gc: GraphicsContext, tileSet: TileSet) {
gc.stroke = Color.BLACK
gc.lineWidth = 0.5
for (x in 0 until (gc.canvas.width / tileSet.tileWidth).roundToInt()) {
gc.strokeLine(x.toDouble() * tileSet.tileWidth, 0.0, x.toDouble() * tileSet.tileWidth, gc.canvas.height)
}
for (y in 0 until (gc.canvas.height / tileSet.tileHeight).roundToInt()) {
gc.strokeLine(0.0, y.toDouble() * tileSet.tileHeight, gc.canvas.width, y.toDouble() * tileSet.tileHeight)
}
val w = gc.canvas.width - 1
val h = gc.canvas.height - 1
gc.strokeLine(0.0, 0.0, w, 0.0)
gc.strokeLine(w, 0.0, w, h)
gc.strokeLine(0.0, h, w, h)
gc.strokeLine(0.0, 0.0, 0.0, h)
}
// private fun createGrid(gc: GraphicsContext, chunkWidth: Int, chunkHeight: Int): WritableImage {
// val grid = WritableImage(gc.canvas.width.toInt(), gc.canvas.height.toInt())
//
// val writer = grid.pixelWriter
// val color = Color.BLACK
// for (x in 0 until grid.width.toInt()) {
// for (y in 0 until grid.height.toInt()) {
// if (x % chunkWidth == 0) {
// writer.setColor(x, y, color)
// }
//
// if (y % chunkHeight == 0) {
// writer.setColor(x, y, color)
// }
// }
// }
//
// val lastX = grid.width.toInt() - 1
// val lastY = grid.height.toInt() - 1
//
// for (x in 0 until grid.width.toInt()) {
// writer.setColor(x, 0, color)
// writer.setColor(x, lastY, color)
// }
//
// for (y in 0 until grid.height.toInt()) {
// writer.setColor(0, y, color)
// writer.setColor(lastX, y, color)
// }
//
// return grid
// }
override fun handleMouseInput(event: MapMouseEvent) {
mouseRow = event.row
mouseColumn = event.column
@@ -63,7 +133,9 @@ class TileSetCanvas(private val gameMapVM: GameMapVM, private val selection: Til
}
companion object {
private const val BACKGROUND_TILE_WIDTH = 5.0
private const val BACKGROUND_TILE_HEIGHT = 5.0
private val BACKGROUND_COLOR1 = Color.color(1.0, 1.0, 1.0, 1.0)
private val BACKGROUND_COLOR2 = Color.color(0.95, 0.95, 0.95, 0.95)
private val BACKGROUND_COLOR2 = Color.color(0.95, 0.95, 0.95, 1.0)
}
}

View File

@@ -1,8 +1,10 @@
package com.bartlomiejpluta.base.editor.tileset.canvas
import com.bartlomiejpluta.base.editor.map.model.brush.Brush
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.render.model.Renderable
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import javafx.scene.canvas.GraphicsContext
import javafx.scene.paint.Color
@@ -10,15 +12,15 @@ import kotlin.math.abs
import kotlin.math.min
class TileSetSelection(private val gameMapVM: GameMapVM, private val brushVM: BrushVM) : Renderable {
class TileSetSelection(private val editorStateVM: EditorStateVM, private val gameMapVM: GameMapVM, private val brushVM: BrushVM) : Renderable {
private var startRow = 0.0
private var startColumn = 0.0
private var offsetRow = 0.0
private var offsetColumn = 0.0
private var x = 0.0
private var y = 0.0
private var width = gameMapVM.tileSet.tileWidth.toDouble()
private var height = gameMapVM.tileSet.tileHeight.toDouble()
private var width = gameMapVM.tileWidth.toDouble()
private var height = gameMapVM.tileHeight.toDouble()
fun begin(row: Double, column: Double) {
@@ -31,10 +33,10 @@ class TileSetSelection(private val gameMapVM: GameMapVM, private val brushVM: Br
}
private fun updateRect(row: Double, column: Double) {
x = min(column, startColumn) * gameMapVM.tileSet.tileWidth
y = min(row, startRow) * gameMapVM.tileSet.tileHeight
width = (offsetColumn + 1) * gameMapVM.tileSet.tileWidth
height = (offsetRow + 1) * gameMapVM.tileSet.tileHeight
x = min(column, startColumn) * gameMapVM.tileWidth
y = min(row, startRow) * gameMapVM.tileHeight
width = (offsetColumn + 1) * gameMapVM.tileWidth
height = (offsetRow + 1) * gameMapVM.tileHeight
}
fun proceed(row: Double, column: Double) {
@@ -55,14 +57,17 @@ class TileSetSelection(private val gameMapVM: GameMapVM, private val brushVM: Br
val rows = offsetRow.toInt() + 1
val columns = offsetColumn.toInt() + 1
val brushArray = Array(rows) { rowIndex ->
Array(columns) { columnIndex ->
gameMapVM.tileSet.getTile(firstRow + rowIndex, firstColumn + columnIndex)
if (editorStateVM.selectedLayer is TileLayer) {
val tileSet = (editorStateVM.selectedLayer as TileLayer).tileSetProperty.value
val brushArray = Array(rows) { rowIndex ->
Array(columns) { columnIndex ->
tileSet.getTile(firstRow + rowIndex, firstColumn + columnIndex)
}
}
}
brushVM.item = Brush.of(brushArray)
brushVM.commit()
brushVM.item = Brush.of(brushArray)
brushVM.commit()
}
}
override fun render(gc: GraphicsContext) {

View File

@@ -1,17 +1,22 @@
package com.bartlomiejpluta.base.editor.tileset.component
import com.bartlomiejpluta.base.editor.map.model.layer.Layer
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent
import com.bartlomiejpluta.base.editor.tileset.canvas.TileSetCanvas
import com.bartlomiejpluta.base.editor.tileset.canvas.TileSetSelection
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import javafx.event.EventHandler
import javafx.scene.canvas.Canvas
import javafx.scene.input.MouseEvent
import tornadofx.doubleBinding
class TileSetPane(private val gameMapVM: GameMapVM, brushVM: BrushVM) : Canvas(), EventHandler<MouseEvent> {
private val selection = TileSetSelection(gameMapVM, brushVM)
private val tileSetCanvas = TileSetCanvas(gameMapVM, selection)
class TileSetPane(editorStateVM: EditorStateVM, private val gameMapVM: GameMapVM, brushVM: BrushVM) : Canvas(),
EventHandler<MouseEvent> {
private val selection = TileSetSelection(editorStateVM, gameMapVM, brushVM)
private val tileSetCanvas = TileSetCanvas(editorStateVM, selection)
init {
onMouseMoved = this
@@ -19,22 +24,37 @@ class TileSetPane(private val gameMapVM: GameMapVM, brushVM: BrushVM) : Canvas()
onMousePressed = this
onMouseReleased = this
width = gameMapVM.tileSet.width.toDouble()
height = gameMapVM.tileSet.height.toDouble()
updateSize(editorStateVM.selectedLayer)
editorStateVM.selectedLayerProperty.addListener { _, _, layer ->
updateSize(layer)
}
brushVM.itemProperty.addListener { _, _, _ -> render() }
render()
}
private fun updateSize(layer: Layer?) {
if (layer is TileLayer) {
val tileSet = layer.tileSetProperty.value
width = tileSet.width.toDouble()
height = tileSet.height.toDouble()
} else {
width = 0.0
height = 0.0
}
render()
}
private fun render() {
tileSetCanvas.render(graphicsContext2D)
}
override fun handle(event: MouseEvent?) {
if (event != null) {
tileSetCanvas.handleMouseInput(MapMouseEvent.of(event, gameMapVM.tileSet))
tileSetCanvas.handleMouseInput(MapMouseEvent.of(event, gameMapVM))
}
tileSetCanvas.render(graphicsContext2D)

View File

@@ -1,6 +1,7 @@
package com.bartlomiejpluta.base.editor.tileset.view.editor
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.tileset.component.TileSetPane
import tornadofx.View
@@ -10,8 +11,9 @@ import tornadofx.scrollpane
class TileSetView : View() {
private val gameMapVM = find<GameMapVM>()
private val brushVM = find<BrushVM>()
private val editorStateVM = find<EditorStateVM>()
private val tileSetPane = TileSetPane(gameMapVM, brushVM)
private val tileSetPane = TileSetPane(editorStateVM, gameMapVM, brushVM)
override val root = scrollpane {
this += tileSetPane