• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Blaze Guardian v1.1

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
  • Like
Reactions: GywGod133
Description.png

Features
Made in Zinc
Leakless
MUI
Customizable (very)

Requires JNGP to be used, but can be tested with the vanilla WorldEditor if not saved.

Screenshots
Screenshot%201.png
Screenshot%202.png
Screenshot%203.png

Source
JASS:
//! zinc
library BlazeGuardianConfig {
	// The rate at which the internal timer updates everything. Currently it's 32 ticks per second.
	public constant real TIMER_UPDATE_RATE = 0.031250000;


	// =========================================================
	// --------------- Spell Core Configuration ----------------
	// =========================================================
	// The ID of the spell Blaze Guardian in the ability editor.
	public constant integer SPELL_ID = 'A001';

	// The area of effect of the shield.
	// Increases by level and determines the size of the shield made of nodes.
	public function SpellAreaOfEffectByLevel(integer spellLevel) -> real {
		return 450.0 + 50.0 * I2R(spellLevel);
	}

	// The amount of damage dealt to units that are knockbacked when the caster is still charging up the shield.
	// It happens before the shield nodes are created and connected.
	public function SpellKnockbackDamageByLevel(integer spellLevel) -> real {
		return 25.0 * spellLevel;
	}

	// The amount of damage dealt to units who touch the shield.
	public function SpellShieldDamageByLevel(integer spellLevel) -> real {
		return 50.0 * spellLevel;
	}

	// The duration of the shield only. It doesn't take into account the charging duration.
	public function SpellDurationByLevel(integer spellLevel) -> real {
		return 4.0 + spellLevel;
	}
	// The animation played when the caster is casting the spell.
	public constant string SPELL_CASTING_ANIMATION = "Spell Channel";

	// The amount of time between the creation of special effects, from the beginning of the spell
	// until the shield disappears.
	public constant real SPELL_CASTING_SFX_INTERVAL = 0.25;

	// The special effects played during the whole spell, at the position of the caster.
	public constant string SPELL_CASTING_SFX1 = "Abilities\\Weapons\\DemolisherFireMissile\\DemolisherFireMissile.mdl";
	public constant string SPELL_CASTING_SFX2 = "Flame Burst.mdl";

	// Determines who long the charging phase lasts.
	// The charging phase happens after the spell is cast and before the fire waves.
	public constant real SPELL_CHARGE_DURATION = 1.0;

	// Determines the Attack Type when dealing damage to units.
	// Considering the game constants haven't been changed, ATTACK_TYPE_CHAOS will ensure
	// the determined amount of damage is dealt in total, ignoring the target defense rating.
	public constant attacktype SPELL_ATTACK_TYPE = ATTACK_TYPE_CHAOS;

	// Determines the Damage Type when dealing damage to units.
	public constant damagetype SPELL_DAMAGE_TYPE = DAMAGE_TYPE_UNIVERSAL;



	// =========================================================
	// ---------------- Fire Wave Configuration ----------------
	// =========================================================
	// The number of fire waves played before the node shield is created.
	public constant integer FIRE_WAVE_COUNT = 2;

	// The amount of time between each fire wave.
	public constant real FIRE_WAVE_DELAY = 0.8;

	// The model of the projectiles that make up the fire waves.
	public constant string FIRE_WAVE_PROJECTILE_SFX = "Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl";

	// The scale size of the projectiles. In this case, it means they will be 150% greater in size.
	public constant real FIRE_WAVE_PROJECTILE_SCALE = 1.5;

	// The number of projectiles on each fire wave.
	public constant integer FIRE_WAVE_PROJECTILE_AMOUNT = 18;

	// The movement speed of each projectile, in Wc3 units per second.
	public constant real FIRE_WAVE_PROJECTILE_SPEED = 1200.0 * TIMER_UPDATE_RATE;

	// The maximum distance, in Wc3 units, that each projectile can move.
	public constant real FIRE_WAVE_PROJECTILE_MAX_DISTANCE = 1600.0;

	// The rotation speed of each projectile, in Radians per second.
	public constant real FIRE_WAVE_PROJECTILE_TURN_SPEED = 270.0 * TIMER_UPDATE_RATE * bj_DEGTORAD;



	// =========================================================
	// ------------------ Shield Configuration -----------------
	// =========================================================
	// The number of nodes that compose the shield.
	public constant integer SHIELD_NODE_AMOUNT = 5;

	// The ID of the unit that represents a shield node.
	public constant integer SHIELD_NODE_UNIT_ID = 'h002';

	// The special effects played when the nodes are created.
	public constant string SHIELD_NODE_SPAWN_SFX1 = "Abilities\\Weapons\\DemolisherFireMissile\\DemolisherFireMissile.mdl";
	public constant string SHIELD_NODE_SPAWN_SFX2 = "Flame Burst.mdl";

	// The models that make up the projectiles.
	// These projectiles run from one node to the next one, continuously.
	public constant string SHIELD_NODE_PROJECTILE_SFX1 = "Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl";
	public constant string SHIELD_NODE_PROJECTILE_SFX2 = "Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile.mdl";

	// The scale size of the projectiles in the shield. In this case, it means they will be 175% bigger.
	public constant real SHIELD_NODE_PROJECTILE_SCALE = 1.75;

	// The movement speed of each projectile, in Wc3 units per second.
	public constant real SHIELD_NODE_PROJECTILE_SPEED = 1200.0 * TIMER_UPDATE_RATE;

	// Each node is linked to the next node, in a straight line. Every enemy unit that is close enough to that
	// link will be knocked back.
	// This variable determines the distance, in Wc3 units, that each enemy needs to be in order to be knocked and
	// suffer damage.
	public constant real SHIELD_COLLISION_RADIUS = 50;

	// The ID of the unit that represents the avatar, the creature that is spawned in the position of the caster.
	public constant integer SHIELD_AVATAR_ID = 'h001';

	// Determines the opacity of the avatar. 0 means fully transparent and 255 opaque.
	public constant integer SHIELD_AVATAR_OPACITY = 190;



	// =========================================================
	// ---------------- Knockback Configuration ----------------
	// =========================================================
	// The movement speed that each unit will be knocked back, in Wc3 units per second.
	public constant real KNOCKBACK_SPEED = 1500.0 * TIMER_UPDATE_RATE;

	// The friction factor applied to the knockback speed, making the knockback effect gradually stop.
	public constant real KNOCKBACK_FRICTION_FACTOR = 0.9;

	// The special effect played eveytime a unit moves when being knocked back.
	public constant string KNOCKBACK_SFX = "Small Dust.mdl";



	// =========================================================
	// ------------------ Dummy Configuration ------------------
	// =========================================================
	// The ID of the dummy unit used for projectiles.
	public constant integer DUMMY_ID = 'h000';

	// The ID of the Locust ability to make dummies untargetable.
	public constant integer LOCUST_SKILL_ID = 'Aloc';

	// The ID of the Crow Form ability to change units' flying height.
	public constant integer FLYING_SKILL_ID = 'Amrf';



	// =========================================================
	// ------------------ Other Configuration ------------------
	// =========================================================
	// Determines whether trees are destroyed when the shield is fully created and when units are knocked back.
	// If this is set to false, knocked units will stop when encountering trees.
	public constant boolean DESTROY_TREES = true;

	// The collision radius when units are knocked against trees.
	public constant real TREE_COLLISION_RADIUS = 100.0;

	// The ability ID used to detect if the destructible is a tree.
	public constant integer TREE_HARVEST_ID = 'Ahrl';

	// The order ID to harvest a tree.
    public constant integer TREE_HARVEST_ORDER_ID = 0xD0032;
}
//! endzinc

JASS:
//! 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

Credits
Baleroc by Blizzard Entertainment
Flame Burst by Judash
Small Dust by Wilzirius aka Wizardum
dummy.mdx by Vexorian
Review and bug hunting by IcemanBo

Changelog
  • Changed TIMER_UPDATE_RATE from 0.03s to 0.031250000s;
  • Fixed check on Vexorian's BoundSentinel library;
  • Replaced GetWidgetLife() checks with IsUnitType(unit, UNIT_TYPE_DEAD);
  • Configurable Attack Type and Damage Type when dealing damage to enemies;
  • Removed AngleSpeed field from WaveProjectile struct;
  • Safety checks on the knockback;
  • Fixed issue with tree collision detection and destruction;
  • Changed Death Type of dummy units to "Can't rise. Does not decay.";
  • Small fix on the EntityManager struct;
  • Spell Release;

Keywords:
zinc shield fire blaze guardian
Contents

Blaze Guardian (Map)

Reviews
12th Dec 2015 IcemanBo: For long time as NeedsFix. Rejected. 14:30, 23rd Apr 2015 IcemanBo: Read my post in thread.

Moderator

M

Moderator

12th Dec 2015
IcemanBo: For long time as NeedsFix. Rejected.

14:30, 23rd Apr 2015
IcemanBo: Read my post in thread.
 
Interesting to see zinc submission in spell section.
It was like dead and then suddenly 2 new zinc spells uploaded at same day o.o
  • Please use the Zinc indicator keyword when you start/end with Zinc.
    ^Later I saw you actually did in map code, but not here in spell description. Please equalize.
  • public constant real TIMER_UPDATE_RATE = 0.03;
    -> Recommended timeout for spells is 0.031250000 because it's exactly 32 ticks/second.

  • JASS:
    // To reduce external dependencies I've included a Zinc version of Vexorian's BoundSentinel library.
    // Set this variable to true if you already have that library loaded in your map.
    public constant boolean DISABLE_MAP_LIMITS = false;
    -> You can check for your own if a library exists or not:
    JASS:
    static if not LIBRARY_LibraryName then
    endif
  • I think all your public members can be private.
  • Public methods are not needed. Or make them private or don't use an additional keyword.
    (not private -> like public)
    Only make public methods if really needed. I can see some where you don't.
  • (GetWidgetLife(this.Caster) <= 0.405)
    ^In JASS we have some higher standards for checking if a unit is dead.
    Or use native IsUnitAlive or check of unit type "dead" and UnitTypeId == 0".
  • 360 * bj_DEGTORAD -> 2*bj_PI.
  • PauseUnit function is unacceptable. It has some unwanted actions, so we don't use it.
  • Attack- Damage type should be configurable.
  • For function SetUnitScale only the first parameter matters.
  • this.AngleSpeed = FIRE_WAVE_PROJECTILE_TURN_SPEED;
    It doesn't need to be a member for each instance since it only refers to a constant.
    Just directly use the constant for your operations instead.
  • Your knockback does not do any safety check for pathing. Unit might get in unwanted locations.
  • Your knockback has an other critical possible flaw. The friction is responsible for the knockback to end.
    User might fuck this up easily and game will crash easily with values >= 1.
  • Change death type of dummies to "does not decay and not raise".
  • Timer can be created at static member declaration,
    Integer can be initialized at static member declaration,
    -> no init function needed only for this.
  • You destroy all destructables, not only trees, bro.

It's a really cool spell! Nice concept, well looking visualy, good effects.
Code is actually written well, but there are some critical flaws that should be fixed.
I suggest you to take usage of external systems, for example for DestroyTrees, KnockBack (maybe).
If there already exist libraries that work for you, then use them!
They are usually fine, reduce your spell code, and you don't have to bother with the whole stuff.
 
Level 9
Joined
Sep 5, 2007
Messages
358
Please use the Zinc indicator keyword when you start/end with Zinc.
^Later I saw you actually did in map code, but not here in spell description. Please equalize.
Fixed.

public constant real TIMER_UPDATE_RATE = 0.03;
-> Recommended timeout for spells is 0.031250000 because it's exactly 32 ticks/second.
Fixed, since most people seems to be using that approach nowadays, but I don't see a great difference there.

JASS:
// To reduce external dependencies I've included a Zinc version of Vexorian's BoundSentinel library.
// Set this variable to true if you already have that library loaded in your map.
public constant boolean DISABLE_MAP_LIMITS = false;
-> You can check for your own if a library exists or not:
JASS:
static if not LIBRARY_LibraryName then
endif
Didn't think of that. Fixed and removed the global constant.

I think all your public members can be private.
Public methods are not needed. Or make them private or don't use an additional keyword.
(not private -> like public)
Only make public methods if really needed. I can see some where you don't.
Actually they can't. Some of the other structs use the public fields of the SpellInstance struct.
Also, in the enum functions I use anonymous functions and since they don't have access to the struct instance scope, I have to make them public to be able to access the fields I need.

And another note, in vJass not private, by default is public. On the other hand, in Zinc, it's the opposite. Not using an access modifier, makes the member private by default.
I always include the access modifiers in members because it makes it easier to read and determine what is public and private. Assuming a default can be rather confusing depending on your background and lead to hard-to-find bugs. It's a good coding practice.

(GetWidgetLife(this.Caster) <= 0.405)
^In JASS we have some higher standards for checking if a unit is dead.
Or use native IsUnitAlive or check of unit type "dead" and UnitTypeId == 0".
Fixed that. A few years ago that was the recommended practice I think. Need to catch up.

360 * bj_DEGTORAD -> 2*bj_PI.
Fixed too.

PauseUnit function is unacceptable. It has some unwanted actions, so we don't use it.
What sort of unwanted actions are you talking about?
If I remove those, the caster will freely start attacking nearby units while the spell is still in effect, which in this case is also unwanted.
What good alternatives do I have, by the way?

Attack- Damage type should be configurable.
Done.

For function SetUnitScale only the first parameter matters.
What do you mean? The function requires all three parameters, or are you saying it doesn't matter what values I put in the other two parameters it will always scale proportionally?

this.AngleSpeed = FIRE_WAVE_PROJECTILE_TURN_SPEED;
It doesn't need to be a member for each instance since it only refers to a constant.
Just directly use the constant for your operations instead.
True, I guess I missed that one. Fixed.

Your knockback does not do any safety check for pathing. Unit might get in unwanted locations.
Your knockback has an other critical possible flaw. The friction is responsible for the knockback to end.
User might fuck this up easily and game will crash easily with values >= 1.
Safety checking done.

Change death type of dummies to "does not decay and not raise".
Also done.

Timer can be created at static member declaration,
Integer can be initialized at static member declaration,
-> no init function needed only for this.
And done as well.

You destroy all destructables, not only trees, bro.
That was dangerous. Fixed.


It's a really cool spell! Nice concept, well looking visualy, good effects.
Code is actually written well, but there are some critical flaws that should be fixed.
I suggest you to take usage of external systems, for example for DestroyTrees, KnockBack (maybe).
If there already exist libraries that work for you, then use them!
They are usually fine, reduce your spell code, and you don't have to bother with the whole stuff.
Thanks a lot! I'm surprised since the standards here are actually pretty high, but I'm really glad you like it.

Ahh I know, but I wanted to make this into a neaty package that could be easily installed anywhere without having to depend on third-party code.
Also it makes for good practice, since I don't code in wc3 for a long time. :)
 
About private/public,... I accept your explaination. It's fine.

Fixed that. A few years ago that was the recommended practice I think. Need to catch up.
Aah, also not all newer guys remember to use it, no worries.

are you saying it doesn't matter what values I put in the other two parameters it will always scale proportionally?
Yes, the two other paramaters for scaling won't change the result. So you can let them go.

KnockBack can push units to unwanted locations. They can stuck inside cliffs, for example.

What good alternatives do I have, by the way?
Hm, stun for example.
 
Top