Construct Quarter – Part 1: Research and Toolchain


Overview

I made the Patchwerk Prototype specifically to prepare myself for making this project. It will implement at least the same five heroes and three bosses that are actually balanced. You can see the new project page here. My ideal target was getting this finished right as reforged comes out. Reforged comes out at the end of the month so that might be a stretch, but it’s at least possible because I’ve done some prep work that should yield massive boosts in productivity.

Research

Thinking back to previous projects, I regretted quickly jumping into the projects and not learning more about the tools I was using.

For Eco-Sim, I regretting not looking into and planning my cross platform goals in the beginning of the project. Adding it at the beginning and then iteratively fixing small problems with new additions would have made it much easier. Leaving it to the end required fixing so many built up incompatibilities throughout the project that it ultimately didn’t feel worth the effort. In addition, the framework I used, Oxygine, had some functionality I was not aware of that could have saved me some work.

For the Patchwerk Prototype, I regretted not learning about Warcraft 3’s scripting language, jass. Even if Reforged moved to a new scripting language, I could have been more productive if I took time at the beginning to learn jass. Also, it would have better familiarized me with the calls available in the Warcraft api.

Rather than jumping right in, I took the time to look for what sort middleware might be available to make Warcraft 3 modding more productive. I was very excited to find a thread on The Hive Workshop about a project that enables Warcraft 3 map making in C#.

Toolchain

I didn’t want to start my project and learn as I went, encountering various issues along the way. To sure up my understanding of using the framework, I implemented Omnislash as described in Wyrmlord’s jass tutorial in C#. To ensure I really understood what I was doing and was front loading as many issues as I could, I decided to follow my steps again and explain them in a tutorial. The final state of the C# spell is shown in the following video.

The code that causes that spell to work is below.

static void spellActions()
{
    // Range around the caster that targets can be hit from 
    const float kSpellRange = 750;
    // The amount of damage each strike deals 
    const float kDamage = 250;
    // Max number of targets the spell can hit 
    const int kMaxTargets = 6;
    // Variable to decrement each time a target is hit 
    int count = kMaxTargets;

    // Gets the unit that cast the spell associated with  
    // this trigger and saves it into a variable 
    unit caster = GetSpellAbilityUnit();
    // Gets the location of the caster 
    float startX = GetUnitX(caster);
    float startY = GetUnitY(caster);

    // Create a group variable to hold the units the spell will hit 
    group targets = CreateGroup();
    // Only units that cause filterCondition to return true will be added to the group
    GroupEnumUnitsInRange(targets, startX, startY, kSpellRange, Condition(filterCondition));
    // Time to play attack animation
    const float kAttackTime = 0.65f;
    int numTargets = System.Math.Min(kMaxTargets, BlzGroupGetSize(targets));
    // Total time the spell should take 
    float followThroughTime = kAttackTime * numTargets;
    // Sets the spell follow through time to the calculated value 
    BlzSetAbilityRealLevelField(GetSpellAbility(), 
                                ABILITY_RLF_FOLLOW_THROUGH_TIME, 0, followThroughTime);

    // Effect model names
    const string blinkName = @"Abilities\Spells\NightElf\Blink\BlinkCaster.mdl";
    const string shockName = @"Abilities\Spells\Items\AIlb\AIlbSpecialArt.mdl";

    // This variable will store the target we're currently hitting 
    // Start with the first unit in the group 
    unit currentTarget = FirstOfGroup(targets);

    // While there's still a target to hit and we have't yet hit max targets
    while (currentTarget != null && count > 0)
    {
        // Get start location for blink effect 
        float oldCasterX = GetUnitX(caster);
        float oldCasterY = GetUnitY(caster);
        // Create blink effect, save it to clean up later 
        effect preBlinkEffect = AddSpecialEffect(blinkName, oldCasterX, oldCasterY);

        //
        // Teleport to, face, and attack enemy 
        //
        const float kTwoPi = 2.0f * War3Api.Blizzard.bj_PI;
        // Get the position of the enemy we're targeting 
        float targetX = GetUnitX(currentTarget);
        float targetY = GetUnitY(currentTarget);
        // Cant occupy same spot as target. If try to, will get pushed
        // out in the same direction every time and it looks bad
        // pick a random angle and calculate an offset in that direction
        float randomOffsetAngle = GetRandomReal(0.0f, kTwoPi);
        const float kOffsetRadius = 50.0f;
        float offsetX = kOffsetRadius * Cos(randomOffsetAngle);
        float offsetY = kOffsetRadius * Sin(randomOffsetAngle); 
        // teleport a slight offset away from target
        SetUnitPosition(caster, targetX + offsetX, targetY + offsetY);
        // Might not be in the exact expected position
        // get position after teleport 
        float newCasterX = GetUnitX(caster);
        float newCasterY = GetUnitY(caster);

        // Spawn another blink at caster's new position
        effect postBlinkEffect = AddSpecialEffect(blinkName, newCasterX, newCasterY);

        // Get the diference between the caster and the target 
        float deltaX = targetX - newCasterX;
        float deltaY = targetY - newCasterY;
        // Take the inverse tangent of that difference vector 
        float angleInRadians = Atan2(deltaY, deltaX);
        // and convert it from radians to degrees 
        float angleInDegrees = War3Api.Blizzard.bj_RADTODEG * angleInRadians;
        // Make the caster face the calculated angle 
        SetUnitFacing(caster, angleInDegrees);
        // Have the caster play its attack animation
        SetUnitAnimation(caster, "attack");
        // Sleep to let the caster play its animation
        TriggerSleepAction(kAttackTime);

        // Have the caster deal damage to the enemy 
        UnitDamageTarget(caster, currentTarget, kDamage, true, false,
            ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, null);

        // Create shock effect on damage attached to the target's chest
        effect shockEffect = AddSpecialEffectTarget(shockName, currentTarget, "chest");
        // Scale up shock effect 
        BlzSetSpecialEffectScale(shockEffect, 1.5f);

        // Remove the unit we just considered from the group 
        GroupRemoveUnit(targets, currentTarget);
        // Get the next unit in the group to consider. If the group is
        // empty, this will return null and break out of the while loop
        currentTarget = FirstOfGroup(targets);
        // decrement count 
        count -= 1;

        // Clean up effects 
        DestroyEffect(preBlinkEffect);
        DestroyEffect(postBlinkEffect);
        DestroyEffect(shockEffect);
    }

    // Certain Warcraft 3 types, like groups, need to be cleaned up 
    DestroyGroup(targets);
}

Next

I’m ready to start the main work of the project, hopefully it’ll be much faster in C#. The first thing I’m going to try to do is connect the map building process with the output of my python balancing scripts (cooldowns/damage and healing/implementation). I’ll also take a step back and look at all the spells implemented in the prototype to see the extent to which they can be generalized in code. The overall goal will be creating a spell framework that makes the addition of new spells and tweaking of spell balance values as easy as possible.