Tooltip details version 2

This commit is contained in:
Sollace 2024-05-28 18:48:06 +01:00
parent 5207f7fefb
commit 1f714b3ed0
No known key found for this signature in database
GPG key ID: E52FACE7B5C773DB
10 changed files with 261 additions and 13 deletions

View file

@ -26,6 +26,10 @@ public interface SpellAttributes {
Identifier STRENGTH = Unicopia.id("strength"); Identifier STRENGTH = Unicopia.id("strength");
Identifier VELOCITY = Unicopia.id("velocity"); Identifier VELOCITY = Unicopia.id("velocity");
Identifier VERTICAL_VELOCITY = Unicopia.id("vertical_velocity"); Identifier VERTICAL_VELOCITY = Unicopia.id("vertical_velocity");
Identifier HANG_TIME = Unicopia.id("hang_time");
Identifier PUSHING_POWER = Unicopia.id("pushing_power");
Identifier CAUSES_LEVITATION = Unicopia.id("causes_levitation");
Identifier AFFECTS = Unicopia.id("affects");
Identifier DAMAGE_TO_TARGET = Unicopia.id("damage_to_target"); Identifier DAMAGE_TO_TARGET = Unicopia.id("damage_to_target");
Identifier SIMULTANIOUS_TARGETS = Unicopia.id("simultanious_targets"); Identifier SIMULTANIOUS_TARGETS = Unicopia.id("simultanious_targets");
Identifier COST_PER_INDIVIDUAL = Unicopia.id("cost_per_individual"); Identifier COST_PER_INDIVIDUAL = Unicopia.id("cost_per_individual");
@ -61,4 +65,11 @@ public interface SpellAttributes {
Text.translatable(Util.createTranslationKey("spell_attribute", id)) Text.translatable(Util.createTranslationKey("spell_attribute", id))
).formatted(Formatting.LIGHT_PURPLE)); ).formatted(Formatting.LIGHT_PURPLE));
} }
public enum ValueType {
REGULAR,
TIME,
PERCENTAGE,
CONDITIONAL
}
} }

View file

@ -0,0 +1,15 @@
package com.minelittlepony.unicopia.ability.magic.spell.attribute;
public enum Affects {
BOTH,
ENTITIES,
BLOCKS;
public boolean allowsBlocks() {
return this == BOTH || this == BLOCKS;
}
public boolean allowsEntities() {
return this == BOTH || this == ENTITIES;
}
}

View file

@ -0,0 +1,63 @@
package com.minelittlepony.unicopia.ability.magic.spell.attribute;
import com.minelittlepony.unicopia.ability.magic.spell.trait.Trait;
import com.minelittlepony.unicopia.client.UnicopiaClient;
import com.minelittlepony.unicopia.client.gui.ItemTraitsTooltipRenderer;
import net.minecraft.item.ItemStack;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.StringHelper;
public enum AttributeFormat {
REGULAR {
@Override
public String formatValue(float value) {
return ItemStack.MODIFIER_FORMAT.format(value);
}
},
TIME {
@Override
public String formatValue(float value) {
return StringHelper.formatTicks((int)Math.abs(value));
}
},
PERCENTAGE {
@Override
public String formatValue(float value) {
return ItemStack.MODIFIER_FORMAT.format((int)(Math.abs(value) * 100)) + "%";
}
};
public abstract String formatValue(float value);
public MutableText getBase(Text attributeName, float value, String comparison, Formatting color) {
return formatAttributeLine(Text.translatable("attribute.modifier." + comparison + ".0", formatValue(value), attributeName).formatted(color));
}
public Text get(Text attributeName, float value) {
return getBase(attributeName, value, "equals", Formatting.LIGHT_PURPLE);
}
public Text getRelative(Text attributeName, float baseValue, float currentValue, boolean detrimental) {
float difference = currentValue - baseValue;
return Text.literal(" (" + (difference > 0 ? "+" : "-") + formatValue(this == PERCENTAGE ? difference / baseValue : difference) + ")").formatted((detrimental ? difference : -difference) < 0 ? Formatting.DARK_GREEN : Formatting.RED);
}
static Text formatTraitDifference(Trait trait, float value) {
boolean known = ItemTraitsTooltipRenderer.isKnown(trait);
boolean canCast = UnicopiaClient.getClientPony() != null && UnicopiaClient.getClientPony().getObservedSpecies().canCast();
Text name = canCast ? known
? trait.getName()
: Text.translatable("spell_attribute.unicopia.added_trait.unknown").formatted(Formatting.YELLOW)
: trait.getName().copy().formatted(Formatting.OBFUSCATED, Formatting.YELLOW);
Text count = Text.literal(ItemStack.MODIFIER_FORMAT.format(value));
return Text.translatable("spell_attribute.unicopia.added_trait." + ((value > 0) ? "plus" : "take"), name, count).formatted(Formatting.DARK_AQUA);
}
static MutableText formatAttributeLine(Text attributeName) {
return Text.literal(" ").append(attributeName).formatted(Formatting.LIGHT_PURPLE);
}
}

View file

@ -0,0 +1,8 @@
package com.minelittlepony.unicopia.ability.magic.spell.attribute;
public enum Permits {
ITEMS,
PASSIVE,
HOSTILE,
PLAYER
}

View file

@ -0,0 +1,88 @@
package com.minelittlepony.unicopia.ability.magic.spell.attribute;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull;
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
import com.minelittlepony.unicopia.ability.magic.spell.trait.SpellTraits;
import com.minelittlepony.unicopia.ability.magic.spell.trait.Trait;
import it.unimi.dsi.fastutil.floats.Float2ObjectFunction;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
public record SpellAttribute<T> (
Trait trait,
Float2ObjectFunction<T> valueGetter,
TooltipFactory tooltipFactory
) implements TooltipFactory {
@Override
public void appendTooltip(CustomisedSpellType<?> type, List<Text> tooltip) {
tooltipFactory.appendTooltip(type, tooltip);
}
public T get(SpellTraits traits) {
return valueGetter.get(traits.get(trait));
}
public static <T extends Number> SpellAttribute<T> create(Identifier id, AttributeFormat format, Trait trait, Float2ObjectFunction<@NotNull T> valueGetter) {
return create(id, format, format, trait, valueGetter, false);
}
public static <T extends Number> SpellAttribute<T> create(Identifier id, AttributeFormat baseFormat, AttributeFormat relativeFormat, Trait trait, Float2ObjectFunction<@NotNull T> valueGetter) {
return create(id, baseFormat, relativeFormat, trait, valueGetter, false);
}
public static <T extends @NotNull Number> SpellAttribute<T> create(Identifier id, AttributeFormat baseFormat, AttributeFormat relativeFormat, Trait trait, Float2ObjectFunction<@NotNull T> valueGetter, boolean detrimental) {
Text name = Text.translatable(Util.createTranslationKey("spell_attribute", id));
return new SpellAttribute<>(trait, valueGetter, (CustomisedSpellType<?> type, List<Text> tooltip) -> {
float traitAmount = type.traits().get(trait);
float traitDifference = type.relativeTraits().get(trait);
float value = valueGetter.get(traitAmount).floatValue();
var b = baseFormat.getBase(name, value, "equals", Formatting.LIGHT_PURPLE);
if (traitDifference != 0) {
tooltip.add(b.append(relativeFormat.getRelative(Text.empty(), valueGetter.get(traitAmount - traitDifference).floatValue(), value, detrimental)));
tooltip.add(AttributeFormat.formatTraitDifference(trait, traitDifference));
} else {
tooltip.add(b);
}
});
}
public static SpellAttribute<Boolean> createConditional(Identifier id, Trait trait, Float2ObjectFunction<Boolean> valueGetter) {
return new SpellAttribute<>(trait, valueGetter, (CustomisedSpellType<?> type, List<Text> tooltip) -> {
Text name = Text.translatable(Util.createTranslationKey("spell_attribute", id));
float difference = type.relativeTraits().get(trait);
Text value = AttributeFormat.formatAttributeLine(name);
if (!valueGetter.get(type.traits().get(trait))) {
value = value.copy().formatted(Formatting.STRIKETHROUGH, Formatting.DARK_GRAY);
}
tooltip.add(value);
if (difference != 0) {
tooltip.add(AttributeFormat.formatTraitDifference(trait, difference));
}
});
}
public static <T extends Enum<T>> SpellAttribute<T> createEnumerated(Identifier id, Trait trait, Float2ObjectFunction<T> valueGetter) {
Function<T, Text> cache = Util.memoize(t -> Text.translatable(Util.createTranslationKey("spell_attribute", id.withPath(id.getPath() + "." + t.name().toLowerCase(Locale.ROOT)))));
return new SpellAttribute<>(trait, valueGetter, (CustomisedSpellType<?> type, List<Text> tooltip) -> {
T t = valueGetter.get(type.traits().get(trait));
if (t != null) {
int max = t.getClass().getEnumConstants().length;
tooltip.add(Text.translatable(" %s (%s/%s)", cache.apply(t), Text.literal("" + (t.ordinal() + 1)).formatted(Formatting.LIGHT_PURPLE), max).formatted(Formatting.DARK_PURPLE));
}
float difference = type.relativeTraits().get(trait);
if (difference != 0) {
tooltip.add(AttributeFormat.formatTraitDifference(trait, difference));
}
});
}
}

View file

@ -0,0 +1,19 @@
package com.minelittlepony.unicopia.ability.magic.spell.attribute;
import java.util.List;
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
import net.minecraft.text.Text;
public interface TooltipFactory {
void appendTooltip(CustomisedSpellType<?> type, List<Text> tooltip);
static TooltipFactory of(TooltipFactory...lines) {
return (type, tooltip) -> {
for (var line : lines) {
line.appendTooltip(type, tooltip);
}
};
}
}

View file

@ -6,10 +6,13 @@ import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.UTags; import com.minelittlepony.unicopia.UTags;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.ability.magic.Caster; import com.minelittlepony.unicopia.ability.magic.Caster;
import com.minelittlepony.unicopia.ability.magic.spell.Situation; import com.minelittlepony.unicopia.ability.magic.spell.Situation;
import com.minelittlepony.unicopia.ability.magic.spell.SpellAttributes; import com.minelittlepony.unicopia.ability.magic.spell.SpellAttributes;
import com.minelittlepony.unicopia.ability.magic.spell.attribute.Affects;
import com.minelittlepony.unicopia.ability.magic.spell.attribute.AttributeFormat;
import com.minelittlepony.unicopia.ability.magic.spell.attribute.SpellAttribute;
import com.minelittlepony.unicopia.ability.magic.spell.attribute.TooltipFactory;
import com.minelittlepony.unicopia.ability.magic.spell.trait.SpellTraits; import com.minelittlepony.unicopia.ability.magic.spell.trait.SpellTraits;
import com.minelittlepony.unicopia.ability.magic.spell.trait.Trait; import com.minelittlepony.unicopia.ability.magic.spell.trait.Trait;
import com.minelittlepony.unicopia.mixin.MixinFallingBlockEntity; import com.minelittlepony.unicopia.mixin.MixinFallingBlockEntity;
@ -27,6 +30,7 @@ import net.minecraft.text.Text;
import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.EntityHitResult; import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.random.Random; import net.minecraft.util.math.random.Random;
import net.minecraft.world.World; import net.minecraft.world.World;
@ -45,13 +49,22 @@ public class CatapultSpell extends AbstractSpell implements ProjectileDelegate.B
private static final float HORIZONTAL_VARIANCE = 0.25F; private static final float HORIZONTAL_VARIANCE = 0.25F;
private static final float MAX_STRENGTH = 120; private static final float MAX_STRENGTH = 120;
private static final SpellAttribute<Float> LAUNCH_SPEED = SpellAttribute.create(SpellAttributes.VERTICAL_VELOCITY, AttributeFormat.REGULAR, AttributeFormat.PERCENTAGE, Trait.STRENGTH, strength -> 0.1F + (MathHelper.clamp(strength, -MAX_STRENGTH, MAX_STRENGTH) - 40) / 16F);
private static final SpellAttribute<Float> HANG_TIME = SpellAttribute.create(SpellAttributes.HANG_TIME, AttributeFormat.TIME, AttributeFormat.PERCENTAGE, Trait.AIR, air -> 50 + (int)MathHelper.clamp(air, 0, 10) * 20F);
private static final SpellAttribute<Float> PUSHING_POWER = SpellAttribute.create(SpellAttributes.PUSHING_POWER, AttributeFormat.REGULAR, Trait.POWER, power -> 1 + MathHelper.clamp(power, 0, 10) / 10F);
private static final SpellAttribute<Boolean> CAUSES_LEVITATION = SpellAttribute.createConditional(SpellAttributes.CAUSES_LEVITATION, Trait.FOCUS, focus -> focus > 50);
private static final SpellAttribute<Affects> AFFECTS = SpellAttribute.createEnumerated(SpellAttributes.AFFECTS, Trait.ORDER, order -> {
if (order <= 0) {
return Affects.BOTH;
} else if (order <= 10) {
return Affects.ENTITIES;
}
return Affects.BLOCKS;
});
static final TooltipFactory TOOLTIP = TooltipFactory.of(LAUNCH_SPEED, HANG_TIME, PUSHING_POWER, CAUSES_LEVITATION, AFFECTS);
static void appendTooltip(CustomisedSpellType<? extends CatapultSpell> type, List<Text> tooltip) { static void appendTooltip(CustomisedSpellType<? extends CatapultSpell> type, List<Text> tooltip) {
float velocity = (float)(0.1F + (type.traits().get(Trait.STRENGTH, -MAX_STRENGTH, MAX_STRENGTH) - 40) / 16D); TOOLTIP.appendTooltip(type, tooltip);
tooltip.add(SpellAttributes.of(SpellAttributes.VERTICAL_VELOCITY, velocity));
int hoverDuration = 50 + (int)type.traits().get(Trait.AIR, 0, 10) * 20;
tooltip.add(SpellAttributes.ofTime(Unicopia.id("hang_time"), hoverDuration));
float power = 1 + type.traits().get(Trait.POWER, 0, 10) / 10F;
tooltip.add(SpellAttributes.of(Unicopia.id("pushing_power"), power));
} }
protected CatapultSpell(CustomisedSpellType<?> type) { protected CatapultSpell(CustomisedSpellType<?> type) {
@ -60,6 +73,10 @@ public class CatapultSpell extends AbstractSpell implements ProjectileDelegate.B
@Override @Override
public void onImpact(MagicProjectileEntity projectile, BlockHitResult hit) { public void onImpact(MagicProjectileEntity projectile, BlockHitResult hit) {
if (!AFFECTS.get(getTraits()).allowsBlocks()) {
return;
}
if (!projectile.isClient() && projectile instanceof MagicBeamEntity source && source.canModifyAt(hit.getBlockPos())) { if (!projectile.isClient() && projectile instanceof MagicBeamEntity source && source.canModifyAt(hit.getBlockPos())) {
createBlockEntity(projectile.getWorld(), hit.getBlockPos(), e -> { createBlockEntity(projectile.getWorld(), hit.getBlockPos(), e -> {
e.setOnGround(true); e.setOnGround(true);
@ -72,6 +89,11 @@ public class CatapultSpell extends AbstractSpell implements ProjectileDelegate.B
@Override @Override
public void onImpact(MagicProjectileEntity projectile, EntityHitResult hit) { public void onImpact(MagicProjectileEntity projectile, EntityHitResult hit) {
if (!projectile.isClient() && projectile instanceof MagicBeamEntity source) { if (!projectile.isClient() && projectile instanceof MagicBeamEntity source) {
Entity e = hit.getEntity();
if (!(e instanceof FallingBlockEntity) && !AFFECTS.get(getTraits()).allowsEntities()) {
return;
}
apply(source, hit.getEntity()); apply(source, hit.getEntity());
} }
} }
@ -95,21 +117,29 @@ public class CatapultSpell extends AbstractSpell implements ProjectileDelegate.B
e.setVelocity(caster.asEntity().getVelocity().multiply(power)); e.setVelocity(caster.asEntity().getVelocity().multiply(power));
} else { } else {
Random rng = caster.asWorld().random; Random rng = caster.asWorld().random;
double launchSpeed = 0.1F + (getTraits().get(Trait.STRENGTH, -MAX_STRENGTH, MAX_STRENGTH) - 40) / 16D;
e.addVelocity( e.addVelocity(
rng.nextTriangular(0, HORIZONTAL_VARIANCE) * 0.1F, rng.nextTriangular(0, HORIZONTAL_VARIANCE) * 0.1F,
launchSpeed, LAUNCH_SPEED.get(getTraits()),
rng.nextTriangular(0, HORIZONTAL_VARIANCE) * 0.1F rng.nextTriangular(0, HORIZONTAL_VARIANCE) * 0.1F
); );
if (e instanceof LivingEntity l) { int hoverDuration = HANG_TIME.get(getTraits()).intValue();
int hoverDuration = 50 + (int)getTraits().get(Trait.AIR, 0, 10) * 20; boolean noGravity = CAUSES_LEVITATION.get(getTraits());
if (e instanceof LivingEntity l) {
if (l.hasStatusEffect(StatusEffects.SLOW_FALLING)) { if (l.hasStatusEffect(StatusEffects.SLOW_FALLING)) {
l.removeStatusEffect(StatusEffects.SLOW_FALLING); l.removeStatusEffect(StatusEffects.SLOW_FALLING);
} }
l.addStatusEffect(new StatusEffectInstance(StatusEffects.SLOW_FALLING, hoverDuration, 1)); l.addStatusEffect(new StatusEffectInstance(StatusEffects.SLOW_FALLING, hoverDuration, 1));
} }
if (noGravity || e instanceof FallingBlockEntity && (!e.getWorld().getBlockState(e.getBlockPos().up()).isReplaceable())) {
if (e instanceof LivingEntity l) {
l.addStatusEffect(new StatusEffectInstance(StatusEffects.LEVITATION, 200, 1));
} else {
e.setNoGravity(true);
}
}
} }
e.velocityDirty = true; e.velocityDirty = true;

View file

@ -68,7 +68,7 @@ public final class SpellType<T extends Spell> implements Affine, SpellPredicate<
public static final SpellType<AwkwardSpell> AWKWARD = register("awkward", builder(AwkwardSpell::new).affinity(Affinity.NEUTRAL).color(0x3A59FF).shape(GemstoneItem.Shape.ICE)); public static final SpellType<AwkwardSpell> AWKWARD = register("awkward", builder(AwkwardSpell::new).affinity(Affinity.NEUTRAL).color(0x3A59FF).shape(GemstoneItem.Shape.ICE));
public static final SpellType<TransformationSpell> TRANSFORMATION = register("transformation", builder(TransformationSpell::new).color(0x19E48E).shape(GemstoneItem.Shape.BRUSH)); public static final SpellType<TransformationSpell> TRANSFORMATION = register("transformation", builder(TransformationSpell::new).color(0x19E48E).shape(GemstoneItem.Shape.BRUSH));
public static final SpellType<FeatherFallSpell> FEATHER_FALL = register("feather_fall", builder(FeatherFallSpell::new).color(0x00EEFF).shape(GemstoneItem.Shape.LAMBDA).traits(FeatherFallSpell.DEFAULT_TRAITS).tooltip(FeatherFallSpell::appendTooltip)); public static final SpellType<FeatherFallSpell> FEATHER_FALL = register("feather_fall", builder(FeatherFallSpell::new).color(0x00EEFF).shape(GemstoneItem.Shape.LAMBDA).traits(FeatherFallSpell.DEFAULT_TRAITS).tooltip(FeatherFallSpell::appendTooltip));
public static final SpellType<CatapultSpell> CATAPULT = register("catapult", builder(CatapultSpell::new).color(0x22FF00).shape(GemstoneItem.Shape.ROCKET).traits(CatapultSpell.DEFAULT_TRAITS).tooltip(CatapultSpell::appendTooltip)); public static final SpellType<CatapultSpell> CATAPULT = register("catapult", builder(CatapultSpell::new).color(0x22FF00).shape(GemstoneItem.Shape.ROCKET).traits(CatapultSpell.DEFAULT_TRAITS).tooltip(CatapultSpell.TOOLTIP::appendTooltip));
public static final SpellType<FireBoltSpell> FIRE_BOLT = register("fire_bolt", builder(FireBoltSpell::new).color(0xFF8811).shape(GemstoneItem.Shape.FLAME).traits(FireBoltSpell.DEFAULT_TRAITS).tooltip(FireBoltSpell::appendTooltip)); public static final SpellType<FireBoltSpell> FIRE_BOLT = register("fire_bolt", builder(FireBoltSpell::new).color(0xFF8811).shape(GemstoneItem.Shape.FLAME).traits(FireBoltSpell.DEFAULT_TRAITS).tooltip(FireBoltSpell::appendTooltip));
public static final SpellType<LightSpell> LIGHT = register("light", builder(LightSpell::new).color(0xEEFFAA).shape(GemstoneItem.Shape.STAR).traits(LightSpell.DEFAULT_TRAITS).tooltip(LightSpell::appendTooltip)); public static final SpellType<LightSpell> LIGHT = register("light", builder(LightSpell::new).color(0xEEFFAA).shape(GemstoneItem.Shape.STAR).traits(LightSpell.DEFAULT_TRAITS).tooltip(LightSpell::appendTooltip));
public static final SpellType<DisplacementSpell> DISPLACEMENT = register("displacement", builder(DisplacementSpell::new).affinity(Affinity.NEUTRAL).color(0x9900FF).stackable().shape(GemstoneItem.Shape.BRUSH).traits(PortalSpell.DEFAULT_TRAITS).tooltip(DisplacementSpell::appendTooltip)); public static final SpellType<DisplacementSpell> DISPLACEMENT = register("displacement", builder(DisplacementSpell::new).affinity(Affinity.NEUTRAL).color(0x9900FF).stackable().shape(GemstoneItem.Shape.BRUSH).traits(PortalSpell.DEFAULT_TRAITS).tooltip(DisplacementSpell::appendTooltip));

View file

@ -52,7 +52,7 @@ public interface EffectUtils {
} }
static Text formatModifierChange(String modifierName, int time, boolean isDetrimental) { static Text formatModifierChange(String modifierName, int time, boolean isDetrimental) {
return Text.literal(" ").append(Text.translatable("attribute.modifier.equals.0", return Text.literal(" ").append(Text.translatable("attribute.modifier." + (time > 0 ? "plus" : "take") + ".0",
StringHelper.formatTicks(Math.abs(time)), StringHelper.formatTicks(Math.abs(time)),
Text.translatable(modifierName) Text.translatable(modifierName)
).formatted((isDetrimental ? time : -time) < 0 ? Formatting.DARK_GREEN : Formatting.RED)); ).formatted((isDetrimental ? time : -time) < 0 ? Formatting.DARK_GREEN : Formatting.RED));
@ -64,4 +64,11 @@ public interface EffectUtils {
Text.translatable(modifierName) Text.translatable(modifierName)
).formatted((isDetrimental ? change : -change) < 0 ? Formatting.DARK_GREEN : Formatting.RED)); ).formatted((isDetrimental ? change : -change) < 0 ? Formatting.DARK_GREEN : Formatting.RED));
} }
static Text formatModifierChange(Text modifierName, float change, boolean isDetrimental) {
return Text.literal(" ").append(Text.translatable("attribute.modifier." + (change > 0 ? "plus" : "take") + ".0",
ItemStack.MODIFIER_FORMAT.format(Math.abs(change)),
modifierName
).formatted((isDetrimental ? change : -change) < 0 ? Formatting.DARK_GREEN : Formatting.RED));
}
} }

View file

@ -584,10 +584,17 @@
"spell.unicopia.dispel_evil": "Dispel Evil", "spell.unicopia.dispel_evil": "Dispel Evil",
"spell.unicopia.dispel_evil.lore": "Casts away any nearby unearthly forces", "spell.unicopia.dispel_evil.lore": "Casts away any nearby unearthly forces",
"spell_attribute.unicopia.added_trait.plus": " + %s x%s",
"spell_attribute.unicopia.added_trait.take": " - %s x%s",
"spell_attribute.unicopia.added_trait.unknown": "Undiscovered Trait",
"spell_attribute.unicopia.cast_on_location": "Applies to location", "spell_attribute.unicopia.cast_on_location": "Applies to location",
"spell_attribute.unicopia.cast_on_person": "Applies to self", "spell_attribute.unicopia.cast_on_person": "Applies to self",
"spell_attribute.unicopia.focused_entity": "Applies to focused entity", "spell_attribute.unicopia.focused_entity": "Applies to focused entity",
"spell_attribute.unicopia.affects.both": "Affects blocks and entities",
"spell_attribute.unicopia.affects.entities": "Affects only entities",
"spell_attribute.unicopia.affects.blocks": "Affects only blocks",
"spell_attribute.unicopia.follows_target": "Follows target", "spell_attribute.unicopia.follows_target": "Follows target",
"spell_attribute.unicopia.causes_levitation": "Causes Levitation",
"spell_attribute.unicopia.permit_items": " Permits Items", "spell_attribute.unicopia.permit_items": " Permits Items",
"spell_attribute.unicopia.permit_passive": "Permits Passive Mobs", "spell_attribute.unicopia.permit_passive": "Permits Passive Mobs",