2020-04-25 15:37:17 +02:00
|
|
|
package com.minelittlepony.unicopia.ability;
|
|
|
|
|
2020-10-09 19:05:12 +02:00
|
|
|
import java.util.Collection;
|
2020-05-06 15:55:25 +02:00
|
|
|
import java.util.EnumMap;
|
2020-10-11 13:25:51 +02:00
|
|
|
import java.util.List;
|
2020-05-06 15:55:25 +02:00
|
|
|
import java.util.Map;
|
2020-04-25 15:37:17 +02:00
|
|
|
import java.util.Optional;
|
2020-10-11 13:25:51 +02:00
|
|
|
|
2021-08-04 15:38:03 +02:00
|
|
|
import org.jetbrains.annotations.Nullable;
|
2020-10-09 14:37:49 +02:00
|
|
|
|
2020-05-06 15:55:25 +02:00
|
|
|
import com.minelittlepony.unicopia.Race;
|
2022-01-04 23:43:07 +01:00
|
|
|
import com.minelittlepony.unicopia.USounds;
|
2020-04-25 15:37:17 +02:00
|
|
|
import com.minelittlepony.unicopia.ability.data.Hit;
|
2020-09-22 15:11:20 +02:00
|
|
|
import com.minelittlepony.unicopia.entity.player.Pony;
|
2020-04-15 14:22:03 +02:00
|
|
|
import com.minelittlepony.unicopia.network.MsgPlayerAbility;
|
2020-04-23 23:44:31 +02:00
|
|
|
import com.minelittlepony.unicopia.network.Channel;
|
2020-04-15 18:12:00 +02:00
|
|
|
import com.minelittlepony.unicopia.util.NbtSerialisable;
|
2021-08-04 15:38:03 +02:00
|
|
|
import com.minelittlepony.unicopia.util.Tickable;
|
2018-09-12 01:29:49 +02:00
|
|
|
|
2021-08-04 15:38:03 +02:00
|
|
|
import net.minecraft.nbt.NbtCompound;
|
2020-04-25 15:37:17 +02:00
|
|
|
import net.minecraft.util.Identifier;
|
2018-09-12 01:29:49 +02:00
|
|
|
|
2020-05-10 17:18:45 +02:00
|
|
|
public class AbilityDispatcher implements Tickable, NbtSerialisable {
|
2018-09-12 01:29:49 +02:00
|
|
|
|
2020-04-15 18:12:00 +02:00
|
|
|
private final Pony player;
|
2018-09-12 01:29:49 +02:00
|
|
|
|
2020-05-06 15:55:25 +02:00
|
|
|
private final Map<AbilitySlot, Stat> stats = new EnumMap<>(AbilitySlot.class);
|
2019-02-03 10:45:45 +01:00
|
|
|
|
2020-10-09 14:37:49 +02:00
|
|
|
@Nullable
|
|
|
|
private Race prevRace;
|
2021-02-13 18:24:25 +01:00
|
|
|
private long maxPage = -1;
|
2020-10-09 14:37:49 +02:00
|
|
|
|
2020-04-25 15:37:17 +02:00
|
|
|
public AbilityDispatcher(Pony player) {
|
2018-09-12 01:29:49 +02:00
|
|
|
this.player = player;
|
|
|
|
}
|
|
|
|
|
2021-12-31 14:31:23 +01:00
|
|
|
public void clear(AbilitySlot slot, ActivationType pressType, long page) {
|
2020-05-10 20:45:07 +02:00
|
|
|
Stat stat = getStat(slot);
|
2020-05-06 15:55:25 +02:00
|
|
|
|
2020-05-10 20:45:07 +02:00
|
|
|
if (stat.canSwitchStates()) {
|
2021-12-31 15:17:41 +01:00
|
|
|
if (pressType == ActivationType.NONE || stat.getAbility(page).filter(ability -> !triggerQuickAction(ability, pressType)).isEmpty()) {
|
2021-12-31 14:31:23 +01:00
|
|
|
stat.setActiveAbility(null);
|
|
|
|
}
|
2018-09-12 01:29:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-23 16:05:04 +02:00
|
|
|
private <T extends Hit> boolean triggerQuickAction(Ability<T> ability, ActivationType pressType) {
|
|
|
|
Optional<T> data = ability.prepareQuickAction(player, pressType);
|
|
|
|
if (ability.onQuickAction(player, pressType, data)) {
|
|
|
|
Channel.CLIENT_PLAYER_ABILITY.send(new MsgPlayerAbility<>(ability, data, pressType));
|
2021-12-31 15:17:41 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-10-11 13:14:11 +02:00
|
|
|
public Optional<Ability<?>> activate(AbilitySlot slot, long page) {
|
2020-05-10 20:45:07 +02:00
|
|
|
Stat stat = getStat(slot);
|
|
|
|
if (stat.canSwitchStates()) {
|
2020-10-11 13:14:11 +02:00
|
|
|
return stat.getAbility(page).flatMap(stat::setActiveAbility);
|
2019-02-03 10:45:45 +01:00
|
|
|
}
|
2020-10-11 13:14:11 +02:00
|
|
|
return Optional.empty();
|
2019-02-03 10:45:45 +01:00
|
|
|
}
|
|
|
|
|
2020-10-09 19:05:12 +02:00
|
|
|
public Collection<Stat> getStats() {
|
|
|
|
return stats.values();
|
|
|
|
}
|
|
|
|
|
2020-05-06 15:55:25 +02:00
|
|
|
public Stat getStat(AbilitySlot slot) {
|
|
|
|
return stats.computeIfAbsent(slot, Stat::new);
|
|
|
|
}
|
|
|
|
|
2022-01-02 17:07:28 +01:00
|
|
|
public boolean isFilled(AbilitySlot slot) {
|
|
|
|
return getStat(slot).getMaxPage() > 0;
|
|
|
|
}
|
|
|
|
|
2020-10-09 14:37:49 +02:00
|
|
|
public long getMaxPage() {
|
2021-02-13 18:24:25 +01:00
|
|
|
if (maxPage < 0 || prevRace != player.getSpecies()) {
|
2020-10-09 14:37:49 +02:00
|
|
|
prevRace = player.getSpecies();
|
2021-02-13 18:24:25 +01:00
|
|
|
maxPage = 0;
|
|
|
|
for (AbilitySlot slot : AbilitySlot.values()) {
|
|
|
|
maxPage = Math.max(maxPage, getStat(slot).getMaxPage() - 1);
|
|
|
|
}
|
2020-10-09 14:37:49 +02:00
|
|
|
}
|
|
|
|
return maxPage;
|
|
|
|
}
|
|
|
|
|
2018-09-12 01:29:49 +02:00
|
|
|
@Override
|
2020-05-10 17:18:45 +02:00
|
|
|
public void tick() {
|
2020-05-10 20:45:07 +02:00
|
|
|
stats.values().forEach(Stat::tick);
|
2018-09-12 01:29:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-08-04 15:38:03 +02:00
|
|
|
public void toNBT(NbtCompound compound) {
|
2020-05-06 15:55:25 +02:00
|
|
|
if (compound.contains("stats")) {
|
|
|
|
stats.clear();
|
2021-08-04 15:38:03 +02:00
|
|
|
NbtCompound li = compound.getCompound("stats");
|
2020-05-06 15:55:25 +02:00
|
|
|
li.getKeys().forEach(key -> {
|
|
|
|
getStat(AbilitySlot.valueOf(key)).fromNBT(li.getCompound(key));
|
|
|
|
});
|
|
|
|
}
|
2018-09-12 01:29:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-08-04 15:38:03 +02:00
|
|
|
public void fromNBT(NbtCompound compound) {
|
|
|
|
NbtCompound li = new NbtCompound();
|
2020-05-06 15:55:25 +02:00
|
|
|
stats.forEach((key, value) -> li.put(key.name(), value.toNBT()));
|
|
|
|
compound.put("stats", li);
|
2018-09-12 01:29:49 +02:00
|
|
|
}
|
2020-05-06 15:55:25 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2020-05-10 20:45:07 +02:00
|
|
|
/**
|
|
|
|
* True once the current ability has been triggered.
|
|
|
|
*/
|
|
|
|
private boolean triggered;
|
|
|
|
|
|
|
|
private Optional<Ability<?>> activeAbility = Optional.empty();
|
|
|
|
|
2020-05-06 15:55:25 +02:00
|
|
|
private Stat(AbilitySlot slot) {
|
|
|
|
this.slot = slot;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the current ability can we swapped out.
|
|
|
|
*/
|
|
|
|
boolean canSwitchStates() {
|
2020-05-10 20:45:07 +02:00
|
|
|
return !activeAbility.isPresent() || (warmup != 0) || (triggered && cooldown == 0);
|
2020-05-06 15:55:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-10-09 19:05:12 +02:00
|
|
|
public double getCost(long page) {
|
|
|
|
if (warmup <= 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return getAbility(page).map(ability -> ability.getCostEstimate(player)).orElse(0D);
|
|
|
|
}
|
|
|
|
|
2020-05-06 15:55:25 +02:00
|
|
|
public void setWarmup(int value) {
|
|
|
|
maxWarmup = value;
|
|
|
|
warmup = value;
|
|
|
|
}
|
|
|
|
|
2020-05-10 20:45:07 +02:00
|
|
|
public void tick() {
|
|
|
|
getActiveAbility().ifPresent(this::activate);
|
|
|
|
}
|
|
|
|
|
|
|
|
private <T extends Hit> void activate(Ability<T> ability) {
|
2020-05-06 15:55:25 +02:00
|
|
|
if (warmup > 0) {
|
|
|
|
warmup--;
|
2020-05-10 20:45:07 +02:00
|
|
|
ability.preApply(player, slot);
|
|
|
|
return;
|
2020-05-06 15:55:25 +02:00
|
|
|
}
|
2020-05-10 20:45:07 +02:00
|
|
|
|
|
|
|
if (cooldown > 0 && cooldown-- > 0) {
|
|
|
|
ability.postApply(player, slot);
|
|
|
|
|
|
|
|
if (cooldown <= 0) {
|
|
|
|
setActiveAbility(null);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (triggered) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-12-19 18:13:15 +01:00
|
|
|
if (ability.canActivate(player.asWorld(), player)) {
|
2020-05-10 20:45:07 +02:00
|
|
|
triggered = true;
|
|
|
|
setCooldown(ability.getCooldownTime(player));
|
|
|
|
|
|
|
|
if (player.isClientPlayer()) {
|
2022-09-23 16:05:04 +02:00
|
|
|
Optional<T> data = ability.prepare(player);
|
2020-05-10 20:45:07 +02:00
|
|
|
|
2022-09-23 16:05:04 +02:00
|
|
|
if (data.isPresent()) {
|
2021-12-31 15:17:41 +01:00
|
|
|
Channel.CLIENT_PLAYER_ABILITY.send(new MsgPlayerAbility<>(ability, data, ActivationType.NONE));
|
2020-05-10 20:45:07 +02:00
|
|
|
} else {
|
2022-12-19 16:03:35 +01:00
|
|
|
player.asEntity().playSound(USounds.GUI_ABILITY_FAIL, 1, 1);
|
2020-05-10 20:45:07 +02:00
|
|
|
setCooldown(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cooldown <= 0) {
|
|
|
|
setActiveAbility(null);
|
2020-05-06 15:55:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-09 14:37:49 +02:00
|
|
|
public Optional<Ability<?>> getAbility(long page) {
|
2022-12-10 00:55:53 +01:00
|
|
|
List<Ability<?>> found = Abilities.BY_SLOT_AND_COMPOSITE_RACE.apply(slot, player.getCompositeRace());
|
2020-10-11 13:25:51 +02:00
|
|
|
if (found.isEmpty()) {
|
|
|
|
return Optional.empty();
|
|
|
|
}
|
|
|
|
|
2022-01-02 17:07:28 +01:00
|
|
|
return Optional.ofNullable(found.get((int)Math.min(found.size() - 1, page)));
|
2020-05-10 20:45:07 +02:00
|
|
|
}
|
|
|
|
|
2020-10-09 14:37:49 +02:00
|
|
|
public long getMaxPage() {
|
2022-12-10 00:55:53 +01:00
|
|
|
return Abilities.BY_SLOT_AND_COMPOSITE_RACE.apply(slot, player.getCompositeRace()).size();
|
2020-10-09 14:37:49 +02:00
|
|
|
}
|
|
|
|
|
2020-10-11 13:14:11 +02:00
|
|
|
protected synchronized Optional<Ability<?>> setActiveAbility(@Nullable Ability<?> power) {
|
2020-05-10 20:45:07 +02:00
|
|
|
if (activeAbility.orElse(null) != power) {
|
|
|
|
triggered = false;
|
|
|
|
activeAbility = Optional.ofNullable(power);
|
|
|
|
setWarmup(activeAbility.map(p -> p.getWarmupTime(player)).orElse(0));
|
|
|
|
setCooldown(0);
|
2020-10-11 13:14:11 +02:00
|
|
|
return activeAbility;
|
2020-05-10 20:45:07 +02:00
|
|
|
}
|
2020-10-11 13:14:11 +02:00
|
|
|
return Optional.empty();
|
2020-05-10 20:45:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected synchronized Optional<Ability<?>> getActiveAbility() {
|
|
|
|
return activeAbility.filter(ability -> {
|
2022-12-10 00:55:53 +01:00
|
|
|
return (!(ability == null || (triggered && warmup == 0 && cooldown == 0)) && player.getCompositeRace().any(ability::canUse));
|
2020-05-10 20:45:07 +02:00
|
|
|
});
|
2020-05-06 15:55:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-08-04 15:38:03 +02:00
|
|
|
public void toNBT(NbtCompound compound) {
|
2020-05-06 15:55:25 +02:00
|
|
|
compound.putInt("warmup", warmup);
|
|
|
|
compound.putInt("cooldown", cooldown);
|
|
|
|
compound.putInt("maxWarmup", maxWarmup);
|
|
|
|
compound.putInt("maxCooldown", maxCooldown);
|
2020-05-10 20:45:07 +02:00
|
|
|
compound.putBoolean("triggered", triggered);
|
|
|
|
getActiveAbility().ifPresent(ability -> {
|
|
|
|
compound.putString("activeAbility", Abilities.REGISTRY.getId(ability).toString());
|
|
|
|
});
|
2020-05-06 15:55:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-08-04 15:38:03 +02:00
|
|
|
public void fromNBT(NbtCompound compound) {
|
2020-05-06 15:55:25 +02:00
|
|
|
warmup = compound.getInt("warmup");
|
|
|
|
cooldown = compound.getInt("cooldown");
|
|
|
|
maxWarmup = compound.getInt("maxWarmup");
|
|
|
|
maxCooldown = compound.getInt("maxCooldown");
|
2020-05-10 20:45:07 +02:00
|
|
|
triggered = compound.getBoolean("triggered");
|
|
|
|
activeAbility = Abilities.REGISTRY.getOrEmpty(new Identifier(compound.getString("activeAbility")));
|
2020-05-06 15:55:25 +02:00
|
|
|
}
|
|
|
|
}
|
2018-09-12 01:29:49 +02:00
|
|
|
}
|