//! zinc
library BlazeGuardian requires BlazeGuardianConfig {
private interface IUpdateable {
integer ID;
method Update() = null;
}
private type UnitEventHandler extends function(unit);
private struct SpellInstance extends IUpdateable {
public static group IgnoreGroup = CreateGroup();
private static SpellInstance Current;
private static group UtilGroup = CreateGroup();
public unit Caster;
public real CasterX;
public real CasterY;
public player Owner;
public real AreaOfEffect;
public real KnockbackDamage;
public real ShieldDamage;
public real ShieldDuration;
public unit Avatar;
private real TimeLeft;
private real SfxCooldown;
private boolean PlayAnimation;
private integer WaveCount;
private Node Nodes[SHIELD_NODE_AMOUNT];
private NodeProjectile NodeProjectiles[SHIELD_NODE_AMOUNT];
private integer Stage;
public static method create(unit caster) -> thistype {
thistype this = thistype.allocate();
integer spellLevel;
this.Caster = caster;
this.CasterX = GetUnitX(caster);
this.CasterY = GetUnitY(caster);
PauseUnit(this.Caster, true);
this.Owner = GetOwningPlayer(this.Caster);
spellLevel = GetUnitAbilityLevel(this.Caster, SPELL_ID);
this.AreaOfEffect = SpellAreaOfEffectByLevel(spellLevel);
this.KnockbackDamage = SpellKnockbackDamageByLevel(spellLevel);
this.ShieldDamage = SpellShieldDamageByLevel(spellLevel);
this.ShieldDuration = SpellDurationByLevel(spellLevel);
this.Stage = 1;
this.TimeLeft = SPELL_CHARGE_DURATION;
this.SfxCooldown = SPELL_CASTING_SFX_INTERVAL;
this.PlayAnimation = true;
this.WaveCount = FIRE_WAVE_COUNT;
EntityManager.Add(this);
return this;
}
public method Update() {
if (IsUnitType(this.Caster, UNIT_TYPE_DEAD)) {
this.destroy();
return;
}
thistype.Current = this;
if (this.Stage == 1) {
this.Stage1();
} else if (this.Stage == 2) {
this.Stage2();
} else if (this.Stage == 3) {
this.Stage3();
}
this.SfxCooldown -= TIMER_UPDATE_RATE;
if (this.SfxCooldown <= 0) {
DestroyEffect(AddSpecialEffect(SPELL_CASTING_SFX1, this.CasterX, this.CasterY));
DestroyEffect(AddSpecialEffect(SPELL_CASTING_SFX2, this.CasterX, this.CasterY));
this.SfxCooldown = SPELL_CASTING_SFX_INTERVAL;
}
}
private method Stage1() {
integer i;
real angle;
// Animations won't play right after the casting the spell so we wait 0.03 seconds to play the
// channel animation.
if (this.PlayAnimation) {
SetUnitAnimation(this.Caster, SPELL_CASTING_ANIMATION);
this.PlayAnimation = false;
}
this.TimeLeft -= TIMER_UPDATE_RATE;
if (this.TimeLeft <= 0) {
this.Stage = 2;
this.TimeLeft = FIRE_WAVE_DELAY;
}
}
private method Stage2() {
integer i, j;
real angle, x, y, facing;
this.TimeLeft -= TIMER_UPDATE_RATE;
if (this.TimeLeft <= 0) {
if (this.WaveCount > 0) {
this.TimeLeft = FIRE_WAVE_DELAY;
this.WaveCount -= 1;
angle = (2.0 * bj_PI) / FIRE_WAVE_PROJECTILE_AMOUNT;
for (i = 0; i < FIRE_WAVE_PROJECTILE_AMOUNT; i += 1) {
WaveProjectile.create(this.CasterX, this.CasterY, angle * i, FIRE_WAVE_PROJECTILE_MAX_DISTANCE);
}
GroupEnumUnitsInRange(thistype.UtilGroup, this.CasterX, this.CasterY, this.AreaOfEffect, function() -> boolean {
unit filterUnit = GetFilterUnit();
Knockback knock;
if (!IsUnitInGroup(filterUnit, thistype.IgnoreGroup) && !IsUnitType(filterUnit, UNIT_TYPE_DEAD) && IsUnitEnemy(filterUnit, Current.Owner) && !IsUnitType(filterUnit, UNIT_TYPE_STRUCTURE)) {
GroupAddUnit(thistype.IgnoreGroup, filterUnit);
UnitDamageTarget(Current.Caster, filterUnit, Current.KnockbackDamage, true, true, SPELL_ATTACK_TYPE, SPELL_DAMAGE_TYPE, WEAPON_TYPE_WHOKNOWS);
knock = Knockback.create(filterUnit, Atan2(GetUnitY(filterUnit) - Current.CasterY, GetUnitX(filterUnit) - Current.CasterX));
knock.OnEnd = function(unit target) {
GroupRemoveUnit(thistype.IgnoreGroup, target);
};
}
filterUnit = null;
return false;
});
} else {
this.Stage = 3;
facing = GetUnitFacing(this.Caster) * bj_DEGTORAD;
this.Avatar = CreateUnit(this.Owner, SHIELD_AVATAR_ID, this.CasterX, this.CasterY, facing * bj_RADTODEG);
PauseUnit(this.Avatar, true);
SetUnitAnimation(this.Avatar, "Stand Swim");
x = this.CasterX - 50.0 * Cos(facing);
y = this.CasterY - 50.0 * Sin(facing);
SetUnitX(this.Avatar, x);
SetUnitY(this.Avatar, y);
SetUnitVertexColor(this.Avatar, 255, 255, 255, 0);
FadeInEffect.create(this.Avatar, 0.25, SHIELD_AVATAR_OPACITY);
angle = (360.0 * bj_DEGTORAD) / SHIELD_NODE_AMOUNT;
for (i = 0; i < SHIELD_NODE_AMOUNT; i += 1) {
x = this.CasterX + this.AreaOfEffect * Cos(facing + angle * i);
y = this.CasterY + this.AreaOfEffect * Sin(facing + angle * i);
this.Nodes[i] = Node.create(x, y);
}
for (i = 0; i < SHIELD_NODE_AMOUNT; i += 1) {
j = i + 1;
if (j >= SHIELD_NODE_AMOUNT) {
j = 0;
}
this.Nodes[i].NextNode = this.Nodes[j];
this.NodeProjectiles[i] = NodeProjectile.create(this.Nodes[i]);
}
static if (DESTROY_TREES) {
Utils.DestroyTrees(this.CasterX, this.CasterY, this.AreaOfEffect);
}
}
}
}
private method Stage3() {
this.ShieldDuration -= TIMER_UPDATE_RATE;
if (this.ShieldDuration > 0) {
GroupEnumUnitsInRange(thistype.UtilGroup, this.CasterX, this.CasterY, this.AreaOfEffect + 100, function() -> boolean {
integer i;
real x, y, angle;
unit filterUnit = GetFilterUnit();
Knockback knock;
if (!IsUnitInGroup(filterUnit, thistype.IgnoreGroup) && !IsUnitType(filterUnit, UNIT_TYPE_DEAD) && IsUnitEnemy(filterUnit, Current.Owner) && !IsUnitType(filterUnit, UNIT_TYPE_STRUCTURE)) {
x = GetUnitX(filterUnit);
y = GetUnitY(filterUnit);
if (Current.CollidesWithShield(x, y)) {
GroupAddUnit(thistype.IgnoreGroup, filterUnit);
angle = Atan2(y - Current.CasterY, x - Current.CasterX);
UnitDamageTarget(Current.Caster, filterUnit, Current.ShieldDamage, true, true, SPELL_ATTACK_TYPE, SPELL_DAMAGE_TYPE, WEAPON_TYPE_WHOKNOWS);
knock = Knockback.create(filterUnit, angle);
knock.OnEnd = function(unit target) {
GroupRemoveUnit(thistype.IgnoreGroup, target);
};
}
}
filterUnit = null;
return false;
});
} else {
this.destroy();
}
}
private method CollidesWithShield(real x, real y) -> boolean {
integer i;
real dist1, dist2, dist3;
for (i = 0; i < SHIELD_NODE_AMOUNT; i += 1) {
dist1 = SquareRoot((x - this.Nodes[i].X) * (x - this.Nodes[i].X) + (y - this.Nodes[i].Y) * (y - this.Nodes[i].Y));
dist2 = SquareRoot((this.Nodes[i].NextNode.X - x) * (this.Nodes[i].NextNode.X - x) + (this.Nodes[i].NextNode.Y - y) * (this.Nodes[i].NextNode.Y - y));
dist3 = SquareRoot((this.Nodes[i].NextNode.X - this.Nodes[i].X) * (this.Nodes[i].NextNode.X - this.Nodes[i].X) + (this.Nodes[i].NextNode.Y - this.Nodes[i].Y) * (this.Nodes[i].NextNode.Y - this.Nodes[i].Y));
if ((dist1 + dist2 - dist3) <= SHIELD_COLLISION_RADIUS) {
return true;
}
}
return false;
}
private method onDestroy() {
integer i;
if (this.Avatar != null) {
FadeOutEffect.create(this.Avatar, 0.5, SHIELD_AVATAR_OPACITY);
}
EntityManager.Remove(this);
PauseUnit(this.Caster, false);
SetUnitAnimation(this.Caster, "stand");
for (i = 0; i < SHIELD_NODE_AMOUNT; i += 1) {
this.NodeProjectiles[i].destroy();
this.Nodes[i].destroy();
}
}
}
private struct WaveProjectile extends IUpdateable {
private unit Dummy;
private real X;
private real Y;
private effect Sfx;
private real Distance;
private real Angle;
private real AngleCos;
private real AngleSin;
public static method create(real x, real y, real angle, real distance) -> thistype {
thistype this = thistype.allocate();
this.Dummy = DummyPool.Get();
this.X = x;
this.Y = y;
this.Sfx = AddSpecialEffectTarget(FIRE_WAVE_PROJECTILE_SFX, this.Dummy, "chest");
SetUnitScale(this.Dummy, FIRE_WAVE_PROJECTILE_SCALE, FIRE_WAVE_PROJECTILE_SCALE, FIRE_WAVE_PROJECTILE_SCALE);
SetUnitX(this.Dummy, x);
SetUnitY(this.Dummy, y);
this.Distance = distance;
this.Angle = angle;
EntityManager.Add(this);
return this;
}
public method Update() {
this.Angle += FIRE_WAVE_PROJECTILE_TURN_SPEED;
SetUnitFacing(this.Dummy, this.Angle);
this.X += FIRE_WAVE_PROJECTILE_SPEED * Cos(this.Angle);
this.Y += FIRE_WAVE_PROJECTILE_SPEED * Sin(this.Angle);
SetUnitX(this.Dummy, this.X);
SetUnitY(this.Dummy, this.Y);
this.Distance -= FIRE_WAVE_PROJECTILE_SPEED;
if (this.Distance <= 0) {
this.destroy();
}
}
private method onDestroy() {
DestroyEffect(this.Sfx);
DummyPool.Release(this.Dummy);
EntityManager.Remove(this);
}
}
private struct Knockback extends IUpdateable {
public UnitEventHandler OnEnd;
private unit Target;
private real X;
private real Y;
private real Speed;
private real Friction;
private real AngleCos;
private real AngleSin;
public static method create(unit target, real angle) -> thistype {
thistype this = thistype.allocate();
this.Target = target;
this.X = GetUnitX(this.Target);
this.Y = GetUnitY(this.Target);
if (KNOCKBACK_FRICTION_FACTOR >= 1.0) {
this.Friction = 0.9;
} else {
this.Friction = KNOCKBACK_FRICTION_FACTOR;
}
this.Speed = KNOCKBACK_SPEED;
this.AngleCos = Cos(angle);
this.AngleSin = Sin(angle);
EntityManager.Add(this);
return this;
}
public method Update() {
this.X += this.Speed * AngleCos;
this.Y += this.Speed * AngleSin;
SetUnitX(this.Target, this.X);
SetUnitY(this.Target, this.Y);
DestroyEffect(AddSpecialEffect(KNOCKBACK_SFX, this.X, this.Y));
this.Speed *= this.Friction;
static if (DESTROY_TREES) {
Utils.DestroyTrees(this.X, this.Y, TREE_COLLISION_RADIUS);
} else {
if (Utils.CollidesWithTrees(this.X, this.Y, TREE_COLLISION_RADIUS)) {
this.destroy();
return;
}
}
if (!Utils.IsTerrainLand(this.X, this.Y) || this.Speed < 1.0) {
this.destroy();
}
}
private method onDestroy() {
this.OnEnd.evaluate(this.Target);
EntityManager.Remove(this);
}
}
private struct Node {
public real X;
public real Y;
public thistype NextNode;
private unit Dummy;
public static method create(real x, real y) -> thistype {
thistype this = thistype.allocate();
this.X = x;
this.Y = y;
this.Dummy = CreateUnit(Player(15), SHIELD_NODE_UNIT_ID, this.X, this.Y, 0.0);
DestroyEffect(AddSpecialEffect(SHIELD_NODE_SPAWN_SFX1, this.X, this.Y));
DestroyEffect(AddSpecialEffect(SHIELD_NODE_SPAWN_SFX2, this.X, this.Y));
return this;
}
private method onDestroy() {
FadeOutEffect.create(this.Dummy, 0.5, 255);
}
}
private struct NodeProjectile extends IUpdateable {
private unit Dummy;
private effect Sfx1;
private effect Sfx2;
private effect Sfx3;
private Node Target;
private real X;
private real Y;
private real Distance;
private real AngleCos;
private real AngleSin;
public static method create(Node node) -> thistype {
thistype this = thistype.allocate();
this.Dummy = DummyPool.Get();
this.Sfx1 = AddSpecialEffectTarget(SHIELD_NODE_PROJECTILE_SFX1, this.Dummy, "chest");
this.Sfx2 = AddSpecialEffectTarget(SHIELD_NODE_PROJECTILE_SFX2, this.Dummy, "chest");
this.Sfx3 = AddSpecialEffectTarget(SHIELD_NODE_PROJECTILE_SFX2, this.Dummy, "chest");
this.X = node.X;
this.Y = node.Y;
SetUnitX(this.Dummy, this.X);
SetUnitY(this.Dummy, this.Y);
SetUnitScale(this.Dummy, SHIELD_NODE_PROJECTILE_SCALE, SHIELD_NODE_PROJECTILE_SCALE, SHIELD_NODE_PROJECTILE_SCALE);
SetUnitFlyHeight(this.Dummy, 50.0, 0.0);
this.AssignTarget(node.NextNode);
EntityManager.Add(this);
return this;
}
private method AssignTarget(Node node) {
real diffX, diffY, angle;
this.Target = node;
diffX = this.Target.X - this.X;
diffY = this.Target.Y - this.Y;
this.Distance = SquareRoot(diffX * diffX + diffY * diffY);
angle = Atan2(this.Target.Y - this.Y, this.Target.X - this.X);
this.AngleCos = Cos(angle);
this.AngleSin = Sin(angle);
SetUnitFacing(this.Dummy, angle);
}
public method Update() {
this.X += SHIELD_NODE_PROJECTILE_SPEED * this.AngleCos;
this.Y += SHIELD_NODE_PROJECTILE_SPEED * this.AngleSin;
SetUnitX(this.Dummy, this.X);
SetUnitY(this.Dummy, this.Y);
this.Distance -= SHIELD_NODE_PROJECTILE_SPEED;
if (this.Distance < SHIELD_NODE_PROJECTILE_SPEED) {
this.AssignTarget(this.Target.NextNode);
}
}
private method onDestroy() {
DummyPool.Release(this.Dummy);
DestroyEffect(this.Sfx1);
DestroyEffect(this.Sfx2);
DestroyEffect(this.Sfx3);
EntityManager.Remove(this);
}
}
private struct FadeInEffect extends IUpdateable {
private unit Target;
private integer Alpha;
private integer AlphaTarget;
private integer Amount;
public static method create(unit target, real duration, integer alpha) -> thistype {
thistype this = thistype.allocate();
this.Target = target;
this.Alpha = 0;
this.AlphaTarget = alpha;
if (this.AlphaTarget <= 0 || this.AlphaTarget > 255) {
this.AlphaTarget = 255;
}
this.Amount = R2I(this.AlphaTarget / duration * TIMER_UPDATE_RATE);
SetUnitVertexColor(this.Target, 255, 255, 255, this.Alpha);
EntityManager.Add(this);
return this;
}
public method Update() {
this.Alpha += this.Amount;
SetUnitVertexColor(this.Target, 255, 255, 255, this.Alpha);
if (this.Alpha >= this.AlphaTarget) {
EntityManager.Remove(this);
this.destroy();
}
}
}
private struct FadeOutEffect extends IUpdateable {
private unit Target;
private integer Alpha;
private integer Amount;
public static method create(unit target, real duration, integer alpha) -> thistype {
thistype this = thistype.allocate();
this.Target = target;
this.Alpha = alpha;
if (this.Alpha <= 0 || this.Alpha > 255) {
this.Alpha = 255;
}
this.Amount = R2I(this.Alpha / duration * TIMER_UPDATE_RATE);
SetUnitVertexColor(this.Target, 255, 255, 255, this.Alpha);
EntityManager.Add(this);
return this;
}
public method Update() {
this.Alpha -= this.Amount;
SetUnitVertexColor(this.Target, 255, 255, 255, this.Alpha);
if (this.Alpha < this.Amount) {
EntityManager.Remove(this);
RemoveUnit(this.Target);
this.destroy();
}
}
}
private struct EntityManager {
private static timer Timer = CreateTimer();
private static IUpdateable Entities[];
private static integer Count = 0;
public static method Add(IUpdateable entity) {
integer index = Count;
Entities[index] = entity;
entity.ID = index;
Count += 1;
if (index == 0) {
TimerStart(Timer, TIMER_UPDATE_RATE, true, function thistype.Update);
}
}
public static method Remove(IUpdateable entity) {
if (entity.ID >= 0 && entity.ID < Count) {
Count -= 1;
Entities[entity.ID] = Entities[Count];
Entities[entity.ID].ID = entity.ID;
entity.ID = -1;
if (Count == 0) {
PauseTimer(Timer);
}
}
}
private static method Update() {
integer i;
for (i = 0; i < Count; i += 1) {
Entities[i].Update();
}
debug {
ClearTextMessages();
BJDebugMsg("Count: " + I2S(thistype.Count));
}
}
}
private struct DummyPool {
private static unit Pool[];
private static integer Count = 0;
// Forces the creation of a new dummy unit, when facing angle is important
public static method New(real facingAngle) -> unit {
unit dummy = CreateUnit(Player(15), DUMMY_ID, 0.0, 0.0, facingAngle);
UnitAddAbility(dummy, FLYING_SKILL_ID);
UnitAddAbility(dummy, LOCUST_SKILL_ID);
UnitRemoveAbility(dummy, FLYING_SKILL_ID);
PauseUnit(dummy, true);
ShowUnit(dummy, true);
return dummy;
}
// Gets a dummy unit from the pool.
public static method Get() -> unit {
unit dummy;
if (Count == 0) {
dummy = New(0.0);
} else {
Count -= 1;
dummy = Pool[Count];
Pool[Count] = null;
ShowUnit(dummy, true);
}
return dummy;
}
// Release a dummy unit back to the pool
public static method Release(unit dummy) {
SetUnitOwner(dummy, Player(15), true);
SetUnitScale(dummy, 1.0, 1.0, 1.0);
SetUnitTimeScale(dummy, 1.0);
SetUnitFlyHeight(dummy, 0.0, 0.0);
SetUnitVertexColor(dummy, 255, 255, 255, 255);
ShowUnit(dummy, false);
Pool[Count] = dummy;
Count += 1;
}
private static method onInit() {
integer i;
for (i = 0; i < 30; i += 1) {
Pool[Count] = New(0.0);
Count += 1;
}
}
}
private struct Utils {
private static boolean Result;
private static unit Dummy;
public static method DestroyTrees(real x, real y, real radius) {
rect area = Rect(x - radius, y - radius, x + radius, y + radius);
EnumDestructablesInRect(area, null, function() {
destructable tree = GetEnumDestructable();
if (GetDestructableLife(tree) > 0.405) {
KillDestructable(tree);
}
tree = null;
});
RemoveRect(area);
area = null;
}
public static method CollidesWithTrees(real x, real y, real collision) -> boolean {
rect area = Rect(x - collision, y - collision, x + collision, y + collision);
thistype.Result = false;
EnumDestructablesInRect(area, null, function() {
destructable tree = GetEnumDestructable();
if (GetDestructableLife(tree) > 0.405 && IssueTargetOrderById(thistype.Dummy, TREE_HARVEST_ORDER_ID, tree)) {
thistype.Result = true;
}
tree = null;
});
RemoveRect(area);
area = null;
return thistype.Result;
}
public static method IsTerrainLand(real x, real y) -> boolean {
return !IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY);
}
private static method onInit() {
thistype.Dummy = DummyPool.Get();
ShowUnit(thistype.Dummy, false);
UnitAddAbility(thistype.Dummy, TREE_HARVEST_ID);
}
}
// Based on BoundSentinel by Vexorian
private struct MapLimits {
private static real BoundsMinX;
private static real BoundsMinY;
private static real BoundsMaxX;
private static real BoundsMaxY;
private static method onInit() {
trigger trig;
region reg;
rect bounds;
rect rc;
static if (!LIBRARY_BoundSentinel) {
trig = CreateTrigger();
reg = CreateRegion();
bounds = GetWorldBounds();
BoundsMinX = GetRectMinX(bounds);
BoundsMinY = GetRectMinY(bounds);
BoundsMaxY = GetRectMaxY(bounds);
BoundsMaxY = GetRectMaxY(bounds);
rc = Rect(BoundsMinX, BoundsMinY, BoundsMaxX, BoundsMaxY);
RegionAddRect(reg, rc);
TriggerRegisterLeaveRegion(trig, reg, null);
TriggerAddCondition(trig, Condition(function() -> boolean {
unit u = GetTriggerUnit();
real x = GetUnitX(u);
real y = GetUnitY(u);
if (x > BoundsMaxX){
x = BoundsMaxX;
} else if (x < BoundsMinX) {
x = BoundsMinX;
}
if (y > BoundsMaxY) {
y = BoundsMaxY;
} else if (y < BoundsMinY) {
y = BoundsMinY;
}
SetUnitX(u, x);
SetUnitY(u, y);
u = null;
return false;
}));
RemoveRect(rc);
RemoveRect(bounds);
rc = null;
bounds = null;
reg = null;
trig = null;
}
}
}
private function onInit() {
trigger spellCastTrigger = CreateTrigger();
integer i;
for (i = 0; i < 12; i += 1) {
TriggerRegisterPlayerUnitEvent(spellCastTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null);
}
TriggerAddCondition(spellCastTrigger, function() -> boolean { return GetSpellAbilityId() == SPELL_ID; } );
TriggerAddAction(spellCastTrigger, function() {
SpellInstance.create(GetTriggerUnit());
});
spellCastTrigger = null;
}
}
//! endzinc