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.
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.
staticvoidspellActions()
{
// Range around the caster that targets can be hit from
constfloat kSpellRange = 750;
// The amount of damage each strike deals
constfloat kDamage = 250;
// Max number of targets the spell can hit
constint 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
// 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);
}
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);
}
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.