From e0826bf1e3eb1c7f4023e177b2b987590e32759d Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 9 Nov 2023 16:26:26 +0000
Subject: [PATCH] Make the seapony transformation into an ability granted if
 you're wearing the necklace

---
 .../com/minelittlepony/unicopia/Race.java     | 19 +++-
 .../unicopia/ability/Abilities.java           |  3 +
 .../unicopia/ability/AbilityDispatcher.java   |  4 +
 .../unicopia/ability/ChangeFormAbility.java   | 99 +++++++++++++++++++
 .../ability/EarthPonyKickAbility.java         |  3 +-
 .../unicopia/entity/player/Pony.java          | 31 +++++-
 6 files changed, 148 insertions(+), 11 deletions(-)
 create mode 100644 src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java

diff --git a/src/main/java/com/minelittlepony/unicopia/Race.java b/src/main/java/com/minelittlepony/unicopia/Race.java
index 1a214161..ac63cfff 100644
--- a/src/main/java/com/minelittlepony/unicopia/Race.java
+++ b/src/main/java/com/minelittlepony/unicopia/Race.java
@@ -27,6 +27,7 @@ import net.minecraft.registry.RegistryKey;
 public record Race (Supplier<Composite> compositeSupplier, Availability availability, boolean canCast, FlightType flightType, boolean canUseEarth, boolean isNocturnal, boolean canHang) implements Affine {
     public static final String DEFAULT_ID = "unicopia:unset";
     public static final Registry<Race> REGISTRY = RegistryUtils.createDefaulted(Unicopia.id("race"), DEFAULT_ID);
+    public static final Registry<Race> COMMAND_REGISTRY = RegistryUtils.createDefaulted(Unicopia.id("race/grantable"), DEFAULT_ID);
     public static final RegistryKey<? extends Registry<Race>> REGISTRY_KEY = REGISTRY.getKey();
     private static final DynamicCommandExceptionType UNKNOWN_RACE_EXCEPTION = new DynamicCommandExceptionType(id -> Text.translatable("race.unknown", id));
 
@@ -35,11 +36,15 @@ public record Race (Supplier<Composite> compositeSupplier, Availability availabi
     }
 
     public static Race register(Identifier id, Availability availability, boolean magic, FlightType flight, boolean earth, boolean nocturnal, boolean canHang) {
-        return Registry.register(REGISTRY, id, new Race(Suppliers.memoize(() -> new Composite(REGISTRY.get(id), null)), availability, magic, flight, earth, nocturnal, canHang));
+        Race race = Registry.register(REGISTRY, id, new Race(Suppliers.memoize(() -> new Composite(REGISTRY.get(id), null, null)), availability, magic, flight, earth, nocturnal, canHang));
+        if (availability.isGrantable()) {
+            Registry.register(COMMAND_REGISTRY, id, race);
+        }
+        return race;
     }
 
     public static RegistryKeyArgumentType<Race> argument() {
-        return RegistryKeyArgumentType.registryKey(REGISTRY_KEY);
+        return RegistryKeyArgumentType.registryKey(COMMAND_REGISTRY.getKey());
     }
 
     /**
@@ -64,8 +69,8 @@ public record Race (Supplier<Composite> compositeSupplier, Availability availabi
         return compositeSupplier.get();
     }
 
-    public Composite composite(@Nullable Race pseudo) {
-        return pseudo == null ? composite() : new Composite(this, pseudo);
+    public Composite composite(@Nullable Race pseudo, @Nullable Race potential) {
+        return pseudo == null && potential == null ? composite() : new Composite(this, pseudo, potential);
     }
 
     @Override
@@ -147,6 +152,10 @@ public record Race (Supplier<Composite> compositeSupplier, Availability availabi
         return this;
     }
 
+    public Race or(Race other) {
+        return isEquine() ? this : other;
+    }
+
     @Override
     public int hashCode() {
         return getId().hashCode();
@@ -194,7 +203,7 @@ public record Race (Supplier<Composite> compositeSupplier, Availability availabi
         return REGISTRY.stream().filter(r -> r.isPermitted(player)).collect(Collectors.toSet());
     }
 
-    public record Composite (Race physical, @Nullable Race pseudo) {
+    public record Composite (Race physical, @Nullable Race pseudo, @Nullable Race potential) {
         public Race collapsed() {
             return pseudo == null ? physical : pseudo;
         }
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java b/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java
index f0964765..dbf06852 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 {
                 .toList();
     });
 
+    // all races
+    Ability<?> CHANGE_FORM = register(new ChangeFormAbility(), "change_form", AbilitySlot.PRIMARY);
+
     // unicorn / alicorn
     Ability<?> CAST = register(new UnicornCastingAbility(), "cast", AbilitySlot.PRIMARY);
     Ability<?> SHOOT = register(new UnicornProjectileAbility(), "shoot", AbilitySlot.PRIMARY);
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/AbilityDispatcher.java b/src/main/java/com/minelittlepony/unicopia/ability/AbilityDispatcher.java
index 14bb8370..d9a977b4 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/AbilityDispatcher.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/AbilityDispatcher.java
@@ -179,6 +179,10 @@ public class AbilityDispatcher implements Tickable, NbtSerialisable {
                     return;
                 }
 
+                if (cooldown > 100 && player.asEntity().isCreative()) {
+                    cooldown = Math.max(10, cooldown - 100);
+                }
+
                 if (cooldown > 0 && cooldown-- > 0) {
                     ability.coolDown(player, slot);
 
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java
new file mode 100644
index 00000000..fb60bdac
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java
@@ -0,0 +1,99 @@
+package com.minelittlepony.unicopia.ability;
+
+import java.util.Optional;
+
+import org.jetbrains.annotations.Nullable;
+
+import com.minelittlepony.unicopia.Race;
+import com.minelittlepony.unicopia.USounds;
+import com.minelittlepony.unicopia.ability.data.Hit;
+import com.minelittlepony.unicopia.entity.player.Pony;
+
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.util.Identifier;
+
+public class ChangeFormAbility implements Ability<Hit> {
+
+    @Override
+    public int getWarmupTime(Pony player) {
+        return 10;
+    }
+
+    @Override
+    public int getCooldownTime(Pony player) {
+        return 1000;
+    }
+
+    @Override
+    public boolean canUse(Race.Composite race) {
+        return race.potential() != null;
+    }
+
+    @Override
+    public boolean canUse(Race race) {
+        return true;
+    }
+
+    @Override
+    public Identifier getIcon(Pony player) {
+        Race potential = player.getCompositeRace().potential();
+        if (potential == null) {
+            return Ability.super.getIcon(player);
+        }
+        return getId().withPath(p -> "textures/gui/ability/" + p + "_" + potential.getId().getPath() + ".png");
+    }
+
+    @Nullable
+    @Override
+    public Optional<Hit> prepare(Pony player) {
+        return Hit.of(canUse(player.getCompositeRace()));
+    }
+
+    @Override
+    public Hit.Serializer<Hit> getSerializer() {
+        return Hit.SERIALIZER;
+    }
+
+    @Override
+    public double getCostEstimate(Pony player) {
+        return 5;
+    }
+
+    @Override
+    public boolean apply(Pony player, Hit data) {
+        if (prepare(player).isEmpty()) {
+            return false;
+        }
+
+        player.subtractEnergyCost(5);
+
+        Race.Composite composite = player.getCompositeRace();
+        Race actualRace = player.getSpecies();
+        player.setSpecies(composite.potential());
+        player.setSuppressedRace(actualRace.availability().isGrantable() ? actualRace : Race.UNSET);
+
+        return true;
+    }
+
+    @Override
+    public void warmUp(Pony player, AbilitySlot slot) {
+        player.getMagicalReserves().getExertion().addPercent(6);
+
+        if (player.getAbilities().getStat(slot).getWarmup() % 5 == 0) {
+            player.asWorld().playSound(null, player.getOrigin(), SoundEvents.BLOCK_BUBBLE_COLUMN_WHIRLPOOL_INSIDE, SoundCategory.PLAYERS);
+        }
+
+        if (player.asWorld().random.nextInt(5) == 0) {
+            player.asWorld().playSound(null, player.getOrigin(), USounds.Vanilla.BLOCK_BUBBLE_COLUMN_BUBBLE_POP, SoundCategory.PLAYERS);
+        }
+
+        player.spawnParticles(ParticleTypes.BUBBLE_COLUMN_UP, 15);
+        player.spawnParticles(ParticleTypes.BUBBLE_POP, 15);
+    }
+
+    @Override
+    public void coolDown(Pony player, AbilitySlot slot) {
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java
index 086032bf..9f6bb09c 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java
@@ -60,8 +60,7 @@ public class EarthPonyKickAbility implements Ability<Pos> {
 
     @Override
     public Identifier getIcon(Pony player) {
-        Identifier id = Abilities.REGISTRY.getId(this);
-        return new Identifier(id.getNamespace(), "textures/gui/ability/" + id.getPath()
+        return getId().withPath(p -> "textures/gui/ability/" + p
             + "_" + player.getObservedSpecies().getId().getPath()
             + "_" + (getKickDirection(player) > 0 ? "forward" : "backward")
             + ".png");
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 63d32a65..221d4dc3 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java
@@ -66,6 +66,7 @@ import net.minecraft.world.GameRules;
 
 public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, UpdateCallback {
     private static final TrackedData<String> RACE = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.STRING);
+    private static final TrackedData<String> SUPPRESSED_RACE = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.STRING);
 
     static final TrackedData<Float> ENERGY = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.FLOAT);
     static final TrackedData<Float> EXHAUSTION = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.FLOAT);
@@ -120,6 +121,7 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
         this.mana = addTicker(new ManaContainer(this));
 
         player.getDataTracker().startTracking(RACE, Race.DEFAULT_ID);
+        player.getDataTracker().startTracking(SUPPRESSED_RACE, Race.DEFAULT_ID);
 
         addTicker(this::updateAnimations);
         addTicker(this::updateBatPonyAbilities);
@@ -223,8 +225,13 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
     @Override
     public void setSpecies(Race race) {
         race = race.validate(entity);
+        Race current = getSpecies();
+        entity.getDataTracker().set(RACE, Race.REGISTRY.getId(race.validate(entity)).toString());
+        if (race != current) {
+            clearSuppressedRace();
+        }
+
         ticksInSun = 0;
-        entity.getDataTracker().set(RACE, Race.REGISTRY.getId(race).toString());
 
         gravity.updateFlightState();
         entity.sendAbilitiesUpdate();
@@ -232,6 +239,18 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
         UCriteria.PLAYER_CHANGE_RACE.trigger(entity);
     }
 
+    public void setSuppressedRace(Race race) {
+        entity.getDataTracker().set(SUPPRESSED_RACE, Race.REGISTRY.getId(race.validate(entity)).toString());
+    }
+
+    public void clearSuppressedRace() {
+        setSuppressedRace(Race.UNSET);
+    }
+
+    private Race getSuppressedRace() {
+        return Race.fromName(entity.getDataTracker().get(SUPPRESSED_RACE), Race.UNSET);
+    }
+
     public TraitDiscovery getDiscoveries() {
         return discoveries;
     }
@@ -354,17 +373,19 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
     @Override
     public boolean beforeUpdate() {
         if (compositeRace.includes(Race.UNSET) || entity.age % 2 == 0) {
+            Race intrinsicRace = getSpecies();
+            Race suppressedRace = getSuppressedRace();
             compositeRace = getSpellSlot()
                     .get(SpellPredicate.IS_MIMIC, true)
                     .map(AbstractDisguiseSpell::getDisguise)
                     .map(EntityAppearance::getAppearance)
                     .flatMap(Pony::of)
                     .map(Pony::getSpecies)
-                    .orElseGet(this::getSpecies).composite(
+                    .orElse(intrinsicRace).composite(
                   AmuletSelectors.UNICORN_AMULET.test(entity) ? Race.UNICORN
                 : AmuletSelectors.ALICORN_AMULET.test(entity) ? Race.ALICORN
-                : AmuletSelectors.PEARL_NECKLACE.test(entity) ? Race.SEAPONY
-                : null
+                : null,
+                AmuletSelectors.PEARL_NECKLACE.test(entity) ? suppressedRace.or(Race.SEAPONY) : null
             );
         }
 
@@ -794,6 +815,7 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
     public void toSyncronisedNbt(NbtCompound compound) {
         super.toSyncronisedNbt(compound);
         compound.putString("playerSpecies", Race.REGISTRY.getId(getSpecies()).toString());
+        compound.putString("suppressedSpecies", Race.REGISTRY.getId(getSuppressedRace()).toString());
         compound.putFloat("magicExhaustion", magicExhaustion);
         compound.putInt("ticksInSun", ticksInSun);
         compound.putBoolean("hasShades", hasShades);
@@ -819,6 +841,7 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
     public void fromSynchronizedNbt(NbtCompound compound) {
         super.fromSynchronizedNbt(compound);
         setSpecies(Race.fromName(compound.getString("playerSpecies"), Race.HUMAN));
+        setSuppressedRace(Race.fromName(compound.getString("suppressedSpecies"), Race.UNSET));
         powers.fromNBT(compound.getCompound("powers"));
         gravity.fromNBT(compound.getCompound("gravity"));
         charms.fromNBT(compound.getCompound("charms"));