# Custom JavaScript Animation

Through JavaScript files, you can add custom animations for maid or chair.

  • This wiki is suitable for Touhou Little Maid mod in 1.12.2 and 1.16.5:
  • 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 call the file path on the corresponding models. I recommend putting it in the animation folder.

Below is the general template:

// This call is only needed when you need to use GlWrapper
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, no use found.
     * @param modelMap The group of the model saved for a hashmap
     */
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
                          headPitch, scale, modelMap) {
        // Script for the model
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

// This call is only needed when you need to use GlWrapper
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 group named 'rotation' from modelMap
        rotation = modelMap.get("rotation");
        // Just in case, we make a simple check to make sure this group existed
        if (rotation != undefined) {
            // Through the function setRotateAngleX in the group, 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);
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Now we add another more complex motion, we have a group 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.

// This call is only needed when you need to use GlWrapper
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 group named 'wing' from modelMap
        wing = modelMap.get("wing");
        // Just in case, we make a simple check to make sure this group 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;
            // Since this method only accepts radian, we need to multiply it by 0.017453292 to convert into radian
            wing.setRotateAngleY(func * 0.017453292);
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

# Hot Reload

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 the following command can reload all animation's data.

  • 1.12.2: /maid_res reload
  • 1.16.5: /tlm pack reload

# Function Documentation

Strikethrough style means that the method is deprecated in 1.16, you can visit the link here (opens new window) to view the source code API.

# 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
getSwingProgress() float Get maid's swinging time
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
isPortableAudioPlay() boolean Whether the maid hold portable audio and play it
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
getAtBiome() String Get maid's biome register name
getAtBiomeTemp() String Get maid's biome temperature enum
onHurt() boolean Whether the maid is on hurt
getHealth() float Get maid's health
getMaxHealth() float Get maid's max health
isSleep() boolean Whether maid is sleep
getFavorability() int Get the maid's favorability
isOnGround() boolean Whether the maid is on ground
getArmorValue() double Get maid's armor value
getSeed() long Get a fixed value, each entity is different, similar to the entity's UUID

# Chair

Function name Return value Note
isRidingPlayer() boolean Whether the chair is sit by the player
hasPassenger() boolean Whether the chair has passenger
getPassengerYaw() float Get chair passenger's yaw
getYaw() float Get chair's yaw
getPassengerPitch() float Get chair passenger's pitch
getDim() int Get chair's dim id
getWorld() World Get chair's world data
getSeed() long Get a fixed value, each entity is different, similar to the entity's UUID

# 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 & limbSwingAmount Parameter

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, Minecraft vanilla uses these two basic data to calculate the animation of the arm and leg swinging.

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 Parameter

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 degrees value (this is how vanilla Minecraft is designed).

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);
1
2

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

# scale Parameter

Floating point, fixed at 0.0625.

A value that has unknown meaning.

# modelMap Parameter

A Map that saves group, using string as keys.

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

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

head = modelMap.get("head");
1

Then we can set various parameter using this head group to make animation.

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

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

# Group

We can get various group via modelMap.get("xxx"), the following are the functions that can be used to the obtained group.

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