From 38f1cc99c8b15e94b8d50f96e353d41cdc670a49 Mon Sep 17 00:00:00 2001 From: Sollace Date: Wed, 14 Oct 2020 21:07:36 +0200 Subject: [PATCH] Added a rainboom ability for pegasi --- .../com/minelittlepony/unicopia/UTags.java | 3 + .../unicopia/ability/Abilities.java | 3 + .../ability/PegasusRainboomAbility.java | 83 ++++++++++ .../unicopia/ability/magic/Caster.java | 3 + .../ability/magic/spell/JoustingSpell.java | 111 +++++++++++++ .../ability/magic/spell/SpellRegistry.java | 1 + .../unicopia/client/URenderers.java | 2 + .../particle/AbstractBillboardParticle.java | 88 +++++++++++ .../particle/OrientedBillboardParticle.java | 58 +++++++ .../client/particle/RainboomParticle.java | 101 ++---------- .../client/particle/RainbowTrailParticle.java | 147 ++++++++++++++++++ .../unicopia/entity/Creature.java | 1 - .../unicopia/entity/EntityPhysics.java | 17 +- .../unicopia/entity/Physics.java | 3 + .../unicopia/entity/player/PlayerPhysics.java | 1 - .../unicopia/entity/player/Pony.java | 1 - .../particle/MagicParticleEffect.java | 2 +- .../OrientedBillboardParticleEffect.java | 83 ++++++++++ .../unicopia/particle/ParticleHandle.java | 5 + .../unicopia/particle/UParticles.java | 5 +- .../projectile/MagicProjectileEntity.java | 12 +- .../unicopia/particles/rainboom_trail.json | 2 + .../textures/particles/rainboom_trail.png | Bin 0 -> 2784 bytes .../data/unicopia/tags/blocks/fragile.json | 41 +++++ 24 files changed, 667 insertions(+), 106 deletions(-) create mode 100644 src/main/java/com/minelittlepony/unicopia/ability/PegasusRainboomAbility.java create mode 100644 src/main/java/com/minelittlepony/unicopia/ability/magic/spell/JoustingSpell.java create mode 100644 src/main/java/com/minelittlepony/unicopia/client/particle/AbstractBillboardParticle.java create mode 100644 src/main/java/com/minelittlepony/unicopia/client/particle/OrientedBillboardParticle.java create mode 100644 src/main/java/com/minelittlepony/unicopia/client/particle/RainbowTrailParticle.java create mode 100644 src/main/java/com/minelittlepony/unicopia/particle/OrientedBillboardParticleEffect.java create mode 100644 src/main/resources/assets/unicopia/particles/rainboom_trail.json create mode 100644 src/main/resources/assets/unicopia/textures/particles/rainboom_trail.png create mode 100644 src/main/resources/data/unicopia/tags/blocks/fragile.json diff --git a/src/main/java/com/minelittlepony/unicopia/UTags.java b/src/main/java/com/minelittlepony/unicopia/UTags.java index c3cb6779..beae7d94 100644 --- a/src/main/java/com/minelittlepony/unicopia/UTags.java +++ b/src/main/java/com/minelittlepony/unicopia/UTags.java @@ -1,6 +1,7 @@ package com.minelittlepony.unicopia; import net.fabricmc.fabric.api.tag.TagRegistry; +import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.tag.Tag; import net.minecraft.util.Identifier; @@ -13,6 +14,8 @@ public interface UTags { Tag FAIRLY_TOXIC = register("fairly_toxic"); Tag SEVERELY_TOXIC = register("severely_toxic"); + Tag FRAGILE = TagRegistry.block(new Identifier("unicopia", "fragile")); + static Tag register(String name) { return TagRegistry.item(new Identifier("unicopia", name)); } diff --git a/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java b/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java index b8a1ebaa..25136c06 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java @@ -24,6 +24,9 @@ public interface Abilities { Ability GROW = register(new EarthPonyGrowAbility(), "grow", AbilitySlot.SECONDARY); Ability STOMP = register(new EarthPonyStompAbility(), "stomp", AbilitySlot.TERTIARY); + // pegasus + Ability RAINBOOM = register(new PegasusRainboomAbility(), "rainboom", AbilitySlot.PRIMARY); + // pegasus / bat / alicorn / changeling Ability CARRY = register(new CarryAbility(), "carry", AbilitySlot.PASSIVE); diff --git a/src/main/java/com/minelittlepony/unicopia/ability/PegasusRainboomAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/PegasusRainboomAbility.java new file mode 100644 index 00000000..ddb5d907 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/ability/PegasusRainboomAbility.java @@ -0,0 +1,83 @@ +package com.minelittlepony.unicopia.ability; + +import javax.annotation.Nullable; + +import com.minelittlepony.unicopia.Race; +import com.minelittlepony.unicopia.ability.data.Hit; +import com.minelittlepony.unicopia.ability.magic.spell.JoustingSpell; +import com.minelittlepony.unicopia.entity.player.Pony; +import com.minelittlepony.unicopia.particle.MagicParticleEffect; +import com.minelittlepony.unicopia.particle.OrientedBillboardParticleEffect; +import com.minelittlepony.unicopia.particle.UParticles; + +import net.minecraft.util.math.Vec3d; + +/** + * Changeling ability to restore health from mobs + */ +public class PegasusRainboomAbility implements Ability { + + @Override + public int getWarmupTime(Pony player) { + return 59; + } + + @Override + public int getCooldownTime(Pony player) { + return 60; + } + + @Override + public boolean canUse(Race race) { + return race.canInteractWithClouds(); + } + + @Nullable + @Override + public Hit tryActivate(Pony player) { + + if (!player.getMaster().isCreative() && player.getMagicalReserves().getMana().getPercentFill() < 0.2F) { + return null; + } + + if (player.getPhysics().isFlying() && !player.hasSpell()) { + return Hit.INSTANCE; + } + + return null; + } + + @Override + public Hit.Serializer getSerializer() { + return Hit.SERIALIZER; + } + + @Override + public double getCostEstimate(Pony player) { + return player.getMagicalReserves().getMana().getMax() * 0.9F; + } + + @Override + public void apply(Pony player, Hit data) { + + if (!player.getMaster().isCreative() && player.getMagicalReserves().getMana().getPercentFill() < 0.2F) { + return; + } + + if (player.getPhysics().isFlying() && !player.hasSpell()) { + player.getMagicalReserves().getMana().multiply(0.1F); + player.addParticle(new OrientedBillboardParticleEffect(UParticles.RAINBOOM_RING, player.getPhysics().getMotionAngle()), player.getOriginVector(), Vec3d.ZERO); + player.setSpell(new JoustingSpell()); + } + } + + @Override + public void preApply(Pony player, AbilitySlot slot) { + player.getMagicalReserves().getExertion().add(6); + } + + @Override + public void postApply(Pony player, AbilitySlot slot) { + player.spawnParticles(MagicParticleEffect.UNICORN, 5); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java index e54a4319..0ee201ab 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java @@ -6,6 +6,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import com.minelittlepony.unicopia.Owned; +import com.minelittlepony.unicopia.entity.Physics; import com.minelittlepony.unicopia.network.EffectSync; import com.minelittlepony.unicopia.particle.ParticleSource; import com.minelittlepony.unicopia.util.VecHelper; @@ -21,6 +22,8 @@ import net.minecraft.world.World; */ public interface Caster extends Owned, Levelled, Affine, Magical, ParticleSource { + Physics getPhysics(); + EffectSync getPrimarySpellSlot(); default void setSpell(@Nullable Spell spell) { diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/JoustingSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/JoustingSpell.java new file mode 100644 index 00000000..0b5d5dc9 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/JoustingSpell.java @@ -0,0 +1,111 @@ +package com.minelittlepony.unicopia.ability.magic.spell; + +import com.minelittlepony.unicopia.Affinity; +import com.minelittlepony.unicopia.UTags; +import com.minelittlepony.unicopia.ability.magic.Caster; +import com.minelittlepony.unicopia.ability.magic.Thrown; +import com.minelittlepony.unicopia.entity.player.Pony; +import com.minelittlepony.unicopia.particle.OrientedBillboardParticleEffect; +import com.minelittlepony.unicopia.particle.ParticleHandle; +import com.minelittlepony.unicopia.particle.UParticles; +import com.minelittlepony.unicopia.util.MagicalDamageSource; +import com.minelittlepony.unicopia.util.PosHelper; +import com.minelittlepony.unicopia.util.shape.Shape; +import com.minelittlepony.unicopia.util.shape.Sphere; + +import net.minecraft.block.BlockState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.GameRules; + +public class JoustingSpell extends AbstractRangedAreaSpell implements Thrown { + + private final int rad = 5; + private final Shape effect_range = new Sphere(false, rad); + + private final ParticleHandle particlEffect = new ParticleHandle(); + + private int age; + + @Override + public String getName() { + return "joust"; + } + + @Override + public Affinity getAffinity() { + return Affinity.GOOD; + } + + @Override + public int getTint() { + return 0xBDBDF9; + } + + @Override + public void setDead() { + super.setDead(); + particlEffect.destroy(); + } + + @Override + public boolean update(Caster source) { + LivingEntity owner = source.getMaster(); + + source.findAllEntitiesInRange(rad).forEach(e -> { + e.damage(MagicalDamageSource.create("rainboom", owner), 6); + }); + PosHelper.getAllInRegionMutable(source.getOrigin(), effect_range).forEach(pos -> { + BlockState state = source.getWorld().getBlockState(pos); + if (state.isIn(UTags.FRAGILE) && canBreak(pos, owner)) { + owner.world.breakBlock(pos, true); + } + }); + + Vec3d motion = source.getEntity().getRotationVec(1).multiply(1.5); + Vec3d velocity = source.getEntity().getVelocity().add(motion); + + while (velocity.length() > 3) { + velocity = velocity.multiply(0.6); + } + + source.getEntity().setVelocity(velocity); + if (source instanceof Pony) { + ((Pony)source).getMagicalReserves().getEnergy().multiply(0.2F); + } + + return !source.getEntity().removed && age++ < 90 + 7 * (source.getLevel().get() + 1); + } + + private boolean canBreak(BlockPos pos, LivingEntity entity) { + + if (entity instanceof PlayerEntity) { + return entity.world.canPlayerModifyAt((PlayerEntity)entity, pos); + } + + return entity.world.getGameRules().getBoolean(GameRules.DO_MOB_GRIEFING); + } + + @Override + public void render(Caster source) { + particlEffect.ifAbsent(source, spawner -> { + spawner.addParticle(UParticles.RAINBOOM_TRAIL, source.getOriginVector(), Vec3d.ZERO); + spawner.addParticle(new OrientedBillboardParticleEffect(UParticles.RAINBOOM_RING, source.getPhysics().getMotionAngle()), source.getOriginVector(), Vec3d.ZERO); + }).ifPresent(p -> p.attach(source)); + } + + @Override + public void toNBT(CompoundTag compound) { + super.toNBT(compound); + compound.putInt("age", age); + } + + @Override + public void fromNBT(CompoundTag compound) { + super.fromNBT(compound); + age = compound.getInt("age"); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/SpellRegistry.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/SpellRegistry.java index 266dc63f..f621f853 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/SpellRegistry.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/SpellRegistry.java @@ -39,6 +39,7 @@ public class SpellRegistry { register(RevealingSpell::new); register(ScorchSpell::new); register(DisguiseSpell::new); + register(JoustingSpell::new); } @Nullable diff --git a/src/main/java/com/minelittlepony/unicopia/client/URenderers.java b/src/main/java/com/minelittlepony/unicopia/client/URenderers.java index 5edbad0f..e9542939 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/URenderers.java +++ b/src/main/java/com/minelittlepony/unicopia/client/URenderers.java @@ -5,6 +5,7 @@ import com.minelittlepony.unicopia.client.particle.ChangelingMagicParticle; import com.minelittlepony.unicopia.client.particle.DiskParticle; import com.minelittlepony.unicopia.client.particle.MagicParticle; import com.minelittlepony.unicopia.client.particle.RainboomParticle; +import com.minelittlepony.unicopia.client.particle.RainbowTrailParticle; import com.minelittlepony.unicopia.client.particle.RaindropsParticle; import com.minelittlepony.unicopia.client.particle.SphereParticle; import com.minelittlepony.unicopia.particle.UParticles; @@ -24,6 +25,7 @@ public interface URenderers { ParticleFactoryRegistry.getInstance().register(UParticles.CHANGELING_MAGIC, createFactory(ChangelingMagicParticle::new)); ParticleFactoryRegistry.getInstance().register(UParticles.RAIN_DROPS, createFactory(RaindropsParticle::new)); ParticleFactoryRegistry.getInstance().register(UParticles.RAINBOOM_RING, RainboomParticle::new); + ParticleFactoryRegistry.getInstance().register(UParticles.RAINBOOM_TRAIL, RainbowTrailParticle::new); ParticleFactoryRegistry.getInstance().register(UParticles.SPHERE, SphereParticle::new); ParticleFactoryRegistry.getInstance().register(UParticles.DISK, DiskParticle::new); diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/AbstractBillboardParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/AbstractBillboardParticle.java new file mode 100644 index 00000000..070ff59c --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/client/particle/AbstractBillboardParticle.java @@ -0,0 +1,88 @@ +package com.minelittlepony.unicopia.client.particle; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleTextureSheet; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; + +public abstract class AbstractBillboardParticle extends Particle { + + protected float scale = 1; + + public AbstractBillboardParticle(ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) { + super(world, x, y, z, velocityX, velocityY, velocityZ); + + } + + @Override + public ParticleTextureSheet getType() { + return ParticleTextureSheet.CUSTOM; + } + + @Override + public void buildGeometry(VertexConsumer drawer, Camera camera, float tickDelta) { + Tessellator te = Tessellator.getInstance(); + BufferBuilder buffer = te.getBuffer(); + + MinecraftClient.getInstance().getTextureManager().bindTexture(getTexture()); + + RenderSystem.disableCull(); + RenderSystem.enableBlend(); + RenderSystem.blendFuncSeparate( + GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA, + GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA + ); + RenderSystem.alphaFunc(516, 0.003921569F); + + + Vec3d cam = camera.getPos(); + + float renderX = (float)(MathHelper.lerp(tickDelta, prevPosX, x) - cam.getX()); + float renderY = (float)(MathHelper.lerp(tickDelta, prevPosY, y) - cam.getY()); + float renderZ = (float)(MathHelper.lerp(tickDelta, prevPosZ, z) - cam.getZ()); + + renderQuads(te, buffer, renderX, renderY, renderZ, tickDelta); + + RenderSystem.enableCull(); + RenderSystem.defaultAlphaFunc(); + RenderSystem.defaultBlendFunc(); + } + + protected abstract void renderQuads(Tessellator te, BufferBuilder buffer, float x, float y, float z, float tickDelta); + + protected void renderQuad(Tessellator te, BufferBuilder buffer, Vector3f[] corners, float alpha, float tickDelta) { + int light = getColorMultiplier(tickDelta); + + buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR_LIGHT); + buffer.vertex(corners[0].getX(), corners[0].getY(), corners[0].getZ()).texture(0, 0).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); + buffer.vertex(corners[1].getX(), corners[1].getY(), corners[1].getZ()).texture(1, 0).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); + buffer.vertex(corners[2].getX(), corners[2].getY(), corners[2].getZ()).texture(1, 1).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); + buffer.vertex(corners[3].getX(), corners[3].getY(), corners[3].getZ()).texture(0, 1).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); + + te.draw(); + } + + protected abstract Identifier getTexture(); + + public float getScale(float tickDelta) { + return scale; + } + + @Override + public Particle scale(float scale) { + this.scale = scale; + return super.scale(scale); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/OrientedBillboardParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/OrientedBillboardParticle.java new file mode 100644 index 00000000..47df9584 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/client/particle/OrientedBillboardParticle.java @@ -0,0 +1,58 @@ +package com.minelittlepony.unicopia.client.particle; + +import com.minelittlepony.unicopia.particle.OrientedBillboardParticleEffect; + +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.Quaternion; + +public abstract class OrientedBillboardParticle extends AbstractBillboardParticle { + + protected boolean fixed; + protected Quaternion rotation = new Quaternion(0, 0, 0, 1); + + public OrientedBillboardParticle(OrientedBillboardParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) { + super(world, x, y, z, velocityX, velocityY, velocityZ); + + fixed = effect.isAngleFixed(); + if (!fixed) { + rotation.hamiltonProduct(Vector3f.POSITIVE_X.getDegreesQuaternion(180 - effect.getYaw())); + rotation.hamiltonProduct(Vector3f.POSITIVE_Y.getDegreesQuaternion(effect.getPitch())); + } + } + + @Override + public void buildGeometry(VertexConsumer drawer, Camera camera, float tickDelta) { + if (fixed) { + rotation = camera.getRotation(); + } + super.buildGeometry(drawer, camera, tickDelta); + } + + @Override + protected void renderQuads(Tessellator te, BufferBuilder buffer, float x, float y, float z, float tickDelta) { + Vector3f[] corners = new Vector3f[]{ + new Vector3f(-1, -1, 0), + new Vector3f(-1, 1, 0), + new Vector3f( 1, 1, 0), + new Vector3f( 1, -1, 0) + }; + float scale = getScale(tickDelta); + + for(int k = 0; k < 4; ++k) { + Vector3f corner = corners[k]; + corner.rotate(rotation); + corner.scale(scale); + corner.add(x, y, z); + } + + float alpha = colorAlpha * (1 - ((float)age / maxAge)); + + renderQuad(te, buffer, corners, alpha, tickDelta); + } + +} diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/RainboomParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/RainboomParticle.java index 1902da88..03a20123 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/particle/RainboomParticle.java +++ b/src/main/java/com/minelittlepony/unicopia/client/particle/RainboomParticle.java @@ -1,111 +1,32 @@ package com.minelittlepony.unicopia.client.particle; -import com.mojang.blaze3d.platform.GlStateManager; -import com.mojang.blaze3d.systems.RenderSystem; +import com.minelittlepony.unicopia.particle.OrientedBillboardParticleEffect; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.particle.Particle; -import net.minecraft.client.particle.ParticleTextureSheet; -import net.minecraft.client.render.BufferBuilder; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.Tessellator; -import net.minecraft.client.render.VertexConsumer; -import net.minecraft.client.render.VertexFormats; -import net.minecraft.client.util.math.Vector3f; import net.minecraft.client.world.ClientWorld; -import net.minecraft.particle.DefaultParticleType; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvents; import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; -import net.minecraft.util.math.Quaternion; -import net.minecraft.util.math.Vec3d; - -public class RainboomParticle extends Particle { +public class RainboomParticle extends OrientedBillboardParticle { private static final Identifier TEXTURE = new Identifier("unicopia", "textures/particles/rainboom_ring.png"); protected float prevBaseSize = 0; protected float baseSize = 0; - protected float scale = 1; - protected Quaternion rotation; - - public RainboomParticle(DefaultParticleType effect, ClientWorld world, double x, double y, double z, double angleX, double angleY, double angleZ) { - super(world, x, y, z); - - rotation = Vector3f.POSITIVE_X.getRadialQuaternion((float)angleX); - rotation.hamiltonProduct(Vector3f.POSITIVE_Y.getRadialQuaternion((float)angleY)); - rotation.hamiltonProduct(Vector3f.POSITIVE_Z.getRadialQuaternion((float)angleZ)); + public RainboomParticle(OrientedBillboardParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) { + super(effect, world, x, y, z, velocityX, velocityY, velocityZ); setMaxAge(40); } @Override - public ParticleTextureSheet getType() { - return ParticleTextureSheet.CUSTOM; + public float getScale(float tickDelta) { + return MathHelper.lerp(tickDelta, prevBaseSize, baseSize) * super.getScale(tickDelta); } @Override - public void buildGeometry(VertexConsumer drawer, Camera camera, float tickDelta) { - Vec3d cam = camera.getPos(); - - float renderX = (float)(MathHelper.lerp(tickDelta, prevPosX, x) - cam.getX()); - float renderY = (float)(MathHelper.lerp(tickDelta, prevPosY, y) - cam.getY()); - float renderZ = (float)(MathHelper.lerp(tickDelta, prevPosZ, z) - cam.getZ()); - - Vector3f[] corners = new Vector3f[]{ - new Vector3f(-1.0F, -1.0F, 0.0F), - new Vector3f(-1.0F, 1.0F, 0.0F), - new Vector3f(1.0F, 1.0F, 0.0F), - new Vector3f(1.0F, -1.0F, 0.0F) - }; - float scale = getSize(tickDelta); - - for(int k = 0; k < 4; ++k) { - Vector3f corner = corners[k]; - corner.rotate(rotation); - corner.scale(scale); - corner.add(renderX, renderY, renderZ); - } - - float alpha = colorAlpha * (1 - ((float)age / maxAge)); - - int light = getColorMultiplier(tickDelta); - - Tessellator te = Tessellator.getInstance(); - BufferBuilder buffer = te.getBuffer(); - - MinecraftClient.getInstance().getTextureManager().bindTexture(TEXTURE); - - RenderSystem.disableCull(); - RenderSystem.enableBlend(); - RenderSystem.blendFuncSeparate( - GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA, - GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA - ); - RenderSystem.alphaFunc(516, 0.003921569F); - - buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR_LIGHT); - buffer.vertex(corners[0].getX(), corners[0].getY(), corners[0].getZ()).texture(0, 0).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); - buffer.vertex(corners[1].getX(), corners[1].getY(), corners[1].getZ()).texture(1, 0).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); - buffer.vertex(corners[2].getX(), corners[2].getY(), corners[2].getZ()).texture(0, 0).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); - buffer.vertex(corners[3].getX(), corners[3].getY(), corners[3].getZ()).texture(0, 1).color(colorRed, colorGreen, colorBlue, alpha).light(light).next(); - - te.draw(); - - RenderSystem.enableCull(); - RenderSystem.defaultAlphaFunc(); - RenderSystem.defaultBlendFunc(); - } - - public float getSize(float tickDelta) { - return MathHelper.lerp(tickDelta, prevBaseSize, baseSize) * scale; - } - - @Override - public Particle scale(float scale) { - this.scale = scale; - return super.scale(scale); + protected Identifier getTexture() { + return TEXTURE; } @Override @@ -115,9 +36,9 @@ public class RainboomParticle extends Particle { prevBaseSize = baseSize; baseSize++; - if (this.age == 1) { - this.world.playSound(x, y, z, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.AMBIENT, 5, 0.3F, true); - this.world.playSound(x, y, z, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.AMBIENT, 10, 1.3F, true); + if (age == 1) { + world.playSound(x, y, z, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.AMBIENT, 5, 0.3F, true); + world.playSound(x, y, z, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.AMBIENT, 10, 1.3F, true); } } } diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/RainbowTrailParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/RainbowTrailParticle.java new file mode 100644 index 00000000..f11a9cac --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/client/particle/RainbowTrailParticle.java @@ -0,0 +1,147 @@ +package com.minelittlepony.unicopia.client.particle; + +import java.util.ArrayList; +import java.util.List; + +import com.minelittlepony.unicopia.ability.magic.Caster; +import com.minelittlepony.unicopia.entity.player.Pony; +import com.minelittlepony.unicopia.particle.ParticleHandle.Attachment; +import com.minelittlepony.unicopia.particle.ParticleHandle.Link; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.particle.DefaultParticleType; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; + +public class RainbowTrailParticle extends AbstractBillboardParticle implements Attachment { + private static final Identifier TEXTURE = new Identifier("unicopia", "textures/particles/rainboom_trail.png"); + + private final List segments = new ArrayList<>(); + + private final Link link = new Link(); + + public RainbowTrailParticle(DefaultParticleType effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) { + super(world, x, y, z, velocityX, velocityY, velocityZ); + segments.add(new Segment(new Vec3d(x, y, z))); + } + + @Override + protected Identifier getTexture() { + return TEXTURE; + } + + @Override + public boolean isStillAlive() { + return age < (maxAge - 1); + } + + @Override + public void attach(Caster caster) { + link.attach(caster); + } + + @Override + public void detach() { + link.detach(); + } + + @Override + public void setAttribute(int key, Object value) { + + } + + @Override + protected void renderQuads(Tessellator te, BufferBuilder buffer, float x, float y, float z, float tickDelta) { + for (int i = 0; i < segments.size() - 1; i++) { + Vector3f[] corners = segments.get(i).getPlane(segments.get(i + 1)); + float scale = getScale(tickDelta); + + for (int k = 0; k < 4; ++k) { + Vector3f corner = corners[k]; + corner.scale(scale); + corner.add(x, y, z); + } + + renderQuad(te, buffer, corners, segments.get(i).getAlpha(), tickDelta); + } + } + + private void follow(Caster caster) { + Vec3d next = caster.getOriginVector(); + + if (segments.isEmpty()) { + segments.add(new Segment(next)); + } else { + Vec3d last = segments.get(segments.size() - 1).position; + if (next.distanceTo(last) > 0.2) { + segments.add(new Segment(next)); + } + } + } + + @Override + public void tick() { + super.tick(); + age = 0; + + if (link.linked()) { + link.ifAbsent(() -> {}).ifPresent(this::follow); + } else { + follow(Pony.of(MinecraftClient.getInstance().player)); + } + + if (segments.size() > 1) { + segments.removeIf(Segment::tick); + } + if (segments.isEmpty()) { + markDead(); + } + } + + private final class Segment { + Vec3d position; + Vector3f offset; + + int age; + int maxAge; + + Segment(Vec3d position) { + this.position = position; + this.offset = new Vector3f((float)(position.getX() - x), (float)(position.getY() - y), (float)(position.getZ() - z)); + this.maxAge = 90; + } + + float getAlpha() { + return colorAlpha * (1 - ((float)age / maxAge)); + } + + boolean tick() { + return segments.indexOf(this) < segments.size() - 1 && age++ >= maxAge; + } + + Vector3f[] getPlane(Segment to) { + float fromX = offset.getX(); + float toX = to.offset.getX(); + + float fromZ = offset.getZ(); + float toZ = to.offset.getZ(); + + float fromTopY = offset.getY() + 1; + float fromBottomY = offset.getY(); + + float toTopY = to.offset.getY() + 1; + float toBottomY = to.offset.getY(); + + return new Vector3f[]{ + new Vector3f(fromX, fromBottomY, fromZ), // bottom left + new Vector3f(fromX, fromTopY, fromZ), // top left + new Vector3f(toX, toTopY, toZ), // top right + new Vector3f(toX, toBottomY, toZ) // bottom right + }; + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java index f3e9aa0d..f1108824 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java @@ -9,7 +9,6 @@ import com.minelittlepony.unicopia.ability.magic.Levelled; import com.minelittlepony.unicopia.ability.magic.Spell; import com.minelittlepony.unicopia.ability.magic.spell.SpellRegistry; import com.minelittlepony.unicopia.network.EffectSync; - import net.minecraft.entity.LivingEntity; import net.minecraft.entity.data.DataTracker; import net.minecraft.entity.data.TrackedData; diff --git a/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java b/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java index 60f6c6f5..1607600f 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java @@ -16,7 +16,7 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; -public class EntityPhysics & Owned> implements Physics, Copieable> { +public class EntityPhysics> implements Physics, Copieable> { private float gravity = 1; @@ -31,6 +31,11 @@ public class EntityPhysics & Owned> implem return false; } + @Override + public Vec3d getMotionAngle() { + return new Vec3d(pony.getMaster().getPitch(1), pony.getMaster().getYaw(1), 0); + } + @Override public double calcGravity(double worldConstant) { return worldConstant * getGravityModifier(); @@ -94,17 +99,11 @@ public class EntityPhysics & Owned> implem @Override public void toNBT(CompoundTag compound) { - if (gravity != 0) { - compound.putFloat("gravity", gravity); - } + compound.putFloat("gravity", gravity); } @Override public void fromNBT(CompoundTag compound) { - if (compound.contains("gravity")) { - gravity = compound.getFloat("gravity"); - } else { - gravity = 0; - } + gravity = compound.getFloat("gravity"); } } diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Physics.java b/src/main/java/com/minelittlepony/unicopia/entity/Physics.java index 4439c049..5bbac8ce 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Physics.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Physics.java @@ -3,11 +3,14 @@ package com.minelittlepony.unicopia.entity; import com.minelittlepony.unicopia.util.NbtSerialisable; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; public interface Physics extends NbtSerialisable { double calcGravity(double worldConstant); + Vec3d getMotionAngle(); + float getGravityModifier(); void setBaseGravityModifier(float constant); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java index c1cceca8..e08030f7 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java @@ -344,5 +344,4 @@ public class PlayerPhysics extends EntityPhysics implements Tickable, Moti pony.getMaster().calculateDimensions(); } - } diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java index 4f537d49..b03149a1 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java @@ -310,7 +310,6 @@ public class Pony implements Caster, Equine, Transmi prevSneaking = entity.isSneaking(); prevLanded = entity.isOnGround(); - if (gravity.isGravityNegative() && entity.getY() > entity.world.getHeight() + 64) { entity.damage(DamageSource.OUT_OF_WORLD, 4.0F); } diff --git a/src/main/java/com/minelittlepony/unicopia/particle/MagicParticleEffect.java b/src/main/java/com/minelittlepony/unicopia/particle/MagicParticleEffect.java index d55ac944..6dbd9af2 100644 --- a/src/main/java/com/minelittlepony/unicopia/particle/MagicParticleEffect.java +++ b/src/main/java/com/minelittlepony/unicopia/particle/MagicParticleEffect.java @@ -14,7 +14,7 @@ import net.minecraft.util.registry.Registry; public class MagicParticleEffect implements ParticleEffect { public static final MagicParticleEffect UNICORN = new MagicParticleEffect(false, 0, 0, 0); - public static final ParticleEffect.Factory UNICORN_FACTORY = new ParticleEffect.Factory() { + public static final ParticleEffect.Factory FACTORY = new ParticleEffect.Factory() { @Override public MagicParticleEffect read(ParticleType particleType, StringReader reader) throws CommandSyntaxException { reader.expect(' '); diff --git a/src/main/java/com/minelittlepony/unicopia/particle/OrientedBillboardParticleEffect.java b/src/main/java/com/minelittlepony/unicopia/particle/OrientedBillboardParticleEffect.java new file mode 100644 index 00000000..df4c9371 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/particle/OrientedBillboardParticleEffect.java @@ -0,0 +1,83 @@ +package com.minelittlepony.unicopia.particle; + +import java.util.Locale; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.ParticleType; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.registry.Registry; + +public class OrientedBillboardParticleEffect implements ParticleEffect { + public static final ParticleEffect.Factory FACTORY = new ParticleEffect.Factory() { + @Override + public OrientedBillboardParticleEffect read(ParticleType type, StringReader reader) throws CommandSyntaxException { + reader.expect(' '); + boolean fixed = reader.readBoolean(); + reader.expect(' '); + float yaw = (float)reader.readDouble(); + reader.expect(' '); + float pitch = (float)reader.readDouble(); + return new OrientedBillboardParticleEffect(type, fixed, yaw, pitch); + } + + @Override + public OrientedBillboardParticleEffect read(ParticleType particleType, PacketByteBuf buf) { + return new OrientedBillboardParticleEffect(particleType, buf.readBoolean(), buf.readFloat(), buf.readFloat()); + } + }; + + private final boolean fixed; + private final float yaw; + private final float pitch; + + private final ParticleType type; + + public OrientedBillboardParticleEffect(ParticleType type, Vec3d orientation) { + this(type, (float)orientation.getX(), (float)orientation.getY()); + } + + public OrientedBillboardParticleEffect(ParticleType type, float yaw, float pitch) { + this(type, true, yaw, pitch); + } + + private OrientedBillboardParticleEffect(ParticleType type, boolean fixed, float yaw, float pitch) { + this.fixed = fixed; + this.yaw = yaw; + this.pitch = pitch; + this.type = type; + } + + public boolean isAngleFixed() { + return fixed; + } + + public float getYaw() { + return yaw; + } + + public float getPitch() { + return pitch; + } + + @Override + public ParticleType getType() { + return type; + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeBoolean(fixed); + buf.writeFloat(yaw); + buf.writeFloat(pitch); + } + + @Override + public String asString() { + return String.format(Locale.ROOT, "%s %b %.2f %.2f", Registry.PARTICLE_TYPE.getId(getType()), fixed, yaw, pitch); + } + +} diff --git a/src/main/java/com/minelittlepony/unicopia/particle/ParticleHandle.java b/src/main/java/com/minelittlepony/unicopia/particle/ParticleHandle.java index 3181de8e..7ee2eda8 100644 --- a/src/main/java/com/minelittlepony/unicopia/particle/ParticleHandle.java +++ b/src/main/java/com/minelittlepony/unicopia/particle/ParticleHandle.java @@ -68,6 +68,11 @@ public class ParticleHandle { this.effect = caster.getSpell(false).getName(); } + public void detach() { + linked = false; + caster = Optional.empty(); + } + public boolean linked() { return linked; } diff --git a/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java b/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java index fb0633d2..a990b5c7 100644 --- a/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java +++ b/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java @@ -8,10 +8,11 @@ import net.minecraft.util.registry.Registry; public interface UParticles { - ParticleType UNICORN_MAGIC = register("unicorn_magic", FabricParticleTypes.complex(MagicParticleEffect.UNICORN_FACTORY)); + ParticleType UNICORN_MAGIC = register("unicorn_magic", FabricParticleTypes.complex(MagicParticleEffect.FACTORY)); DefaultParticleType CHANGELING_MAGIC = register("changeling_magic", FabricParticleTypes.simple()); - DefaultParticleType RAINBOOM_RING = register("rainboom_ring", FabricParticleTypes.simple()); + ParticleType RAINBOOM_RING = register("rainboom_ring", FabricParticleTypes.complex(OrientedBillboardParticleEffect.FACTORY)); + DefaultParticleType RAINBOOM_TRAIL = register("rainboom_trail", FabricParticleTypes.simple()); DefaultParticleType RAIN_DROPS = register("rain_drops", FabricParticleTypes.simple()); diff --git a/src/main/java/com/minelittlepony/unicopia/projectile/MagicProjectileEntity.java b/src/main/java/com/minelittlepony/unicopia/projectile/MagicProjectileEntity.java index c37ea048..1b3211d2 100644 --- a/src/main/java/com/minelittlepony/unicopia/projectile/MagicProjectileEntity.java +++ b/src/main/java/com/minelittlepony/unicopia/projectile/MagicProjectileEntity.java @@ -7,6 +7,8 @@ import com.minelittlepony.unicopia.ability.magic.Magical; import com.minelittlepony.unicopia.ability.magic.Spell; import com.minelittlepony.unicopia.ability.magic.Thrown; import com.minelittlepony.unicopia.ability.magic.spell.SpellRegistry; +import com.minelittlepony.unicopia.entity.EntityPhysics; +import com.minelittlepony.unicopia.entity.Physics; import com.minelittlepony.unicopia.network.Channel; import com.minelittlepony.unicopia.network.EffectSync; import com.minelittlepony.unicopia.network.MsgSpawnProjectile; @@ -49,6 +51,8 @@ public class MagicProjectileEntity extends ThrownItemEntity implements Magical, private final EffectSync effectDelegate = new EffectSync(this, EFFECT); + private final EntityPhysics physics = new EntityPhysics<>(this); + private BlockPos lastBlockPos; public MagicProjectileEntity(EntityType type, World world) { @@ -97,6 +101,11 @@ public class MagicProjectileEntity extends ThrownItemEntity implements Magical, return LEVELS; } + @Override + public Physics getPhysics() { + return physics; + } + @Override public Affinity getAffinity() { return hasSpell() ? Affinity.NEUTRAL : getSpell(true).getAffinity(); @@ -206,7 +215,7 @@ public class MagicProjectileEntity extends ThrownItemEntity implements Magical, @Override public void readCustomDataFromTag(CompoundTag compound) { super.readCustomDataFromTag(compound); - + physics.fromNBT(compound); if (compound.contains("effect")) { setSpell(SpellRegistry.instance().createEffectFromNBT(compound.getCompound("effect"))); } @@ -215,6 +224,7 @@ public class MagicProjectileEntity extends ThrownItemEntity implements Magical, @Override public void writeCustomDataToTag(CompoundTag compound) { super.writeCustomDataToTag(compound); + physics.toNBT(compound); if (hasSpell()) { compound.put("effect", SpellRegistry.toNBT(getSpell(true))); diff --git a/src/main/resources/assets/unicopia/particles/rainboom_trail.json b/src/main/resources/assets/unicopia/particles/rainboom_trail.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/src/main/resources/assets/unicopia/particles/rainboom_trail.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/main/resources/assets/unicopia/textures/particles/rainboom_trail.png b/src/main/resources/assets/unicopia/textures/particles/rainboom_trail.png new file mode 100644 index 0000000000000000000000000000000000000000..92da123f67c183e79490c5cd216a148733e9da44 GIT binary patch literal 2784 zcmV<63Lo`}P)00004XF*Lt006O% z3;baP000U)X+uL$P-t&-Z*ypGa3D!TLm+T+Z)Rz1WdHz3iJg{rR8-d%htIutdZEoQ z6e&aRy$v9}H>uJ@VVD_UC<6{NG_fI~0ue<-1QkJoA_k0xBC#Thg@9ne9*`iQ#9$Or zQF$}6R&?d%y_c8YA7_1QpS|}zXYYO1x&V;8{kgn!SPFnNo`4_X6{c}T{8k*B#$jdxfFg<9uYy1K45IaYvHg`_dOZM)Sy63ve6hvv z1)yUy0P^?0*fb9UASvow`@mQCp^4`uNg&9uGcn1|&Nk+9SjOUl{-OWr@Hh0;_l(8q z{wNRKos+;6rV8ldy0Owz(}jF`W(JeRp&R{qi2rfmU!TJ;gp(Kmm5I1s5m_f-n#TRsj}B0%?E`vOzxB2#P=n*a3EfYETOrKoe*ICqM@{4K9Go;5xVgZi5G4 z1dM~{UdP6d+Yd3o?MrAqM0Kc|iV92owdyL5UC#5<>aVCa44|hpM4E zs0sQWIt5*Tu0n&*J!lk~f_{hI!w5`*sjxDv4V%CW*ah~3!{C*0BD@;TgA3v9a1~q+ zAA{TB3-ERLHar49hi4Ih5D^-ph8Q6X#0?2VqLBoIkE}zAkxHZUgRb+f=nat zP#6>iMMoK->`~sRLq)(kHo*Vn{;LcG6+edD1=7D>9j^O?D{Qg|tCDK{ym)H7&wDr6*;uGTJg8GHjVbnL{!cWyUB7MT6o-VNo_w8Yq`2<5Ub)hw4L3rj}5@qxMs0 zWMyP6Wy582WNT#4$d1qunl{acmP#w5ouJ*Jy_Zv#bCKi7ZIf$}8d zZdVy&)LYdbX%I9R8VMQ|8r>Q*nyQ)sn)#Z|n)kKvS`4iu ztvy=3T65Yu+7a4Yv^%sXb>ww?bn(=Yu(!=O6^iuTp>)p_Y^{w=i z^lS773}6Fm1Fpe-gF!>Ip{*g$u-szvGhed;vo5pW&GpS$<~8QGEXWp~7V9lKEnZq0SaK{6Sl+dwSOr*Z zvFf(^Xl-N7w{EeXveC4Ov)N}e%%C!Y7^RFWwrE>d+x51mZQt2h+X?JW*!^a2WS?Sx z)P8cQ&Qi|OhNWW;>JChYI)@QQx?`Nj^#uJBl~d&PK+RZLOLos~K(b5>qmrMN0})tOkySZ3_W zICNY@+|jrX%s^&6b2i>5eqa0y%Z;^%^_=a@u3%4b9605ii3Ep)@`TAmhs0fpQ%O!q zl}XcFH*PieWwLj2ZSq`7V9Mc?h17`D)-+sNT-qs~3@?S(ldh7UlRlVXkWrK|vf6I- z?$tAVKYn8-l({mqQ$Q8{O!WzMg`0(=S&msXS#Pt$vrpzo=kRj+a`kh!z=6$;c zwT88(J6|n-WB%w`m$h~4pmp)YIh_ z3ETV2tjiAU!0h1dxU-n=E9e!)6|Z;4?!H=SSy{V>ut&IOq{_dl zbFb#!9eY1iCsp6Bajj|Hr?hX|zPbJE{X++w546-O*Ot`2Kgd0Jx6Z4syT zu9enWavU5N9)I?I-1m1*_?_rJ$vD~agVqoG+9++s?NEDe`%Fht$4F;X=in*dQ{7$m zU2Q)a|9JSc+Uc4zvS-T963!N$T{xF_ZuWe}`RNOZ7sk3{yB}PPym+f8xTpV;-=!;; zJuhGEb?H5K#o@~7t9DmUU1MD9xNd#Dz0azz?I)|B+WM{g+Xrk0I&awC=o(x)cy`EX z=)z6+o0o6-+`4{y+3mqQ%kSJBju{@g%f35#FZJHb`&swrA8dGtepviS>QUumrN{L@ z>;2q1Vm)$Z)P1z?N$8UYW2~{~zhwUMVZ87u`Dx{Z>O|9|`Q+&->FRy-Sjp7DHs zy69KwU-!MxeeuI@&cF4|M9z%AfP?@5 z`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u03}I8K~xCWV`Q}d{-3dof#K