diff --git a/src/main/java/com/minelittlepony/unicopia/block/data/DragonBreathStore.java b/src/main/java/com/minelittlepony/unicopia/block/data/DragonBreathStore.java new file mode 100644 index 00000000..9c4b9369 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/block/data/DragonBreathStore.java @@ -0,0 +1,128 @@ +package com.minelittlepony.unicopia.block.data; + +import java.util.*; + +import com.minelittlepony.unicopia.Unicopia; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.*; +import net.minecraft.util.Identifier; +import net.minecraft.world.PersistentState; +import net.minecraft.world.World; + +public class DragonBreathStore extends PersistentState { + private static final long PURGE_INTERVAL = 1000 * 60 * 60; // 1 hour + private static final long MAX_MESSAGE_HOLD_TIME = PURGE_INTERVAL * 24; // 24 hours + private static final Identifier ID = Unicopia.id("dragon_breath"); + + public static DragonBreathStore get(World world) { + return WorldOverlay.getPersistableStorage(world, ID, DragonBreathStore::new, DragonBreathStore::new); + } + + private final Map> payloads = new HashMap<>(); + + private final Object locker = new Object(); + + DragonBreathStore(World world, NbtCompound compound) { + this(world); + compound.getKeys().forEach(key -> { + compound.getList(key, NbtElement.COMPOUND_TYPE).forEach(entry -> { + put(key, new Entry((NbtCompound)entry)); + }); + }); + } + + DragonBreathStore(World world) { + + } + + @Override + public NbtCompound writeNbt(NbtCompound compound) { + synchronized (locker) { + payloads.forEach((id, uuids) -> { + NbtList list = new NbtList(); + uuids.forEach(entry -> { + if (entry.created < System.currentTimeMillis() - MAX_MESSAGE_HOLD_TIME) { + list.add(entry.toNBT(new NbtCompound())); + } + }); + compound.put(id, list); + }); + + return compound; + } + } + + public List popEntries(String recipient) { + synchronized (locker) { + List entries = doPurge().get(recipient); + if (entries == null) { + return List.of(); + } + + long now = System.currentTimeMillis(); + List collected = new ArrayList<>(); + entries.removeIf(entry -> { + if (entry.created < now - 1000) { + collected.add(entry); + return true; + } + return false; + }); + return collected; + } + } + + public List peekEntries(String recipient) { + synchronized (locker) { + return doPurge().getOrDefault(recipient, List.of()); + } + } + + public void put(String recipient, ItemStack payload) { + synchronized (locker) { + doPurge(); + if (peekEntries(recipient).stream().noneMatch(i -> { + if (ItemStack.canCombine(i.payload(), payload)) { + int combinedCount = i.payload().getCount() + payload.getCount(); + if (combinedCount <= i.payload().getMaxCount()) { + i.payload().setCount(combinedCount); + return true; + } + } + return false; + })) { + put(recipient, new Entry(System.currentTimeMillis() + (long)(Math.random() * 1999), payload)); + } + } + } + + private void put(String recipient, Entry entry) { + payloads.computeIfAbsent(recipient, id -> new ArrayList<>()).add(entry); + } + + private Map> doPurge() { + long now = System.currentTimeMillis(); + if (now % PURGE_INTERVAL == 0) { + payloads.entrySet().removeIf(entry -> { + entry.getValue().removeIf(e -> e.created < now - MAX_MESSAGE_HOLD_TIME); + return entry.getValue().isEmpty(); + }); + } + return payloads; + } + + public record Entry( + long created, + ItemStack payload) { + + public Entry(NbtCompound compound) { + this(compound.getLong("created"), ItemStack.fromNbt(compound.getCompound("payload"))); + } + + public NbtCompound toNBT(NbtCompound compound) { + compound.putLong("created", created); + compound.put("payload", payload().writeNbt(new NbtCompound())); + return compound; + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Living.java b/src/main/java/com/minelittlepony/unicopia/entity/Living.java index 753a9830..fe2a3313 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Living.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Living.java @@ -12,19 +12,26 @@ import com.minelittlepony.unicopia.ability.magic.SpellContainer; import com.minelittlepony.unicopia.ability.magic.SpellPredicate; import com.minelittlepony.unicopia.ability.magic.SpellContainer.Operation; import com.minelittlepony.unicopia.ability.magic.spell.Situation; +import com.minelittlepony.unicopia.block.data.DragonBreathStore; import com.minelittlepony.unicopia.item.UItems; import com.minelittlepony.unicopia.network.datasync.EffectSync; +import com.minelittlepony.unicopia.particle.ParticleUtils; import com.minelittlepony.unicopia.projectile.ProjectileImpactListener; import com.minelittlepony.unicopia.util.MagicalDamageSource; +import com.minelittlepony.unicopia.util.VecHelper; -import net.minecraft.entity.Entity; -import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.*; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.projectile.ProjectileEntity; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.sound.SoundEvents; import net.minecraft.util.Hand; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; public abstract class Living implements Equine, Caster { @@ -108,6 +115,35 @@ public abstract class Living implements Equine, Caste prevSneaking = entity.isSneaking(); prevLanded = entity.isOnGround(); + + if (!entity.world.isClient && (entity instanceof PlayerEntity || entity.hasCustomName())) { + + Vec3d targetPos = entity.getRotationVector().multiply(2).add(entity.getEyePos()); + + if (entity.getWorld().isAir(new BlockPos(targetPos))) { + DragonBreathStore store = DragonBreathStore.get(entity.world); + String name = entity.getDisplayName().getString(); + store.popEntries(name).forEach(stack -> { + Vec3d randomPos = targetPos.add(VecHelper.supply(() -> entity.getRandom().nextTriangular(0.1, 0.5))); + + if (!entity.getWorld().isAir(new BlockPos(randomPos))) { + store.put(name, stack.payload()); + } + + for (int i = 0; i < 10; i++) { + ParticleUtils.spawnParticle(entity.world, ParticleTypes.FLAME, randomPos.add( + VecHelper.supply(() -> entity.getRandom().nextTriangular(0.1, 0.5)) + ), Vec3d.ZERO); + } + + ItemEntity item = EntityType.ITEM.create(entity.world); + item.setStack(stack.payload()); + item.setPosition(randomPos); + item.world.spawnEntity(item); + entity.world.playSoundFromEntity(null, entity, SoundEvents.ITEM_FIRECHARGE_USE, entity.getSoundCategory(), 1, 1); + }); + } + } } @Override diff --git a/src/main/java/com/minelittlepony/unicopia/item/DragonBreathScrollItem.java b/src/main/java/com/minelittlepony/unicopia/item/DragonBreathScrollItem.java new file mode 100644 index 00000000..064d6ef6 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/item/DragonBreathScrollItem.java @@ -0,0 +1,42 @@ +package com.minelittlepony.unicopia.item; + +import java.util.UUID; + +import com.minelittlepony.unicopia.block.data.DragonBreathStore; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.world.World; + +public class DragonBreathScrollItem extends Item { + + public DragonBreathScrollItem(Settings settings) { + super(settings); + } + + @Override + public TypedActionResult use(World world, PlayerEntity player, Hand hand) { + ItemStack stack = player.getStackInHand(hand); + ItemStack payload = player.getStackInHand(hand == Hand.MAIN_HAND ? Hand.OFF_HAND : Hand.MAIN_HAND); + + if (payload.isEmpty() || !stack.hasCustomName()) { + return TypedActionResult.fail(stack); + } + + stack.split(1); + if (!world.isClient) { + DragonBreathStore.get(world).put(stack.getName().getString(), payload.split(1)); + } + player.playSound(SoundEvents.ITEM_FIRECHARGE_USE, 1, 1); + return TypedActionResult.consume(stack); + } + + public static ItemStack setRecipient(ItemStack stack, UUID recipient) { + stack.getOrCreateSubNbt("recipient").putUuid("id", recipient); + return stack; + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/item/UItems.java b/src/main/java/com/minelittlepony/unicopia/item/UItems.java index e3b87415..32437ec4 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/UItems.java +++ b/src/main/java/com/minelittlepony/unicopia/item/UItems.java @@ -81,6 +81,8 @@ public interface UItems { Item GOLDEN_FEATHER = register("golden_feather", new Item(new Item.Settings().rarity(Rarity.UNCOMMON).group(ItemGroup.MATERIALS))); Item GOLDEN_WING = register("golden_wing", new Item(new Item.Settings().rarity(Rarity.UNCOMMON).group(ItemGroup.MATERIALS))); + Item DRAGON_BREATH_SCROLL = register("dragon_breath_scroll", new DragonBreathScrollItem(new Item.Settings().rarity(Rarity.UNCOMMON).group(ItemGroup.TOOLS))); + Item BUTTERFLY_SPAWN_EGG = register("butterfly_spawn_egg", new SpawnEggItem(UEntities.BUTTERFLY, 0x222200, 0xaaeeff, new Item.Settings().group(ItemGroup.MISC))); Item BUTTERFLY = register("butterfly", new Item(new Item.Settings().group(ItemGroup.FOOD).food(UFoodComponents.INSECTS))); diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json index 1fe194b0..7dc6bc87 100644 --- a/src/main/resources/assets/unicopia/lang/en_us.json +++ b/src/main/resources/assets/unicopia/lang/en_us.json @@ -36,6 +36,7 @@ "item.unicopia.crystal_heart": "Crystal Heart", "item.unicopia.crystal_shard": "Crystal Shard", + "item.unicopia.dragon_breath_scroll": "Dragon's Breath Scroll", "item.unicopia.gemstone": "Gemstone", "item.unicopia.gemstone.enchanted": "%s Gem", "item.unicopia.gemstone.obfuscated": "Mysterious Gem", diff --git a/src/main/resources/assets/unicopia/models/item/dragon_breath_scroll.json b/src/main/resources/assets/unicopia/models/item/dragon_breath_scroll.json new file mode 100644 index 00000000..b834492d --- /dev/null +++ b/src/main/resources/assets/unicopia/models/item/dragon_breath_scroll.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "unicopia:item/dragon_breath_scroll" + } +} diff --git a/src/main/resources/assets/unicopia/textures/item/dragon_breath_scroll.png b/src/main/resources/assets/unicopia/textures/item/dragon_breath_scroll.png new file mode 100644 index 00000000..d22a517f Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/item/dragon_breath_scroll.png differ diff --git a/src/main/resources/data/unicopia/recipes/dragon_breath_scroll.json b/src/main/resources/data/unicopia/recipes/dragon_breath_scroll.json new file mode 100644 index 00000000..468565ca --- /dev/null +++ b/src/main/resources/data/unicopia/recipes/dragon_breath_scroll.json @@ -0,0 +1,15 @@ +{ + "type": "unicopia:spellbook/crafting", + "material": { + "item": "minecraft:paper" + }, + "traits": { + "fire": 1 + }, + "ingredients": [ + { "item": "minecraft:paper" } + ], + "result": { + "item": "unicopia:dragon_breath_scroll" + } +} \ No newline at end of file diff --git a/src/main/resources/data/unicopia/spellbook/chapters/air_magic.json b/src/main/resources/data/unicopia/spellbook/chapters/air_magic.json index 3ade21fd..362e746c 100644 --- a/src/main/resources/data/unicopia/spellbook/chapters/air_magic.json +++ b/src/main/resources/data/unicopia/spellbook/chapters/air_magic.json @@ -23,13 +23,20 @@ "title": "", "level": 1, "elements": [ - "The Commander has also very graciously allowed me access to her library to continue my studies. I'm excited to see what combining unicorn and pegasus magics might bring." + "The Commander has also very graciously allowed me access to her library to continue my studies. I'm excited to see what combining unicorn and pegasus magics might bring.", + "At the princess' behest", + "- Starswirl the Bearded" ] }, + { - "title": "", + "title": "2nd Hoof '12", "level": 0, - "elements": [] + "elements": [ + "Apologies for the, um, unusual entry in the appendices for today. It appears some little gremlin managed to obscond with my journal.", + "At the princess' behest, so dreadfully sorry", + "- Starswirl the Bearded" + ] }, { "title": "Air Magic I", diff --git a/src/main/resources/data/unicopia/spellbook/chapters/crystal_heart.json b/src/main/resources/data/unicopia/spellbook/chapters/crystal_heart.json index 6202170f..08d537c0 100644 --- a/src/main/resources/data/unicopia/spellbook/chapters/crystal_heart.json +++ b/src/main/resources/data/unicopia/spellbook/chapters/crystal_heart.json @@ -32,12 +32,31 @@ ] }, { - "title": "2nd Mare '12", + "title": "5th Mare '12", "level": 0, "elements": [ "Other accounts say that this artefact only functions when mounted on a specific pedestal of diamond blocks, like a beacon." ] }, + + { + "title": "Dragon's Breath Scroll", + "level": 0, + "elements": [ + { "item": { "item": "unicopia:dragon_breath_scroll" } }, + "Status: Confirmed", + "It's, um a scroll that you write somepony's name on it and you hold it in one hoof and something in the other hoof and, like, um it goes whooosh and the item is sent to that pony", + "- XOXOX Lulu" + ] + }, + { + "title": "2nd Hoof '12", + "level": 0, + "elements": [ + "P.S. Uncle Starswirly is a dunderhead", + { "recipe": "unicopia:dragon_breath_scroll" } + ] + }, { "title": "Bangle of Comradery", "level": 0,