diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCharmTracker.java b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCharmTracker.java new file mode 100644 index 00000000..71551b6a --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCharmTracker.java @@ -0,0 +1,98 @@ +package com.minelittlepony.unicopia.entity.player; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import com.minelittlepony.unicopia.ability.magic.Affine; +import com.minelittlepony.unicopia.util.NbtSerialisable; +import com.minelittlepony.unicopia.util.Tickable; + +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +public class PlayerCharmTracker implements Tickable, NbtSerialisable { + + private final Pony pony; + + private final ItemTracker armour = new ItemTracker(); + + PlayerCharmTracker(Pony pony) { + this.pony = pony; + } + + @Override + public void tick() { + armour.update(pony.getMaster().getInventory().armor.stream()); + } + + public ItemTracker getArmour() { + return armour; + } + + @Override + public void toNBT(NbtCompound compound) { + compound.put("armour", armour.toNBT()); + } + + @Override + public void fromNBT(NbtCompound compound) { + armour.fromNBT(compound.getCompound("armour")); + } + + public class ItemTracker implements NbtSerialisable { + private final Map items = new HashMap<>(); + + public void update(Stream stacks) { + Set found = new HashSet<>(); + stacks.forEach(stack -> { + if (stack.getItem() instanceof Charm) { + items.compute((Charm)stack.getItem(), (item, prev) -> prev == null ? 1 : prev + 1); + found.add((Charm)stack.getItem()); + } + }); + items.entrySet().removeIf(e -> { + if (!found.contains(e.getKey())) { + e.getKey().onRemoved(pony, e.getValue()); + return true; + } + return false; + }); + } + + public int getTicks(Charm charm) { + return items.getOrDefault(charm.asItem(), 0); + } + + public boolean contains(Charm charm) { + return getTicks(charm) > 0; + } + + @Override + public void toNBT(NbtCompound compound) { + items.forEach((charm, count) -> { + compound.putInt(Registry.ITEM.getId(charm.asItem()).toString(), count); + }); + } + + @Override + public void fromNBT(NbtCompound compound) { + items.clear(); + compound.getKeys().stream().map(Identifier::tryParse) + .filter(Objects::nonNull) + .map(id -> Map.entry(Registry.ITEM.get(id), compound.getInt(id.toString()))) + .filter(i -> i.getKey() instanceof Charm && i.getValue() > 0) + .forEach(item -> items.put((Charm)item.getKey(), item.getValue())); + } + } + + public interface Charm extends Affine, ItemConvertible { + void onRemoved(Pony pony, int timeWorn); + } +} 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 66f2a12e..1a518617 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java @@ -14,6 +14,7 @@ import com.minelittlepony.unicopia.Race; import com.minelittlepony.unicopia.UTags; import com.minelittlepony.unicopia.WorldTribeManager; import com.minelittlepony.unicopia.ability.AbilityDispatcher; +import com.minelittlepony.unicopia.ability.magic.Affine; import com.minelittlepony.unicopia.ability.magic.Spell; import com.minelittlepony.unicopia.ability.magic.spell.SpellType; import com.minelittlepony.unicopia.advancement.UCriteria; @@ -22,6 +23,7 @@ import com.minelittlepony.unicopia.entity.PonyContainer; import com.minelittlepony.unicopia.entity.Living; import com.minelittlepony.unicopia.entity.Trap; import com.minelittlepony.unicopia.entity.effect.SunBlindnessStatusEffect; +import com.minelittlepony.unicopia.item.UItems; import com.minelittlepony.unicopia.item.toxin.FoodType; import com.minelittlepony.unicopia.item.toxin.Toxicity; import com.minelittlepony.unicopia.item.toxin.Toxin; @@ -71,6 +73,7 @@ public class Pony extends Living implements Transmittable, Copieab private final AbilityDispatcher powers = new AbilityDispatcher(this); private final PlayerPhysics gravity = new PlayerPhysics(this); + private final PlayerCharmTracker charms = new PlayerCharmTracker(this); private final PlayerAttributes attributes = new PlayerAttributes(this); private final PlayerCamera camera = new PlayerCamera(this); private final ManaContainer mana; @@ -100,7 +103,7 @@ public class Pony extends Living implements Transmittable, Copieab super(player, EFFECT); this.mana = new ManaContainer(this); this.levels = new PlayerLevelStore(this); - this.tickers = Lists.newArrayList(gravity, mana, attributes); + this.tickers = Lists.newArrayList(gravity, mana, attributes, charms); player.getDataTracker().startTracking(RACE, Race.HUMAN.ordinal()); } @@ -135,6 +138,10 @@ public class Pony extends Living implements Transmittable, Copieab return mana; } + public PlayerCharmTracker getCharms() { + return charms; + } + @Override public LevelStore getLevel() { return levels; @@ -395,12 +402,22 @@ public class Pony extends Living implements Transmittable, Copieab } public Optional trySleep(BlockPos pos) { + + if (UItems.ALICORN_AMULET.isApplicable(entity)) { + return Optional.of(new TranslatableText("block.unicopia.bed.not_tired")); + } + return findAllSpellsInRange(10) .filter(p -> p instanceof Pony && ((Pony)p).isEnemy(this)) .findFirst() .map(p -> new TranslatableText("block.unicopia.bed.not_safe")); } + @Override + public boolean isEnemy(Affine other) { + return getCharms().getArmour().contains(UItems.ALICORN_AMULET) || super.isEnemy(other); + } + public void onEat(ItemStack stack) { if (getSpecies() == Race.CHANGELING) { Toxin.POISON.afflict(getMaster(), FoodType.RAW_MEAT, Toxicity.SAFE, stack); @@ -416,6 +433,7 @@ public class Pony extends Living implements Transmittable, Copieab compound.put("powers", powers.toNBT()); compound.put("gravity", gravity.toNBT()); + compound.put("charms", charms.toNBT()); getSpellSlot().get(true).ifPresent(effect ->{ compound.put("effect", SpellType.toNBT(effect)); @@ -430,6 +448,7 @@ public class Pony extends Living implements Transmittable, Copieab powers.fromNBT(compound.getCompound("powers")); gravity.fromNBT(compound.getCompound("gravity")); + charms.fromNBT(compound.getCompound("charms")); magicExhaustion = compound.getFloat("magicExhaustion"); diff --git a/src/main/java/com/minelittlepony/unicopia/item/AlicornAmuletItem.java b/src/main/java/com/minelittlepony/unicopia/item/AlicornAmuletItem.java new file mode 100644 index 00000000..15cfc6df --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/item/AlicornAmuletItem.java @@ -0,0 +1,259 @@ +package com.minelittlepony.unicopia.item; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.Affinity; +import com.minelittlepony.unicopia.AwaitTickQueue; +import com.minelittlepony.unicopia.entity.IItemEntity; +import com.minelittlepony.unicopia.entity.ItemImpl; +import com.minelittlepony.unicopia.entity.ItemImpl.TickableItem; +import com.minelittlepony.unicopia.entity.player.MagicReserves; +import com.minelittlepony.unicopia.entity.player.PlayerCharmTracker; +import com.minelittlepony.unicopia.entity.player.Pony; +import com.minelittlepony.unicopia.util.MagicalDamageSource; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.item.v1.FabricItemSettings; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.Entity; +import net.minecraft.entity.Entity.RemovalReason; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.ItemEntity; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.ChatUtil; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.LocalDifficulty; +import net.minecraft.world.World; +import net.minecraft.world.explosion.Explosion.DestructionType; + +public class AlicornAmuletItem extends AmuletItem implements PlayerCharmTracker.Charm, ItemImpl.ClingyItem, TickableItem { + + public AlicornAmuletItem(FabricItemSettings settings) { + super(settings, 0, new AmuletItem.ModifiersBuilder() + .add(EntityAttributes.GENERIC_ARMOR, 9000) + .add(EntityAttributes.GENERIC_ARMOR_TOUGHNESS, 9000) + .add(EntityAttributes.GENERIC_KNOCKBACK_RESISTANCE, 9000) + .add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 100) + .add(EntityAttributes.GENERIC_ATTACK_KNOCKBACK, 20) + .build()); + } + + @Override + public Affinity getAffinity() { + return Affinity.BAD; + } + + @Environment(EnvType.CLIENT) + @Override + public void appendTooltip(ItemStack stack, @Nullable World world, List tooltip, TooltipContext tooltipContext) { + Pony iplayer = Pony.of(MinecraftClient.getInstance().player); + + if (iplayer != null) { + int attachedTime = iplayer.getCharms().getArmour().getTicks(this); + if (attachedTime > 0) { + tooltip.add(new TranslatableText(getTranslationKey() + ".lore", ChatUtil.ticksToString(attachedTime))); + } + } + } + + @Override + public ParticleEffect getParticleEffect(IItemEntity entity) { + return ((ItemEntity)entity).world.random.nextBoolean() ? ParticleTypes.LARGE_SMOKE : ParticleTypes.FLAME; + } + + @Override + public boolean isClingy(ItemStack stack) { + return true; + } + + @Override + public float getFollowDistance(IItemEntity entity) { + return Math.max(20, ItemImpl.ClingyItem.super.getFollowDistance(entity)); + } + + @Override + public float getFollowSpeed(IItemEntity entity) { + return Math.max(0.12F, ItemImpl.ClingyItem.super.getFollowSpeed(entity)); + } + + @Override + public void interactWithPlayer(IItemEntity item, PlayerEntity player) { + ItemEntity entity = (ItemEntity)item; + + if (!player.world.isClient && !entity.isRemoved()) { + if (player.getPos().distanceTo(entity.getPos()) < 0.5) { + if (entity.world.random.nextInt(150) == 0) { + entity.setPickupDelay(0); + entity.onPlayerCollision(player); + + if (player.getMainHandStack().getItem() == this) { + TypedActionResult result = use(player.world, player, Hand.MAIN_HAND); + + if (result.getResult() == ActionResult.SUCCESS) { + entity.setPickupDelay(1000); + entity.setRemoved(RemovalReason.DISCARDED); + } + } + } + } + } + } + + @Override + public void onRemoved(Pony pony, int timeWorn) { + float attachedTime = timeWorn / 100F; + + LocalDifficulty difficulty = pony.getWorld().getLocalDifficulty(pony.getOrigin()); + float amount = attachedTime * (1 + difficulty.getClampedLocalDifficulty()); + + amount = Math.min(amount, pony.getMaster().getMaxHealth()); + + pony.getMaster().getHungerManager().setFoodLevel(1); + pony.getMaster().damage(MagicalDamageSource.ALICORN_AMULET, amount); + pony.getMaster().addStatusEffect(new StatusEffectInstance(StatusEffects.NAUSEA, 200, 1)); + + if (attachedTime > 120) { + pony.getMaster().takeKnockback(1, 1, 1); + } + } + + @Override + public void inventoryTick(ItemStack stack, World world, Entity entity, int slot, boolean selected) { + + if (!(entity instanceof PlayerEntity)) { + return; + } + if (world.isClient) { + return; + } + + PlayerEntity player = (PlayerEntity)entity; + + if (selected && !isApplicable(player) && world.random.nextInt(320) == 0) { + use(world, player, Hand.MAIN_HAND); + return; + } + + Pony pony = Pony.of(player); + + if (!pony.getCharms().getArmour().contains(this)) { + return; + } + + float attachedTime = pony.getCharms().getArmour().getTicks(this); + + MagicReserves reserves = pony.getMagicalReserves(); + + if (player.getHealth() < player.getMaxHealth()) { + player.heal(0.5F); + } else if (player.canConsume(false)) { + player.getHungerManager().add(1, 0); + } else { + player.removeStatusEffect(StatusEffects.NAUSEA); + } + + if (reserves.getExertion().get() < reserves.getExertion().getMax()) { + reserves.getExertion().add(2); + } + + if (reserves.getEnergy().get() < 0.005F + (attachedTime / 1000000)) { + reserves.getEnergy().add(2); + } + + if (attachedTime == 1) { + world.playSound(null, player.getBlockPos(), SoundEvents.ENTITY_ELDER_GUARDIAN_CURSE, SoundCategory.PLAYERS, 3, 1); + } + + // attempt to play 3 tricks every tick + Trick.ALL.stream().filter(trick -> trick.play(attachedTime, player)).limit(3).toList(); + + if (stack.getDamage() >= getMaxDamage() - 1) { + stack.damage(10, player, p -> p.sendEquipmentBreakStatus(EquipmentSlot.CHEST)); + + player.damage(MagicalDamageSource.ALICORN_AMULET, player.getMaxHealth() - 0.01F); + player.getHungerManager().setFoodLevel(1); + + Vec3d pos = player.getPos(); + + player.world.createExplosion(player, pos.x, pos.y, pos.z, 10, DestructionType.NONE); + + AwaitTickQueue.scheduleTask(player.world, w -> { + w.createExplosion(player, pos.x, pos.y, pos.z, 6, DestructionType.BREAK); + }, 50); + } + } + + @Override + public ActionResult onGroundTick(IItemEntity item) { + ItemEntity entity = (ItemEntity)item; + + if (entity.world.random.nextInt(500) == 0) { + entity.world.playSound(null, entity.getBlockPos(), SoundEvents.AMBIENT_CAVE, SoundCategory.HOSTILE, 0.5F, 1); + } + + return ActionResult.PASS; + } + + public static class Trick { + private static final List ALL = new ArrayList<>(); + + public static final Trick SPOOK = new Trick(0, 1050, player -> player.world.playSound(null, player.getBlockPos(), SoundEvents.AMBIENT_NETHER_WASTES_MOOD, SoundCategory.PLAYERS, 3, 1)); + public static final Trick WITHER = new Trick(20000, 100, player -> { + StatusEffectInstance effect = new StatusEffectInstance(player.world.random.nextInt(32000) == 0 ? StatusEffects.WITHER : StatusEffects.HUNGER, 300, 3); + effect.setPermanent(true); + player.addStatusEffect(effect); + }); + public static final Trick POKE = new Trick(13000, 300, player -> player.damage(MagicalDamageSource.ALICORN_AMULET, 1F)); + public static final Trick SPIN = new Trick(6000, 300, player -> player.setYaw(player.getYaw() + 180)); + public static final Trick BUTTER_FINGERS = new Trick(1000, 300, player -> player.dropSelectedItem(false)); + public static final Trick MOVE = new Trick(3000, 300, player -> { + float amount = player.world.random.nextFloat() - 0.5F; + boolean sideways = player.world.random.nextBoolean(); + player.addVelocity(sideways ? 0 : amount, 0, sideways ? amount : 0); + }); + public static final Trick SWING = new Trick(2000, 100, player -> player.swingHand(Hand.MAIN_HAND)); + public static final Trick BAD_JOO_JOO = new Trick(1000, 10, player -> { + if (!player.hasStatusEffect(StatusEffects.BAD_OMEN)) { + player.addStatusEffect(new StatusEffectInstance(StatusEffects.BAD_OMEN, 300, 3)); + } + }); + + private final int minTime; + private final int chance; + private final Consumer action; + + public Trick(int minTime, int chance, Consumer action) { + this.minTime = minTime; + this.chance = chance; + this.action = action; + ALL.add(this); + } + + public boolean play(float ticks, PlayerEntity player) { + if (ticks > minTime && (chance <= 0 || player.world.random.nextInt(chance) == 0)) { + action.accept(player); + return true; + } + return false; + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/item/AmuletItem.java b/src/main/java/com/minelittlepony/unicopia/item/AmuletItem.java index 4a08b607..f48ceeb5 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/AmuletItem.java +++ b/src/main/java/com/minelittlepony/unicopia/item/AmuletItem.java @@ -2,6 +2,7 @@ package com.minelittlepony.unicopia.item; import java.util.List; import java.util.Optional; +import java.util.UUID; import org.jetbrains.annotations.Nullable; @@ -35,13 +36,13 @@ public class AmuletItem extends WearableItem { private final ImmutableMultimap modifiers; public AmuletItem(FabricItemSettings settings, int maxEnergy) { - this(settings, maxEnergy, ImmutableMultimap.builder()); + this(settings, maxEnergy, ImmutableMultimap.of()); } - public AmuletItem(FabricItemSettings settings, int maxEnergy, ImmutableMultimap.Builder modifiers) { + public AmuletItem(FabricItemSettings settings, int maxEnergy, ImmutableMultimap modifiers) { super(settings); this.maxEnergy = maxEnergy; - this.modifiers = modifiers.build(); + this.modifiers = modifiers; } @Override @@ -89,7 +90,7 @@ public class AmuletItem extends WearableItem { } public boolean isApplicable(ItemStack stack) { - return stack.getItem() == this && getEnergy(stack) > 0; + return stack.getItem() == this && (!isChargable() || getEnergy(stack) > 0); } public boolean isApplicable(LivingEntity entity) { @@ -123,4 +124,19 @@ public class AmuletItem extends WearableItem { stack.getOrCreateTag().putFloat("energy", energy); } } + + public static class ModifiersBuilder { + private static final UUID SLOT_UUID = UUID.fromString("9F3D476D-C118-4544-8365-64846904B48E"); + + private final ImmutableMultimap.Builder modifiers = new ImmutableMultimap.Builder<>(); + + public ModifiersBuilder add(EntityAttribute attribute, double amount) { + modifiers.put(attribute, new EntityAttributeModifier(SLOT_UUID, "Armor modifier", amount, EntityAttributeModifier.Operation.ADDITION)); + return this; + } + + public ImmutableMultimap build() { + return modifiers.build(); + } + } } diff --git a/src/main/java/com/minelittlepony/unicopia/item/UItems.java b/src/main/java/com/minelittlepony/unicopia/item/UItems.java index ec48a494..8ce6c7b1 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/UItems.java +++ b/src/main/java/com/minelittlepony/unicopia/item/UItems.java @@ -2,6 +2,7 @@ package com.minelittlepony.unicopia.item; import java.util.ArrayList; import java.util.List; + import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.item.enchantment.UEnchantments; import com.minelittlepony.unicopia.item.toxin.UFoodComponents; @@ -72,13 +73,15 @@ public interface UItems { Item GOLDEN_WING = register("golden_wing", new Item(new Item.Settings().rarity(Rarity.UNCOMMON).group(ItemGroup.MATERIALS))); AmuletItem PEGASUS_AMULET = register("pegasus_amulet", new AmuletItem(new FabricItemSettings() + .maxCount(1) .maxDamage(890) .rarity(Rarity.UNCOMMON) .group(ItemGroup.DECORATIONS), 900)); - /*AmuletItem ALICORN_AMULET = register("alicorn_amulet", new AmuletItem(new AmuletItem.Settings("alicorn_amulet") - .toughness(900000000) - .resistance(90000000) - .group(ItemGroup.DECORATIONS), 0));*/ + AlicornAmuletItem ALICORN_AMULET = register("alicorn_amulet", new AlicornAmuletItem(new FabricItemSettings() + .maxCount(1) + .maxDamage(1000) + .rarity(Rarity.RARE) + .group(ItemGroup.DECORATIONS))); static T register(String name, T item) { ITEMS.add(item); diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json index 1048a48a..dc3f075c 100644 --- a/src/main/resources/assets/unicopia/lang/en_us.json +++ b/src/main/resources/assets/unicopia/lang/en_us.json @@ -1,6 +1,7 @@ { "block.unicopia.bed.not_safe": "You may not rest here, there are enemies nearby", + "block.unicopia.bed.not_tired": "You do not feel tired right now", "ability.unicopia.empty_hooves": "I need to find a jar", "ability.unicopia.indoors": "I can't see the sky from here", @@ -53,6 +54,9 @@ "item.unicopia.pegasus_amulet.lore": "Grants temporary flight to whoever wears it", "item.unicopia.amulet.energy": "Energy: %d / %d", + "item.unicopia.alicorn_amulet": "Alicorn Amulet", + "item.unicopia.alicorn_amulet.lore": "Time worn: %d", + "item.unicopia.music_disc_pet": "Music Disc", "item.unicopia.music_disc_pet.desc": "Danial Ingram - pet", "item.unicopia.music_disc_popular": "Music Disc", diff --git a/src/main/resources/assets/unicopia/models/item/alicorn_amulet.json b/src/main/resources/assets/unicopia/models/item/alicorn_amulet.json new file mode 100644 index 00000000..1606db9f --- /dev/null +++ b/src/main/resources/assets/unicopia/models/item/alicorn_amulet.json @@ -0,0 +1,18 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "unicopia:item/alicorn_amulet" + }, + "display": { + "thirdperson": { + "rotation": [ -90, 0, 0 ], + "translation": [ 0, 1, -3 ], + "scale": [ 0.55, 0.55, 0.55 ] + }, + "firstperson": { + "rotation": [ 0, -135, 25 ], + "translation": [ 0, 4, 2 ], + "scale": [ 1.7, 1.7, 1.7 ] + } + } +} diff --git a/src/main/resources/assets/unicopia/textures/models/armor/alicorn_amulet.png b/src/main/resources/assets/unicopia/textures/models/armor/alicorn_amulet.png new file mode 100644 index 00000000..1ef9b9ae Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/models/armor/alicorn_amulet.png differ