2026-02-18 04:02:51 -05:00
|
|
|
package kamkeel.npcs.controllers;
|
|
|
|
|
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.Ability;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.IAbilityAction;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.IAbilityExtender;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.IAbilityFieldProvider;
|
2026-02-18 04:57:28 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.IChainedAbilityFieldProvider;
|
2026-02-18 04:02:51 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.UserType;
|
2026-02-18 05:13:51 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityCharge;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityCutter;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityDash;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityHazard;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityHeal;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilitySlam;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityTrap;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityVortex;
|
2026-02-17 14:32:13 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaserShot;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySlicer;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityWall;
|
2026-02-14 21:06:12 -05:00
|
|
|
import net.minecraft.entity.EntityLivingBase;
|
2026-02-18 05:13:51 -05:00
|
|
|
import net.minecraft.entity.player.EntityPlayer;
|
2026-01-09 15:29:00 -05:00
|
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
|
|
|
import noppes.npcs.CustomNpcs;
|
|
|
|
|
import noppes.npcs.LogWriter;
|
|
|
|
|
import noppes.npcs.api.handler.IAbilityHandler;
|
|
|
|
|
import noppes.npcs.util.NBTJsonUtil;
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
2026-02-18 05:13:51 -05:00
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.HashSet;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.LinkedHashSet;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Set;
|
|
|
|
|
import java.util.UUID;
|
2026-02-16 01:15:52 -05:00
|
|
|
import java.util.function.Predicate;
|
2026-01-09 15:29:00 -05:00
|
|
|
import java.util.function.Supplier;
|
2026-02-17 20:44:27 -05:00
|
|
|
import java.util.regex.Pattern;
|
2026-01-09 15:29:00 -05:00
|
|
|
|
|
|
|
|
public class AbilityController implements IAbilityHandler {
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
public static AbilityController Instance = new AbilityController();
|
2026-01-09 15:29:00 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// ── Core Registries ──────────────────────────────────────────────────────
|
|
|
|
|
private final Map<String, Supplier<Ability>> abilityTypes = new LinkedHashMap<>();
|
|
|
|
|
private final Map<String, Ability> builtAbilities = new LinkedHashMap<>();
|
2026-02-17 23:08:12 -05:00
|
|
|
private final Map<String, Ability> customAbilities = new LinkedHashMap<>(); // name → Ability
|
|
|
|
|
private final Map<String, Ability> customAbilitiesById = new LinkedHashMap<>(); // UUID → Ability
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// ── Extension Points ─────────────────────────────────────────────────────
|
2026-02-06 20:25:52 -05:00
|
|
|
private final Map<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
|
2026-02-14 14:43:25 -05:00
|
|
|
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
|
2026-02-18 04:57:28 -05:00
|
|
|
private final List<IChainedAbilityFieldProvider> chainedFieldProviders = new ArrayList<>();
|
2026-02-14 21:06:12 -05:00
|
|
|
private final List<IAbilityExtender> extenders = new ArrayList<>();
|
2026-02-16 01:15:52 -05:00
|
|
|
private final List<Predicate<EntityPlayer>> flightCheckers = new ArrayList<>();
|
2026-02-06 20:25:52 -05:00
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
// ── Legacy Migration ─────────────────────────────────────────────────────
|
|
|
|
|
private static final Pattern UUID_PATTERN =
|
|
|
|
|
Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
|
|
|
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
|
|
2026-02-18 04:02:51 -05:00
|
|
|
// ── Chained Abilities ──────────────────────────────────────────────────
|
|
|
|
|
private final Map<String, ChainedAbility> chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility
|
|
|
|
|
private final Map<String, ChainedAbility> chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility
|
|
|
|
|
private int chainedAbilityRevision = 0;
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// ── Derived State ────────────────────────────────────────────────────────
|
2026-02-12 20:30:12 -05:00
|
|
|
private final Set<String> builtInTypeIds = new HashSet<>();
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
public AbilityController() {
|
2026-01-30 05:01:54 -05:00
|
|
|
registerBuiltinTypes();
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-02-14 14:43:25 -05:00
|
|
|
// TYPE REGISTRATION
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public void registerType(String factoryKey, Supplier<Ability> factory) {
|
|
|
|
|
Ability temp = factory.get();
|
|
|
|
|
String typeId = temp.getTypeId();
|
|
|
|
|
if (abilityTypes.containsKey(typeId)) {
|
|
|
|
|
LogWriter.info("AbilityController: Overwriting type: " + typeId);
|
|
|
|
|
}
|
|
|
|
|
abilityTypes.put(typeId, factory);
|
|
|
|
|
if (temp.isBuiltIn()) {
|
|
|
|
|
builtInTypeIds.add(typeId);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
}
|
2026-01-09 15:29:00 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public Ability create(String typeId) {
|
|
|
|
|
Supplier<Ability> factory = abilityTypes.get(typeId);
|
|
|
|
|
return factory != null ? factory.get() : null;
|
|
|
|
|
}
|
2026-01-09 15:29:00 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public Ability fromNBT(NBTTagCompound nbt) {
|
|
|
|
|
if (nbt == null || !nbt.hasKey("typeId")) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
String typeId = nbt.getString("typeId");
|
|
|
|
|
Supplier<Ability> factory = abilityTypes.get(typeId);
|
|
|
|
|
if (factory == null) {
|
|
|
|
|
LogWriter.info("AbilityController: Unknown ability type: " + typeId);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ability ability = factory.get();
|
|
|
|
|
ability.readNBT(nbt);
|
|
|
|
|
return ability;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void registerBuiltinTypes() {
|
|
|
|
|
registerType("cnpc:slam", AbilitySlam::new);
|
|
|
|
|
registerType("cnpc:heavy_hit", AbilityHeavyHit::new);
|
|
|
|
|
registerType("cnpc:cutter", AbilityCutter::new);
|
|
|
|
|
|
|
|
|
|
registerType("cnpc:sweeper", AbilitySweeper::new);
|
|
|
|
|
registerType("cnpc:projectile", AbilityProjectile::new);
|
|
|
|
|
registerType("cnpc:orb", AbilityOrb::new);
|
|
|
|
|
registerType("cnpc:disc", AbilityDisc::new);
|
|
|
|
|
registerType("cnpc:laser_shot", AbilityLaserShot::new);
|
|
|
|
|
registerType("cnpc:beam", AbilityBeam::new);
|
|
|
|
|
|
|
|
|
|
registerType("cnpc:charge", AbilityCharge::new);
|
|
|
|
|
registerType("cnpc:dash", AbilityDash::new);
|
|
|
|
|
registerType("cnpc:teleport", AbilityTeleport::new);
|
|
|
|
|
registerType("cnpc:vortex", AbilityVortex::new);
|
|
|
|
|
registerType("cnpc:shockwave", AbilityShockwave::new);
|
|
|
|
|
|
|
|
|
|
registerType("cnpc:guard", AbilityGuard::new);
|
|
|
|
|
registerType("cnpc:heal", AbilityHeal::new);
|
|
|
|
|
|
|
|
|
|
registerType("cnpc:hazard", AbilityHazard::new);
|
|
|
|
|
registerType("cnpc:trap", AbilityTrap::new);
|
2026-02-16 08:54:55 +00:00
|
|
|
|
2026-02-17 14:32:13 -05:00
|
|
|
registerType("cnpc:dome", AbilityDome::new);
|
|
|
|
|
registerType("cnpc:wall", AbilityWall::new);
|
|
|
|
|
registerType("cnpc:shield", AbilityShield::new);
|
|
|
|
|
registerType("cnpc:slicer", AbilitySlicer::new);
|
2026-02-14 14:43:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// BUILT-IN ABILITIES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
public void registerAbility(String name, Ability ability) {
|
|
|
|
|
if (builtAbilities.containsKey(name)) {
|
|
|
|
|
LogWriter.info("AbilityController: Overwriting built-in ability: " + name);
|
|
|
|
|
}
|
|
|
|
|
if (!ability.isBuiltIn()) {
|
|
|
|
|
ability.setName(name);
|
|
|
|
|
}
|
|
|
|
|
builtAbilities.put(name, ability);
|
|
|
|
|
LogWriter.info("Registered ability: " + name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Ability getAbility(String name) {
|
|
|
|
|
return builtAbilities.get(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Ability getAbilityByDisplayName(String displayName) {
|
|
|
|
|
for (Ability ability : builtAbilities.values()) {
|
|
|
|
|
if (ability.getName().equals(displayName)) {
|
|
|
|
|
return ability;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
return null;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public Set<String> getAbilityNames() {
|
|
|
|
|
return new LinkedHashSet<>(builtAbilities.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// CUSTOM ABILITIES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
// Incremented on any custom ability change; used by AbilitySlot for cache invalidation
|
|
|
|
|
private int customAbilityRevision = 0;
|
|
|
|
|
|
|
|
|
|
public void load() {
|
|
|
|
|
customAbilities.clear();
|
2026-02-17 23:08:12 -05:00
|
|
|
customAbilitiesById.clear();
|
2026-02-17 20:44:27 -05:00
|
|
|
int migrated = 0;
|
2026-02-14 14:43:25 -05:00
|
|
|
|
|
|
|
|
File dir = getDir();
|
|
|
|
|
File[] files = dir.exists() ? dir.listFiles() : null;
|
|
|
|
|
if (files != null) {
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
|
|
|
|
try {
|
|
|
|
|
String filename = file.getName();
|
2026-02-17 20:44:27 -05:00
|
|
|
String key = filename.substring(0, filename.length() - 5);
|
2026-02-14 14:43:25 -05:00
|
|
|
|
|
|
|
|
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
|
|
|
|
Ability ability = fromNBT(nbt);
|
2026-02-17 20:44:27 -05:00
|
|
|
if (ability == null) continue;
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Skip built-in abilities that were erroneously saved as custom files
|
|
|
|
|
if (ability.isBuiltIn()) {
|
|
|
|
|
file.delete();
|
|
|
|
|
LogWriter.info("Removed stale built-in ability file: " + filename);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
// Legacy migration: UUID-named files get renamed to name-based
|
|
|
|
|
if (UUID_PATTERN.matcher(key).matches()) {
|
|
|
|
|
String name = ability.getName();
|
|
|
|
|
if (name == null || name.isEmpty()) {
|
|
|
|
|
name = key; // Fallback to UUID if no name stored
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure unique name
|
|
|
|
|
while (hasCustomAbilityName(name) || nameFileExists(dir, name)) {
|
|
|
|
|
name = name + "_";
|
|
|
|
|
}
|
|
|
|
|
ability.setName(name);
|
|
|
|
|
|
|
|
|
|
// Rename file from UUID.json to name.json
|
|
|
|
|
File newFile = new File(dir, name + ".json");
|
|
|
|
|
if (file.renameTo(newFile)) {
|
|
|
|
|
LogWriter.info("Migrated ability file: " + key + ".json -> " + name + ".json");
|
|
|
|
|
} else {
|
|
|
|
|
LogWriter.error("Failed to rename ability file: " + key + ".json -> " + name + ".json");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key = name;
|
|
|
|
|
migrated++;
|
2026-02-14 14:43:25 -05:00
|
|
|
}
|
2026-02-17 20:44:27 -05:00
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Ensure ability has a UUID; generate one for legacy abilities
|
|
|
|
|
String uuid = ability.getId();
|
|
|
|
|
if (uuid == null || uuid.isEmpty() || uuid.equals(key)) {
|
|
|
|
|
// Legacy: id was the name or empty — generate a real UUID
|
|
|
|
|
uuid = UUID.randomUUID().toString();
|
|
|
|
|
ability.setId(uuid);
|
|
|
|
|
// Re-save file with the new UUID
|
|
|
|
|
try {
|
|
|
|
|
NBTJsonUtil.SaveFile(file, ability.writeNBT());
|
|
|
|
|
} catch (Exception ex) {
|
|
|
|
|
LogWriter.error("Failed to save UUID for ability: " + key, ex);
|
|
|
|
|
}
|
|
|
|
|
migrated++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ability.setName(key);
|
2026-02-17 20:44:27 -05:00
|
|
|
customAbilities.put(key, ability);
|
2026-02-17 23:08:12 -05:00
|
|
|
customAbilitiesById.put(uuid, ability);
|
2026-02-14 14:43:25 -05:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
|
|
|
|
|
customAbilityRevision++;
|
2026-02-17 20:44:27 -05:00
|
|
|
if (migrated > 0) {
|
2026-02-17 23:08:12 -05:00
|
|
|
LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)");
|
2026-02-17 20:44:27 -05:00
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
|
2026-02-18 04:02:51 -05:00
|
|
|
|
|
|
|
|
loadChainedAbilities();
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
private boolean nameFileExists(File dir, String name) {
|
|
|
|
|
return new File(dir, name + ".json").exists();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean saveCustomAbility(Ability ability) {
|
|
|
|
|
if (ability == null) return false;
|
2026-02-17 23:08:12 -05:00
|
|
|
if (ability.isBuiltIn()) return false;
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
String name = ability.getName();
|
|
|
|
|
if (name == null || name.isEmpty()) return false;
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
String uuid = ability.getId();
|
|
|
|
|
boolean isNew = (uuid == null || uuid.isEmpty());
|
|
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
if (isNew) {
|
2026-02-17 23:08:12 -05:00
|
|
|
// Generate UUID for new ability
|
|
|
|
|
uuid = UUID.randomUUID().toString();
|
|
|
|
|
ability.setId(uuid);
|
|
|
|
|
|
|
|
|
|
// Ensure unique name
|
2026-02-17 20:44:27 -05:00
|
|
|
while (hasCustomAbilityName(name)) {
|
|
|
|
|
name = name + "_";
|
|
|
|
|
}
|
|
|
|
|
ability.setName(name);
|
2026-02-17 23:08:12 -05:00
|
|
|
} else {
|
|
|
|
|
// Existing ability — check for rename via UUID lookup
|
|
|
|
|
Ability existing = customAbilitiesById.get(uuid);
|
|
|
|
|
if (existing != null) {
|
|
|
|
|
String oldName = existing.getName();
|
|
|
|
|
if (!oldName.equals(name)) {
|
|
|
|
|
// Name changed! Ensure new name is unique (excluding self)
|
|
|
|
|
String testName = name;
|
|
|
|
|
while (customAbilities.containsKey(testName) && !testName.equals(oldName)) {
|
|
|
|
|
testName = testName + "_";
|
|
|
|
|
}
|
|
|
|
|
if (!testName.equals(name)) {
|
|
|
|
|
name = testName;
|
|
|
|
|
ability.setName(name);
|
|
|
|
|
}
|
2026-02-17 20:44:27 -05:00
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Delete old file and remove old map entry
|
|
|
|
|
File oldFile = new File(getDir(), oldName + ".json");
|
|
|
|
|
if (oldFile.exists()) {
|
|
|
|
|
oldFile.delete();
|
|
|
|
|
}
|
|
|
|
|
customAbilities.remove(oldName);
|
|
|
|
|
}
|
2026-02-17 20:44:27 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
File dir = getDir();
|
2026-02-17 20:44:27 -05:00
|
|
|
File fileNew = new File(dir, name + ".json_new");
|
|
|
|
|
File fileCurrent = new File(dir, name + ".json");
|
2026-01-09 15:29:00 -05:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
NBTTagCompound nbt = ability.writeNBT();
|
|
|
|
|
NBTJsonUtil.SaveFile(fileNew, nbt);
|
|
|
|
|
|
|
|
|
|
if (fileCurrent.exists()) {
|
|
|
|
|
fileCurrent.delete();
|
|
|
|
|
}
|
|
|
|
|
fileNew.renameTo(fileCurrent);
|
|
|
|
|
if (fileNew.exists()) {
|
|
|
|
|
fileNew.delete();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
customAbilities.put(name, ability);
|
2026-02-17 23:08:12 -05:00
|
|
|
customAbilitiesById.put(uuid, ability);
|
2026-02-14 14:43:25 -05:00
|
|
|
customAbilityRevision++;
|
2026-02-17 23:08:12 -05:00
|
|
|
LogWriter.info("Saved custom ability: " + name + " [" + uuid + "]");
|
2026-01-30 05:01:54 -05:00
|
|
|
SyncController.syncAllCustomAbilities();
|
2026-01-09 15:29:00 -05:00
|
|
|
return true;
|
|
|
|
|
} catch (Exception e) {
|
2026-02-17 20:44:27 -05:00
|
|
|
LogWriter.error("Error saving custom ability: " + name, e);
|
2026-01-09 15:29:00 -05:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
public boolean deleteCustomAbility(String name) {
|
|
|
|
|
if (name == null || name.isEmpty()) return false;
|
2026-01-09 15:29:00 -05:00
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
Ability removed = customAbilities.remove(name);
|
2026-01-30 05:01:54 -05:00
|
|
|
if (removed == null) return false;
|
2026-01-09 15:29:00 -05:00
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Also remove from UUID index
|
|
|
|
|
String uuid = removed.getId();
|
|
|
|
|
if (uuid != null && !uuid.isEmpty()) {
|
|
|
|
|
customAbilitiesById.remove(uuid);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
File dir = getDir();
|
2026-02-17 20:44:27 -05:00
|
|
|
File file = new File(dir, name + ".json");
|
2026-01-09 15:29:00 -05:00
|
|
|
if (file.exists()) {
|
|
|
|
|
file.delete();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
customAbilityRevision++;
|
2026-02-17 20:44:27 -05:00
|
|
|
LogWriter.info("Deleted custom ability: " + name);
|
2026-01-30 05:01:54 -05:00
|
|
|
SyncController.syncAllCustomAbilities();
|
2026-01-09 15:29:00 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
public Ability getCustomAbility(String name) {
|
|
|
|
|
return customAbilities.get(name);
|
2026-02-14 14:43:25 -05:00
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 20:44:27 -05:00
|
|
|
public Set<String> getCustomAbilityNames() {
|
2026-02-14 14:43:25 -05:00
|
|
|
return new LinkedHashSet<>(customAbilities.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Map<String, Ability> getCustomAbilities() {
|
|
|
|
|
return customAbilities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setCustomAbilities(Map<String, Ability> synced) {
|
|
|
|
|
customAbilities.clear();
|
2026-02-17 23:08:12 -05:00
|
|
|
customAbilitiesById.clear();
|
2026-02-14 14:43:25 -05:00
|
|
|
customAbilities.putAll(synced);
|
2026-02-17 23:08:12 -05:00
|
|
|
for (Ability ability : synced.values()) {
|
|
|
|
|
String uuid = ability.getId();
|
|
|
|
|
if (uuid != null && !uuid.isEmpty()) {
|
|
|
|
|
customAbilitiesById.put(uuid, ability);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
customAbilityRevision++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getCustomAbilityRevision() {
|
|
|
|
|
return customAbilityRevision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private File getDir() {
|
|
|
|
|
File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities");
|
|
|
|
|
if (!dir.exists()) {
|
|
|
|
|
dir.mkdirs();
|
2026-02-12 20:30:12 -05:00
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
return dir;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 04:02:51 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// CHAINED ABILITIES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
private void loadChainedAbilities() {
|
|
|
|
|
chainedAbilities.clear();
|
|
|
|
|
chainedAbilitiesById.clear();
|
|
|
|
|
|
|
|
|
|
File dir = getChainedDir();
|
|
|
|
|
File[] files = dir.exists() ? dir.listFiles() : null;
|
|
|
|
|
if (files != null) {
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
|
|
|
|
try {
|
|
|
|
|
String filename = file.getName();
|
|
|
|
|
String key = filename.substring(0, filename.length() - 5);
|
|
|
|
|
|
|
|
|
|
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
|
|
|
|
ChainedAbility chain = new ChainedAbility();
|
|
|
|
|
chain.readNBT(nbt);
|
|
|
|
|
|
|
|
|
|
chain.setName(key);
|
|
|
|
|
chainedAbilities.put(key, chain);
|
|
|
|
|
|
|
|
|
|
String uuid = chain.getId();
|
|
|
|
|
if (uuid != null && !uuid.isEmpty()) {
|
|
|
|
|
chainedAbilitiesById.put(uuid, chain);
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chainedAbilityRevision++;
|
|
|
|
|
LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean saveChainedAbility(ChainedAbility chain) {
|
|
|
|
|
if (chain == null) return false;
|
|
|
|
|
|
|
|
|
|
String name = chain.getName();
|
|
|
|
|
if (name == null || name.isEmpty()) return false;
|
|
|
|
|
|
|
|
|
|
String uuid = chain.getId();
|
|
|
|
|
boolean isNew = (uuid == null || uuid.isEmpty());
|
|
|
|
|
|
|
|
|
|
if (isNew) {
|
|
|
|
|
uuid = UUID.randomUUID().toString();
|
|
|
|
|
chain.setId(uuid);
|
|
|
|
|
|
|
|
|
|
while (hasChainedAbilityName(name)) {
|
|
|
|
|
name = name + "_";
|
|
|
|
|
}
|
|
|
|
|
chain.setName(name);
|
|
|
|
|
} else {
|
|
|
|
|
ChainedAbility existing = chainedAbilitiesById.get(uuid);
|
|
|
|
|
if (existing != null) {
|
|
|
|
|
String oldName = existing.getName();
|
|
|
|
|
if (!oldName.equals(name)) {
|
|
|
|
|
String testName = name;
|
|
|
|
|
while (chainedAbilities.containsKey(testName) && !testName.equals(oldName)) {
|
|
|
|
|
testName = testName + "_";
|
|
|
|
|
}
|
|
|
|
|
if (!testName.equals(name)) {
|
|
|
|
|
name = testName;
|
|
|
|
|
chain.setName(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File oldFile = new File(getChainedDir(), oldName + ".json");
|
|
|
|
|
if (oldFile.exists()) {
|
|
|
|
|
oldFile.delete();
|
|
|
|
|
}
|
|
|
|
|
chainedAbilities.remove(oldName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File dir = getChainedDir();
|
|
|
|
|
File fileNew = new File(dir, name + ".json_new");
|
|
|
|
|
File fileCurrent = new File(dir, name + ".json");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
NBTTagCompound nbt = chain.writeNBT();
|
|
|
|
|
NBTJsonUtil.SaveFile(fileNew, nbt);
|
|
|
|
|
|
|
|
|
|
if (fileCurrent.exists()) {
|
|
|
|
|
fileCurrent.delete();
|
|
|
|
|
}
|
|
|
|
|
fileNew.renameTo(fileCurrent);
|
|
|
|
|
if (fileNew.exists()) {
|
|
|
|
|
fileNew.delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chainedAbilities.put(name, chain);
|
|
|
|
|
chainedAbilitiesById.put(uuid, chain);
|
|
|
|
|
chainedAbilityRevision++;
|
|
|
|
|
LogWriter.info("Saved chained ability: " + name + " [" + uuid + "]");
|
|
|
|
|
SyncController.syncAllChainedAbilities();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error saving chained ability: " + name, e);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean deleteChainedAbility(String name) {
|
|
|
|
|
if (name == null || name.isEmpty()) return false;
|
|
|
|
|
|
|
|
|
|
ChainedAbility removed = chainedAbilities.remove(name);
|
|
|
|
|
if (removed == null) return false;
|
|
|
|
|
|
|
|
|
|
String uuid = removed.getId();
|
|
|
|
|
if (uuid != null && !uuid.isEmpty()) {
|
|
|
|
|
chainedAbilitiesById.remove(uuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File dir = getChainedDir();
|
|
|
|
|
File file = new File(dir, name + ".json");
|
|
|
|
|
if (file.exists()) {
|
|
|
|
|
file.delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chainedAbilityRevision++;
|
|
|
|
|
LogWriter.info("Deleted chained ability: " + name);
|
|
|
|
|
SyncController.syncAllChainedAbilities();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ChainedAbility resolveChainedAbility(String key) {
|
|
|
|
|
if (key == null || key.isEmpty()) return null;
|
|
|
|
|
|
|
|
|
|
ChainedAbility byId = chainedAbilitiesById.get(key);
|
|
|
|
|
if (byId != null) return byId.deepCopy();
|
|
|
|
|
|
|
|
|
|
ChainedAbility chain = chainedAbilities.get(key);
|
|
|
|
|
if (chain != null) return chain.deepCopy();
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, ChainedAbility> entry : chainedAbilities.entrySet()) {
|
|
|
|
|
if (entry.getKey().equalsIgnoreCase(key)) {
|
|
|
|
|
return entry.getValue().deepCopy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean canResolveChainedAbility(String key) {
|
|
|
|
|
if (key == null || key.isEmpty()) return false;
|
|
|
|
|
if (chainedAbilitiesById.containsKey(key)) return true;
|
|
|
|
|
if (chainedAbilities.containsKey(key)) return true;
|
|
|
|
|
for (String name : chainedAbilities.keySet()) {
|
|
|
|
|
if (name.equalsIgnoreCase(key)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public ChainedAbility getChainedAbility(String name) {
|
|
|
|
|
ChainedAbility chain = chainedAbilities.get(name);
|
|
|
|
|
return chain != null ? chain.deepCopy() : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ChainedAbility getChainedAbilityByUUID(String uuid) {
|
|
|
|
|
return chainedAbilitiesById.get(uuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Set<String> getChainedAbilityNamesSet() {
|
|
|
|
|
return new LinkedHashSet<>(chainedAbilities.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Map<String, ChainedAbility> getChainedAbilities() {
|
|
|
|
|
return chainedAbilities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String[] getChainedAbilityNames() {
|
|
|
|
|
return chainedAbilities.keySet().toArray(new String[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean hasChainedAbilityName(String name) {
|
|
|
|
|
return chainedAbilities.containsKey(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean deleteChainedAbilityByName(String name) {
|
|
|
|
|
return deleteChainedAbility(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean saveChainedAbility(noppes.npcs.api.ability.IChainedAbility chain) {
|
|
|
|
|
if (chain instanceof ChainedAbility) {
|
|
|
|
|
return saveChainedAbility((ChainedAbility) chain);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getChainedAbilityRevision() {
|
|
|
|
|
return chainedAbilityRevision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setChainedAbilities(Map<String, ChainedAbility> synced) {
|
|
|
|
|
chainedAbilities.clear();
|
|
|
|
|
chainedAbilitiesById.clear();
|
|
|
|
|
chainedAbilities.putAll(synced);
|
|
|
|
|
for (ChainedAbility chain : synced.values()) {
|
|
|
|
|
String uuid = chain.getId();
|
|
|
|
|
if (uuid != null && !uuid.isEmpty()) {
|
|
|
|
|
chainedAbilitiesById.put(uuid, chain);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
chainedAbilityRevision++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private File getChainedDir() {
|
|
|
|
|
File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities" + File.separator + "chained");
|
|
|
|
|
if (!dir.exists()) {
|
|
|
|
|
dir.mkdirs();
|
|
|
|
|
}
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-02-14 14:43:25 -05:00
|
|
|
// RESOLUTION
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
public Ability resolveAbility(String key) {
|
|
|
|
|
if (key == null || key.isEmpty()) return null;
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Custom: UUID lookup (most common for persistent references)
|
|
|
|
|
Ability byUuid = customAbilitiesById.get(key);
|
|
|
|
|
if (byUuid != null) return byUuid.deepCopy();
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// Built-in: exact name
|
|
|
|
|
Ability builtIn = builtAbilities.get(key);
|
|
|
|
|
if (builtIn != null) return builtIn.deepCopy();
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// Built-in: case-insensitive name
|
|
|
|
|
for (Map.Entry<String, Ability> entry : builtAbilities.entrySet()) {
|
2026-02-05 22:49:40 -05:00
|
|
|
if (entry.getKey().equalsIgnoreCase(key)) {
|
2026-02-14 14:43:25 -05:00
|
|
|
return entry.getValue().deepCopy();
|
2026-02-05 22:49:40 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// Built-in: registry key / ID
|
|
|
|
|
for (Ability ability : builtAbilities.values()) {
|
2026-02-05 22:49:40 -05:00
|
|
|
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) {
|
2026-02-14 14:43:25 -05:00
|
|
|
return ability.deepCopy();
|
2026-02-05 22:49:40 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Custom: exact name (fallback for legacy references)
|
2026-01-30 05:01:54 -05:00
|
|
|
Ability custom = customAbilities.get(key);
|
2026-02-14 14:43:25 -05:00
|
|
|
if (custom != null) return custom.deepCopy();
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
// Custom: case-insensitive name
|
2026-02-05 22:49:40 -05:00
|
|
|
for (Ability ability : customAbilities.values()) {
|
|
|
|
|
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) {
|
2026-02-14 14:43:25 -05:00
|
|
|
return ability.deepCopy();
|
2026-02-05 22:49:40 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
return null;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 22:49:40 -05:00
|
|
|
public Set<String> getAbilityKeys() {
|
|
|
|
|
Set<String> keys = new LinkedHashSet<>();
|
2026-02-14 14:43:25 -05:00
|
|
|
for (Ability ability : builtAbilities.values()) {
|
2026-02-05 22:49:40 -05:00
|
|
|
String id = ability.getId();
|
|
|
|
|
if (id != null && !id.isEmpty()) {
|
|
|
|
|
keys.add(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-17 23:08:12 -05:00
|
|
|
// Custom abilities keyed by UUID (stable references)
|
2026-02-15 19:21:30 -05:00
|
|
|
for (Ability ability : customAbilities.values()) {
|
2026-02-17 23:08:12 -05:00
|
|
|
String id = ability.getId();
|
|
|
|
|
if (id != null && !id.isEmpty()) {
|
|
|
|
|
keys.add(id);
|
2026-02-15 19:21:30 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 22:49:40 -05:00
|
|
|
return keys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Set<String> getPlayerAbilityKeys() {
|
|
|
|
|
Set<String> keys = new LinkedHashSet<>();
|
2026-02-14 14:43:25 -05:00
|
|
|
for (Ability ability : builtAbilities.values()) {
|
2026-02-05 22:49:40 -05:00
|
|
|
if (ability.getAllowedBy().allowsPlayer()) {
|
|
|
|
|
String id = ability.getId();
|
|
|
|
|
if (id != null && !id.isEmpty()) {
|
|
|
|
|
keys.add(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-17 23:08:12 -05:00
|
|
|
// Custom abilities keyed by UUID (stable references)
|
2026-02-15 19:21:30 -05:00
|
|
|
for (Ability ability : customAbilities.values()) {
|
|
|
|
|
if (ability.getAllowedBy().allowsPlayer()) {
|
2026-02-17 23:08:12 -05:00
|
|
|
String id = ability.getId();
|
|
|
|
|
if (id != null && !id.isEmpty()) {
|
|
|
|
|
keys.add(id);
|
2026-02-15 19:21:30 -05:00
|
|
|
}
|
2026-02-05 22:49:40 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return keys;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean hasAbility(String key) {
|
2026-02-17 23:08:12 -05:00
|
|
|
return builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|
|
|
|
|
|| customAbilitiesById.containsKey(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Ability getCustomAbilityByUUID(String uuid) {
|
|
|
|
|
return customAbilitiesById.get(uuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Ability getCustomAbilityByName(String name) {
|
|
|
|
|
return customAbilities.get(name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:39:14 -05:00
|
|
|
/**
|
|
|
|
|
* Check if a key can be resolved to a valid ability without creating a deep copy.
|
|
|
|
|
* Uses the same lookup chain as {@link #resolveAbility(String)}.
|
|
|
|
|
*/
|
|
|
|
|
public boolean canResolveAbility(String key) {
|
|
|
|
|
if (key == null || key.isEmpty()) return false;
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Custom: UUID lookup (most common for persistent references)
|
|
|
|
|
if (customAbilitiesById.containsKey(key)) return true;
|
|
|
|
|
|
2026-02-16 01:39:14 -05:00
|
|
|
// Built-in: exact name
|
|
|
|
|
if (builtAbilities.containsKey(key)) return true;
|
|
|
|
|
|
|
|
|
|
// Built-in: case-insensitive name
|
|
|
|
|
for (String name : builtAbilities.keySet()) {
|
|
|
|
|
if (name.equalsIgnoreCase(key)) return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Built-in: registry key / ID
|
|
|
|
|
for (Ability ability : builtAbilities.values()) {
|
|
|
|
|
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Custom: exact name (fallback for legacy references)
|
2026-02-16 01:39:14 -05:00
|
|
|
if (customAbilities.containsKey(key)) return true;
|
|
|
|
|
|
|
|
|
|
// Custom: case-insensitive name
|
|
|
|
|
for (Ability ability : customAbilities.values()) {
|
|
|
|
|
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// UNIFIED ACTION RESOLUTION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve any action key to an IAbilityAction.
|
|
|
|
|
* Checks abilities first, then chained abilities.
|
|
|
|
|
*/
|
|
|
|
|
public IAbilityAction resolveAction(String key) {
|
|
|
|
|
if (key == null || key.isEmpty()) return null;
|
|
|
|
|
|
|
|
|
|
Ability ability = resolveAbility(key);
|
|
|
|
|
if (ability != null) return ability;
|
|
|
|
|
|
2026-02-18 04:02:51 -05:00
|
|
|
return resolveChainedAbility(key);
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a key can be resolved as any action (ability or chain).
|
|
|
|
|
*/
|
|
|
|
|
public boolean canResolveAction(String key) {
|
|
|
|
|
if (canResolveAbility(key)) return true;
|
2026-02-18 04:02:51 -05:00
|
|
|
return canResolveChainedAbility(key);
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-02-14 14:43:25 -05:00
|
|
|
// EXTENSION POINTS
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public void registerVariant(String typeId, AbilityVariant variant) {
|
|
|
|
|
externalVariants.computeIfAbsent(typeId, k -> new ArrayList<>()).add(variant);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public List<AbilityVariant> getVariantsForType(String typeId) {
|
|
|
|
|
List<AbilityVariant> result = new ArrayList<>();
|
|
|
|
|
Ability temp = create(typeId);
|
|
|
|
|
if (temp != null) {
|
|
|
|
|
result.addAll(temp.getVariants());
|
2026-02-12 20:30:12 -05:00
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
List<AbilityVariant> ext = externalVariants.get(typeId);
|
|
|
|
|
if (ext != null) {
|
2026-02-15 18:05:07 -05:00
|
|
|
// If no built-in variants exist but external ones do,
|
|
|
|
|
// inject a "Base" variant so the user always gets a choice
|
|
|
|
|
if (result.isEmpty()) {
|
2026-02-18 05:13:51 -05:00
|
|
|
result.add(new AbilityVariant("ability.variant.base", a -> {
|
|
|
|
|
}));
|
2026-02-15 18:05:07 -05:00
|
|
|
}
|
2026-02-14 14:43:25 -05:00
|
|
|
result.addAll(ext);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public void registerFieldProvider(IAbilityFieldProvider provider) {
|
|
|
|
|
fieldProviders.add(provider);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 14:43:25 -05:00
|
|
|
public List<IAbilityFieldProvider> getFieldProviders() {
|
|
|
|
|
return fieldProviders;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 04:57:28 -05:00
|
|
|
public void registerChainedFieldProvider(IChainedAbilityFieldProvider provider) {
|
|
|
|
|
chainedFieldProviders.add(provider);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<IChainedAbilityFieldProvider> getChainedFieldProviders() {
|
|
|
|
|
return chainedFieldProviders;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 21:06:12 -05:00
|
|
|
public void registerExtender(IAbilityExtender extender) {
|
|
|
|
|
extenders.add(extender);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<IAbilityExtender> getExtenders() {
|
|
|
|
|
return extenders;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:15:52 -05:00
|
|
|
public void registerFlightChecker(Predicate<EntityPlayer> checker) {
|
|
|
|
|
flightCheckers.add(checker);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isPlayerFlying(EntityPlayer player) {
|
|
|
|
|
if (player.capabilities.isFlying) return true;
|
|
|
|
|
for (Predicate<EntityPlayer> checker : flightCheckers) {
|
|
|
|
|
if (checker.test(player)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 21:06:12 -05:00
|
|
|
/**
|
|
|
|
|
* Fire onAbilityStart on all extenders. Returns false if ANY extender cancels.
|
|
|
|
|
*/
|
|
|
|
|
public boolean fireOnAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) {
|
|
|
|
|
for (IAbilityExtender ext : extenders) {
|
|
|
|
|
if (!ext.onAbilityStart(ability, caster, target)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fire onAbilityTick on all extenders. Returns false if ANY extender says to interrupt.
|
|
|
|
|
*/
|
|
|
|
|
public boolean fireOnAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
|
|
|
|
AbilityPhase phase, int tick) {
|
|
|
|
|
for (IAbilityExtender ext : extenders) {
|
|
|
|
|
if (!ext.onAbilityTick(ability, caster, target, phase, tick)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fire onAbilityComplete on all extenders.
|
|
|
|
|
*/
|
|
|
|
|
public void fireOnAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
|
|
|
|
boolean interrupted) {
|
|
|
|
|
for (IAbilityExtender ext : extenders) {
|
|
|
|
|
ext.onAbilityComplete(ability, caster, target, interrupted);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fire onAbilityDamage on all extenders. Chain of responsibility — first true wins.
|
|
|
|
|
*/
|
|
|
|
|
public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
|
|
|
|
float damage, float knockback, float knockbackUp,
|
|
|
|
|
double knockbackDirX, double knockbackDirZ) {
|
|
|
|
|
for (IAbilityExtender ext : extenders) {
|
|
|
|
|
if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp,
|
2026-02-18 05:13:51 -05:00
|
|
|
knockbackDirX, knockbackDirZ)) {
|
2026-02-14 21:06:12 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-02-14 14:43:25 -05:00
|
|
|
// IAbilityHandler
|
2026-01-09 15:29:00 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String[] getTypes() {
|
2026-02-14 14:43:25 -05:00
|
|
|
return abilityTypes.keySet().toArray(new String[0]);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 20:30:12 -05:00
|
|
|
public boolean isBuiltInType(String typeId) {
|
|
|
|
|
return builtInTypeIds.contains(typeId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
@Override
|
|
|
|
|
public boolean hasType(String typeId) {
|
2026-02-14 14:43:25 -05:00
|
|
|
return abilityTypes.containsKey(typeId);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 18:31:47 -03:00
|
|
|
public boolean isAllowedByPlayer(String typeId) {
|
|
|
|
|
if (abilityTypes.containsKey(typeId)) {
|
|
|
|
|
Ability temp = abilityTypes.get(typeId).get();
|
|
|
|
|
return temp.getAllowedBy() == UserType.PLAYER_ONLY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isAllowedByNPC(String typeId) {
|
|
|
|
|
if (abilityTypes.containsKey(typeId)) {
|
|
|
|
|
Ability temp = abilityTypes.get(typeId).get();
|
|
|
|
|
return temp.getAllowedBy() == UserType.NPC_ONLY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isAllowedByBoth(String typeId) {
|
|
|
|
|
if (abilityTypes.containsKey(typeId)) {
|
|
|
|
|
Ability temp = abilityTypes.get(typeId).get();
|
|
|
|
|
return temp.getAllowedBy() == UserType.BOTH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
@Override
|
|
|
|
|
public String[] getAbilityNameArray() {
|
2026-02-14 14:43:25 -05:00
|
|
|
return builtAbilities.keySet().toArray(new String[0]);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean hasAbilityName(String name) {
|
2026-02-14 14:43:25 -05:00
|
|
|
return builtAbilities.containsKey(name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-02-17 20:44:27 -05:00
|
|
|
public String[] getCustomAbilityNameArray() {
|
2026-01-30 05:01:54 -05:00
|
|
|
return customAbilities.keySet().toArray(new String[0]);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-02-17 20:44:27 -05:00
|
|
|
public boolean hasCustomAbilityName(String name) {
|
|
|
|
|
return customAbilities.containsKey(name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-02-17 20:44:27 -05:00
|
|
|
public boolean deleteCustomAbilityByName(String name) {
|
|
|
|
|
return deleteCustomAbility(name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
2026-02-17 23:22:03 -05:00
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|