# 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 |