diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/CatapultSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/CatapultSpell.java
new file mode 100644
index 00000000..a3f85b2c
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/CatapultSpell.java
@@ -0,0 +1,101 @@
+package com.minelittlepony.unicopia.ability.magic.spell.effect;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import com.minelittlepony.unicopia.ability.magic.Caster;
+import com.minelittlepony.unicopia.ability.magic.spell.ProjectileSpell;
+import com.minelittlepony.unicopia.ability.magic.spell.Situation;
+import com.minelittlepony.unicopia.ability.magic.spell.trait.SpellTraits;
+import com.minelittlepony.unicopia.ability.magic.spell.trait.Trait;
+import com.minelittlepony.unicopia.projectile.MagicProjectileEntity;
+import com.minelittlepony.unicopia.util.RayTraceHelper;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.FallingBlockEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.EntityHitResult;
+import net.minecraft.util.hit.HitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+
+public class CatapultSpell extends AbstractSpell implements ProjectileSpell {
+    public static final SpellTraits DEFAULT_TRAITS = new SpellTraits.Builder()
+            .with(Trait.FOCUS, 50)
+            .with(Trait.KNOWLEDGE, 1)
+            .with(Trait.EARTH, 60)
+            .with(Trait.STRENGTH, 50)
+            .build();
+
+    private static final float HORIZONTAL_VARIANCE = 0.25F;
+    private static final float MAX_STRENGTH = 120;
+
+    protected CatapultSpell(SpellType<?> type, SpellTraits traits) {
+        super(type, traits);
+    }
+
+    @Override
+    public void onImpact(MagicProjectileEntity projectile, BlockPos pos, BlockState state) {
+        if (!projectile.isClient()) {
+            createBlockEntity(projectile.world, pos, e -> apply(projectile, e));
+        }
+    }
+
+    @Override
+    public void onImpact(MagicProjectileEntity projectile, Entity entity) {
+        if (!projectile.isClient()) {
+            apply(projectile, entity);
+        }
+    }
+
+    @Override
+    public boolean tick(Caster<?> caster, Situation situation) {
+        if (situation == Situation.PROJECTILE) {
+            return true;
+        }
+
+        getTarget(caster, e -> apply(caster, e));
+        return situation == Situation.PROJECTILE;
+    }
+
+    protected void apply(Caster<?> caster, Entity e) {
+        Vec3d vel = caster.getEntity().getVelocity();
+        e.addVelocity(
+                ((caster.getWorld().random.nextFloat() * HORIZONTAL_VARIANCE) - HORIZONTAL_VARIANCE + vel.x * 0.8F) * 0.1F,
+                0.1F + (getTraits().get(Trait.STRENGTH, -MAX_STRENGTH, MAX_STRENGTH) - 50) / 16D,
+                ((caster.getWorld().random.nextFloat() * HORIZONTAL_VARIANCE) - HORIZONTAL_VARIANCE + vel.z * 0.8F) * 0.1F
+        );
+        e.world.spawnEntity(e);
+    }
+
+    protected void getTarget(Caster<?> caster, Consumer<Entity> apply) {
+        if (caster.isClient()) {
+            return;
+        }
+
+        double maxDistance = 2 + (getTraits().get(Trait.FOCUS) - 50) * 8;
+
+        HitResult ray = RayTraceHelper.doTrace(caster.getMaster(), maxDistance, 1, EntityPredicates.EXCEPT_SPECTATOR).getResult();
+
+        if (ray.getType() == HitResult.Type.ENTITY) {
+            EntityHitResult result = (EntityHitResult)ray;
+            Optional.ofNullable(result.getEntity()).ifPresent(apply);
+        } else if (ray.getType() == HitResult.Type.BLOCK) {
+            createBlockEntity(caster.getWorld(), ((BlockHitResult)ray).getBlockPos(), apply);
+        }
+    }
+
+    static void createBlockEntity(World world, BlockPos bpos, Consumer<Entity> apply) {
+        Vec3d pos = Vec3d.ofBottomCenter(bpos);
+        FallingBlockEntity e = new FallingBlockEntity(world, pos.x, pos.y, pos.z, world.getBlockState(bpos));
+        world.removeBlock(bpos, true);
+        e.setOnGround(false);
+        e.timeFalling = Integer.MIN_VALUE;
+
+        apply.accept(e);
+        world.spawnEntity(e);
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java
index 5eca5cc6..3bf3ec26 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java
@@ -16,6 +16,9 @@ import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 
+/**
+ * Converts surrounding blocks and entities into their nether equivalent
+ */
 public class InfernoSpell extends FireSpell {
 
     protected InfernoSpell(SpellType<?> type, SpellTraits traits) {
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java
index 47f7e03c..97b6fd12 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java
@@ -56,8 +56,9 @@ public final class SpellType<T extends Spell> implements Affine, SpellPredicate<
     public static final SpellType<SiphoningSpell> DRAINING = register("draining", Affinity.BAD, 0xe308ab, true, SiphoningSpell::new);
     public static final SpellType<RevealingSpell> REVEALING = register("reveal", Affinity.GOOD, 0x5CE81F, true, RevealingSpell::new);
     public static final SpellType<AwkwardSpell> AWKWARD = register("awkward", Affinity.GOOD, 0xE1239C, true, AwkwardSpell::new);
-    public static final SpellType<TransformationSpell> TRANSFORMATION = register("transformation", Affinity.NEUTRAL, 0x3A59AA, true, TransformationSpell::new);
+    public static final SpellType<TransformationSpell> TRANSFORMATION = register("transformation", Affinity.GOOD, 0x3A59AA, true, TransformationSpell::new);
     public static final SpellType<FeatherFallSpell> FEATHER_FALL = register("feather_fall", Affinity.GOOD, 0x00EEFF, true, FeatherFallSpell.DEFAULT_TRAITS, FeatherFallSpell::new);
+    public static final SpellType<CatapultSpell> CATAPULT = register("catapult", Affinity.GOOD, 0x33FF00, true, CatapultSpell.DEFAULT_TRAITS, CatapultSpell::new);
 
     private final Identifier id;
     private final Affinity affinity;
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/trait/SpellTraits.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/trait/SpellTraits.java
index 6439baee..edc2bc2a 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/trait/SpellTraits.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/trait/SpellTraits.java
@@ -11,6 +11,7 @@ import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -50,13 +51,21 @@ public final class SpellTraits implements Iterable<Map.Entry<Trait, Float>> {
         return factor == 0 ? EMPTY : map(v -> v * factor);
     }
 
+    public SpellTraits add(float amount) {
+        return amount == 0 ? this : map(v -> v + amount);
+    }
+
     public SpellTraits map(Function<Float, Float> function) {
+        return map((k, v) -> function.apply(v));
+    }
+
+    public SpellTraits map(BiFunction<Trait, Float, Float> function) {
         if (isEmpty()) {
             return this;
         }
 
         Map<Trait, Float> newMap = new EnumMap<>(traits);
-        newMap.entrySet().forEach(entry -> entry.setValue(function.apply(entry.getValue())));
+        newMap.entrySet().forEach(entry -> entry.setValue(function.apply(entry.getKey(), entry.getValue())));
         return fromEntries(newMap.entrySet().stream()).orElse(EMPTY);
     }