diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt new file mode 100644 index 0000000..f703dca --- /dev/null +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt @@ -0,0 +1,318 @@ +package korlibs.korge.fleks.assets + +import korlibs.image.tiles.* +import korlibs.korge.fleks.utils.* +import korlibs.korge.ldtk.* +import korlibs.korge.ldtk.view.* +import korlibs.math.* +import korlibs.memory.* +import kotlinx.serialization.* +import kotlin.math.* + +class AssetLevelData { + + internal val worldData = WorldData() + // TODO: move into WorldData + internal val levelDataMaps: MutableMap = mutableMapOf() + internal val configDeserializer = EntityConfigSerializer() + + private var gameObjectCnt = 0 + + private var gridVaniaWidth: Int = 0 + private var gridVaniaHeight: Int = 0 + + /** + * @param hasParallax - This level uses a parallax background, so we need to set the world size accordingly. + * TODO check if we need this also later - do we have only one active world level? + */ + fun loadLevelData(ldtkWorld: LDTKWorld, type: AssetType, hasParallax: Boolean) { + gridVaniaWidth = ldtkWorld.ldtk.worldGridWidth ?: 1 // this is also the size of each sub-level + gridVaniaHeight = ldtkWorld.ldtk.worldGridHeight ?: 1 + + // Get the highest values for X and Y axis - this will be the size of the grid vania array + var maxLevelOffsetX = 0 + var maxLevelOffsetY = 0 + ldtkWorld.ldtk.levels.forEach { ldtkLevel -> + if (maxLevelOffsetX < ldtkLevel.worldX) maxLevelOffsetX = ldtkLevel.worldX + if (maxLevelOffsetY < ldtkLevel.worldY) maxLevelOffsetY = ldtkLevel.worldY + } + + // Create grid vania array + val sizeX: Int = (maxLevelOffsetX / gridVaniaWidth) + 1 + val sizeY: Int = (maxLevelOffsetY / gridVaniaHeight) + 1 + worldData.levelGridVania = List(sizeX) { List(sizeY) { LevelData() } } + + // Set the size of the world + worldData.width = (sizeX * gridVaniaWidth).toFloat() + worldData.height = (sizeY * gridVaniaHeight).toFloat() + + if (maxLevelOffsetX == 0) println("WARNING: Level width is 0!") + + // Save TileMapData for each Level and layer combination from LDtk world + ldtkWorld.ldtk.levels.forEach { ldtkLevel -> + val globalLevelPosX = ldtkLevel.worldX + val globalLevelPosY = ldtkLevel.worldY + val levelX: Int = globalLevelPosX / gridVaniaWidth + val levelY: Int = globalLevelPosY / gridVaniaHeight + + loadLevel(worldData.levelGridVania[levelX][levelY], ldtkWorld, ldtkLevel, type) + } + println("Gridvania size: $sizeX x $sizeY)") + } + + fun reloadAsset(ldtkWorld: LDTKWorld, type: AssetType) { + // Reload all levels from ldtk world file + ldtkWorld.ldtk.levels.forEach { ldtkLevel -> + ldtkLevel.layerInstances?.forEach { ldtkLayer -> + val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] + + if (tilesetExt != null) { + // Get index of level in the worldData Grid vania array + val levelX: Int = ldtkLevel.worldX / gridVaniaWidth + val levelY: Int = ldtkLevel.worldY / gridVaniaHeight + + storeTiles(worldData.levelGridVania[levelX][levelY], ldtkLayer, tilesetExt, ldtkLevel.identifier, ldtkLayer.identifier, type) + println("\nTriggering asset change for LDtk level : ${ldtkLevel.identifier}_${ldtkLayer.identifier}") + } + } + } + } + + fun removeAssets(type: AssetType) { + levelDataMaps.values.removeAll { it.type == type } + } + + // TODO: remove "level" + private fun storeTiles(levelData: LevelData, ldtkLayer: LayerInstance, tilesetExt: ExtTileset, level: String, layer: String, type: AssetType) { + val tileMapData = TileMapData( + width = ldtkLayer.cWid, + height = ldtkLayer.cHei, + tileSet = if (tilesetExt.tileset != null) tilesetExt.tileset!! else TileSet.EMPTY + ) + val gridSize = tilesetExt.def.tileGridSize + val tilesetWidth = tilesetExt.def.pxWid + val cellsTilesPerRow = tilesetWidth / gridSize + + for (tile in ldtkLayer.autoLayerTiles + ldtkLayer.gridTiles) { + val (px, py) = tile.px + val x = px / gridSize + val y = py / gridSize + val (tileX, tileY) = tile.src + val dx = px % gridSize + val dy = py % gridSize + val tx = tileX / gridSize + val ty = tileY / gridSize + val tileId = ty * cellsTilesPerRow + tx + val flipX = tile.f.hasBitSet(0) + val flipY = tile.f.hasBitSet(1) + + // Get stack level depending on if the tile overlaps its neighbour cells i.e. the tile has an offset (dx, dy) + when { + (dx == 0 && dy == 0) -> { + val stackLevel = tileMapData.data.getStackLevel(x, y) + tileMapData.data.set( + x, + y, + stackLevel, + value = Tile( + tile = tileId, + offsetX = dx, + offsetY = dy, + flipX = flipX, + flipY = flipY, + rotate = false + ).raw + ) + } + + (dx == 0 && dy != 0) -> { + val stackLevel = max(tileMapData.data.getStackLevel(x, y), tileMapData.data.getStackLevel(x, y + 1)) + tileMapData.data.set( + x, + y, + stackLevel, + value = Tile( + tile = tileId, + offsetX = dx, + offsetY = dy, + flipX = flipX, + flipY = flipY, + rotate = false + ).raw + ) + tileMapData.data.set(x, y + 1, stackLevel, value = Tile.ZERO.raw) + } + + (dx != 0 && dy == 0) -> { + val stackLevel = max(tileMapData.data.getStackLevel(x, y), tileMapData.data.getStackLevel(x + 1, y)) + tileMapData.data.set( + x, + y, + stackLevel, + value = Tile( + tile = tileId, + offsetX = dx, + offsetY = dy, + flipX = flipX, + flipY = flipY, + rotate = false + ).raw + ) + tileMapData.data.set(x + 1, y, stackLevel, value = Tile.ZERO.raw) + } + + else -> { + val stackLevel = max( + tileMapData.data.getStackLevel(x, y), + tileMapData.data.getStackLevel(x, y + 1), + tileMapData.data.getStackLevel(x + 1, y), + tileMapData.data.getStackLevel(x + 1, y + 1) + ) + tileMapData.data.set( + x, + y, + stackLevel, + value = Tile( + tile = tileId, + offsetX = dx, + offsetY = dy, + flipX = flipX, + flipY = flipY, + rotate = false + ).raw + ) + tileMapData.data.set(x, y + 1, stackLevel, value = Tile.ZERO.raw) + tileMapData.data.set(x + 1, y, stackLevel, value = Tile.ZERO.raw) + tileMapData.data.set(x + 1, y + 1, stackLevel, value = Tile.ZERO.raw) + } + } + } + levelData.layerTileMaps[layer] = tileMapData + + // TODO: remove below lines + // Create new map for level layers and store layer in it + val layerTileMaps = mutableMapOf() + layerTileMaps[layer] = tileMapData + // Add layer map to level Maps + if (!levelDataMaps.contains(level)) { + val levelData = LevelData( + type = type, + gridSize = gridSize, + width = (ldtkLayer.cWid * gridSize).toFloat(), + height = (ldtkLayer.cHei * gridSize).toFloat(), + layerTileMaps = layerTileMaps + ) + levelDataMaps[level] = levelData + } else { + levelDataMaps[level]!!.layerTileMaps[layer] = tileMapData + } + } + + private fun loadLevel(levelData: LevelData, ldtkWorld: LDTKWorld, ldtkLevel: Level, type: AssetType) { + val levelName = ldtkLevel.identifier + + ldtkLevel.layerInstances?.forEach { ldtkLayer -> + val layerName = ldtkLayer.identifier + val gridSize = ldtkLayer.gridSize + + // Check if layer contains entity data -> create EntityConfigs and store them fo + if (ldtkLayer.entityInstances.isNotEmpty()) { + ldtkLayer.entityInstances.forEach { entity -> + // Create YAML string of an entity config from LDtk + val yamlString = StringBuilder() + // Sanity check - entity needs to have a field 'entityConfig' + if (entity.fieldInstances.firstOrNull { it.identifier == "entityConfig" } != null) { + + if (entity.tags.firstOrNull { it == "unique" } != null) { + // Add scripts without unique count value - they are unique by name because they exist only once + yamlString.append("name: ${levelName}_${entity.identifier}\n") + } + else { + // Add other game objects with a unique name as identifier + yamlString.append("name: ${levelName}_${entity.identifier}_${gameObjectCnt++}\n") + } + + // Add position of entity = (level position in the world) + (grid position within the level) + (pivot point) + // TODO: Take level position in world into account + val entityPosX: Int = /*levelPosX +*/ (entity.gridPos.x * gridSize) + (entity.pivot[0] * gridSize).toInt() + val entityPosY: Int = /*levelPosY +*/ (entity.gridPos.y * gridSize) + (entity.pivot[1] * gridSize).toInt() + + + // Add position of entity + entity.tags.firstOrNull { it == "positionable" }?.let { + yamlString.append("x: $entityPosX\n") + yamlString.append("y: $entityPosY\n") + } + + // Add all other fields of entity + entity.fieldInstances.forEach { field -> + if (field.identifier != "EntityConfig") yamlString.append("${field.identifier}: ${field.value}\n") + } + println("INFO: Game object '${entity.identifier}' loaded for '$levelName'") + println("\n$yamlString") + + try { + // By deserializing the YAML string we get an EntityConfig object which itself registers in the EntityFactory + val entityConfig: EntityConfig = configDeserializer.yaml().decodeFromString(yamlString.toString()) + + // TODO: We need to store only the name of the entity config for later dynamically spawning of entities + // We need to store the entity configs in a 2D array depending on its position in the level + // Then later we will spawn the entities depending on the position in the level + levelData.entities.add(entityConfig.name) + + println("INFO: Registering entity config '${entity.identifier}' for '$levelName'") + } catch (e: Throwable) { + println("ERROR: Loading entity config - $e") + } + + } else println("ERROR: Game object with name '${entity.identifier}' has no field entityConfig!") + } + + // + levelData.width = (ldtkLayer.cWid * gridSize).toFloat() + levelData.height = (ldtkLayer.cHei * gridSize).toFloat() + + // Create new level data if it does not exist yet + if (!levelDataMaps.contains(levelName)) { + levelDataMaps[levelName] = levelData + } else { + levelDataMaps[levelName]!!.entities + } + } + + // Check if layer has tile set -> store tile map data + val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] + if (tilesetExt != null) { + storeTiles(levelData, ldtkLayer, tilesetExt, levelName, layerName, type) + } + } + } + + /** + * + * @param tileSize - Size of a grid cell in pixels (e.g. 16 for 16x16 tile size) + * @param width - Width of whole level in pixels + * @param height - Height of whole level in pixels + */ + data class WorldData( + var tileSize: Int = 16, + + var width: Float = 0f, + var height: Float = 0f, + var levelWidth: Int = 0, // TODO: Check if we need it outside of this class - level renderer + var levelHeight: Int = 0, + + var levelGridVania: List> = listOf() + ) + + // Data class for storing level data like grizSize, width, height, entities, tileMapData + data class LevelData( + var type: AssetType = AssetType.COMMON, // TODO: Remove + val gridSize: Int = 16, // TODO: Remove + var width: Float = 0f, + var height: Float = 0f, + + val entities: MutableList = mutableListOf(), + val layerTileMaps: MutableMap = mutableMapOf() + ) +} diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt index 52ecd67..78dcf6a 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt @@ -3,12 +3,10 @@ package korlibs.korge.fleks.assets import korlibs.datastructure.* import korlibs.image.font.* import korlibs.image.format.* -import korlibs.image.tiles.* import korlibs.io.async.launchImmediately import korlibs.io.file.* import korlibs.io.file.std.resourcesVfs import korlibs.korge.ldtk.view.* -import korlibs.memory.* import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.native.concurrent.* @@ -191,7 +189,7 @@ class ResourceDirWatcherConfiguration( delay(500) val ldtkWorld = resourcesVfs[assetConfig.folder + "/" + config.fileName].readLDTKWorld(extrude = true) - assetStore.levelMapAssets.reloadAsset(ldtkWorld, assetUpdater.type) + assetStore.assetLevelData.reloadAsset(ldtkWorld, assetUpdater.type) // Guard period until reloading is activated again - this is used for debouncing watch messages delay(100) diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt index a2d2f97..7e385fd 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt @@ -10,12 +10,8 @@ import korlibs.image.font.readBitmapFont import korlibs.image.format.* import korlibs.image.tiles.* import korlibs.io.file.std.resourcesVfs -import korlibs.korge.fleks.utils.* -import korlibs.korge.ldtk.* import korlibs.korge.ldtk.view.* -import korlibs.memory.* import korlibs.time.Stopwatch -import kotlinx.serialization.* import kotlin.collections.set @@ -42,7 +38,7 @@ class AssetStore { internal var currentLevelAssetConfig: AssetModel = AssetModel() internal var specialAssetConfig: AssetModel = AssetModel() - internal val levelMapAssets: LevelMapAssets = LevelMapAssets() + internal val assetLevelData: AssetLevelData = AssetLevelData() // TODO: MutableMap> = mutableMapOf() internal var backgrounds: MutableMap> = mutableMapOf() internal var images: MutableMap> = mutableMapOf() internal var fonts: MutableMap> = mutableMapOf() @@ -70,32 +66,35 @@ class AssetStore { } fun getTileMapData(level: String, layer: String) : TileMapData = - if (levelMapAssets.levelDataMaps.contains(level)) { - if (levelMapAssets.levelDataMaps[level]!!.layerTileMaps.contains(layer)) levelMapAssets.levelDataMaps[level]!!.layerTileMaps[layer]!! + if (assetLevelData.levelDataMaps.contains(level)) { + if (assetLevelData.levelDataMaps[level]!!.layerTileMaps.contains(layer)) assetLevelData.levelDataMaps[level]!!.layerTileMaps[layer]!! else error("AssetStore: TileMap layer '$layer' for level '$level' not found!") } else error("AssetStore: Level map for level '$level' not found!") fun getEntities(level: String) : List = - if (levelMapAssets.levelDataMaps.contains(level)) { - levelMapAssets.levelDataMaps[level]!!.entities + if (assetLevelData.levelDataMaps.contains(level)) { + assetLevelData.levelDataMaps[level]!!.entities } else error("AssetStore: Entities for level '$level' not found!") fun getLevelHeight(level: String) : Float = - if (levelMapAssets.levelDataMaps.contains(level)) { - levelMapAssets.levelDataMaps[level]!!.height + if (assetLevelData.levelDataMaps.contains(level)) { + assetLevelData.levelDataMaps[level]!!.height } else error("AssetStore: Height for level '$level' not found!") fun getLevelWidth(level: String) : Float = - if (levelMapAssets.levelDataMaps.contains(level)) { - levelMapAssets.levelDataMaps[level]!!.width + if (assetLevelData.levelDataMaps.contains(level)) { + assetLevelData.levelDataMaps[level]!!.width } else error("AssetStore: Width for level '$level' not found!") - fun getWorldHeight() : Float = levelMapAssets.worldData.height.toFloat() - fun getWorldWidth(): Float = levelMapAssets.worldData.width.toFloat() + // TODO: to be removed + fun getWorldHeight() : Float = assetLevelData.worldData.height.toFloat() + fun getWorldWidth(): Float = assetLevelData.worldData.width.toFloat() + + fun getWorldData(name: String) : AssetLevelData.WorldData = assetLevelData.worldData fun getNinePatch(name: String) : NinePatchBmpSlice = if (images.contains(name)) { @@ -156,7 +155,7 @@ class AssetStore { // Update maps of music, images, ... assetConfig.tileMaps.forEach { tileMap -> val ldtkWorld = resourcesVfs[assetConfig.folder + "/" + tileMap.fileName].readLDTKWorld(extrude = true) - levelMapAssets.loadLevelData(ldtkWorld, type, tileMap.hasParallax) + assetLevelData.loadLevelData(ldtkWorld, type, tileMap.hasParallax) } assetConfig.sounds.forEach { sound -> @@ -230,6 +229,6 @@ class AssetStore { images.values.removeAll { it.first == type } fonts.values.removeAll { it.first == type } sounds.values.removeAll { it.first == type } - levelMapAssets.removeAssets(type) + assetLevelData.removeAssets(type) } } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/LevelMapAssets.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/LevelMapAssets.kt deleted file mode 100644 index 7789f00..0000000 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/LevelMapAssets.kt +++ /dev/null @@ -1,226 +0,0 @@ -package korlibs.korge.fleks.assets - -import korlibs.image.tiles.* -import korlibs.korge.fleks.utils.* -import korlibs.korge.ldtk.* -import korlibs.korge.ldtk.view.* -import korlibs.math.* -import korlibs.memory.* -import kotlinx.serialization.* -import kotlin.math.* - -class LevelMapAssets { - - internal val worldData = WorldData() - // TODO: move into WorldData - internal val levelDataMaps: MutableMap = mutableMapOf() - internal val configDeserializer = EntityConfigSerializer() - - private var gameObjectCnt = 0 - - fun loadLevelData(ldtkWorld: LDTKWorld, type: AssetType, hasParallax: Boolean) { - createWorldDataMap(ldtkWorld, hasParallax) - - // Save TileMapData for each Level and layer combination from LDtk world - ldtkWorld.ldtk.levels.forEach { ldtkLevel -> - - val levelPosX = ldtkLevel.worldX - val levelPosY = ldtkLevel.worldY - - val levelName = ldtkLevel.identifier - ldtkLevel.layerInstances?.forEach { ldtkLayer -> - val layerName = ldtkLayer.identifier - val gridSize = ldtkLayer.gridSize - - // Check if layer has tile set -> store tile map data - val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] - if (tilesetExt != null) { - storeTiles(ldtkLayer, tilesetExt, levelName, layerName, type) - } - // Check if layer contains entity data -> create EntityConfigs and store them fo - if (ldtkLayer.entityInstances.isNotEmpty()) { - val entityNames = mutableListOf() - - ldtkLayer.entityInstances.forEach { entity -> - // Create YAML string of an entity config from LDtk - val yamlString = StringBuilder() - // Sanity check - entity needs to have a field 'entityConfig' - if (entity.fieldInstances.firstOrNull { it.identifier == "entityConfig" } != null) { - - if (entity.tags.firstOrNull { it == "unique" } != null) { - // Add scripts without unique count value - they are unique by name because they exist only once - yamlString.append("name: ${levelName}_${entity.identifier}\n") - } - else { - // Add other game objects with a unique name as identifier - yamlString.append("name: ${levelName}_${entity.identifier}_${gameObjectCnt++}\n") - } - - // Add position of entity = (level position in the world) + (grid position within the level) + (pivot point) - // TODO: Take level position in world into account - val entityPosX: Int = /*levelPosX +*/ (entity.gridPos.x * gridSize) + (entity.pivot[0] * gridSize).toInt() - val entityPosY: Int = /*levelPosY +*/ (entity.gridPos.y * gridSize) + (entity.pivot[1] * gridSize).toInt() - - - // Add position of entity - entity.tags.firstOrNull { it == "positionable" }?.let { - yamlString.append("x: $entityPosX\n") - yamlString.append("y: $entityPosY\n") - } - - // Add all other fields of entity - entity.fieldInstances.forEach { field -> - if (field.identifier != "EntityConfig") yamlString.append("${field.identifier}: ${field.value}\n") - } - println("INFO: Game object '${entity.identifier}' loaded for '$levelName'") - println("\n$yamlString") - - try { - // By deserializing the YAML string we get an EntityConfig object which itself registers in the EntityFactory - val entityConfig: EntityConfig = configDeserializer.yaml().decodeFromString(yamlString.toString()) - - // TODO: We need to store only the name of the entity config for later dynamically spawning of entities - // We need to store the entity configs in a 2D array depending on its position in the level - // Then later we will spawn the entities depending on the position in the level - entityNames.add(entityConfig.name) - - println("INFO: Registering entity config '${entity.identifier}' for '$levelName'") - } catch (e: Throwable) { - println("ERROR: Loading entity config - $e") - } - - } else println("ERROR: Game object with name '${entity.identifier}' has no field entityConfig!") - } - - // Create new level data if it does not exist yet - if (!levelDataMaps.contains(levelName)) { - levelDataMaps[levelName] = LevelData( - type = type, - gridSize = gridSize, - width = (ldtkLayer.cWid * gridSize).toFloat(), - height = (ldtkLayer.cHei * gridSize).toFloat(), - entities = entityNames, - layerTileMaps = mutableMapOf() - ) - } else { - levelDataMaps[levelName]!!.entities - } - } - } - } - } - - fun reloadAsset(ldtkWorld: LDTKWorld, type: AssetType) { - // Reload all levels from ldtk world file - ldtkWorld.ldtk.levels.forEach { ldtkLevel -> - ldtkLevel.layerInstances?.forEach { ldtkLayer -> - val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] - - if (tilesetExt != null) { - storeTiles(ldtkLayer, tilesetExt, ldtkLevel.identifier, ldtkLayer.identifier, type) - println("\nTriggering asset change for LDtk level : ${ldtkLevel.identifier}_${ldtkLayer.identifier}") - } - } - } - } - - fun removeAssets(type: AssetType) { - levelDataMaps.values.removeAll { it.type == type } - } - - private fun storeTiles(ldtkLayer: LayerInstance, tilesetExt: ExtTileset, level: String, layer: String, type: AssetType) { - val tileMapData = TileMapData( - width = ldtkLayer.cWid, - height = ldtkLayer.cHei, - tileSet = if(tilesetExt.tileset != null) tilesetExt.tileset!! else TileSet.EMPTY - ) - val gridSize = tilesetExt.def.tileGridSize - val tilesetWidth = tilesetExt.def.pxWid - val cellsTilesPerRow = tilesetWidth / gridSize - - for (tile in ldtkLayer.autoLayerTiles + ldtkLayer.gridTiles) { - val (px, py) = tile.px - val x = px / gridSize - val y = py / gridSize - val (tileX, tileY) = tile.src - val dx = px % gridSize - val dy = py % gridSize - val tx = tileX / gridSize - val ty = tileY / gridSize - val tileId = ty * cellsTilesPerRow + tx - val flipX = tile.f.hasBitSet(0) - val flipY = tile.f.hasBitSet(1) - - // Get stack level depending on if the tile overlaps its neighbour cells i.e. the tile has an offset (dx, dy) - when { - (dx == 0 && dy == 0) -> { - val stackLevel = tileMapData.data.getStackLevel(x, y) - tileMapData.data.set(x, y, stackLevel, value = Tile(tile = tileId, offsetX = dx, offsetY = dy, flipX = flipX, flipY = flipY, rotate = false).raw) - } - (dx == 0 && dy != 0) -> { - val stackLevel = max(tileMapData.data.getStackLevel(x, y), tileMapData.data.getStackLevel(x, y + 1)) - tileMapData.data.set(x, y, stackLevel, value = Tile(tile = tileId, offsetX = dx, offsetY = dy, flipX = flipX, flipY = flipY, rotate = false).raw) - tileMapData.data.set(x, y + 1, stackLevel, value = Tile.ZERO.raw) - } - (dx != 0 && dy == 0) -> { - val stackLevel = max(tileMapData.data.getStackLevel(x, y), tileMapData.data.getStackLevel(x + 1, y)) - tileMapData.data.set(x, y, stackLevel, value = Tile(tile = tileId, offsetX = dx, offsetY = dy, flipX = flipX, flipY = flipY, rotate = false).raw) - tileMapData.data.set(x + 1, y, stackLevel, value = Tile.ZERO.raw) - } - else -> { - val stackLevel = max(tileMapData.data.getStackLevel(x, y), tileMapData.data.getStackLevel(x, y + 1), tileMapData.data.getStackLevel(x + 1, y), tileMapData.data.getStackLevel(x + 1, y + 1)) - tileMapData.data.set(x, y, stackLevel, value = Tile(tile = tileId, offsetX = dx, offsetY = dy, flipX = flipX, flipY = flipY, rotate = false).raw) - tileMapData.data.set(x, y + 1, stackLevel, value = Tile.ZERO.raw) - tileMapData.data.set(x + 1, y, stackLevel, value = Tile.ZERO.raw) - tileMapData.data.set(x + 1, y + 1, stackLevel, value = Tile.ZERO.raw) - } - } - } - // Create new map for level layers and store layer in it - val layerTileMaps = mutableMapOf() - layerTileMaps[layer] = tileMapData - // Add layer map to level Maps - if (!levelDataMaps.contains(level)) { - val levelData = LevelData( - type = type, - gridSize = gridSize, - width = (ldtkLayer.cWid * gridSize).toFloat(), - height = (ldtkLayer.cHei * gridSize).toFloat(), - entities = listOf(), - layerTileMaps = layerTileMaps - ) - levelDataMaps[level] = levelData - } else { - levelDataMaps[level]!!.layerTileMaps[layer] = tileMapData - } - } - - private fun createWorldDataMap(ldtkWorld: LDTKWorld, hasParallax: Boolean) { - - if (hasParallax) { - // TODO: Get world height and width from WorldGridvania 2D array - // * 2 because level 1 has still double height - worldData.width = 2 * (ldtkWorld.ldtk.worldGridWidth ?: 1) // gridVaniaWidth - worldData.height = 2 * (ldtkWorld.ldtk.worldGridHeight ?: 1) // gridVaniaHeight - } - - // TODO: fill WorldData class with levels - - } - - data class WorldData( - var width: Int = 0, - var height: Int = 0, - val levelGridVania: MutableList> = mutableListOf() - ) - - // Data class for storing level data like grizSize, width, height, entities, tileMapData - data class LevelData( - val type: AssetType, - val gridSize: Int, - val width: Float, - val height: Float, - val entities: List, - val layerTileMaps: MutableMap - ) -} diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt index c06af11..897719a 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt @@ -23,9 +23,9 @@ data class ParallaxEffectConfig( override fun World.entityConfigure(entity: Entity) : Entity { entity.configure { it += PositionComponent( - x = this@ParallaxEffectConfig.x, + x = this@ParallaxEffectConfig.x, // global position in screen coordinates y = this@ParallaxEffectConfig.y - ) // global position for the whole parallax background + ) it += MotionComponent( velocityX = -12f // world units (16 pixels) per second (??? TODO: this needs to be ckecked) ) diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt index dff5d48..7bcdd19 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt @@ -5,7 +5,6 @@ import korlibs.io.file.std.* import korlibs.korge.fleks.assets.* import korlibs.korge.fleks.entity.EntityFactory import korlibs.korge.fleks.utils.* -import korlibs.korge.ldtk.* import kotlinx.serialization.* import kotlinx.serialization.modules.* @@ -23,7 +22,7 @@ object GameStateManager { * Register a new serializers module for the entity config serializer. */ fun register(name: String, module: SerializersModule) { - assetStore.levelMapAssets.configDeserializer.register(name, module) + assetStore.assetLevelData.configDeserializer.register(name, module) } /** @@ -38,7 +37,7 @@ object GameStateManager { val gameStateConfigString = gameStateConfigVfs.readString() try { - gameStateConfig = assetStore.levelMapAssets.configDeserializer.yaml().decodeFromString(gameStateConfigString) + gameStateConfig = assetStore.assetLevelData.configDeserializer.yaml().decodeFromString(gameStateConfigString) } catch (e: Throwable) { gameStateConfig = GameStateConfig("", true, "", "", "") println("ERROR: Loading game state config - $e") @@ -55,7 +54,7 @@ object GameStateManager { val gameConfigString = vfs.readString() try { - val commonConfig = assetStore.levelMapAssets.configDeserializer.yaml().decodeFromString(gameConfigString) + val commonConfig = assetStore.assetLevelData.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.COMMON, assetConfig = commonConfig) } catch (e: Throwable) { println("ERROR: Loading common assets - $e") } @@ -81,7 +80,7 @@ object GameStateManager { var gameConfigString = worldVfs.readString() try { - val worldConfig = assetStore.levelMapAssets.configDeserializer.yaml().decodeFromString(gameConfigString) + val worldConfig = assetStore.assetLevelData.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.WORLD, assetConfig = worldConfig) } catch (e: Throwable) { @@ -94,7 +93,7 @@ object GameStateManager { // if (levelVfs.exists()) { gameConfigString = levelVfs.readString() try { - val worldConfig = assetStore.levelMapAssets.configDeserializer.yaml().decodeFromString(gameConfigString) + val worldConfig = assetStore.assetLevelData.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.LEVEL, assetConfig = worldConfig) } catch (e: Throwable) { @@ -108,7 +107,7 @@ object GameStateManager { // if (vfs.exists()) { gameConfigString = vfs.readString() try { - val specialConfig = assetStore.levelMapAssets.configDeserializer.yaml().decodeFromString(gameConfigString) + val specialConfig = assetStore.assetLevelData.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.SPECIAL, assetConfig = specialConfig) } catch (e: Throwable) { diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt index 9ca7aa6..27f4570 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt @@ -50,6 +50,7 @@ class LevelMapRenderSystem( layerNames.forEach { layerName -> val tileMap = assetStore.getTileMapData(levelName, layerName) + val worldData = assetStore.getWorldData(levelName) val tileSet = tileMap.tileSet val tileSetWidth = tileSet.width val tileSetHeight = tileSet.height @@ -71,7 +72,9 @@ class LevelMapRenderSystem( ctx.useBatcher { batch -> - // TODO: Render multiple levels when we approach the border between levels in the world + + // 2. Check which levels the view port of the camera is touching + // TODO // Render one level for (l in 0 until tileMap.maxLevel) { diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt index 204ae09..35aa512 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt @@ -33,10 +33,31 @@ class CameraSystem( val camera: Entity = world.getMainCamera() val cameraPosition = camera[PositionComponent] + val viewPortHalf = camera[SizeComponent] + + val lastCameraPosX = cameraPosition.x + //val lastCameraPosY = cameraPosition.y + + // Calculate the difference between the camera and the entity to follow val xDiff = followPosition.x - cameraPosition.x val yDiff = followPosition.y - cameraPosition.y - cameraPosition.x += xDiff * factor - cameraPosition.y += yDiff * factor + // Move the camera towards the entity to follow + val newCameraPositionX = cameraPosition.x + xDiff * factor + val newCameraPositionY = cameraPosition.y + yDiff * factor + + // Keep camera within world bounds + cameraPosition.x = + if (newCameraPositionX < viewPortHalf.width) viewPortHalf.width + else if (newCameraPositionX > worldWidth - viewPortHalf.width) worldWidth - viewPortHalf.width + else newCameraPositionX + cameraPosition.y = + if (newCameraPositionY < viewPortHalf.height) viewPortHalf.height + else if (newCameraPositionY > worldHeight - viewPortHalf.height) worldHeight - viewPortHalf.height + else newCameraPositionY + + // Move parallax layers if camera moves + val cameraDistX = cameraPosition.x - lastCameraPosX + //val cameraDistY = cameraPosition.y - lastCameraPosY val parallaxFamily = world.family { all(ParallaxComponent, MotionComponent) } parallaxFamily.forEach { parallaxEntity -> @@ -45,27 +66,20 @@ class CameraSystem( val viewPortHeight = camera[SizeIntComponent].height // Convert pixel distance of camera movement in the level to velocity for parallax layers - val distanceInWorldUnits = (xDiff * factor) * worldToPixelRatioInv // (distance in pixel) / (world to pixel ratio) + val distanceInWorldUnits = cameraDistX * worldToPixelRatioInv // (distance in pixel) / (world to pixel ratio) motion.velocityX = -distanceInWorldUnits / deltaTime // world units per delta-time - // TODO: Move parallax layer also vertically -// position.y = - - // We need the camera position in world coordinates - val cameraVerticalPosition = cameraPosition.y // TODO: add the position of the level in the world - val worldActiveHeight = worldHeight //- viewPortHeight ??? - val ratio = cameraVerticalPosition / worldActiveHeight // [0...1] + // Camera position is in world coordinates + val ratio = cameraPosition.y / worldHeight // range: [0...1] + // TODO: Get vertical parallax offset from parallax config + val parallaxOffset = -36f // Get the global position of the parallax layer in screen coordinates - val parallaxVerticalMax = viewPortHeight - parallaxHeight - val parallaxVerticalPosition = ratio * parallaxVerticalMax + val parallaxVerticalMax = viewPortHeight - parallaxHeight - parallaxOffset + val parallaxVerticalPosition = ratio * parallaxVerticalMax + parallaxOffset position.y = parallaxVerticalPosition // println("parallax y: $position.y") - - // parallax layer image height: 375 - - } } }