package kamkeel.npcs.controllers.data.ability; import kamkeel.npcs.controllers.data.ability.type.*; import kamkeel.npcs.controllers.SyncController; 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; import java.util.*; import java.util.function.Supplier; /** * Central controller for ability types and presets. *

* Three maps: *

*/ public class AbilityController implements IAbilityHandler { public static AbilityController Instance = new AbilityController(); /** Type factories keyed by typeId (lang key, e.g. "ability.cnpc.slam"). */ private final Map> typeFactories = new LinkedHashMap<>(); /** Built-in pre-configured abilities keyed by unique name. */ private final Map abilities = new LinkedHashMap<>(); /** User-created custom ability presets keyed by UUID. */ private final Map customAbilities = new LinkedHashMap<>(); /** Version counter — incremented on any custom ability add/modify/delete. * Used by AbilitySlot for cache invalidation. */ private int version = 0; /** External damage handler (e.g., DBC Addon). */ private IAbilityDamageHandler damageHandler = null; /** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */ private final List fieldProviders = new ArrayList<>(); public AbilityController() { registerBuiltinTypes(); } // ═══════════════════════════════════════════════════════════════════════════ // LOADING / SAVING CUSTOM ABILITIES // ═══════════════════════════════════════════════════════════════════════════ /** * Load custom ability presets from world directory. * Called when world loads. Files are named {@code .json}. */ public void load() { customAbilities.clear(); 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 { String filename = file.getName(); String uuid = filename.substring(0, filename.length() - 5); // strip .json NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); Ability ability = fromNBT(nbt); if (ability != null) { ability.setId(uuid); customAbilities.put(uuid, ability); } } catch (Exception e) { LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); } } LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); } private File getDir() { File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities"); if (!dir.exists()) { dir.mkdirs(); } return dir; } /** * Save a custom ability preset. If the ability has no id, generates a UUID. * Stores in customAbilities map and writes to disk. */ 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); } File dir = getDir(); File fileNew = new File(dir, uuid + ".json_new"); File fileCurrent = new File(dir, uuid + ".json"); try { NBTTagCompound nbt = ability.writeNBT(); NBTJsonUtil.SaveFile(fileNew, nbt); if (fileCurrent.exists()) { fileCurrent.delete(); } fileNew.renameTo(fileCurrent); if (fileNew.exists()) { fileNew.delete(); } customAbilities.put(uuid, ability); version++; LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]"); SyncController.syncAllCustomAbilities(); return true; } catch (Exception e) { LogWriter.error("Error saving custom ability: " + uuid, e); return false; } } /** * Delete a custom ability preset by UUID. */ public boolean deleteCustomAbility(String uuid) { if (uuid == null || uuid.isEmpty()) return false; Ability removed = customAbilities.remove(uuid); if (removed == null) return false; File dir = getDir(); File file = new File(dir, uuid + ".json"); if (file.exists()) { file.delete(); } version++; LogWriter.info("Deleted custom ability: " + uuid); SyncController.syncAllCustomAbilities(); return true; } // ═══════════════════════════════════════════════════════════════════════════ // BUILT-IN ABILITY REGISTRY // ═══════════════════════════════════════════════════════════════════════════ /** * Register a pre-configured built-in ability by name. * These are looked up by name in {@link #resolveAbility(String)}. * * @param name Unique name (e.g., "Jump Attack", "Ki Blast") * @param ability Pre-configured ability instance */ public void registerAbility(String name, Ability ability) { if (abilities.containsKey(name)) { LogWriter.info("AbilityController: Overwriting built-in ability: " + name); } ability.setName(name); abilities.put(name, ability); LogWriter.info("Registered ability: " + name); } // ═══════════════════════════════════════════════════════════════════════════ // RESOLUTION (used by AbilitySlot, PlayerAbilityData, etc.) // ═══════════════════════════════════════════════════════════════════════════ /** * Resolve an ability by key. Checks built-in abilities first (by name or registry key), * then custom abilities (by UUID or name). Returns a copy safe to modify for * custom abilities, or the singleton instance for built-in abilities. * * @param key The ability name, registry key (built-in) or UUID (custom) * @return The resolved ability, or null if not found */ public Ability resolveAbility(String key) { if (key == null || key.isEmpty()) return null; // Check built-in abilities first (exact match by name) Ability builtIn = abilities.get(key); if (builtIn != null) { return builtIn; } // Check built-in abilities by name (case-insensitive) for (Map.Entry entry : abilities.entrySet()) { if (entry.getKey().equalsIgnoreCase(key)) { return entry.getValue(); } } // Check built-in abilities by registry key / ID for (Ability ability : abilities.values()) { if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) { return ability; } } // Check custom abilities (exact UUID match) Ability custom = customAbilities.get(key); if (custom != null) { return fromNBT(custom.writeNBT()); } // Check custom abilities by name (case-insensitive) for (Ability ability : customAbilities.values()) { if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) { return fromNBT(ability.writeNBT()); } } return null; } /** * Get all keys that can be used to look up abilities (for tab completion). * Returns registry keys for built-in abilities (no spaces) and UUIDs for custom. */ public Set getAbilityKeys() { Set keys = new LinkedHashSet<>(); // Built-in abilities - use the ID (registry key for BuiltInAbility) for (Ability ability : abilities.values()) { String id = ability.getId(); if (id != null && !id.isEmpty()) { keys.add(id); } } // Custom abilities - use UUID keys.addAll(customAbilities.keySet()); return keys; } /** * Get keys for player-usable abilities only (for tab completion). */ public Set getPlayerAbilityKeys() { Set keys = new LinkedHashSet<>(); // Built-in abilities - use the ID (registry key for BuiltInAbility) for (Ability ability : abilities.values()) { if (ability.getAllowedBy().allowsPlayer()) { String id = ability.getId(); if (id != null && !id.isEmpty()) { keys.add(id); } } } // Custom abilities - use UUID (if player-usable) for (Map.Entry entry : customAbilities.entrySet()) { if (entry.getValue().getAllowedBy().allowsPlayer()) { keys.add(entry.getKey()); } } return keys; } /** * Check if an ability key exists (either built-in or custom). */ public boolean hasAbility(String key) { return abilities.containsKey(key) || customAbilities.containsKey(key); } // ═══════════════════════════════════════════════════════════════════════════ // 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 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 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 getCustomAbilities() { return customAbilities; } /** Replace the custom abilities map (used by client-side sync). */ public void setCustomAbilities(Map synced) { customAbilities.clear(); customAbilities.putAll(synced); version++; } /** Get the version counter (incremented on custom ability changes). */ public int getVersion() { return version; } // ═══════════════════════════════════════════════════════════════════════════ // IAbilityHandler INTERFACE IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════════════ @Override public String[] getTypes() { return typeFactories.keySet().toArray(new String[0]); } @Override public boolean hasType(String typeId) { 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); } @Override public String[] getCustomAbilityIdArray() { return customAbilities.keySet().toArray(new String[0]); } @Override public boolean hasCustomAbilityId(String uuid) { return customAbilities.containsKey(uuid); } @Override public boolean deleteCustomAbilityById(String uuid) { return deleteCustomAbility(uuid); } // ═══════════════════════════════════════════════════════════════════════════ // TYPE REGISTRATION // ═══════════════════════════════════════════════════════════════════════════ /** * Register a new ability type. * Extracts the typeId from a temporary instance and stores in typeFactories. * * @param factoryKey Unused legacy key — the typeId from the created instance is used * @param factory Factory lambda that creates a new instance */ public void registerType(String factoryKey, Supplier factory) { Ability temp = factory.get(); String typeId = temp.getTypeId(); if (typeFactories.containsKey(typeId)) { LogWriter.info("AbilityController: Overwriting type: " + typeId); } typeFactories.put(typeId, factory); } /** * Create an ability from NBT data. * Uses the "typeId" field to look up in typeFactories. */ public Ability fromNBT(NBTTagCompound nbt) { if (nbt == null || !nbt.hasKey("typeId")) { return null; } String typeId = nbt.getString("typeId"); Supplier factory = typeFactories.get(typeId); 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. * * @param typeId The ability typeId (e.g., "ability.cnpc.slam") */ public Ability create(String typeId) { Supplier factory = typeFactories.get(typeId); if (factory == null) { return null; } return factory.get(); } /** * Register built-in ability types. */ private void registerBuiltinTypes() { // Melee/AOE damage abilities registerType("cnpc:slam", AbilitySlam::new); registerType("cnpc:heavy_hit", AbilityHeavyHit::new); registerType("cnpc:cutter", AbilityCutter::new); // Ranged/Projectile abilities registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); registerType("cnpc:dual_orb", AbilityOrbDual::new); registerType("cnpc:orb_barrage", AbilityOrbBarrage::new); registerType("cnpc:dual_disc", AbilityDisc::new); registerType("cnpc:dual_disc", AbilityDiscDual::new); registerType("cnpc:laser_shot", AbilityLaserShot::new); registerType("cnpc:beam", AbilityBeam::new); registerType("cnpc:dual_beam", AbilityBeamDual::new); // 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); } // ═══════════════════════════════════════════════════════════════════════════ // EXTERNAL DAMAGE HANDLER // ═══════════════════════════════════════════════════════════════════════════ /** * Register an external damage handler for abilities. * When set, the handler is called before default damage application. * If the handler returns true, default damage is skipped. */ public void registerDamageHandler(IAbilityDamageHandler handler) { this.damageHandler = handler; } /** Get the registered damage handler, or null if none. */ public IAbilityDamageHandler getDamageHandler() { return damageHandler; } // ═══════════════════════════════════════════════════════════════════════════ // EXTERNAL FIELD PROVIDERS // ═══════════════════════════════════════════════════════════════════════════ /** * Register an external field provider for ability GUIs. * Providers are called during getAllDefinitions() to inject custom tabs/fields. */ public void registerFieldProvider(IAbilityFieldProvider provider) { fieldProviders.add(provider); } /** Get all registered field providers. */ public List getFieldProviders() { return fieldProviders; } }