2026-01-09 15:29:00 -05:00
|
|
|
package kamkeel.npcs.controllers.data.ability;
|
|
|
|
|
|
2026-01-31 21:55:39 -03:00
|
|
|
import kamkeel.npcs.controllers.data.ability.type.*;
|
2026-01-30 05:01:54 -05:00
|
|
|
import kamkeel.npcs.controllers.SyncController;
|
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-01-27 01:33:12 -03:00
|
|
|
import java.util.*;
|
2026-01-09 15:29:00 -05:00
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Central controller for ability types and presets.
|
2026-01-22 23:07:38 -05:00
|
|
|
* <p>
|
2026-01-30 05:01:54 -05:00
|
|
|
* Three maps:
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>{@code typeFactories} — keyed by typeId (lang key), creates new instances of each ability type</li>
|
|
|
|
|
* <li>{@code abilities} — built-in pre-configured abilities, keyed by unique name</li>
|
|
|
|
|
* <li>{@code customAbilities} — user-created presets, keyed by UUID</li>
|
|
|
|
|
* </ul>
|
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-01-30 05:01:54 -05:00
|
|
|
/** Type factories keyed by typeId (lang key, e.g. "ability.cnpc.slam"). */
|
|
|
|
|
private final Map<String, Supplier<Ability>> typeFactories = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
|
|
/** Built-in pre-configured abilities keyed by unique name. */
|
|
|
|
|
private final Map<String, Ability> abilities = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
|
|
/** User-created custom ability presets keyed by UUID. */
|
|
|
|
|
private final Map<String, Ability> customAbilities = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
|
|
/** Version counter — incremented on any custom ability add/modify/delete.
|
|
|
|
|
* Used by AbilitySlot for cache invalidation. */
|
|
|
|
|
private int version = 0;
|
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
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// LOADING / SAVING CUSTOM ABILITIES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Load custom ability presets from world directory.
|
|
|
|
|
* Called when world loads. Files are named {@code <uuid>.json}.
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
|
|
|
|
public void load() {
|
2026-01-30 05:01:54 -05:00
|
|
|
customAbilities.clear();
|
2026-01-09 15:29:00 -05:00
|
|
|
File dir = getDir();
|
|
|
|
|
if (!dir.exists()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File[] files = dir.listFiles();
|
|
|
|
|
if (files == null) return;
|
|
|
|
|
|
|
|
|
|
for (File file : files) {
|
|
|
|
|
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
|
|
|
|
try {
|
2026-01-30 05:01:54 -05:00
|
|
|
String filename = file.getName();
|
|
|
|
|
String uuid = filename.substring(0, filename.length() - 5); // strip .json
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
|
|
|
|
Ability ability = fromNBT(nbt);
|
2026-01-30 05:01:54 -05:00
|
|
|
if (ability != null) {
|
|
|
|
|
ability.setId(uuid);
|
|
|
|
|
customAbilities.put(uuid, ability);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
2026-01-30 05:01:54 -05:00
|
|
|
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private File getDir() {
|
|
|
|
|
File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities");
|
|
|
|
|
if (!dir.exists()) {
|
|
|
|
|
dir.mkdirs();
|
|
|
|
|
}
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Save a custom ability preset. If the ability has no id, generates a UUID.
|
|
|
|
|
* Stores in customAbilities map and writes to disk.
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean saveCustomAbility(Ability ability) {
|
|
|
|
|
if (ability == null) return false;
|
|
|
|
|
|
|
|
|
|
String uuid = ability.getId();
|
|
|
|
|
if (uuid == null || uuid.isEmpty()) {
|
|
|
|
|
uuid = UUID.randomUUID().toString();
|
|
|
|
|
ability.setId(uuid);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File dir = getDir();
|
2026-01-30 05:01:54 -05:00
|
|
|
File fileNew = new File(dir, uuid + ".json_new");
|
|
|
|
|
File fileCurrent = new File(dir, uuid + ".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-01-30 05:01:54 -05:00
|
|
|
customAbilities.put(uuid, ability);
|
|
|
|
|
version++;
|
|
|
|
|
LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]");
|
|
|
|
|
SyncController.syncAllCustomAbilities();
|
2026-01-09 15:29:00 -05:00
|
|
|
return true;
|
|
|
|
|
} catch (Exception e) {
|
2026-01-30 05:01:54 -05:00
|
|
|
LogWriter.error("Error saving custom ability: " + uuid, e);
|
2026-01-09 15:29:00 -05:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Delete a custom ability preset by UUID.
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean deleteCustomAbility(String uuid) {
|
|
|
|
|
if (uuid == null || uuid.isEmpty()) return false;
|
2026-01-09 15:29:00 -05:00
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
Ability removed = customAbilities.remove(uuid);
|
|
|
|
|
if (removed == null) return false;
|
2026-01-09 15:29:00 -05:00
|
|
|
|
|
|
|
|
File dir = getDir();
|
2026-01-30 05:01:54 -05:00
|
|
|
File file = new File(dir, uuid + ".json");
|
2026-01-09 15:29:00 -05:00
|
|
|
if (file.exists()) {
|
|
|
|
|
file.delete();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
version++;
|
|
|
|
|
LogWriter.info("Deleted custom ability: " + uuid);
|
|
|
|
|
SyncController.syncAllCustomAbilities();
|
2026-01-09 15:29:00 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// BUILT-IN ABILITY REGISTRY
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Register a pre-configured built-in ability by name.
|
|
|
|
|
* These are looked up by name in {@link #resolveAbility(String)}.
|
2026-01-09 15:29:00 -05:00
|
|
|
*
|
2026-01-30 05:01:54 -05:00
|
|
|
* @param name Unique name (e.g., "Jump Attack", "Ki Blast")
|
|
|
|
|
* @param ability Pre-configured ability instance
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public void registerAbility(String name, Ability ability) {
|
|
|
|
|
if (abilities.containsKey(name)) {
|
|
|
|
|
LogWriter.info("AbilityController: Overwriting built-in ability: " + name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
ability.setName(name);
|
|
|
|
|
abilities.put(name, ability);
|
|
|
|
|
LogWriter.info("Registered ability: " + name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// RESOLUTION (used by AbilitySlot, PlayerAbilityData, etc.)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-09 15:29:00 -05:00
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Resolve an ability by key. Checks built-in abilities first (by name),
|
|
|
|
|
* then custom abilities (by UUID). Returns a copy safe to modify.
|
|
|
|
|
*
|
|
|
|
|
* @param key The ability name (built-in) or UUID (custom)
|
|
|
|
|
* @return A copy of the resolved ability, or null if not found
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public Ability resolveAbility(String key) {
|
|
|
|
|
if (key == null || key.isEmpty()) return null;
|
|
|
|
|
|
|
|
|
|
// Check built-in abilities first
|
|
|
|
|
Ability builtIn = abilities.get(key);
|
|
|
|
|
if (builtIn != null) {
|
|
|
|
|
return fromNBT(builtIn.writeNBT());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check custom abilities
|
|
|
|
|
Ability custom = customAbilities.get(key);
|
|
|
|
|
if (custom != null) {
|
|
|
|
|
return fromNBT(custom.writeNBT());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-30 05:01:54 -05:00
|
|
|
* Check if an ability key exists (either built-in or custom).
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean hasAbility(String key) {
|
|
|
|
|
return abilities.containsKey(key) || customAbilities.containsKey(key);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// ACCESSORS
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/** Get a built-in ability by name. Returns the stored instance (not a copy). */
|
|
|
|
|
public Ability getAbility(String name) {
|
|
|
|
|
return abilities.get(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get all built-in ability names. */
|
|
|
|
|
public Set<String> getAbilityNames() {
|
|
|
|
|
return new LinkedHashSet<>(abilities.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get a custom ability by UUID. Returns the stored instance (not a copy). */
|
|
|
|
|
public Ability getCustomAbility(String uuid) {
|
|
|
|
|
return customAbilities.get(uuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Check if a custom ability exists by UUID. */
|
|
|
|
|
public boolean hasCustomAbility(String uuid) {
|
|
|
|
|
return customAbilities.containsKey(uuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get all custom ability UUIDs. */
|
|
|
|
|
public Set<String> getCustomAbilityIds() {
|
|
|
|
|
return new LinkedHashSet<>(customAbilities.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get the display name for a custom ability by UUID. */
|
|
|
|
|
public String getCustomAbilityName(String uuid) {
|
|
|
|
|
Ability a = customAbilities.get(uuid);
|
|
|
|
|
return a != null ? a.getName() : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get the custom abilities map (used by SyncController). */
|
|
|
|
|
public Map<String, Ability> getCustomAbilities() {
|
|
|
|
|
return customAbilities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Replace the custom abilities map (used by client-side sync). */
|
|
|
|
|
public void setCustomAbilities(Map<String, Ability> synced) {
|
|
|
|
|
customAbilities.clear();
|
|
|
|
|
customAbilities.putAll(synced);
|
|
|
|
|
version++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get the version counter (incremented on custom ability changes). */
|
|
|
|
|
public int getVersion() {
|
|
|
|
|
return version;
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// IAbilityHandler INTERFACE IMPLEMENTATION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String[] getTypes() {
|
2026-01-30 05:01:54 -05:00
|
|
|
return typeFactories.keySet().toArray(new String[0]);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean hasType(String typeId) {
|
2026-01-30 05:01:54 -05:00
|
|
|
return typeFactories.containsKey(typeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String[] getAbilityNameArray() {
|
|
|
|
|
return abilities.keySet().toArray(new String[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean hasAbilityName(String name) {
|
|
|
|
|
return abilities.containsKey(name);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-01-30 05:01:54 -05:00
|
|
|
public String[] getCustomAbilityIdArray() {
|
|
|
|
|
return customAbilities.keySet().toArray(new String[0]);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean hasCustomAbilityId(String uuid) {
|
|
|
|
|
return customAbilities.containsKey(uuid);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean deleteCustomAbilityById(String uuid) {
|
|
|
|
|
return deleteCustomAbility(uuid);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// TYPE REGISTRATION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register a new ability type.
|
2026-01-30 05:01:54 -05:00
|
|
|
* Extracts the typeId from a temporary instance and stores in typeFactories.
|
2026-01-09 15:29:00 -05:00
|
|
|
*
|
2026-01-30 05:01:54 -05:00
|
|
|
* @param factoryKey Unused legacy key — the typeId from the created instance is used
|
2026-01-22 23:07:38 -05:00
|
|
|
* @param factory Factory lambda that creates a new instance
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-10 23:06:13 -05:00
|
|
|
public void registerType(String factoryKey, Supplier<Ability> factory) {
|
|
|
|
|
Ability temp = factory.get();
|
2026-01-30 05:01:54 -05:00
|
|
|
String typeId = temp.getTypeId();
|
|
|
|
|
if (typeFactories.containsKey(typeId)) {
|
|
|
|
|
LogWriter.info("AbilityController: Overwriting type: " + typeId);
|
|
|
|
|
}
|
|
|
|
|
typeFactories.put(typeId, factory);
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create an ability from NBT data.
|
2026-01-30 05:01:54 -05:00
|
|
|
* Uses the "typeId" field to look up in typeFactories.
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
|
|
|
|
public Ability fromNBT(NBTTagCompound nbt) {
|
|
|
|
|
if (nbt == null || !nbt.hasKey("typeId")) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String typeId = nbt.getString("typeId");
|
2026-01-30 05:01:54 -05:00
|
|
|
Supplier<Ability> factory = typeFactories.get(typeId);
|
2026-01-09 15:29:00 -05:00
|
|
|
if (factory == null) {
|
|
|
|
|
LogWriter.info("AbilityController: Unknown ability type: " + typeId);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ability ability = factory.get();
|
|
|
|
|
ability.readNBT(nbt);
|
|
|
|
|
return ability;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new empty ability of the given type.
|
2026-01-22 23:07:38 -05:00
|
|
|
*
|
2026-01-30 05:01:54 -05:00
|
|
|
* @param typeId The ability typeId (e.g., "ability.cnpc.slam")
|
2026-01-09 15:29:00 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public Ability create(String typeId) {
|
|
|
|
|
Supplier<Ability> factory = typeFactories.get(typeId);
|
2026-01-09 15:29:00 -05:00
|
|
|
if (factory == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-01-10 23:06:13 -05:00
|
|
|
return factory.get();
|
2026-01-09 15:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register built-in ability types.
|
|
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
private void registerBuiltinTypes() {
|
2026-01-09 15:29:00 -05:00
|
|
|
// Melee/AOE damage abilities
|
|
|
|
|
registerType("cnpc:slam", AbilitySlam::new);
|
|
|
|
|
registerType("cnpc:heavy_hit", AbilityHeavyHit::new);
|
|
|
|
|
registerType("cnpc:cutter", AbilityCutter::new);
|
|
|
|
|
|
|
|
|
|
// Ranged/Projectile abilities
|
2026-01-25 20:39:01 -05:00
|
|
|
registerType("cnpc:sweeper", AbilitySweeper::new);
|
2026-01-09 15:29:00 -05:00
|
|
|
registerType("cnpc:projectile", AbilityProjectile::new);
|
|
|
|
|
registerType("cnpc:orb", AbilityOrb::new);
|
2026-01-31 21:56:14 -03:00
|
|
|
registerType("cnpc:dual_orb", AbilityDualOrb::new);
|
2026-01-25 20:39:01 -05:00
|
|
|
registerType("cnpc:disc", AbilityDisc::new);
|
|
|
|
|
registerType("cnpc:laser_shot", AbilityLaserShot::new);
|
|
|
|
|
registerType("cnpc:beam", AbilityEnergyBeam::new);
|
2026-01-31 21:55:39 -03:00
|
|
|
registerType("cnpc:dual_beam", AbilityDualBeam::new);
|
2026-01-09 15:29:00 -05:00
|
|
|
|
|
|
|
|
// Movement abilities
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Defensive abilities
|
|
|
|
|
registerType("cnpc:guard", AbilityGuard::new);
|
|
|
|
|
registerType("cnpc:heal", AbilityHeal::new);
|
|
|
|
|
|
|
|
|
|
// Zone/Trap abilities
|
|
|
|
|
registerType("cnpc:hazard", AbilityHazard::new);
|
|
|
|
|
registerType("cnpc:trap", AbilityTrap::new);
|
|
|
|
|
}
|
|
|
|
|
}
|