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.