package com.minelittlepony.unicopia.ability; import java.util.Collection; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; import org.jetbrains.annotations.Nullable; import com.minelittlepony.unicopia.*; import com.minelittlepony.unicopia.ability.data.Hit; import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.network.MsgPlayerAbility; import com.minelittlepony.unicopia.network.Channel; import com.minelittlepony.unicopia.util.NbtSerialisable; import com.minelittlepony.unicopia.util.Tickable; import net.minecraft.nbt.NbtCompound; import net.minecraft.registry.RegistryWrapper.WrapperLookup; import net.minecraft.util.Identifier; public class AbilityDispatcher implements Tickable, NbtSerialisable { private final Pony player; private final Map stats = new EnumMap<>(AbilitySlot.class); @Nullable private Race prevRace; private long maxPage = -1; public AbilityDispatcher(Pony player) { this.player = player; } public void clear(AbilitySlot slot, ActivationType pressType, long page) { Stat stat = getStat(slot); if (stat.canSwitchStates()) { stat.clear(pressType, page); } } public Optional> activate(AbilitySlot slot, long page) { Stat stat = getStat(slot); if (stat.canSwitchStates()) { return stat.getAbility(page).flatMap(stat::setActiveAbility); } return Optional.empty(); } public Collection getStats() { return stats.values(); } public Optional getActiveStat() { return stats.values().stream().filter(stat -> stat.getFillProgress() > 0).findFirst(); } public Stat getStat(AbilitySlot slot) { return stats.computeIfAbsent(slot, Stat::new); } public boolean isFilled(AbilitySlot slot) { return getStat(slot).getMaxPage() > 0; } public int getMaxPage() { Race newRace = player.getCompositeRace().collapsed(); if (maxPage < 0 || prevRace != newRace) { prevRace = newRace; maxPage = 0; for (AbilitySlot slot : AbilitySlot.values()) { maxPage = Math.max(maxPage, getStat(slot).getMaxPage() - 1); } } return (int)maxPage; } @Override public void tick() { stats.values().forEach(Stat::tick); } @Override public void toNBT(NbtCompound compound, WrapperLookup lookup) { if (compound.contains("stats")) { stats.clear(); NbtCompound li = compound.getCompound("stats"); li.getKeys().forEach(key -> { getStat(AbilitySlot.valueOf(key)).fromNBT(li.getCompound(key), lookup); }); } } @Override public void fromNBT(NbtCompound compound, WrapperLookup lookup) { NbtCompound li = new NbtCompound(); stats.forEach((key, value) -> li.put(key.name(), value.toNBT(lookup))); compound.put("stats", li); } public class Stat implements NbtSerialisable { /** * Ticks of warmup before an ability is triggered. */ private int warmup; private int maxWarmup; /** * Ticks of cooldown after an ability has been triggered. */ private int cooldown; private int maxCooldown; public final AbilitySlot slot; /** * True once the current ability has been triggered. */ private boolean triggered; private Optional> activeAbility = Optional.empty(); private Stat(AbilitySlot slot) { this.slot = slot; } /** * Returns true if the current ability can we swapped out. */ boolean canSwitchStates() { return !activeAbility.isPresent() || (warmup != 0) || (triggered && cooldown == 0); } public int getRemainingCooldown() { return cooldown; } public float getFillProgress() { float cooldown = getWarmup(); if (cooldown <= 0 || cooldown >= 1) { return getCooldown(); } return 1 - cooldown; } public float getCooldown() { return maxCooldown <= 0 ? 0 : ((float)cooldown / (float)maxCooldown); } public void setCooldown(int value) { cooldown = value; maxCooldown = value; } public float getWarmup() { return maxWarmup <= 0 ? 0 : ((float)warmup / (float)maxWarmup); } public double getCost(long page) { if (warmup <= 0) { return 0; } return getAbility(page).map(ability -> ability.getCostEstimate(player)).orElse(0D); } public void setWarmup(int value) { maxWarmup = value; warmup = value; } public void tick() { Optional> activeAbility = getActiveAbility(); if (activeAbility.isEmpty()) { if (warmup > 0) { warmup--; } if (cooldown > 0) { cooldown--; } } getActiveAbility().ifPresent(ability -> { if (warmup > 0) { warmup--; ability.warmUp(player, slot); return; } if (cooldown > 100 && player.asEntity().isCreative()) { cooldown = Math.max(10, cooldown - 100); } if (cooldown > 0 && cooldown-- > 0) { ability.coolDown(player, slot); if (cooldown <= 0) { setActiveAbility(null); } return; } tryFire(ability); }); } private void tryFire(Ability ability) { if (triggered) { return; } triggered = true; setCooldown(ability.getCooldownTime(player)); if (player.isClientPlayer()) { Optional data = ability.prepare(player); warmup = 0; if (data.isPresent()) { InteractionManager.getInstance().sendPlayerLookAngles(player.asEntity()); Channel.CLIENT_PLAYER_ABILITY.sendToServer(new MsgPlayerAbility<>(ability, data, ActivationType.NONE)); } else { player.asEntity().playSound(USounds.GUI_ABILITY_FAIL, 1, 1); setCooldown(0); } } if (cooldown <= 0) { setActiveAbility(null); } } public Optional> getAbility(long page) { List> found = Abilities.BY_SLOT_AND_COMPOSITE_RACE.apply(slot, player.getCompositeRace()); if (found.isEmpty()) { return Optional.empty(); } return Optional.ofNullable(found.get((int)Math.min(found.size() - 1, page))); } public void clear(ActivationType pressType, long page) { if (pressType == ActivationType.NONE || getAbility(page).filter(ability -> !triggerQuickAction(ability, pressType)).isEmpty()) { if (warmup > 0) { getActiveAbility().filter(Ability::activateOnEarlyRelease).ifPresentOrElse(this::tryFire, () -> setActiveAbility(null)); } else { setActiveAbility(null); } } } private boolean triggerQuickAction(Ability ability, ActivationType pressType) { Optional data = ability.prepareQuickAction(player, pressType); if (ability.onQuickAction(player, pressType, data)) { Channel.CLIENT_PLAYER_ABILITY.sendToServer(new MsgPlayerAbility<>(ability, data, pressType)); return true; } return false; } public long getMaxPage() { return Abilities.BY_SLOT_AND_COMPOSITE_RACE.apply(slot, player.getCompositeRace()).size(); } protected synchronized Optional> setActiveAbility(@Nullable Ability power) { if (activeAbility.orElse(null) != power) { triggered = false; activeAbility = Optional.ofNullable(power); setWarmup(activeAbility.map(p -> p.getWarmupTime(player)).orElse(0)); setCooldown(0); return activeAbility; } return Optional.empty(); } public synchronized Optional> getActiveAbility() { return activeAbility.filter(ability -> { return (!(ability == null || (triggered && warmup == 0 && cooldown == 0)) && ability.canUse(player.getCompositeRace())); }); } @Override public void toNBT(NbtCompound compound, WrapperLookup lookup) { compound.putInt("warmup", warmup); compound.putInt("cooldown", cooldown); compound.putInt("maxWarmup", maxWarmup); compound.putInt("maxCooldown", maxCooldown); compound.putBoolean("triggered", triggered); getActiveAbility().ifPresent(ability -> { compound.putString("activeAbility", Abilities.REGISTRY.getId(ability).toString()); }); } @Override public void fromNBT(NbtCompound compound, WrapperLookup lookup) { warmup = compound.getInt("warmup"); cooldown = compound.getInt("cooldown"); maxWarmup = compound.getInt("maxWarmup"); maxCooldown = compound.getInt("maxCooldown"); triggered = compound.getBoolean("triggered"); activeAbility = Abilities.REGISTRY.getOrEmpty(Identifier.of(compound.getString("activeAbility"))); } } }