DDD ZenScript Examples - yeelp/Distinct-Damage-Descriptions GitHub Wiki

This page covers a set of examples on how you can use ZenScript with DDD to do some really cool things! It presents the examples in a walkthrough style guide.

This page also assumes the following:

  • You understand the basics of how DDD works. If you don't, the Config and Damage Calculation Walkthrough are good primers.
  • You know the basics of scripting with ZenScript in CraftTweaker. The 1.12.2 wiki for CraftTweaker is here.
  • You're familiar with the CraftTweaker features added by DDD. This wiki contains articles on all of those features.

Skeleton Lightning Arrows

For this example, we'll give Skeletons the ability to "shoot lightning arrows" that inflict lightning damage instead of its regular damage, but only under certain conditions:

  • The weather has to be thundering.
  • The Skeleton and its target has to be in the rain or in water.

To start, we'll create a new .zs file. You can name it whatever you like. We'll be using ContentTweaker to create a distribution and we'll define the context of that distribution to match the conditions we mentioned above. We'll use the DistributionBuilder to create our distribution, and we'll assign a weight of 1 (100%) to the built-in lightning damage.

#loader contenttweaker
#modloaded distinctdamagedescriptions

val dist = mods.ddd.distributions.DistributionBuilder.create("lightningArrow");

dist.setWeight("lightning", 1);

That's the easy part, Now we need to set the distribution's context using the isContextApplicable ZenSetter. Remember, the distribution's context function is a function that returns true if the distribution should be used and false if it shouldn't. It takes this distribution (an ICustomDistribution), the IDamageSource involved and the attacked IEntityLivingBase (in this case, it will effectively represent the target our Skeleton is shooting). For this example, we'll assign the following names to these three parameters:

  • thisDist will represent the ICustomDistribution.
  • src will represent the IDamageSource.
  • target will represent the attacked IEntityLivingBase.

We'll set up what are called "guard statements". If you want to check a series of conditions (like the conditions for our context function), using a series of nested if-else blocks can make the code harder to read. Guard statements fix that; instead of checking for the conditions we're looking for, we check for the opposite. Since our context function returns false when we don't want this distribution to apply, we'll use guard statements and return false. This may sound confusing - it's helpful to see it in action. We begin by handling each condition one at a time.

To determine if it is thundering, we're going to need some world info. Thankfully, all Entities keep a reference to their World, so we can take that route to determine if it is thundering. We also need to be weary because what if it is thundering in the Overworld but we're in the Nether? Will that make a difference? In order to avoid the whole fiasco, we'll add an additional check to make sure we're in what's called a surface world. Surface worlds are worlds like the Overworld, but not the Nether. This check might not be needed, but it doesn't hurt to have it just in case. If you have additional mods that add extra dimensions, this check may help this distribution from being use in dimensions where it really shouldn't activate.

if(!target.world.isSurfaceWorld() || !target.world.worldInfo.isThundering()) {
    return false;
}

This is our guard statement. If the target's world is NOT a surface world (so, if it isn't like the Overworld) or if it ISN'T thundering, then we definitely don't need to use this distribution, so we'll return false and be done with it. If it IS a surface world and it is thundering, then script execution won't enter this if block and it'll keep chugging along to the next part of our script. We've effectively filtered out the conditions of "not surface world" and "not thundering" from our context!

Next, lets check to make sure our Skeleton is the cause. A Skeleton will be the cause if the source is projectile damage (it shot the target) and the Skeleton was the true source (the immediate source would be the arrow that hit the target). We'll check the id of the IEntityDefinition of the target that was hit against the id of a Skeleton to see if they match, again with guard statements (the skeleton check could probably be done a bunch of different ways, this is just one of them).

if(!src.isProjectile() || src.trueSource.definition.id != <entity:minecraft:skeleton>.id) {
    return false;
}

Lastly, the check for in rain or water. Thankfully, Entities track if they are wet on their own, so there's no additional work to do. Just check if both the Skeleton and the target are wet.

if(!src.trueSource.isWet() || !target.isWet()) {
    return false;
}

If execution passes those three guard statements, then that means the target is in a surface world that is thundering, the damage the target took came from an arrow shot by a Skeleton and both the Skeleton and the target are wet (so they must be in the rain or in water). That sounds like our context! After the third guard statement, we can just return true. Remember to build() the distribution after or it won't work in game!

Altogether, our script looks like this:

#loader contenttweaker
#modloaded distinctdamagedescriptions

val dist = mods.ddd.distributions.DistributionBuilder.create("lightningArrow");

dist.setWeight("lightning", 1);
dist.isContextApplicable = function(thisDist, src, target) {
    //Check for thundering, also check for surface world (like the Overworld)
    if(!target.world.isSurfaceWorld() || !target.world.worldInfo.isThundering()) {
        return false;
    }
    //check for projectile damage, and if a Skeleton was the cause
    if(!src.isProjectile() || src.trueSource.definition.id != <entity:minecraft:skeleton>.id) {
        return false;
    }
    //check if both targets are wet
    if(!src.trueSource.isWet() || !target.isWet()) {
        return false;
    }
    return true;
};
dist.build();

You could extends this script with additional checks for Strays and Wither Skeletons for extra functionality!

Mobs Immune to Diamond Weapons

For our next example, lets make certain mobs immune to diamond weapons. For this, we'll be creating creature types to lump the mobs we want immune together and then we'll use a GatherDefensesEvent to make give these mobs temporary immunity when hit with diamond weapons.

For an added challenge, let's also make baby zombies immune to diamond weapons, but not adult zombies (Spoiler: we'll use a separate creature type for this!).

Let's define the diamond weapons using an IIngredient.

val diamondWeapons = <minecraft:diamond_sword> | <minecraft:diamond_axe> | <minecraft:diamond_pickaxe> | <minecraft:diamond_shovel> | <minecraft:diamond_hoe>;

Next, lets define an array of the mobs we want immune. This is useful because we can edit the script to add and remove mobs really easily! NOTE: Since we're casting an array type here (which we need to do when we create an array) we MUST import the type we are casting to! (in this case crafttweaker.entity.IEntityDefinition)

val diamondImmuneEntities = [<entity:minecraft:creeper>, <entity:minecraft:sheep>, <entity:minecraft:chicken>, <entity:minecraft:stray>] as IEntityDefinition[];

For the baby zombie, we'll create a separate array for mobs that should only be immune when babies. This example will only have one entry in this array, making the array superfluous, but it's better here because we can extend the functionality by adding more mobs to the list really easily.

val diamondImmuneChildOnly = [<entity:minecraft:zombie>] as IEntityDefinition[];

Next, we'll create the creature types we need. Remember, if you reference a creature type that doesn't exist, DDD will make a new one for you, so we'll use that to our advantage by pretending the creature types we want already exist. We'll use a loop to add each entity to the creature type.

for def in diamondImmuneEntities {
    <dddcreaturetype:diamondimmune>.addEntityToType(def);
}
for def in diamondImmuneChildOnly {
    <dddcreatureType:diamondimmune>.addEntityToType(def);
    <dddcreatureType:diamondimmunechild>.addEntityToType(def);
}

Zombies get added to both the diamond immune type and a second type, which we'll use as an indicator later to signify we need to check if they are a baby.

Next up, our event handle. We'll need to check if the attacker is attacking with a diamond weapon and if the defender is diamond immune. If so, we'll grant the defender immunity to all of DDD's damage types.

We need a check to make sure the attacker is an IEntityLivingBase. The attacker ZenGetter from IDDDEvent returns an IEntity, so we need a check and a cast to IEntityLivingBase. We'll also save the defender in a variable as well so we don't have to type event.defender all the time.

if(event.attacker instanceof IEntityLivingBase) {
    val attacker as IEntityLivingBase = event.attacker;
    val defender = event.defender;
    ...
}

To check if the attacker is attacking with a diamond weapon, we can use our IIngredient from earlier. We need to be careful though! If our attacker isn't holding anything, we'll get a NullPointerException when we try to make the match! We can add an additional check at the beginning to check if the main hand item is null, and then check against our IIngredient. We'll also add the check for the diamond immune creature type here as well. We can combine all three conditions using logical AND (&&).

if(!isNull(attacker.mainHandHeldItem) && diamondWeapons.matches(attacker.mainHandHeldItem) && defender.creatureType has <dddcreaturetype:diamondimmune>) {
    ...
}

This works because of a little trick called "short circuiting". First, ZenScript will do the !isNull(attacker.mainHandHeldItem) check. If that evaluates to true, then the attacker is holding an item. If it evaluates to false, then the entire if condition will evaluate to false because false && <anything> == false. We say that false is a "zero of conjunction" (the operator for logical AND is called conjunction). "AND-ing" with false is exactly like multiplying by zero. ZenScript knows that anything "AND-ed" with false is false, so it actually skips evaluating the rest of the condition! This is good, because if it continued, it'd run into a NullPointerException when matching an empty item against our IIngredient!

Next, we'll have our additional check for the baby zombie. What we want is "If the defender is a diamond immune child, then only proceed if they actually are a child." We can use the condition !(defender.creatureType has <dddcreaturetype:diamondimmunechild>) || defender.isChild to accomplish that. You can verify for yourself this works (|| is logical OR, also called disjunction). If the mob is a diamond immune child, then the left side of the disjunction is false and so the whole condition only evaluates to true if the right side is true. The right side checks if the mob is a child. If the mob isn't a diamond immune child, the left side of the disjunction evaluates to true and the condition "short circuits" once again since anything "OR-ed" with true is true. Now we can iterate over all the registered DDD types and grant the mob immunity to those types.

Our final script looks like this:

#modloaded distinctdamagedescriptions
import mods.ddd.events.DDDEvents;
import crafttweaker.entity.IEntityDefinition;
import crafttweaker.entity.IEntityLivingBase;
import crafttweaker.item.IItemStack;

val diamondWeapons = <minecraft:diamond_sword> | <minecraft:diamond_axe> | <minecraft:diamond_pickaxe> | <minecraft:diamond_shovel> | <minecraft:diamond_hoe>; 
val diamondImmuneEntities = [<entity:minecraft:creeper>, <entity:minecraft:sheep>, <entity:minecraft:chicken>, <entity:minecraft:stray>] as IEntityDefinition[];
val diamondImmuneChildOnly = [<entity:minecraft:zombie>] as IEntityDefinition[];

for def in diamondImmuneEntities {
    <dddcreaturetype:diamondimmune>.addEntityToType(def);
}
for def in diamondImmuneChildOnly {
    <dddcreatureType:diamondimmune>.addEntityToType(def);
    <dddcreatureType:diamondimmunechild>.addEntityToType(def);
}

mods.ddd.events.DDDEvents.onGatherDefenses(function(event as mods.ddd.events.GatherDefensesEvent) {
    if(event.attacker instanceof IEntityLivingBase) {
        val attacker as IEntityLivingBase = event.attacker;
        val defender = event.defender;
        if(!isNull(attacker.mainHandHeldItem) && diamondWeapons.matches(attacker.mainHandHeldItem) && defender.creatureType has <dddcreaturetype:diamondimmune>) {
            if(!(defender.creatureType has <dddcreaturetype:diamondimmunechild>) || defender.isChild) {
                for type in mods.ddd.damagetypes.IDDDDamageType.getAllTypes() {
                    event.grantImmunity(type);
                }
            }
        }
    }
});

Also note that since this is immunity provided with DDD, the Sly Strike enchantment works as you'd expect. Putting Sly Strike on a diamond weapon will allow that diamond weapon to damage diamond immune mobs!

True Damage

In this example, we'll create a "true" damage type - a damage type that will bypass all mob resistances and armor effectiveness. We'll define the context where this true damage will occur.

Our context will be a charged Creeper explosion. We want 50% of the damage from a charged Creeper to be this "true" damage that will basically be unblockable.

We'll need to create both the damage type and the distribution. Thankfully, we can do both within the same .zs file.

First, let's create the damage type using ContentTweaker and DDD's DamageTypeBuilder. Since this damage type isn't going to appear on tooltips, we only really need the bare minimum information to create this type. Just for completeness, we'll give it a display name for debugging purposes.

#loader contenttweaker
#modloaded distinctdamagedescriptions

val damageType = mods.ddd.DamageTypeBuilder.create("true");
damageType.displayName = "True";
damageType.register();

Next, we need to define the distribution for the charged Creeper explosion. Just like the example with the Skeleton Lightning Arrows, we will use the DistributionBuilder.

val trueDist = mods.ddd.distributions.DistributionBuilder.create("trueexplosion");
trueDist.setWeight("true", 0.5);
trueDist.setWeight("bludgeoning", 0.5);

We've defined the distribution weights for 50% bludgeoning and 50% of this "true" damage. Now we need to define the context. Again, much like the Skeleton Lightning Arrows, we'll define the context using a series of guard statements; statements that check for the conditions where the distribution would not apply and return false if those checks succeed. To check for a charged Creeper explosion, we can break it down to the following series of checks.

  • The damage source is an explosion.
  • The damage source's true or immediate source was caused by a Creeper.
  • The Creeper was powered.

For the context function, let's use the following parameter names:

Checking for an explosion is easy. The IDamageSource has a ZenGetter for it. So our guard will check if there is no explosion, and return false.

if(!src.explosion) {
    return false;
}

Next, we need to check and make sure that there is an IEntityLivingBase as the source before checking if it is a Creeper. If the true source is an IEntityLivingBase, we'll cast it to that type, for use later. Since we casted to IEntityLivingBase, we need to add an import statement at the top.

if(!(src.trueSource instanceof IEntityLivingBase)) {
    return false;
}
val mob as IEntityLivingBase = src.trueSource;

Now we check if the entity is a creeper using the id field of the true source's IEntityDefinition.

if(mob.definition.id != <entity:minecraft:creeper>.id) {
    return false;
}

Now we need a little bit of extra knowledge on how Creeper's store their powered state. Checking the Minecraft Wiki on Creepers, if a Creeper becomes a charged Creeper, there is a flag in the NBT data for that Creeper that gets set. That flag is called the powered flag. HOWEVER, naturally spawned Creepers don't have this flag at all (this was discovered by looking at the decompiled Minecraft source code, but could be found out via trial and error) in their NBT data. So we need to check to see if that flag exists in the true source's NBT data before we check to see if it is true or not. We can return the result of checking if the Creeper is powered or not after we check if the flag exists.

if(isNull(mob.nbt.powered)) {
    return false;
}
return mob.nbt.powered.asBool();

To be safe, lets import IData in case we need it when casting the powered flag to DataBool. Don't forget the build() call at the end!

Our end script looks like this:

#loader contenttweaker
#modloaded distinctdamagedescriptions
import crafttweaker.entity.IEntityLivingBase;
import crafttweaker.data.IData;

val damageType = mods.ddd.DamageTypeBuilder.create("true");
damageType.displayName = "True";
damageType.register();

val trueDist = mods.ddd.distributions.DistributionBuilder.create("trueexplosion");
trueDist.setWeight("true", 0.5);
trueDist.setWeight("bludgeoning", 0.5);

trueDist.isContextApplicable = function(thisDist, src, target) {
    if(!src.explosion) {
        return false;
    }
    if(!(src.trueSource instanceof IEntityLivingBase)) {
        return false;
    }
    val mob as IEntityLivingBase = src.trueSource;
    if(mob.definition.id != <entity:minecraft:creeper>.id) {
        return false;
    }
    if(isNull(mob.nbt.powered)) {
        return false;
    }
    return mob.nbt.powered.asBool();
};
trueDist.build();

Now some mods may add additional creeper variants. What about them? If the creeper variant can be powered, the same flag is likely to be used so there's no problem there. However, we can create a CreatureTypeDefinition that represents all the creepers we have in our pack, and add all those creepers to that type. Then instead of checking the IEntityDefinition id, we can instead check the mob for its creature type to see if it has this "creeper" creature type.

Enchantment Damage

In this example, we'll have the Smite enchantment cause a weapon to do more radiant damage. This won't add extra damage; we'll just alter the damage it inflicts on hit. As of 1.7.0, this is better accomplished by using an ItemModifierBuilder instead.

We'll use DDD's DetermineDamageEvent to change the damage that gets inflicted if Smite is present. We'll effectively shift the item's damage distribution to be 20% "more" radiant damage per level of Smite.

We begin with an event handle for the DetermineDamageEvent.

mods.ddd.events.DDDEvents.onDetermineDamage(function(event as mods.ddd.events.DetermineDamageEvent) {
    ...
});

In this event handle, we need to check if the attacker is something that can even hold an item - an IEntityLivingBase. We'll cast the event attacker if it is, and grab its weapon and check if it exists and is enchanted.

if(event.attacker instanceof IEntityLivingBase) {
    val attacker as IEntityLivingBase = event.attacker;
    val item = attacker.mainHandHeldItem;
    if(!isNull(item) && item.isEnchanted) {
        ...
    }
}

Now, we iterate over the enchantments on the item, and look for Smite. We can use the Enchantment Bracket Handler and compare the ids of the respective IEnchantmentDefinition. If we find it, we'll record the level of the enchantment and determine our radiant damage amount, which is 20% per level.

NOTE: We use min to cap the radiant damage amount at 100%. We don't want to add extra damage here, we just want to change what the damage is.

for enchant in item.enchantments {
    if(enchant.definition.id == <enchantment:minecraft:smite>.id) {
        val amount = min(1, 0.2*enchant.level);
        ...
    }
}

Now to change the damage, we will iterate over all the DDD damage types that are registered. We'll need to keep a running total of the damage inflicted so we know how much damage was inflicted total. We'll need that later. We'll modify the damage being inflicted to be 1-our radiant damage percentage amount. For example, if we have Smite I, we'll decrease every other type's damage to be 80% of what it was and give that 20% we took off to radiant damage.

var damage = 0;
for type in mods.ddd.damagetypes.IDDDDamageType.getAllTypes() {
    if(event.getDamage(type) > 0) {
        damage += event.getDamage(type);
        event.setDamage(type, (1-amount)*event.getDamage(type));
    }
}
...

Now to give that removed damage to the radiant type. That's as simple as multiplying the damage that radiant does by our radiant damage percentage amount and add it to any existing radiant damage. Again, using Smite I as an example, we'd be multiplying our total damage by 0.2 and adding it to any existing radiant damage.

event.setDamage(<dddtype:radiant>, event.getDamage(<dddtype:radiant>) + amount*damage);

We'll add whatever imports we need and we're done! Our final script looks like this:

#modloaded distinctdamagedescriptions
import crafttweaker.entity.IEntityLivingBase;
import mods.ddd.events.DetermineDamageEvent;

mods.ddd.events.DDDEvents.onDetermineDamage(function(event as mods.ddd.events.DetermineDamageEvent) {
    if(event.attacker instanceof IEntityLivingBase) {
        val attacker as IEntityLivingBase = event.attacker;
        val item = attacker.mainHandHeldItem;
        if(!isNull(item) && item.isEnchanted) {
            for enchant in item.enchantments {
                if(enchant.definition.id == <enchantment:minecraft:smite>.id) {
                    val amount = min(1, 0.2*enchant.level);
                    var damage = 0;
                    for type in mods.ddd.damagetypes.IDDDDamageType.getAllTypes() {
                        if(event.getDamage(type) > 0) {
                            damage += event.getDamage(type);
                            event.setDamage(<dddtype:radiant>, event.getDamage(<dddtype:radiant>) + amount*damage);
                        }
                    }
                    event.setDamage(<dddtype:radiant>, (1+amount)*event.getDamage(<dddtype:radiant>));
                }
            }
        }
    }
});

In practice, let's say we enchant an Iron Sword (80% Slashing, 20% Piercing, 6 damage base) with Smite III. This is the following change:

Type Damage Initially Damage After
Slashing 4.8 1.92
Piercing 1.2 0.48
Radiant 0 3.6
Total 6 6

Note the total damage didn't change, we just inflicted more radiant damage! Now Smite will destroy Zombies and Skeletons with ease given their default radiant weakness!

⚠️ **GitHub.com Fallback** ⚠️