package net.torvald.terrarum.modulebasegame import com.badlogic.gdx.Gdx import com.badlogic.gdx.InputAdapter import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.TextureRegion import net.torvald.terrarum.* import net.torvald.terrarum.blockproperties.Block import net.torvald.terrarum.blockproperties.BlockCodex import net.torvald.terrarum.blockproperties.BlockPropUtil import net.torvald.terrarum.gameactors.* import net.torvald.terrarum.itemproperties.ItemID import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid import net.torvald.terrarum.modulebasegame.gameworld.GameWorldExtension import net.torvald.terrarum.modulebasegame.gameworld.WorldTime import net.torvald.terrarum.modulebasegame.ui.Notification import net.torvald.terrarum.modulebasegame.ui.UIPaletteSelector import net.torvald.terrarum.modulebasegame.weather.WeatherMixer import net.torvald.terrarum.realestate.LandUtil import net.torvald.terrarum.serialise.WriteLayerDataZip.FILE_FOOTER import net.torvald.terrarum.serialise.WriteLayerDataZip.PAYLOAD_FOOTER import net.torvald.terrarum.serialise.WriteLayerDataZip.PAYLOAD_HEADER import net.torvald.terrarum.serialise.toLittle import net.torvald.terrarum.serialise.toLittleShort import net.torvald.terrarum.serialise.toULittle48 import net.torvald.terrarum.ui.UICanvas import net.torvald.terrarum.ui.UINSMenu import net.torvald.terrarum.worlddrawer.CreateTileAtlas.TILE_SIZE import net.torvald.terrarum.worlddrawer.LightmapRenderer import net.torvald.terrarum.worlddrawer.WorldCamera import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack import java.io.File import java.io.FileOutputStream /** * Created by minjaesong on 2018-07-06. */ class BuildingMaker(batch: SpriteBatch) : IngameInstance(batch) { private val menuYaml = Yaml(""" - File - New flat ter. - New rand. ter. - Export… - Export sel… - Import… - Save terrain… - Load terrain… - - Exit to Title : net.torvald.terrarum.modulebasegame.YamlCommandExit - Tool - Pencil : net.torvald.terrarum.modulebasegame.YamlCommandToolPencil - Eraser : net.torvald.terrarum.modulebasegame.YamlCommandToolPencilErase - Wall Hammer : net.torvald.terrarum.modulebasegame.YamlCommandToolPencilEraseWall - Eyedropper : net.torvald.terrarum.modulebasegame.YamlCommandToolEyedropper - Add Selection : net.torvald.terrarum.modulebasegame.YamlCommandToolMarquee - Remove Sel. : net.torvald.terrarum.modulebasegame.YamlCommandToolMarqueeErase - Clear Sel. : net.torvald.terrarum.modulebasegame.YamlCommandToolMarqueeClear - Move Selected - - Hide/Show Sel. : net.torvald.terrarum.modulebasegame.YamlCommandToolToggleMarqueeOverlay - - Undo - Redo - Time - Morning : net.torvald.terrarum.modulebasegame.YamlCommandSetTimeMorning - Noon : net.torvald.terrarum.modulebasegame.YamlCommandSetTimeNoon - Dusk : net.torvald.terrarum.modulebasegame.YamlCommandSetTimeDusk - Night : net.torvald.terrarum.modulebasegame.YamlCommandSetTimeNight - Set… - Weather - Sunny - Raining """.trimIndent()) private val timeNow = System.currentTimeMillis() / 1000 val gameWorld = GameWorldExtension(1, 1024, 256, timeNow, timeNow, 0) init { // ghetto world for building println("[BuildingMaker] Generating builder world...") for (y in 0 until gameWorld.height) { gameWorld.setTileWall(0, y, Block.ILLUMINATOR_RED) gameWorld.setTileWall(gameWorld.width - 1, y, Block.ILLUMINATOR_RED) gameWorld.setTileTerrain(0, y, Block.ILLUMINATOR_RED_OFF) gameWorld.setTileTerrain(gameWorld.width - 1, y, Block.ILLUMINATOR_RED_OFF) } for (y in 150 until gameWorld.height) { for (x in 1 until gameWorld.width - 1) { // wall layer gameWorld.setTileWall(x, y, Block.DIRT) // terrain layer gameWorld.setTileTerrain(x, y, if (y == 150) Block.GRASS else Block.DIRT) } } // set time to summer gameWorld.time.addTime(WorldTime.DAY_LENGTH * 32) world = gameWorld } override var actorNowPlaying: ActorHumanoid? = MovableWorldCamera(this) val uiToolbox = UINSMenu("Menu", 100, menuYaml) val notifier = Notification() val uiPaletteSelector = UIPaletteSelector(this) val uiPalette = UIBuildingMakerBlockChooser(this) val uiPenMenu = UIBuildingMakerPenMenu(this) val uiContainer = ArrayList() private val pensMustShowSelection = arrayOf( PENMODE_MARQUEE, PENMODE_MARQUEE_ERASE ) var currentPenMode = PENMODE_PENCIL set(value) { field = value if (value in pensMustShowSelection) { showSelection = true } } var currentPenTarget = PENTARGET_TERRAIN val selection = ArrayList() val blockMarkings = TextureRegionPack(Gdx.files.internal("assets/graphics/blocks/block_markings_common.tga"), 16, 16) internal var showSelection = true val blockPointingCursor = object : ActorWithBody(Actor.RenderOrder.OVERLAY) { override var referenceID: ActorID? = 1048575 // custom refID override val hitbox = Hitbox(0.0, 0.0, 16.0, 16.0) init { this.actorValue[AVKey.LUMR] = 1.0 this.actorValue[AVKey.LUMG] = 1.0 } override fun drawBody(batch: SpriteBatch) { batch.color = toolCursorColour[currentPenMode] batch.draw(blockMarkings.get(currentPenMode, 0), hitbox.startX.toFloat(), hitbox.startY.toFloat()) } override fun drawGlow(batch: SpriteBatch) { } override fun dispose() { } override fun update(delta: Float) { hitbox.setPosition( Terrarum.mouseTileX * 16.0, Terrarum.mouseTileY * 16.0 ) } override fun onActorValueChange(key: String, value: Any?) { } override fun run() { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } private val blockMarkerColour = Color(0xe0e0e0ff.toInt()) internal fun blockPosToRefID(x: Int, y: Int) = 1048576 + (y * gameWorld.width + x) private var _testMarkerDrawCalls = 0L private fun generateNewBlockMarkerVisible(x: Int, y: Int) = object : ActorWithBody(Actor.RenderOrder.OVERLAY) { override var referenceID: ActorID? = blockPosToRefID(x, y) // custom refID override val hitbox = Hitbox(x * 16.0, y * 16.0, 16.0, 16.0) override fun drawBody(batch: SpriteBatch) { batch.color = blockMarkerColour drawSpriteInGoodPosition(blockMarkings.get(2,0), batch) } private fun drawSpriteInGoodPosition(sprite: TextureRegion, batch: SpriteBatch) { val leftsidePadding = world.width.times(TILE_SIZE) - WorldCamera.width.ushr(1) val rightsidePadding = WorldCamera.width.ushr(1) if (hitbox.startX in WorldCamera.x - hitbox.width..WorldCamera.x + WorldCamera.width.toDouble() && hitbox.startY in WorldCamera.y - hitbox.height..WorldCamera.y + WorldCamera.height.toDouble()) { if (WorldCamera.xCentre > leftsidePadding && hitbox.centeredX <= rightsidePadding) { // camera center neg, actor center pos batch.draw(sprite, hitbox.startX.toFloat() + world.width * TILE_SIZE, hitbox.startY.toFloat() ) } else if (WorldCamera.xCentre < rightsidePadding && hitbox.centeredY >= leftsidePadding) { // camera center pos, actor center neg batch.draw(sprite, hitbox.startX.toFloat() - world.width * TILE_SIZE, hitbox.startY.toFloat() ) } else { batch.draw(sprite, hitbox.startX.toFloat(), hitbox.startY.toFloat() ) } _testMarkerDrawCalls += 1 } } override fun drawGlow(batch: SpriteBatch) { } override fun update(delta: Float) { } override fun onActorValueChange(key: String, value: Any?) { } override fun run() { TODO("not implemented") } override fun dispose() { } } internal fun addBlockMarker(x: Int, y: Int) { try { val a = generateNewBlockMarkerVisible(x, y) addNewActor(a) actorsRenderOverlay.add(a) selection.add(Point2i(x, y)) } catch (e: Error) { } } internal fun removeBlockMarker(x: Int, y: Int) { try { val a = getActorByID(blockPosToRefID(x, y)) removeActor(a) actorsRenderOverlay.remove(a) selection.remove(Point2i(x, y)) } catch (e: IllegalArgumentException) { // no actor to erase, do nothing } } companion object { const val PENMODE_PENCIL = 0 const val PENMODE_PENCIL_ERASE = 1 const val PENMODE_MARQUEE = 2 const val PENMODE_MARQUEE_ERASE = 3 const val PENMODE_EYEDROPPER = 4 const val PENTARGET_TERRAIN = 1 const val PENTARGET_WALL = 2 val toolCursorColour = arrayOf( Color.YELLOW, Color.YELLOW, Color.MAGENTA, Color.MAGENTA, Color.WHITE ) const val DEFAULT_POI_NAME = "The Yucky Panopticon" } private val actorsRenderOverlay = ArrayList() // can be hidden (e.g. hide sel.) private val essentialOverlays = ArrayList() init { gameWorld.time.setTimeOfToday(WorldTime.HOUR_SEC * 10) gameWorld.globalLight = Color(.8f,.8f,.8f,.8f) essentialOverlays.add(blockPointingCursor) uiContainer.add(uiToolbox) uiContainer.add(uiPaletteSelector) uiContainer.add(notifier) uiContainer.add(uiPalette) uiContainer.add(uiPenMenu) uiToolbox.setPosition(0, 0) uiToolbox.isVisible = true uiToolbox.invocationArgument = arrayOf(this) uiPaletteSelector.setPosition(Terrarum.WIDTH - uiPaletteSelector.width, 0) uiPaletteSelector.isVisible = true notifier.setPosition( (Terrarum.WIDTH - notifier.width) / 2, Terrarum.HEIGHT - notifier.height) actorNowPlaying?.setPosition(512 * 16.0, 149 * 16.0) uiPalette.setPosition(200, 100) LightmapRenderer.fireRecalculateEvent() } override fun show() { Gdx.input.inputProcessor = BuildingMakerController(this) super.show() } private var updateAkku = 0.0 override fun render(delta: Float) { Gdx.graphics.setTitle(Ingame.getCanonicalTitle()) // ASYNCHRONOUS UPDATE AND RENDER // val dt = Gdx.graphics.rawDeltaTime updateAkku += dt var i = 0L while (updateAkku >= delta) { AppLoader.measureDebugTime("Ingame.update") { updateGame(delta) } updateAkku -= delta i += 1 } AppLoader.setDebugTime("Ingame.updateCounter", i) // render? just do it anyway AppLoader.measureDebugTime("Ingame.render") { renderGame() } AppLoader.setDebugTime("Ingame.render-Light", (AppLoader.debugTimers["Ingame.render"] as Long) - ((AppLoader.debugTimers["Renderer.LightTotal"] as? Long) ?: 0) ) } private var mouseOnUI = false internal var tappedOnUI = false // when true, even if the UI is closed, pen won't work unless your pen is lifted // must be set to TRUE by UIs private fun updateGame(delta: Float) { WeatherMixer.update(delta, actorNowPlaying, gameWorld) blockPointingCursor.update(delta) actorNowPlaying?.update(delta) var overwriteMouseOnUI = false uiContainer.forEach { it.update(delta) if (it.isVisible && it.mouseUp) { overwriteMouseOnUI = true } } mouseOnUI = (overwriteMouseOnUI || uiPenMenu.isVisible) WorldCamera.update(world, actorNowPlaying) // make pen work HERE // when LEFT mouse is down if (!tappedOnUI && Gdx.input.isButtonPressed(AppLoader.getConfigInt("mouseprimary")) && !mouseOnUI) { makePenWork(Terrarum.mouseTileX, Terrarum.mouseTileY) // TODO drag support using bresenham's algo // for some reason it just doesn't work... } else if (!uiPenMenu.isVisible && Gdx.input.isButtonPressed(AppLoader.getConfigInt("mousesecondary"))) { // open pen menu // position the menu to where the cursor is uiPenMenu.posX = Terrarum.mouseScreenX - uiPenMenu.width / 2 uiPenMenu.posY = Terrarum.mouseScreenY - uiPenMenu.height / 2 uiPenMenu.posX = uiPenMenu.posX.coerceIn(0, Terrarum.WIDTH - uiPenMenu.width) uiPenMenu.posY = uiPenMenu.posY.coerceIn(0, Terrarum.HEIGHT - uiPenMenu.height) // actually open uiPenMenu.setAsOpen() } BlockPropUtil.dynamicLumFuncTickClock() } private fun renderGame() { _testMarkerDrawCalls = 0L IngameRenderer.invoke(false, world as GameWorldExtension, actorsRenderOverlay = if (showSelection) actorsRenderOverlay + essentialOverlays else essentialOverlays, uisToDraw = uiContainer) AppLoader.setDebugTime("Test.MarkerDrawCalls", _testMarkerDrawCalls) } override fun resize(width: Int, height: Int) { IngameRenderer.resize(Terrarum.WIDTH, Terrarum.HEIGHT) uiToolbox.setPosition(0, 0) notifier.setPosition( (Terrarum.WIDTH - notifier.width) / 2, Terrarum.HEIGHT - notifier.height) println("[BuildingMaker] Resize event") } fun setPencilColour(itemID: ItemID) { uiPaletteSelector.fore = itemID currentPenMode = PENMODE_PENCIL currentPenTarget = PENTARGET_TERRAIN // TERRAIN is arbitrary chosen to prevent possible conflict; for the pencil itself this property does nothing } override fun dispose() { blockMarkings.dispose() uiPenMenu.dispose() } private fun makePenWork(x: Int, y: Int) { val world = gameWorld val palSelection = uiPaletteSelector.fore when (currentPenMode) { // test paint terrain layer PENMODE_PENCIL -> { if (palSelection < BlockCodex.MAX_TERRAIN_TILES) world.setTileTerrain(x, y, palSelection) else if (palSelection < 2 * BlockCodex.MAX_TERRAIN_TILES) world.setTileWall(x, y, palSelection - BlockCodex.MAX_TERRAIN_TILES) } PENMODE_PENCIL_ERASE -> { if (currentPenTarget and PENTARGET_WALL != 0) world.setTileWall(x, y, Block.AIR) else world.setTileTerrain(x, y, Block.AIR) } PENMODE_EYEDROPPER -> { uiPaletteSelector.fore = if (world.getTileFromTerrain(x, y) == Block.AIR) world.getTileFromWall(x, y)!! + BlockCodex.MAX_TERRAIN_TILES else world.getTileFromTerrain(x, y)!! } PENMODE_MARQUEE -> { addBlockMarker(x, y) } PENMODE_MARQUEE_ERASE -> { removeBlockMarker(x, y) } } } private fun getSelectionTotalDimension(): Point2i { selection.sortBy { it.y * world.width + it.x } return selection.last() - selection.first() } private fun serialiseSelection(outfile: File) { // save format: sparse list encoded in following binary format: /* Header: TEaT0bLD -- magic: Terrarum Attachment Int8 version number -- always 1 Int8 number of layers -- always 2 or 3 Int8 number of payloads -- always 2 or 3 int8 compression algorithm -- always 1 (DEFLATE) Int16 width The rest: payloads defined in the map data format Payloads: array of (Int48 tileAddress, UInt16 blockID) Payload names: TerL, WalL, WirL for Terrain, Wall and Wire respectively Footer: EndTEM \xFF\xFE -- magic: end of attachment with BOM Endian: LITTLE */ // proc: // translate boxes so that leftmost point is (0,0) // write to the list using translated coords val payloads = arrayOf("WalL", "TerL", "WirL") val selectionDim = getSelectionTotalDimension() val fos = FileOutputStream(outfile) // write header fos.write("TEaT0bLD".toByteArray()) fos.write(byteArrayOf(1,3,3,1)) fos.write(selectionDim.x.toLittleShort()) // write wall -> terrain -> wire (order defined in GameWorld.TERRAIN/WALL/WIRE) payloads.forEachIndexed { index, it -> fos.write(PAYLOAD_HEADER); fos.write(it.toByteArray()) selection.forEach { val tile = world.getTileFrom(index, it.x, it.y)!! val addr = LandUtil.getBlockAddr(world, it.x - selectionDim.x, it.y - selectionDim.y) fos.write(addr.toULittle48()) fos.write(tile.toLittle()) } fos.write(PAYLOAD_FOOTER) } fos.write(FILE_FOOTER) fos.close() } } class BuildingMakerController(val screen: BuildingMaker) : InputAdapter() { override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { screen.uiContainer.forEach { it.touchUp(screenX, screenY, pointer, button) } screen.tappedOnUI = false return true } override fun mouseMoved(screenX: Int, screenY: Int): Boolean { screen.uiContainer.forEach { it.mouseMoved(screenX, screenY) } return true } override fun keyTyped(character: Char): Boolean { screen.uiContainer.forEach { it.keyTyped(character) } return true } override fun scrolled(amount: Int): Boolean { screen.uiContainer.forEach { it.scrolled(amount) } return true } override fun keyUp(keycode: Int): Boolean { screen.uiContainer.forEach { it.keyUp(keycode) } return true } override fun touchDragged(screenX: Int, screenY: Int, pointer: Int): Boolean { screen.uiContainer.forEach { it.touchDragged(screenX, screenY, pointer) } return true } override fun keyDown(keycode: Int): Boolean { screen.uiContainer.forEach { it.keyDown(keycode) } return true } override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { screen.uiContainer.forEach { it.touchDown(screenX, screenY, pointer, button) } return true } } class MovableWorldCamera(val parent: BuildingMaker) : ActorHumanoid(0, usePhysics = false) { init { referenceID = Terrarum.PLAYER_REF_ID isNoClip = true setHitboxDimension(1, 1, 0, 0) actorValue[AVKey.SPEED] = 8.0 actorValue[AVKey.SPEEDBUFF] = 1.0 actorValue[AVKey.ACCEL] = ActorHumanoid.WALK_ACCEL_BASE actorValue[AVKey.ACCELBUFF] = 4.0 actorValue[AVKey.JUMPPOWER] = 0.0 actorValue[AVKey.FRICTIONMULT] = 4.0 } // TODO resize-aware private var coerceInStart = Point2d( (Terrarum.WIDTH - hitbox.width) / 2.0, (Terrarum.HEIGHT - hitbox.height) / 2.0 ) private var coerceInEnd = Point2d( parent.world.width * TILE_SIZE - (Terrarum.WIDTH - hitbox.width) / 2.0, parent.world.height * TILE_SIZE - (Terrarum.HEIGHT - hitbox.height) / 2.0 ) override fun update(delta: Float) { super.update(delta) // confine the camera so it won't wrap this.hitbox.hitboxStart.setCoerceIn(coerceInStart, coerceInEnd) } override fun drawBody(batch: SpriteBatch) { } override fun drawGlow(batch: SpriteBatch) { } override fun onActorValueChange(key: String, value: Any?) { } } class YamlCommandExit : YamlInvokable { override fun invoke(args: Array) { Terrarum.setScreen(TitleScreen(Terrarum.batch)) } } class YamlCommandSetTimeMorning : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).gameWorld.time.setTimeOfToday(WorldTime.parseTime("7h00")) } } class YamlCommandSetTimeNoon : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).gameWorld.time.setTimeOfToday(WorldTime.parseTime("12h30")) } } class YamlCommandSetTimeDusk : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).gameWorld.time.setTimeOfToday(WorldTime.parseTime("18h40")) } } class YamlCommandSetTimeNight : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).gameWorld.time.setTimeOfToday(WorldTime.parseTime("0h30")) } } class YamlCommandToolPencil : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).currentPenMode = BuildingMaker.PENMODE_PENCIL (args[0] as BuildingMaker).currentPenTarget = BuildingMaker.PENTARGET_TERRAIN } } class YamlCommandToolPencilErase : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).currentPenMode = BuildingMaker.PENMODE_PENCIL_ERASE (args[0] as BuildingMaker).currentPenTarget = BuildingMaker.PENTARGET_TERRAIN } } class YamlCommandToolPencilEraseWall : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).currentPenMode = BuildingMaker.PENMODE_PENCIL_ERASE (args[0] as BuildingMaker).currentPenTarget = BuildingMaker.PENTARGET_WALL } } class YamlCommandToolEyedropper : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).currentPenMode = BuildingMaker.PENMODE_EYEDROPPER (args[0] as BuildingMaker).currentPenTarget = BuildingMaker.PENTARGET_TERRAIN + BuildingMaker.PENTARGET_WALL } } class YamlCommandToolMarquee : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).currentPenMode = BuildingMaker.PENMODE_MARQUEE } } class YamlCommandToolMarqueeErase : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).currentPenMode = BuildingMaker.PENMODE_MARQUEE_ERASE } } class YamlCommandToolMarqueeClear : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).selection.toList().forEach { (args[0] as BuildingMaker).removeBlockMarker(it.x, it.y) } } } class YamlCommandToolToggleMarqueeOverlay : YamlInvokable { override fun invoke(args: Array) { (args[0] as BuildingMaker).showSelection = !(args[0] as BuildingMaker).showSelection } }