# 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 Bone 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 bone named rotation' we want this bone 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 bone named 'rotation' from modelMap
        rotation = modelMap.get("rotation");
        // Just in case, we make a simple check to make sure this bone existed
        if (rotation != undefined) {
            // Through the function setRotateAngleX in the bone, 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 bone 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 bone named 'wing' from modelMap
        wing = modelMap.get("wing");
        // Just in case, we make a simple check to make sure this skelbonested
        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.

After you load the model resource pack you made, just use /maid_res reload command can reload all animation's data.

# 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
hasLeggings() boolean After maid wears leggings, returns true
hasBoots() boolean After maid wears boots, returns true
getHelmet() String After maid wears helmet, returns helmet item's registry name
getChestPlate() String After maid wears chestplate, returns chestplate item's registry name
getLeggings() String After maid wears leggings, returns leggings item's registry name
getBoots() String After maid wears boots, returns boots item's registry name
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
hasBackpack() boolean Whether maid wearing backpack
getBackpackLevel() int Get maid's backpack level
hasSasimono() boolean Whether maid wearing sasimono
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
getWorld() World Get maid's world data
getTask() String Get maid's task, such as attack,ranged_attack
hasItemMainhand() boolean Whether maid has mainhand item
hasItemOffhand() boolean Whether maid has offhand item
getItemMainhand() String Get maid mainhand item's registry name
getItemOffhand() String Get maid offhand item's registry name
inWater() boolean Whether maid in water
inRain() boolean Whether maid in rain
getHealth() float Get maid's health
getMaxHealth() float Get maid's max health
isSleep() boolean Whether maid is sleep
getArmorValue() double Get maid's armor value

# Cushion (Chair)

Function name Return value Note
isRidingPlayer() boolean Whether the cushion is sit by the player
hasPassenger() boolean Whether the cushion has passenger
getPassengerYaw() float Get cushion passenger's yaw
getYaw() float Get cushion's yaw
getPassengerPitch() float Get cushion passenger's pitch
getDim() int Get cushion's dim id
getWorld() World Get cushion's world data

# World

Function name Return value Note
getWorldTime() long Get world's time (tick, 0-24000)
isDay() boolean Whether the world is day
isNight() boolean Whether the world is night
isRaining() boolean Whether the world is raining
isThundering() boolean Whether the world is thundering

# 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 bone, using string as keys.

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

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

head = modelMap.get("head");

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

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

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

# Bone target

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

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