# CUstom Animation

Version 1.2.0 added function for custom animation, now through JavaScript files, you can add custom animations.

# Instruction

  • This instruction is valid for Touhou Little Maid mod version 1.2.0 and above:
  • Basic understanding for JavaScript language;
  • Some high school mathematical knowledge, especially towards Trigonometric function and polar coordinates;
  • For editing script software, VSCode is recommended, all related script files requires to be saved using UTF-8 without BOM.

# Basic format

Animation script can be put in any location of the folder, you only need to map the file location on the corresponding models. But for the sake of uniformity, by default it will be placed under animation folder.

Below is the general template:

var GlWrapper = Java.type("com.github.tartaricacid.touhoulittlemaid.client.animation.script.GlWrapper");

Java.asJSONCompatible({
    /**
     * @param entity Entity that requires the corresponding animation
     * @param limbSwing The walking speed of the entity (think of it as the speedometer of a car)
     * @param limbSwingAmount The total walking distance of the entity (think of it as the odometer of a car)
     * @param ageInTicks The tick time of an entity, the value that constantly increase from 0
     * @param netHeadYaw The yaw for the head of the entity
     * @param headPitch The pitch for the head of the entity
     * @param scale Param for scaling the entity, default is 0.0625
     * @param modelMap The skeleton of the model saved for a map
     */
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
                          headPitch, scale, modelMap) {
        // Script for a model
    }
})

Here we have a simple example, current model has a skeleton named 'rotation', we want this skeleton to make a rotational movement around the X axis, the movement speed is around 1 degree every tick (which is one rotation every 18 seconds), we can write it as below.

var GlWrapper = Java.type("com.github.tartaricacid.touhoulittlemaid.client.animation.script.GlWrapper");

Java.asJSONCompatible({
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
                          headPitch, scale, modelMap) {
        // First obtain a skeleton named 'rotation' from modelMap
        rotation = modelMap.get("rotation");
        // Just in case, we make a simple check to make sure this skeleton existed
        if (rotation != undefined) {
            // Through the function setRotateAngleX in the skeleton, we set its X axis angle
            // ageInTicks is tick time for the entity, a value that constantly increases starting from 0
            // Through remainder operator (which is % sign), set the value between 0~360
            // Since this method only accepts radian, we need to multiply it by  0.017453292 to convert into radian
            // And with that we achieved the animation of rotating 1 degree every tick
            rotation.setRotateAngleX(ageInTicks % 360 * 0.017453292);
        }
    }
})

Now we add another more complex motion, we have a skeleton named 'wing', and we want a constant back and forth oscillating motion.

Oscillate around Y axis, at a degree between '-20°~40°', and one cycle is completed every 5 second.

Trigonometry function fits our need, as you can use sine or cosine for this, we will be using sine function.

var GlWrapper = Java.type("com.github.tartaricacid.touhoulittlemaid.client.animation.script.GlWrapper");

Java.asJSONCompatible({
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
        headPitch, scale, modelMap) {
        // First obtain a skeleton named 'wing' from modelMap
        wing = modelMap.get("wing");
        // Just in case, we make a simple check to make sure this skeleton existed
        if (wing != undefined) {
            // One complete cycle every 5 second, which is 100 tick
            // Using multiplication and remainder operator we can achieve this requirement
            var time = (ageInTicks * 3.6) % 360;
            // This is using the Math function in JavaScript 
            // Construct sine function, and obtain a periodic function between -20~40
            var func = 30 * Math.sin(time * 0.017453292) + 10;
            // Lastly we apply the parameter
            wing.setRotateAngleY(func * 0.017453292);
        }
    }
})

All other complex motion can be achieved through the related functions.

# Hot reload function ingame

Since you can't determine if the animation is correct just by looking at the functions, we added a function to hot reload the animation ingame. you just need to select the script you're editing, after each save the game will automatically reload the script, so you can quickly adjust it if anything is wrong.

001

After clicking this button there will be a windows pop up to select the files (It may be blocked by the game, minimize the game so you can see the file selection windows).

After selecting the file, it will enter the hot reload mode. Now any saved changes to the script will be automatically loaded.

WARNING

Exiting this menu will automatically close hot reload.

# Function documentation

# entity parameter

Depending on the target of the added animation, the function that can be used by entity differs as well.

# Maid

Function name Return value Note
hasHelmet() boolean After maid wears helmet, returns 'true'
hasChestPlate() boolean After maid wears chestplate, returns 'true'
hasBoots() boolean After maid wears boots, returns 'true'
isBegging() boolean Whether maid is in begging mode
isSwingingArms() boolean If maid is using arms, this function will return 'true'
isRiding() boolean Whether maid is in riding mode
isSitting() boolean Whether maid is in standby mode
isHoldTrolley() boolean Whether maid is carrying trolley or other entities
isRidingMarisaBroom() boolean Whether maid is riding Marisa Broom
isRidingPlayer() boolean Whether maid is riding player
isHoldVehicle() boolean Whether maid is riding vehicle
isSwingLeftHand() boolean Whether the maid is swinging left or right arm, return 'false' if it's the right
getLeftHandRotation() float[3] Get the left arm rotation data
getRightHandRotation() float[3] Get the right arm rotation data
getDim() int Get the dimension where the maid is in

# Cushion (Chair)

Function name Return value Note
isRidingPlayer() boolean Whether the cushion is sit by the player

# limbSwing and limbSwingAmount param

These are floating points, limbSwing is the walking speed of the entity (think of it as the speedometer of a car), limbSwingAmount is the total walking distance of the entity (think of it as the odometer of a car).

These two data are mainly used on the rotation of the legs and limbs, the original version has a description of the motion using basic math formula, using these two data.

Math.cos(limbSwing * 0.6662) * limbSwingAmount(left hand)

-Math.cos(limbSwing * 0.6662) * limbSwingAmount(right hand)

Math.cos(limbSwing * 0.6662) * limbSwingAmount * 1.4(left leg)

-Math.cos(limbSwing * 0.6662) * limbSwingAmount * 1.4(right leg)

Changing the value 0.6662 will control the frequency of the swing, multiplied by the coeffecient of the formula (for example, the leg uses '1.4' as the coeffecient) to change the amplitude of the swing.

Using the vanilla Minecraft formula for arm and leg swinging can make a more natural swinging animation.

# ageInTicks param

Floating point, a variable that self-increase from 0 every tick, a self-changing parameter that's used in most animation function.

# netHeadYaw and headPitch parameter

Both are floating point, and are angle value (this is how vanilla Minecraft is designed). A parameter input when head is moving.

Normally this parameter can be used as a rotation angle, you just need to change it into radian.

head.setRotateAngleX(headPitch * 0.017453292);
head.setRotateAngleY(netHeadYaw * 0.017453292);

WARNING

If the coeffiecient in this section is set to be larger than '0.017453292', there may have an head deflection error issue.

# scale parameter

Floating point, fixed at 0.0625.

A value that has unknown meaning.

# modelMap parameter

A Map that saves skeleton, using string as keys.

You can get the corresponding skeleton through 'modelMap.get("xxx")'. If there is no skeleton that matches the name, return 'undefined'

Let's say we want to get the target skeleton 'head':

head = modelMap.get("head");

Then we can set various parameter using this 'head' skeleton to produce animation.

Of course, as a precaution, it's best to set a check for this skeleton, to make sure it's there.

head = modelMap.get("head");
if (head != undefined) {
    // Making various animation
}

# Sekeleton target

We can get various target via modelMap.get("xxx"), these are the skeleton targets.

Function name Return value Note
setRotateAngleX(float rotateAngleX) None Set the skeleton's X angle
setRotateAngleY(float rotateAngleY) None Set the skeleton's Y angle
setRotateAngleZ(float rotateAngleZ) None Set the skeleton's Z angle
setOffsetX(float offsetX) None Set the skeleton's X coordianate offset
setOffsetY(float offsetY) None Set the skeleton's Y coordianate offset
setOffsetZ(float offsetZ) None Set the skeleton's Z coordianate offset
setHidden(boolean hidden) None Set if the skeleton is hidden
getRotateAngleX(float rotateAngleX) float Obtain the skeleton's X angle
getRotateAngleY(float rotateAngleY) float Obtain the skeleton's Y angle
getRotateAngleZ(float rotateAngleZ) float Obtain the skeleton's Z angle
getOffsetX(float offsetX) float Obtain the skeleton's X coordianate offset
getOffsetY(float offsetY) float Obtain the skeleton's Y coordianate offset
getOffsetZ(float offsetZ) float Obtain the skeleton's Y coordianate offset
isHidden(boolean hidden) boolean Check if the skeleton is hidden

# GlWrapper

On the top of the script we used a tool called 'GlWrapper', that can make various translation, rotation and scaling operations.

Function name Return value Note
translate(float x, float y, float z) None Move the entity to coordiate x y z
rotate(float angle, float x, float y, float z) None Using a straight line(0, 0, 0) (x, y, z) as axis, rotate it by 'angle' degree.
scale(float x, float y, float z) None Scale entity on three axis by x y z times