mirror of
https://github.com/Sollace/Unicopia.git
synced 2024-11-27 23:27:59 +01:00
Reimplemented the portal spell
This commit is contained in:
parent
a32a14f134
commit
734c256822
16 changed files with 537 additions and 236 deletions
|
@ -1,227 +0,0 @@
|
||||||
package com.minelittlepony.unicopia;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import com.google.common.base.Suppliers;
|
|
||||||
import com.minelittlepony.unicopia.network.Channel;
|
|
||||||
import com.minelittlepony.unicopia.network.MsgBlockDestruction;
|
|
||||||
import com.minelittlepony.unicopia.util.NbtSerialisable;
|
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
||||||
import net.minecraft.block.BlockState;
|
|
||||||
import net.minecraft.nbt.NbtCompound;
|
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
|
||||||
import net.minecraft.server.world.ServerWorld;
|
|
||||||
import net.minecraft.server.world.ThreadedAnvilChunkStorage;
|
|
||||||
import net.minecraft.util.math.BlockPos;
|
|
||||||
import net.minecraft.util.math.ChunkPos;
|
|
||||||
import net.minecraft.world.PersistentState;
|
|
||||||
import net.minecraft.world.World;
|
|
||||||
|
|
||||||
public class BlockDestructionManager extends PersistentState {
|
|
||||||
public static final int DESTRUCTION_COOLDOWN = 50;
|
|
||||||
public static final int UNSET_DAMAGE = -1;
|
|
||||||
public static final int MAX_DAMAGE = 10;
|
|
||||||
|
|
||||||
private final Destruction emptyDestruction = new Destruction();
|
|
||||||
|
|
||||||
private final World world;
|
|
||||||
|
|
||||||
private final Long2ObjectMap<Chunk> chunks = new Long2ObjectOpenHashMap<>();
|
|
||||||
|
|
||||||
private final Object locker = new Object();
|
|
||||||
|
|
||||||
public static Supplier<BlockDestructionManager> create(World world) {
|
|
||||||
if (world instanceof ServerWorld serverWorld) {
|
|
||||||
return Suppliers.memoize(() -> {
|
|
||||||
return serverWorld.getPersistentStateManager().getOrCreate(
|
|
||||||
compound -> new BlockDestructionManager(world, compound),
|
|
||||||
() -> new BlockDestructionManager(world),
|
|
||||||
"unicopia:destruction_manager"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Suppliers.memoize(() -> new BlockDestructionManager(world));
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockDestructionManager(World world) {
|
|
||||||
this.world = world;
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockDestructionManager(World world, NbtCompound compound) {
|
|
||||||
this(world);
|
|
||||||
NbtCompound d = compound.getCompound("chunks");
|
|
||||||
d.getKeys().forEach(id -> {
|
|
||||||
chunks.computeIfAbsent(Long.valueOf(id), Chunk::new).fromNBT(d.getCompound(id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NbtCompound writeNbt(NbtCompound compound) {
|
|
||||||
NbtCompound destructions = new NbtCompound();
|
|
||||||
this.chunks.forEach((id, chunk) -> {
|
|
||||||
destructions.put(id.toString(), chunk.toNBT());
|
|
||||||
});
|
|
||||||
compound.put("chunks", destructions);
|
|
||||||
return compound;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getBlockDestruction(BlockPos pos) {
|
|
||||||
return getChunk(pos).getBlockDestruction(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chunk getChunk(BlockPos pos) {
|
|
||||||
return chunks.computeIfAbsent(new ChunkPos(pos).toLong(), Chunk::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBlockDestruction(BlockPos pos, int amount) {
|
|
||||||
synchronized (locker) {
|
|
||||||
getChunk(pos).setBlockDestruction(pos, amount);
|
|
||||||
markDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int damageBlock(BlockPos pos, int amount) {
|
|
||||||
if (amount == 0) {
|
|
||||||
return getBlockDestruction(pos);
|
|
||||||
}
|
|
||||||
amount = Math.max(getBlockDestruction(pos), 0) + amount;
|
|
||||||
setBlockDestruction(pos, amount);
|
|
||||||
return amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onBlockChanged(BlockPos pos, BlockState oldState, BlockState newstate) {
|
|
||||||
if (oldState.getBlock() != newstate.getBlock()) {
|
|
||||||
setBlockDestruction(pos, UNSET_DAMAGE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void tick() {
|
|
||||||
synchronized (locker) {
|
|
||||||
chunks.long2ObjectEntrySet().removeIf(entry -> entry.getValue().tick());
|
|
||||||
|
|
||||||
if (world instanceof ServerWorld) {
|
|
||||||
chunks.forEach((chunkPos, chunk) -> chunk.sendUpdates((ServerWorld)world));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Chunk implements NbtSerialisable {
|
|
||||||
private final Long2ObjectMap<Destruction> destructions = new Long2ObjectOpenHashMap<>();
|
|
||||||
|
|
||||||
private final long pos;
|
|
||||||
|
|
||||||
Chunk(long pos) {
|
|
||||||
this.pos = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getBlockDestruction(BlockPos pos) {
|
|
||||||
return destructions.getOrDefault(pos.asLong(), emptyDestruction).amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBlockDestruction(BlockPos pos, int amount) {
|
|
||||||
destructions.computeIfAbsent(pos.asLong(), p -> new Destruction()).set(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean tick() {
|
|
||||||
destructions.long2ObjectEntrySet().removeIf(e -> e.getValue().tick());
|
|
||||||
return destructions.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendUpdates(ServerWorld world) {
|
|
||||||
if (!world.getChunkManager().isChunkLoaded(ChunkPos.getPackedX(pos), ChunkPos.getPackedZ(pos))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ThreadedAnvilChunkStorage storage = world.getChunkManager().threadedAnvilChunkStorage;
|
|
||||||
|
|
||||||
List<ServerPlayerEntity> players = storage.getPlayersWatchingChunk(new ChunkPos(pos), false);
|
|
||||||
|
|
||||||
if (!players.isEmpty()) {
|
|
||||||
Long2ObjectOpenHashMap<Integer> values = new Long2ObjectOpenHashMap<>();
|
|
||||||
|
|
||||||
destructions.forEach((blockPos, item) -> {
|
|
||||||
if (item.dirty) {
|
|
||||||
item.dirty = false;
|
|
||||||
values.put(blockPos.longValue(), (Integer)item.amount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
MsgBlockDestruction msg = new MsgBlockDestruction(values);
|
|
||||||
|
|
||||||
if (msg.toBuffer().writerIndex() > 1048576) {
|
|
||||||
throw new IllegalStateException("Payload may not be larger than 1048576 bytes. Here's what we were trying to send: ["
|
|
||||||
+ values.size() + "]\n"
|
|
||||||
+ Arrays.toString(values.values().stream().mapToInt(Integer::intValue).toArray()));
|
|
||||||
}
|
|
||||||
|
|
||||||
players.forEach(player -> {
|
|
||||||
if (player instanceof ServerPlayerEntity) {
|
|
||||||
Channel.SERVER_BLOCK_DESTRUCTION.send(player, msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void toNBT(NbtCompound compound) {
|
|
||||||
NbtCompound states = new NbtCompound();
|
|
||||||
destructions.forEach((id, state) -> {
|
|
||||||
states.put(id.toString(), state.toNBT());
|
|
||||||
});
|
|
||||||
compound.put("states", states);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fromNBT(NbtCompound compound) {
|
|
||||||
NbtCompound d = compound.getCompound("states");
|
|
||||||
chunks.clear();
|
|
||||||
d.getKeys().forEach(id -> {
|
|
||||||
destructions.computeIfAbsent(Long.valueOf(id), i -> new Destruction()).fromNBT(d.getCompound(id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Destruction implements NbtSerialisable {
|
|
||||||
int amount = UNSET_DAMAGE;
|
|
||||||
int age = DESTRUCTION_COOLDOWN;
|
|
||||||
boolean dirty;
|
|
||||||
|
|
||||||
boolean tick() {
|
|
||||||
if (age-- > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amount >= 0) {
|
|
||||||
set(amount - 1);
|
|
||||||
}
|
|
||||||
return amount < 0 || age-- <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void set(int amount) {
|
|
||||||
this.age = DESTRUCTION_COOLDOWN;
|
|
||||||
this.amount = amount >= 0 && amount < MAX_DAMAGE ? amount : UNSET_DAMAGE;
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void toNBT(NbtCompound compound) {
|
|
||||||
compound.putInt("destruction", amount);
|
|
||||||
compound.putInt("age", age);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fromNBT(NbtCompound compound) {
|
|
||||||
amount = compound.getInt("destruction");
|
|
||||||
age = compound.getInt("age");
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Source {
|
|
||||||
BlockDestructionManager getDestructionManager();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,7 @@ import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType;
|
||||||
import com.minelittlepony.unicopia.ability.magic.spell.trait.TraitLoader;
|
import com.minelittlepony.unicopia.ability.magic.spell.trait.TraitLoader;
|
||||||
import com.minelittlepony.unicopia.advancement.UCriteria;
|
import com.minelittlepony.unicopia.advancement.UCriteria;
|
||||||
import com.minelittlepony.unicopia.block.UBlocks;
|
import com.minelittlepony.unicopia.block.UBlocks;
|
||||||
|
import com.minelittlepony.unicopia.block.data.BlockDestructionManager;
|
||||||
import com.minelittlepony.unicopia.block.state.StateMapLoader;
|
import com.minelittlepony.unicopia.block.state.StateMapLoader;
|
||||||
import com.minelittlepony.unicopia.command.Commands;
|
import com.minelittlepony.unicopia.command.Commands;
|
||||||
import com.minelittlepony.unicopia.container.SpellbookChapterLoader;
|
import com.minelittlepony.unicopia.container.SpellbookChapterLoader;
|
||||||
|
|
|
@ -5,11 +5,11 @@ import java.util.List;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.minelittlepony.unicopia.BlockDestructionManager;
|
|
||||||
import com.minelittlepony.unicopia.Race;
|
import com.minelittlepony.unicopia.Race;
|
||||||
import com.minelittlepony.unicopia.ability.data.Hit;
|
import com.minelittlepony.unicopia.ability.data.Hit;
|
||||||
import com.minelittlepony.unicopia.ability.data.Pos;
|
import com.minelittlepony.unicopia.ability.data.Pos;
|
||||||
import com.minelittlepony.unicopia.ability.data.tree.TreeType;
|
import com.minelittlepony.unicopia.ability.data.tree.TreeType;
|
||||||
|
import com.minelittlepony.unicopia.block.data.BlockDestructionManager;
|
||||||
import com.minelittlepony.unicopia.client.minelittlepony.MineLPConnector;
|
import com.minelittlepony.unicopia.client.minelittlepony.MineLPConnector;
|
||||||
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
||||||
import com.minelittlepony.unicopia.entity.player.Pony;
|
import com.minelittlepony.unicopia.entity.player.Pony;
|
||||||
|
|
|
@ -2,9 +2,9 @@ package com.minelittlepony.unicopia.ability;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import com.minelittlepony.unicopia.BlockDestructionManager;
|
|
||||||
import com.minelittlepony.unicopia.Race;
|
import com.minelittlepony.unicopia.Race;
|
||||||
import com.minelittlepony.unicopia.ability.data.Hit;
|
import com.minelittlepony.unicopia.ability.data.Hit;
|
||||||
|
import com.minelittlepony.unicopia.block.data.BlockDestructionManager;
|
||||||
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
||||||
import com.minelittlepony.unicopia.entity.player.Pony;
|
import com.minelittlepony.unicopia.entity.player.Pony;
|
||||||
import com.minelittlepony.unicopia.item.UItems;
|
import com.minelittlepony.unicopia.item.UItems;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.minelittlepony.unicopia.ability.data.Hit;
|
||||||
import com.minelittlepony.unicopia.ability.magic.spell.HomingSpell;
|
import com.minelittlepony.unicopia.ability.magic.spell.HomingSpell;
|
||||||
import com.minelittlepony.unicopia.ability.magic.spell.Spell;
|
import com.minelittlepony.unicopia.ability.magic.spell.Spell;
|
||||||
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
|
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
|
||||||
|
import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType;
|
||||||
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
||||||
import com.minelittlepony.unicopia.entity.player.Pony;
|
import com.minelittlepony.unicopia.entity.player.Pony;
|
||||||
import com.minelittlepony.unicopia.item.AmuletItem;
|
import com.minelittlepony.unicopia.item.AmuletItem;
|
||||||
|
@ -105,9 +106,9 @@ public class UnicornCastingAbility implements Ability<Hit> {
|
||||||
if (newSpell.getResult() != ActionResult.FAIL) {
|
if (newSpell.getResult() != ActionResult.FAIL) {
|
||||||
CustomisedSpellType<?> spell = newSpell.getValue();
|
CustomisedSpellType<?> spell = newSpell.getValue();
|
||||||
|
|
||||||
boolean remove = player.getSpellSlot().removeIf(spell, true);
|
boolean removed = player.getSpellSlot().removeIf(spell.isEmpty() ? spell : SpellType.PORTAL.negate().and(spell), true);
|
||||||
player.subtractEnergyCost(remove ? 2 : 4);
|
player.subtractEnergyCost(removed ? 2 : 4);
|
||||||
if (!remove) {
|
if (!removed) {
|
||||||
Spell s = spell.apply(player);
|
Spell s = spell.apply(player);
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
player.spawnParticles(ParticleTypes.LARGE_SMOKE, 6);
|
player.spawnParticles(ParticleTypes.LARGE_SMOKE, 6);
|
||||||
|
|
|
@ -108,6 +108,19 @@ public class PlaceableSpell extends AbstractDelegatingSpell {
|
||||||
return !isDead();
|
return !isDead();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches this spell from the placed version.
|
||||||
|
* This spell and the placed entity effectively become independent.
|
||||||
|
*
|
||||||
|
* @return The previous cast spell entity if one existed, otherwise empty.
|
||||||
|
*/
|
||||||
|
protected Optional<CastSpellEntity> detach(Caster<?> source) {
|
||||||
|
return getSpellEntity(source).map(e -> {
|
||||||
|
castEntity.set(null);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyed(Caster<?> source) {
|
public void onDestroyed(Caster<?> source) {
|
||||||
if (!source.isClient()) {
|
if (!source.isClient()) {
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.minelittlepony.unicopia.ability.magic.spell.effect;
|
||||||
|
|
||||||
|
import com.minelittlepony.unicopia.USounds;
|
||||||
|
import com.minelittlepony.unicopia.ability.magic.Caster;
|
||||||
|
import com.minelittlepony.unicopia.ability.magic.spell.Situation;
|
||||||
|
import com.minelittlepony.unicopia.block.data.Ether;
|
||||||
|
import com.minelittlepony.unicopia.entity.CastSpellEntity;
|
||||||
|
import com.minelittlepony.unicopia.entity.EntityReference;
|
||||||
|
import com.minelittlepony.unicopia.particle.*;
|
||||||
|
import com.minelittlepony.unicopia.particle.ParticleHandle.Attachment;
|
||||||
|
import com.minelittlepony.unicopia.util.shape.Sphere;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
import net.minecraft.util.math.Vec3d;
|
||||||
|
|
||||||
|
public class PortalSpell extends AbstractSpell {
|
||||||
|
private final EntityReference<CastSpellEntity> teleportationTarget = new EntityReference<>();
|
||||||
|
|
||||||
|
private boolean publishedPosition;
|
||||||
|
|
||||||
|
private final ParticleHandle particlEffect = new ParticleHandle();
|
||||||
|
|
||||||
|
protected PortalSpell(CustomisedSpellType<?> type) {
|
||||||
|
super(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean apply(Caster<?> caster) {
|
||||||
|
return toPlaceable().apply(caster);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tick(Caster<?> source, Situation situation) {
|
||||||
|
if (situation == Situation.GROUND) {
|
||||||
|
teleportationTarget.getPosition().ifPresentOrElse(
|
||||||
|
targetPos -> tickWithTargetLink(source, targetPos),
|
||||||
|
() -> advertiseAvailable(source)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!publishedPosition) {
|
||||||
|
publishedPosition = true;
|
||||||
|
Ether.get(source.getReferenceWorld()).put(getType(), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.isClient()) {
|
||||||
|
Vec3d origin = source.getOriginVector();
|
||||||
|
|
||||||
|
source.spawnParticles(origin, new Sphere(true, 2, 1, 0, 1), 17, pos -> {
|
||||||
|
source.addParticle(new MagicParticleEffect(getType().getColor()), pos, Vec3d.ZERO);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickWithTargetLink(Caster<?> source, Vec3d targetPos) {
|
||||||
|
particlEffect.update(getUuid(), source, spawner -> {
|
||||||
|
spawner.addParticle(new SphereParticleEffect(UParticles.DISK, getType().getColor(), 0.9F, 2), source.getOriginVector(), Vec3d.ZERO);
|
||||||
|
}).ifPresent(p -> {
|
||||||
|
p.setAttribute(Attachment.ATTR_COLOR, getType().getColor());
|
||||||
|
});
|
||||||
|
|
||||||
|
Vec3d center = source.getOriginVector();
|
||||||
|
source.findAllEntitiesInRange(1).filter(e -> true).forEach(entity -> {
|
||||||
|
if (!entity.hasPortalCooldown() && entity.timeUntilRegen <= 0) {
|
||||||
|
Vec3d destination = entity.getPos().subtract(center).add(targetPos);
|
||||||
|
entity.resetPortalCooldown();
|
||||||
|
entity.timeUntilRegen = 100;
|
||||||
|
|
||||||
|
entity.playSound(USounds.ENTITY_PLAYER_UNICORN_TELEPORT, 1, 1);
|
||||||
|
entity.teleport(destination.x, destination.y, destination.z);
|
||||||
|
}
|
||||||
|
ParticleUtils.spawnParticles(new MagicParticleEffect(getType().getColor()), entity, 7);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void advertiseAvailable(Caster<?> source) {
|
||||||
|
Ether ether = Ether.get(source.getReferenceWorld());
|
||||||
|
ether.getIds(getType())
|
||||||
|
.stream()
|
||||||
|
.filter(id -> !id.referenceEquals(source.getEntity()))
|
||||||
|
.findAny()
|
||||||
|
.ifPresent(ref -> {
|
||||||
|
ether.remove(getType(), ref.getId().get());
|
||||||
|
teleportationTarget.copyFrom((EntityReference<CastSpellEntity>)ref);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyed(Caster<?> caster) {
|
||||||
|
Ether.get(caster.getReferenceWorld()).remove(getType(), caster.getEntity().getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toNBT(NbtCompound compound) {
|
||||||
|
super.toNBT(compound);
|
||||||
|
compound.putBoolean("publishedPosition", publishedPosition);
|
||||||
|
compound.put("teleportationTarget", teleportationTarget.toNBT());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromNBT(NbtCompound compound) {
|
||||||
|
super.fromNBT(compound);
|
||||||
|
publishedPosition = compound.getBoolean("publishedPosition");
|
||||||
|
teleportationTarget.fromNBT(compound.getCompound("teleportationTarget"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,6 +62,7 @@ public final class SpellType<T extends Spell> implements Affine, SpellPredicate<
|
||||||
public static final SpellType<CatapultSpell> CATAPULT = register("catapult", Affinity.GOOD, 0x22FF00, true, CatapultSpell.DEFAULT_TRAITS, CatapultSpell::new);
|
public static final SpellType<CatapultSpell> CATAPULT = register("catapult", Affinity.GOOD, 0x22FF00, true, CatapultSpell.DEFAULT_TRAITS, CatapultSpell::new);
|
||||||
public static final SpellType<FireBoltSpell> FIRE_BOLT = register("fire_bolt", Affinity.GOOD, 0xFF8811, true, FireBoltSpell.DEFAULT_TRAITS, FireBoltSpell::new);
|
public static final SpellType<FireBoltSpell> FIRE_BOLT = register("fire_bolt", Affinity.GOOD, 0xFF8811, true, FireBoltSpell.DEFAULT_TRAITS, FireBoltSpell::new);
|
||||||
public static final SpellType<LightSpell> LIGHT = register("light", Affinity.GOOD, 0xEEFFAA, true, LightSpell.DEFAULT_TRAITS, LightSpell::new);
|
public static final SpellType<LightSpell> LIGHT = register("light", Affinity.GOOD, 0xEEFFAA, true, LightSpell.DEFAULT_TRAITS, LightSpell::new);
|
||||||
|
public static final SpellType<PortalSpell> PORTAL = register("portal", Affinity.GOOD, 0x99FFFF, true, LightSpell.DEFAULT_TRAITS, PortalSpell::new);
|
||||||
|
|
||||||
public static void bootstrap() {}
|
public static void bootstrap() {}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
package com.minelittlepony.unicopia.block.data;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import com.google.common.base.Suppliers;
|
||||||
|
import com.minelittlepony.unicopia.Unicopia;
|
||||||
|
import com.minelittlepony.unicopia.network.Channel;
|
||||||
|
import com.minelittlepony.unicopia.network.MsgBlockDestruction;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import net.minecraft.block.BlockState;
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
public class BlockDestructionManager {
|
||||||
|
private static final Identifier ID = Unicopia.id("destruction_manager");
|
||||||
|
|
||||||
|
public static final int DESTRUCTION_COOLDOWN = 50;
|
||||||
|
public static final int UNSET_DAMAGE = -1;
|
||||||
|
public static final int MAX_DAMAGE = 10;
|
||||||
|
|
||||||
|
private final WorldOverlay<Destruction> chunks;
|
||||||
|
|
||||||
|
public static Supplier<BlockDestructionManager> create(World world) {
|
||||||
|
return Suppliers.memoize(() -> new BlockDestructionManager(world));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockDestructionManager(World world) {
|
||||||
|
this.chunks = WorldOverlay.getOverlay(world, ID, w -> new WorldOverlay<>(world, Destruction::new, this::sendUpdates));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBlockDestruction(BlockPos pos) {
|
||||||
|
Destruction destr = chunks.getState(pos);
|
||||||
|
return destr == null ? UNSET_DAMAGE : destr.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockDestruction(BlockPos pos, int amount) {
|
||||||
|
chunks.getOrCreateState(pos).set(amount);
|
||||||
|
chunks.markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int damageBlock(BlockPos pos, int amount) {
|
||||||
|
if (amount == 0) {
|
||||||
|
return getBlockDestruction(pos);
|
||||||
|
}
|
||||||
|
amount = Math.max(getBlockDestruction(pos), 0) + amount;
|
||||||
|
setBlockDestruction(pos, amount);
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onBlockChanged(BlockPos pos, BlockState oldState, BlockState newstate) {
|
||||||
|
if (oldState.getBlock() != newstate.getBlock()) {
|
||||||
|
setBlockDestruction(pos, UNSET_DAMAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
chunks.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendUpdates(Long2ObjectMap<Destruction> destructions, List<ServerPlayerEntity> players) {
|
||||||
|
Long2ObjectOpenHashMap<Integer> values = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
destructions.forEach((blockPos, item) -> {
|
||||||
|
if (item.dirty) {
|
||||||
|
item.dirty = false;
|
||||||
|
values.put(blockPos.longValue(), (Integer)item.amount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MsgBlockDestruction msg = new MsgBlockDestruction(values);
|
||||||
|
|
||||||
|
if (msg.toBuffer().writerIndex() > 1048576) {
|
||||||
|
throw new IllegalStateException("Payload may not be larger than 1048576 bytes. Here's what we were trying to send: ["
|
||||||
|
+ values.size() + "]\n"
|
||||||
|
+ Arrays.toString(values.values().stream().mapToInt(Integer::intValue).toArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
players.forEach(player -> {
|
||||||
|
if (player instanceof ServerPlayerEntity) {
|
||||||
|
Channel.SERVER_BLOCK_DESTRUCTION.send(player, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Destruction implements WorldOverlay.State {
|
||||||
|
int amount = UNSET_DAMAGE;
|
||||||
|
int age = DESTRUCTION_COOLDOWN;
|
||||||
|
boolean dirty;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tick() {
|
||||||
|
if (age-- > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount >= 0) {
|
||||||
|
set(amount - 1);
|
||||||
|
}
|
||||||
|
return amount < 0 || age-- <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(int amount) {
|
||||||
|
this.age = DESTRUCTION_COOLDOWN;
|
||||||
|
this.amount = amount >= 0 && amount < MAX_DAMAGE ? amount : UNSET_DAMAGE;
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toNBT(NbtCompound compound) {
|
||||||
|
compound.putInt("destruction", amount);
|
||||||
|
compound.putInt("age", age);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromNBT(NbtCompound compound) {
|
||||||
|
amount = compound.getInt("destruction");
|
||||||
|
age = compound.getInt("age");
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Source {
|
||||||
|
BlockDestructionManager getDestructionManager();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.minelittlepony.unicopia.block.data;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import com.minelittlepony.unicopia.Unicopia;
|
||||||
|
import com.minelittlepony.unicopia.ability.magic.Caster;
|
||||||
|
import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType;
|
||||||
|
import com.minelittlepony.unicopia.entity.EntityReference;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.*;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
import net.minecraft.world.PersistentState;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
public class Ether extends PersistentState {
|
||||||
|
private static final Identifier ID = Unicopia.id("ether");
|
||||||
|
|
||||||
|
public static Ether get(World world) {
|
||||||
|
return WorldOverlay.getPersistableStorage(world, ID, Ether::new, Ether::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<Identifier, Set<EntityReference<?>>> advertisingEndpoints = new HashMap<>();
|
||||||
|
|
||||||
|
private final Object locker = new Object();
|
||||||
|
|
||||||
|
Ether(World world, NbtCompound compound) {
|
||||||
|
this(world);
|
||||||
|
compound.getKeys().forEach(key -> {
|
||||||
|
Identifier typeId = Identifier.tryParse(key);
|
||||||
|
if (typeId != null) {
|
||||||
|
Set<EntityReference<?>> uuids = getIds(typeId);
|
||||||
|
compound.getList(key, NbtElement.COMPOUND_TYPE).forEach(entry -> {
|
||||||
|
uuids.add(new EntityReference<>((NbtCompound)entry));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ether(World world) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NbtCompound writeNbt(NbtCompound compound) {
|
||||||
|
synchronized (locker) {
|
||||||
|
advertisingEndpoints.forEach((id, uuids) -> {
|
||||||
|
NbtList list = new NbtList();
|
||||||
|
uuids.forEach(uuid -> list.add(uuid.toNBT()));
|
||||||
|
compound.put(id.toString(), list);
|
||||||
|
});
|
||||||
|
|
||||||
|
return compound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(SpellType<?> spellType, Caster<?> caster) {
|
||||||
|
synchronized (locker) {
|
||||||
|
getIds(spellType.getId()).add(new EntityReference<>(caster.getEntity()));
|
||||||
|
}
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(SpellType<?> spellType, UUID id) {
|
||||||
|
synchronized (locker) {
|
||||||
|
Identifier typeId = spellType.getId();
|
||||||
|
Set<EntityReference<?>> refs = advertisingEndpoints.get(typeId);
|
||||||
|
if (refs != null) {
|
||||||
|
refs.removeIf(ref -> ref.getId().orElse(Util.NIL_UUID).equals(id));
|
||||||
|
if (refs.isEmpty()) {
|
||||||
|
advertisingEndpoints.remove(typeId);
|
||||||
|
}
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<EntityReference<?>> getIds(SpellType<?> spellType) {
|
||||||
|
return getIds(spellType.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<EntityReference<?>> getIds(Identifier typeId) {
|
||||||
|
synchronized (locker) {
|
||||||
|
return advertisingEndpoints.computeIfAbsent(typeId, i -> new HashSet<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package com.minelittlepony.unicopia.block.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.*;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import com.minelittlepony.unicopia.util.NbtSerialisable;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
import net.minecraft.server.world.ThreadedAnvilChunkStorage;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.util.math.ChunkPos;
|
||||||
|
import net.minecraft.world.PersistentState;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
public class WorldOverlay<T extends WorldOverlay.State> extends PersistentState {
|
||||||
|
private final World world;
|
||||||
|
|
||||||
|
private final Long2ObjectMap<Chunk> chunks = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
private final Object locker = new Object();
|
||||||
|
|
||||||
|
private final Supplier<T> factory;
|
||||||
|
@Nullable
|
||||||
|
private final BiConsumer<Long2ObjectMap<T>, List<ServerPlayerEntity>> updateSender;
|
||||||
|
|
||||||
|
public static <T extends PersistentState> T getPersistableStorage(World world, Identifier id, BiFunction<World, NbtCompound, T> loadFunc, Function<World, T> factory) {
|
||||||
|
if (world instanceof ServerWorld serverWorld) {
|
||||||
|
return serverWorld.getPersistentStateManager().getOrCreate(
|
||||||
|
compound -> loadFunc.apply(world, compound),
|
||||||
|
() -> factory.apply(world),
|
||||||
|
id.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return factory.apply(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends State> WorldOverlay<T> getOverlay(World world, Identifier id, Supplier<T> factory, @Nullable BiConsumer<Long2ObjectMap<T>, List<ServerPlayerEntity>> updateSender) {
|
||||||
|
return getOverlay(world, id, w -> new WorldOverlay<>(w, factory, updateSender));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends State> WorldOverlay<T> getOverlay(World world, Identifier id, Function<World, WorldOverlay<T>> overlayFactory) {
|
||||||
|
return getPersistableStorage(world, id, (w, tag) -> {
|
||||||
|
WorldOverlay<T> overlay = overlayFactory.apply(w);
|
||||||
|
overlay.readNbt(tag);
|
||||||
|
return overlay;
|
||||||
|
}, overlayFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldOverlay(World world, Supplier<T> factory, @Nullable BiConsumer<Long2ObjectMap<T>, List<ServerPlayerEntity>> updateSender) {
|
||||||
|
this.world = world;
|
||||||
|
this.factory = factory;
|
||||||
|
this.updateSender = updateSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NbtCompound writeNbt(NbtCompound compound) {
|
||||||
|
NbtCompound destructions = new NbtCompound();
|
||||||
|
this.chunks.forEach((id, chunk) -> {
|
||||||
|
destructions.put(id.toString(), chunk.toNBT());
|
||||||
|
});
|
||||||
|
compound.put("chunks", destructions);
|
||||||
|
return compound;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readNbt(NbtCompound compound) {
|
||||||
|
NbtCompound d = compound.getCompound("chunks");
|
||||||
|
d.getKeys().forEach(id -> {
|
||||||
|
chunks.computeIfAbsent(Long.valueOf(id), Chunk::new).fromNBT(d.getCompound(id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public T getState(BlockPos pos) {
|
||||||
|
return getChunk(pos).getState(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getOrCreateState(BlockPos pos) {
|
||||||
|
synchronized (locker) {
|
||||||
|
return getChunk(pos).getOrCreateState(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chunk getChunk(BlockPos pos) {
|
||||||
|
return chunks.computeIfAbsent(new ChunkPos(pos).toLong(), Chunk::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(BlockPos pos, @Nullable T state) {
|
||||||
|
synchronized (locker) {
|
||||||
|
getChunk(pos).setState(pos, state);
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
synchronized (locker) {
|
||||||
|
chunks.long2ObjectEntrySet().removeIf(entry -> entry.getValue().tick());
|
||||||
|
|
||||||
|
if (world instanceof ServerWorld) {
|
||||||
|
chunks.forEach((chunkPos, chunk) -> chunk.sendUpdates((ServerWorld)world));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Chunk implements NbtSerialisable {
|
||||||
|
private final Long2ObjectMap<T> states = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
private final long pos;
|
||||||
|
|
||||||
|
Chunk(long pos) {
|
||||||
|
this.pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public T getState(BlockPos pos) {
|
||||||
|
return states.get(pos.asLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getOrCreateState(BlockPos pos) {
|
||||||
|
return states.computeIfAbsent(pos.asLong(), l -> factory.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(BlockPos pos, @Nullable T state) {
|
||||||
|
if (state == null) {
|
||||||
|
states.remove(pos.asLong());
|
||||||
|
} else {
|
||||||
|
states.put(pos.asLong(), state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean tick() {
|
||||||
|
states.long2ObjectEntrySet().removeIf(e -> e.getValue().tick());
|
||||||
|
return states.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendUpdates(ServerWorld world) {
|
||||||
|
if (updateSender == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world.getChunkManager().isChunkLoaded(ChunkPos.getPackedX(pos), ChunkPos.getPackedZ(pos))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadedAnvilChunkStorage storage = world.getChunkManager().threadedAnvilChunkStorage;
|
||||||
|
|
||||||
|
List<ServerPlayerEntity> players = storage.getPlayersWatchingChunk(new ChunkPos(pos), false);
|
||||||
|
|
||||||
|
if (!players.isEmpty()) {
|
||||||
|
updateSender.accept(states, players);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toNBT(NbtCompound compound) {
|
||||||
|
NbtCompound states = new NbtCompound();
|
||||||
|
this.states.forEach((id, state) -> {
|
||||||
|
states.put(id.toString(), state.toNBT());
|
||||||
|
});
|
||||||
|
compound.put("states", states);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromNBT(NbtCompound compound) {
|
||||||
|
NbtCompound d = compound.getCompound("states");
|
||||||
|
chunks.clear();
|
||||||
|
d.getKeys().forEach(id -> {
|
||||||
|
states.computeIfAbsent(Long.valueOf(id), i -> factory.get()).fromNBT(d.getCompound(id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface State extends NbtSerialisable {
|
||||||
|
boolean tick();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,8 @@ package com.minelittlepony.unicopia.client;
|
||||||
|
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.minelittlepony.unicopia.BlockDestructionManager;
|
import com.minelittlepony.unicopia.block.data.BlockDestructionManager;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
import net.minecraft.client.render.BlockBreakingInfo;
|
import net.minecraft.client.render.BlockBreakingInfo;
|
||||||
|
|
|
@ -13,7 +13,7 @@ public class DiskParticle extends SphereParticle {
|
||||||
|
|
||||||
public DiskParticle(SphereParticleEffect effect, ClientWorld w, double x, double y, double z, double rX, double rY, double rZ) {
|
public DiskParticle(SphereParticleEffect effect, ClientWorld w, double x, double y, double z, double rX, double rY, double rZ) {
|
||||||
super(effect, w, x, y, z, 0, 0, 0);
|
super(effect, w, x, y, z, 0, 0, 0);
|
||||||
rotation = new Quaternion((float)rX, (float)rY, (float)rZ, true);
|
rotation = new Quaternion((float)effect.getOffset().x, (float)effect.getOffset().y, (float)effect.getOffset().z, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -5,7 +5,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
import com.minelittlepony.unicopia.BlockDestructionManager;
|
import com.minelittlepony.unicopia.block.data.BlockDestructionManager;
|
||||||
|
|
||||||
import net.minecraft.block.BlockState;
|
import net.minecraft.block.BlockState;
|
||||||
import net.minecraft.server.world.ServerWorld;
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
import com.minelittlepony.unicopia.BlockDestructionManager;
|
import com.minelittlepony.unicopia.block.data.BlockDestructionManager;
|
||||||
import com.minelittlepony.unicopia.entity.collision.EntityCollisions;
|
import com.minelittlepony.unicopia.entity.collision.EntityCollisions;
|
||||||
import com.minelittlepony.unicopia.entity.duck.RotatedView;
|
import com.minelittlepony.unicopia.entity.duck.RotatedView;
|
||||||
|
|
||||||
|
|
|
@ -152,6 +152,8 @@
|
||||||
"spell.unicopia.vortex.lore": "Creates a magnetic force that pulls in other targets",
|
"spell.unicopia.vortex.lore": "Creates a magnetic force that pulls in other targets",
|
||||||
"spell.unicopia.dark_vortex": "Dark Vortex",
|
"spell.unicopia.dark_vortex": "Dark Vortex",
|
||||||
"spell.unicopia.dark_vortex.lore": "Creates a black hole from which nothing can escape",
|
"spell.unicopia.dark_vortex.lore": "Creates a black hole from which nothing can escape",
|
||||||
|
"spell.unicopia.portal": "Teleportation",
|
||||||
|
"spell.unicopia.portal.lore": "Connects two points in space for fast travel between",
|
||||||
"spell.unicopia.necromancy": "Necromancy",
|
"spell.unicopia.necromancy": "Necromancy",
|
||||||
"spell.unicopia.necromancy.lore": "Summons undead minions from beyond the grave",
|
"spell.unicopia.necromancy.lore": "Summons undead minions from beyond the grave",
|
||||||
"spell.unicopia.siphoning": "Life Sapping",
|
"spell.unicopia.siphoning": "Life Sapping",
|
||||||
|
|
Loading…
Reference in a new issue