mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-09 13:21:51 +09:00
adproperties now has transforms list; assembler can make transformed skeleton
new fun: LinearSearch(By)
This commit is contained in:
@@ -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<String, List<ADPropertyObject>>()
|
||||
|
||||
/** 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<String>; private set
|
||||
lateinit var bodypartFiles: List<String>; private set
|
||||
/** properties that are being used as skeletons */
|
||||
/** properties that are being used as skeletons (SKELETON_STAND) */
|
||||
lateinit var skeletons: HashMap<String, Skeleton>; private set
|
||||
/** properties that are recognised as animations */
|
||||
/** properties that are recognised as animations (ANIM_RUN, ANIM)IDLE) */
|
||||
lateinit var animations: HashMap<String, Animation>; private set
|
||||
/** an "animation frame" property (ANIM_RUN_1, ANIM_RUN_2) */
|
||||
lateinit var transforms: HashMap<String, List<Transform>>; 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<String>()
|
||||
val skeletons = HashMap<String, Skeleton>()
|
||||
val animations = HashMap<String, Animation>()
|
||||
val animFrames = HashMap<String, Int>()
|
||||
val transforms = HashMap<String, List<Transform>>()
|
||||
// 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<ADPropertyObject>.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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Image?>(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)
|
||||
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<Joint>, transforms: List<Transform>): List<Pair<String, ADPropertyObject.Vector2i>> {
|
||||
// make our mutable list
|
||||
val transformOutput = ArrayList<Pair<String, ADPropertyObject.Vector2i>>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,11 @@ class SpriteAssemblerApp : JFrame() {
|
||||
private val panelBodypartsList = JList<String>()
|
||||
private val panelImageFilesList = JList<String>()
|
||||
private val panelSkeletonsList = JList<String>()
|
||||
private val panelTransformsList = JList<String>()
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -724,3 +724,18 @@ fun interpolateLinear(scale: Double, startValue: Double, endValue: Double): Doub
|
||||
}
|
||||
return (1.0 - scale) * startValue + scale * endValue
|
||||
}
|
||||
|
||||
fun <T> List<T>.linearSearch(selector: (T) -> Boolean): Int? {
|
||||
this.forEachIndexed { index, it ->
|
||||
if (selector.invoke(it)) return index
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
fun <T> List<T>.linearSearchBy(selector: (T) -> Boolean): T? {
|
||||
this.forEach {
|
||||
if (selector.invoke(it)) return it
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user