diff --git a/src/net/torvald/spriteassembler/ADProperties.kt b/src/net/torvald/spriteassembler/ADProperties.kt index cc3434180..7bf9f55ce 100644 --- a/src/net/torvald/spriteassembler/ADProperties.kt +++ b/src/net/torvald/spriteassembler/ADProperties.kt @@ -1,5 +1,6 @@ package net.torvald.spriteassembler +import net.torvald.terrarum.linearSearchBy import java.io.InputStream import java.io.Reader import java.util.* @@ -18,19 +19,27 @@ data class Animation(val name: String, val delay: Float, val row: Int, val frame override fun toString() = "$name delay: $delay, row: $row, frames: $frames, skeleton: ${skeleton.name}" } +/** Later the 'translate' can be changed so that it represents affine transformation (Matrix2d) */ +data class Transform(val joint: Joint, val translate: ADPropertyObject.Vector2i) { + override fun toString() = "$joint transform: $translate" + fun getTransformVector() = joint.position + translate +} + class ADProperties { private val javaProp = Properties() /** Every key is CAPITALISED */ private val propTable = HashMap>() - /** list of bodyparts used by all the skeletons */ + /** list of bodyparts used by all the skeletons (HEAD, UPPER_TORSO, LOWER_TORSO) */ lateinit var bodyparts: List; private set lateinit var bodypartFiles: List; private set - /** properties that are being used as skeletons */ + /** properties that are being used as skeletons (SKELETON_STAND) */ lateinit var skeletons: HashMap; private set - /** properties that are recognised as animations */ + /** properties that are recognised as animations (ANIM_RUN, ANIM)IDLE) */ lateinit var animations: HashMap; private set + /** an "animation frame" property (ANIM_RUN_1, ANIM_RUN_2) */ + lateinit var transforms: HashMap>; private set private val reservedProps = listOf("SPRITESHEET", "EXTENSION") private val animMustContain = listOf("DELAY", "ROW", "SKELETON") @@ -40,6 +49,8 @@ class ADProperties { private val animFrameSuffixRegex = Regex("""_[0-9]+""") + private val ALL_JOINT = Joint("ALL", ADPropertyObject.Vector2i(0, 0)) + constructor(reader: Reader) { javaProp.load(reader) continueLoad() @@ -59,13 +70,14 @@ class ADProperties { } // set reserved values for the animation: filename, extension - baseFilename = get("SPRITESHEET")!![0].variable - extension = get("EXTENSION")!![0].variable + baseFilename = get("SPRITESHEET")[0].variable + extension = get("EXTENSION")[0].variable val bodyparts = HashSet() val skeletons = HashMap() val animations = HashMap() val animFrames = HashMap() + val transforms = HashMap>() // scan every props, write down anim frames for later use propTable.keys.forEach { if (animFrameSuffixRegex.containsMatchIn(it)) { @@ -94,16 +106,16 @@ class ADProperties { // if it is indeed anim, populate animations list if (propsHashMap.containsKey("SKELETON")) { val skeletonName = propsHashMap["SKELETON"] as String - val skeletonDef = get(skeletonName) ?: throw Error("Skeleton definition for $skeletonName not found") + val skeletonDef = get(skeletonName) - skeletons.put(skeletonName, Skeleton(skeletonName, skeletonDef.toJoints())) - animations.put(s, Animation( + skeletons[skeletonName] = Skeleton(skeletonName, skeletonDef.toJoints()) + animations[s] = Animation( s, propsHashMap["DELAY"] as Float, (propsHashMap["ROW"] as Float).toInt(), animFrames[s]!!, Skeleton(skeletonName, skeletonDef.toJoints()) - )) + ) } } @@ -114,13 +126,44 @@ class ADProperties { } } + // populate transforms + animations.forEach { t, u -> + for (fc in 1..u.frames) { + val frameName = "${t}_$fc" + val prop = get(frameName) + + var emptyList = prop.size == 1 && prop[0].variable.isEmpty() + + val transformList = if (!emptyList) { + List(prop.size) { index -> + val jointNameToSearch = prop[index].variable.toUpperCase() + val joint = if (jointNameToSearch == "ALL") + ALL_JOINT + else + u.skeleton.joints.linearSearchBy { it.name == jointNameToSearch } + ?: throw NullPointerException("No such joint: $jointNameToSearch") + val translate = prop[index].input as ADPropertyObject.Vector2i + + Transform(joint, translate) + } + } + else { + // to make real empty list + List(0) { Transform(ALL_JOINT, ADPropertyObject.Vector2i(0, 0)) } + } + + transforms[frameName] = transformList + } + } + this.bodyparts = bodyparts.toList().sorted() this.skeletons = skeletons this.animations = animations this.bodypartFiles = this.bodyparts.map { toFilename(it) } + this.transforms = transforms } - operator fun get(identifier: String) = propTable[identifier.toUpperCase()] + operator fun get(identifier: String) = propTable[identifier.toUpperCase()]!! val keys get() = propTable.keys fun containsKey(key: String) = propTable.containsKey(key) @@ -129,14 +172,14 @@ class ADProperties { fun toFilename(partName: String) = "${this.baseFilename}${partName.toLowerCase()}${this.extension}" - fun getAnimByFrameName(frameName: String): Animation { - return animations[getAnimNameFromFrame(frameName)]!! - } + fun getAnimByFrameName(frameName: String) = animations[getAnimNameFromFrame(frameName)]!! + fun getSkeleton(name: String) = skeletons[name]!! + fun getTransform(name: String) = transforms[name]!! private fun getAnimNameFromFrame(s: String) = s.substring(0 until s.lastIndexOf('_')) private fun List.toJoints() = List(this.size) { - Joint(this[it].variable, this[it].input!! as ADPropertyObject.Vector2i) + Joint(this[it].variable.toUpperCase(), this[it].input!! as ADPropertyObject.Vector2i) } } @@ -167,7 +210,7 @@ class ADPropertyObject(propertyRaw: String) { if (isADvariable(propertyRaw)) { variable = propPair[0] - val inputStr = propPair[1]!! + val inputStr = propPair[1] if (isADivec2(inputStr)) { type = ADPropertyType.IVEC2 @@ -211,7 +254,9 @@ class ADPropertyObject(propertyRaw: String) { } data class Vector2i(var x: Int, var y: Int) { - override fun toString() = "$x, $y" + override fun toString() = "($x, $y)" + + operator fun plus(other: Vector2i) = Vector2i(this.x + other.x, this.y + other.y) } enum class ADPropertyType { diff --git a/src/net/torvald/spriteassembler/ANIMATION_DESCRIPTION_LANGUAGE.md b/src/net/torvald/spriteassembler/ANIMATION_DESCRIPTION_LANGUAGE.md index 4c1820a25..b7d3e597c 100644 --- a/src/net/torvald/spriteassembler/ANIMATION_DESCRIPTION_LANGUAGE.md +++ b/src/net/torvald/spriteassembler/ANIMATION_DESCRIPTION_LANGUAGE.md @@ -97,6 +97,14 @@ Remember that 'variables' are contained within 'properties' |ROW|variable: float|which row the animation goes in the spritesheet| |SKELETON|variable: string_pair|Which skeleton this animation uses +#### Transforms + +Things like ```LEG_RIGHT -1,0``` within ```ANIM_RUN_3``` are called 'Transform' + +|Name|Type|Meaning| +|---|---|---| +|ALL|variable: ivec2|Shifts (translates) everything by set value| + ### Notes * All indices are one-based @@ -104,6 +112,7 @@ Remember that 'variables' are contained within 'properties' ## Operation * Each line describes transformation -* Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur! +* Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur! (really?) * The Field is an identifier the game code -- sprite assembler -- recognises. * The Field of animation's name is the name the game code looks for. Example: ```this.setAnim("ANIM_RUN")``` +* Coord system is Y-Up, meaning Y=0 is the bottommost position. diff --git a/src/net/torvald/spriteassembler/AssembleFrameJava.kt b/src/net/torvald/spriteassembler/AssembleFrameJava.kt index 05fd27976..733e80fa7 100644 --- a/src/net/torvald/spriteassembler/AssembleFrameJava.kt +++ b/src/net/torvald/spriteassembler/AssembleFrameJava.kt @@ -1,6 +1,6 @@ package net.torvald.spriteassembler -import net.torvald.terrarum.AppLoader.printdbg +import net.torvald.terrarum.linearSearch import java.awt.Image import java.awt.Toolkit import java.awt.image.BufferedImage @@ -16,6 +16,7 @@ object AssembleFrameAWT { operator fun invoke(properties: ADProperties, frameName: String, assembleConfig: AssembleConfig = AssembleConfig()) { val theAnim = properties.getAnimByFrameName(frameName) val skeleton = theAnim.skeleton.joints.reversed() + val transforms = properties.getTransform(frameName) val bodyparts = Array(skeleton.size) { // if file does not exist, null it val file = File("assets/" + properties.toFilename(skeleton[it].name)) @@ -33,9 +34,13 @@ object AssembleFrameAWT { val canvas = BufferedImage(assembleConfig.fw, assembleConfig.fh, BufferedImage.TYPE_4BYTE_ABGR) - //printdbg(this, "==============================") - - properties[frameName].forEach { printdbg(this, it) } + println("Frame name: $frameName") + transforms.forEach { println(it) } + println("==========================") + println("Transformed skeleton:") + AssembleFrameBase.makeTransformList(skeleton, transforms).forEach { (name, transform) -> + println("$name transformedOut: $transform") + } } } @@ -46,4 +51,28 @@ object AssembleFrameAWT { * @param ox Origin-X, leftmost point being zero * @param oy Origin-Y, bottommost point being zero */ -data class AssembleConfig(val fw: Int = 48, val fh: Int = 56, val ox: Int = 29, val oy: Int = 0) \ No newline at end of file +data class AssembleConfig(val fw: Int = 48, val fh: Int = 56, val ox: Int = 29, val oy: Int = 0) + +object AssembleFrameBase { + /** + * Returns joints list with tranform applied. + * @param skeleton list of joints + * @param transform ordered list of transforms should be applied. First come first serve. + * @return List of pairs that contains joint name on left, final transform value on right + */ + fun makeTransformList(joints: List, transforms: List): List> { + // make our mutable list + val transformOutput = ArrayList>() + joints.forEach { + transformOutput.add(it.name to it.position) + } + + // process transform queue + transforms.forEach { transform -> + val jointToMoveIndex = transformOutput.linearSearch { it.first == transform.joint.name }!! + transformOutput[jointToMoveIndex] = transformOutput[jointToMoveIndex].first to transform.getTransformVector() + } + + return transformOutput.toList() + } +} \ No newline at end of file diff --git a/src/net/torvald/spriteassembler/SpriteAssemblerApp.kt b/src/net/torvald/spriteassembler/SpriteAssemblerApp.kt index 01f9c468e..d202034c6 100644 --- a/src/net/torvald/spriteassembler/SpriteAssemblerApp.kt +++ b/src/net/torvald/spriteassembler/SpriteAssemblerApp.kt @@ -26,10 +26,11 @@ class SpriteAssemblerApp : JFrame() { private val panelBodypartsList = JList() private val panelImageFilesList = JList() private val panelSkeletonsList = JList() + private val panelTransformsList = JList() private val panelCode = JTextPane() private val statBar = JTextArea("Null.") - private var adProperties: ADProperties? = null + private lateinit var adProperties: ADProperties private val props = Properties() private val lang = Properties() @@ -93,12 +94,14 @@ class SpriteAssemblerApp : JFrame() { panelBodypartsList.model = DefaultListModel() panelImageFilesList.model = DefaultListModel() panelSkeletonsList.model = DefaultListModel() + panelTransformsList.model = DefaultListModel() val panelPartsList = JTabbedPane(JTabbedPane.TOP) panelPartsList.add("Animations", JScrollPane(panelAnimationsList)) panelPartsList.add("Bodyparts", JScrollPane(panelBodypartsList)) panelPartsList.add("Images", JScrollPane(panelImageFilesList)) panelPartsList.add("Skeletons", JScrollPane(panelSkeletonsList)) + panelPartsList.add("Transforms", JScrollPane(panelTransformsList)) val panelDataView = JSplitPane(JSplitPane.VERTICAL_SPLIT, JScrollPane(panelProperties), panelPartsList) @@ -116,7 +119,7 @@ class SpriteAssemblerApp : JFrame() { val propRoot = DefaultMutableTreeNode("Properties") - adProperties?.forEach { s, list -> + adProperties.forEach { s, list -> // build tree node for the properties display val propNode = DefaultMutableTreeNode(s) propRoot.add(propNode) @@ -133,21 +136,25 @@ class SpriteAssemblerApp : JFrame() { panelSkeletonsList.model = DefaultListModel() // populate animations view - adProperties!!.animations.forEach { + adProperties.animations.forEach { (panelAnimationsList.model as DefaultListModel).addElement("${it.value}") } // populate bodyparts view - adProperties!!.bodyparts.forEach { partName -> + adProperties.bodyparts.forEach { partName -> (panelBodypartsList.model as DefaultListModel).addElement(partName) } // populate image file list view - adProperties!!.bodypartFiles.forEach { partName -> + adProperties.bodypartFiles.forEach { partName -> (panelImageFilesList.model as DefaultListModel).addElement(partName) } // populate skeletons view - adProperties!!.skeletons.forEach { + adProperties.skeletons.forEach { (panelSkeletonsList.model as DefaultListModel).addElement("${it.value}") } + // populate transforms view + adProperties.transforms.forEach { + (panelTransformsList.model as DefaultListModel).addElement("$it") + } } catch (fehler: Throwable) { displayError("ERROR_PARSE_FAIL", fehler) diff --git a/src/net/torvald/spriteassembler/test_sprite.properties b/src/net/torvald/spriteassembler/test_sprite.properties index e546a55e1..7cf7fff75 100644 --- a/src/net/torvald/spriteassembler/test_sprite.properties +++ b/src/net/torvald/spriteassembler/test_sprite.properties @@ -21,12 +21,12 @@ SKELETON_STAND=HEADGEAR 0,32;HAIR_FORE 0,32;\ # skeleton_stand is used for testing purpose ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND -ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0 -ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1 -ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1 -ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1 +ANIM_RUN_1=LEG_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_LEFT -1,0;FOOT_LEFT -1,0 +ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_LEFT 0,1;FOOT_LEFT 0,1 +ANIM_RUN_3=LEG_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_LEFT 1,1;FOOT_LEFT 1,1 +ANIM_RUN_4=ALL 0,1;LEG_RIGHT 0,1;LEG_LEFT 0,-1 ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND ANIM_IDLE_1= ! ANIM_IDLE_1 will not make any transformation -ANIM_IDLE_2=UPPER_TORSO 0,-1 +ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;ARM_REST_LEFT 0,-1;HAND_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_RIGHT 0,-1 diff --git a/src/net/torvald/terrarum/Terrarum.kt b/src/net/torvald/terrarum/Terrarum.kt index 24c33587e..c654df9e8 100644 --- a/src/net/torvald/terrarum/Terrarum.kt +++ b/src/net/torvald/terrarum/Terrarum.kt @@ -724,3 +724,18 @@ fun interpolateLinear(scale: Double, startValue: Double, endValue: Double): Doub } return (1.0 - scale) * startValue + scale * endValue } + +fun List.linearSearch(selector: (T) -> Boolean): Int? { + this.forEachIndexed { index, it -> + if (selector.invoke(it)) return index + } + + return null +} +fun List.linearSearchBy(selector: (T) -> Boolean): T? { + this.forEach { + if (selector.invoke(it)) return it + } + + return null +} diff --git a/src/net/torvald/terrarum/tests/ADLParsingTest.kt b/src/net/torvald/terrarum/tests/ADLParsingTest.kt index d5fcb60cb..1cf48a38a 100644 --- a/src/net/torvald/terrarum/tests/ADLParsingTest.kt +++ b/src/net/torvald/terrarum/tests/ADLParsingTest.kt @@ -32,15 +32,15 @@ class ADLParsingTest { # skeleton_stand is used for testing purpose ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND - ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0 - ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1 - ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1 - ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1 + ANIM_RUN_1=LEG_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_LEFT -1,0;FOOT_LEFT -1,0 + ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_LEFT 0,1;FOOT_LEFT 0,1 + ANIM_RUN_3=LEG_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_LEFT 1,1;FOOT_LEFT 1,1 + ANIM_RUN_4=ALL 0,1;LEG_RIGHT 0,1;LEG_LEFT 0,-1 ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND ANIM_IDLE_1= ! ANIM_IDLE_1 will not make any transformation - ANIM_IDLE_2=UPPER_TORSO 0,-1 + ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;ARM_REST_LEFT 0,-1;HAND_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_RIGHT 0,-1 """.trimIndent() operator fun invoke() { diff --git a/src/net/torvald/terrarum/tests/SpriteAssemblerTest.kt b/src/net/torvald/terrarum/tests/SpriteAssemblerTest.kt index 3465c2c5d..402138893 100644 --- a/src/net/torvald/terrarum/tests/SpriteAssemblerTest.kt +++ b/src/net/torvald/terrarum/tests/SpriteAssemblerTest.kt @@ -1,7 +1,7 @@ package net.torvald.terrarum.tests import net.torvald.spriteassembler.ADProperties -import net.torvald.spriteassembler.AssembleFrameGdxPixmap +import net.torvald.spriteassembler.AssembleFrameAWT import java.io.StringReader /** @@ -11,7 +11,7 @@ class SpriteAssemblerTest { operator fun invoke() { val properties = ADProperties(StringReader(ADLParsingTest().TEST_STR)) - AssembleFrameGdxPixmap.invoke(properties, "ANIM_RUN_1") + AssembleFrameAWT.invoke(properties, "ANIM_RUN_1") } }