Accelerate monster animations using Python

This would be the forum for questions about how to work with mod making tools which can be a problem of its own.

Moderator: Paul Siramy

User avatar
mmpx222
Junior Member
Paladin
Posts: 154
Joined: Sat Apr 26, 2014 9:19 am
Korea South

Accelerate monster animations using Python

Post by mmpx222 » Sat Mar 14, 2020 6:28 am

Here's a Python script I wrote to speed up monster animations. It changes the attack and spellcasting animation speeds of monsters so that they are at least as fast as a set minimum (default is 256). Monsters whose attack/casting animations are faster than this minimum are unaffected.

This script requires two Python tools, d2animdata and d2txt. You will have to install both using:

Code: Select all

pip install d2animdata
pip install d2txt
Then you can copy the following script and save it as accelerate_anim.py.

Code: Select all

"""Speeds up slow monster attack & skill casting animations in AnimData.D2."""

import argparse
from collections import defaultdict
from typing import DefaultDict, List, Set

import d2animdata
from d2txt import D2TXT

# A COF name consists of 3 components:
#
#   DIGHHTH = DI + GH + HTH
#             ^^   ^^   ^^^
#             (1)  (2)  (3)
#
# (1) Token: A 2-letter code that represents the general type of character or
#   monster. It specifies the name of a directory under data/global/chars/ (for
#   character animations) or data/global/monsters/ (for monster animations) in
#   the MPQ files. Also used by the Code column of MonStats.txt, and possibly
#   others.
# (2) Animation mode: A 2-letter code that specifies the name of a directory
#   under each token directory in the MPQ files. Also used by various columns in
#   MonStats.txt, MonStats2.txt, and possibly others.
# (3) Hit class: A 3-letter code that represents the weapon type associated with
#   an animation. Most monsters just use HTH.
#
# This short guide is based on:
#   https://d2mods.info/resources/infinitum/tut_files/token-tutorial.html


# Based on PlrType.txt
PLAYER_TOKENS = {
    "AM": "Amazon",
    "SO": "Sorceress",
    "NE": "Necromancer",
    "PA": "Paladin",
    "BA": "Barbarian",
    "DZ": "Druid",
    "AI": "Assassin",
}


# Based on https://d2mods.info/forum/viewtopic.php?t=2780
ANIMATION_MODE_NAMES = {
    "NU": "Neutral",
    "DT": "Death",
    "DD": "Dead (Corpse)",
    "A1": "Attack 1",
    "A2": "Attack 2",
    "S1": "Skill 1",
    "S2": "Skill 2",
    "S3": "Skill 3",
    "S4": "Skill 4",
    "TN": "Town Neutral",
    "TW": "Town Walk",
    "KK": "Kick",
    "SC": "Cast",
    "GH": "Get Hit",
    "BL": "Block",
    "WL": "Walk",
    "RN": "Run",
    "TH": "Throw",
}


def make_token_to_monsters(monstats: D2TXT) -> DefaultDict[str, Set[str]]:
    """Returns a mapping of tokens to actively hostile monsters."""
    token_to_monsters = defaultdict(set)
    for row in monstats:
        alignment = int(row["Align"] or 0)
        # Skip if friendly (NPCs, summons) or unaligned (e.g. cows)
        if alignment != 0 or row["npc"]:
            continue
        # Skip if AI is not hostile
        if row["AI"].lower() in {"idle", "npc"}:
            continue

        token = row["Code"].upper()
        base_id = row["BaseId"]
        token_to_monsters[token].add(base_id)
    return token_to_monsters


def main(argv: List[str] = None) -> None:
    """Entrypoint for the CLI script."""
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser.add_argument("animdata_d2", help="AnimData.D2 file to modify")
    parser.add_argument(
        "monstats_txt", help="MonStats.txt to use for reference (read only)"
    )
    parser.add_argument(
        "--min-speed",
        default=256,
        type=int,
        help="Minimum animation speed for attacking and casting spells",
    )

    args = parser.parse_args(argv)
    with open(args.animdata_d2, mode="rb") as animdata_file:
        animdata = d2animdata.load(animdata_file)

    token_to_monsters = make_token_to_monsters(D2TXT.load_txt(args.monstats_txt))

    num_anims_updated = 0
    for record in animdata:
        token = record.cof_name[:2]
        anim_mode = record.cof_name[2:4]

        # Only include monster tokens
        if token not in token_to_monsters:
            continue

        # Select attack and spell-casting animations only
        if anim_mode not in {"A1", "A2", "S1", "S2", "S3", "S4", "SC", "TH"}:
            continue

        # Filter only animations that are slower than default (256)
        if record.animation_speed >= args.min_speed:
            continue

        record.animation_speed = args.min_speed
        num_anims_updated += 1

    with open(args.animdata_d2, mode="wb") as animdata_file:
        d2animdata.dump(animdata, animdata_file)

    # Phrozen Keep doesn't allow code that calls the print function!
    # This is a workaround
    print_ = print
    print_(f"{num_anims_updated} animations updated")

if __name__ == "__main__":
    main()

You also need AnimData.D2 and MonStats.txt to run this script. I'm sure most modders will know how and where to find them :)

Finally, run the following command:

Code: Select all

python accelerate_anim.py path/to/AnimData.D2 path/to/MonStats.txt
Now your monsters will swing their swords and cast spells quickly. Of course, you will also need to adjust AIDel in MonStats.txt to make them actually useful.
D2TXT / D2INI - Python scripts for editing TXT files, or converting between TXT ↔ INI files

Return to “Tools”